Architecture: Agent prompt assembly
Status: Draft
Last updated: 2026-05-25
Audience: Engineers wiring AgentRunService.execute() and the assistants reasoning about how a user-defined Agent's identity, memory, and constraints get into an AI call.
1. Purpose
When a user-defined Agent runs (heartbeat tick, task execution, or chat reply), the platform must assemble a coherent system message + user-message stack that:
- Carries the Agent's identity (
SOUL.md) and role (AGENTS.md). - Tells the Agent what to do right now — heartbeat directive (
HEARTBEAT.md), task description, or chat thread context. - Constrains it via permissions (
TOOLS.mdand theagents.permissionsJSON column). - Augments it with skills (resolved per the hierarchy in
agents-skills-tasks.md§8). - Provides memory (recent runs, recent activity, scope state) without blowing the token budget.
- Honors the Work / Mission / Idea WorkAdvancedPrompts (existing per-scope prompt overrides) when the run is scoped to one.
The platform's existing AiFacadeService (packages/agent/src/facades/ai.facade.ts) does not assemble system messages today — pipeline plugins do that internally. The new AgentRunService.execute() will assemble the message itself and pass the final composed payload to AiFacadeService.createChatCompletion() / createStreamingChatCompletion() / askJson() via the existing messages parameter.
This doc specifies the canonical assembly order so different Agent triggers (heartbeat / task / chat) produce comparable prompts.
2. Assembly order — system message
The system message is a concatenation of named segments. Lower index = earlier in the message = higher priority for the model's attention.
| # | Segment | Source | Budget (tokens) | Required? |
|---|---|---|---|---|
| 1 | Identity (SOUL.md) | Agent's SOUL.md | full | ✓ |
| 2 | Role (AGENTS.md) | Agent's AGENTS.md | full | ✓ |
| 3 | Capabilities | agents.capabilities TEXT column | full | optional |
| 4 | Operating loop | Agent's HEARTBEAT.md (heartbeat run only) OR a per-trigger preamble (task / chat) | full | ✓ |
| 5 | Tools the Agent may call | Agent's TOOLS.md filtered by permissions.canCallExternalTools etc. | up to 1500 | ✓ |
| 6 | Active Skills | Skills resolved by SkillBindingRepository.resolveActive (Skills spec §3.3). Progressive disclosure. | maxSkillContextTokens (default 4000) | optional |
| 7 | Scope WorkAdvancedPrompts | For Work-scoped Agents, the relevant WorkAdvancedPrompts column (work-advanced-prompts.entity.ts) | full | optional |
| 8 | Scope context | Mission description / Idea description / Work initial_prompt — depends on scope | up to 800 | ✓ |
| 9 | Memory: recent activity | Last N=20 activity_log rows for this Agent's scope (filtered to events the Agent should "know about") | up to 1200 | optional |
| 10 | Memory: last N runs | Compact JSON of the Agent's last 5 agent_runs summaries | up to 800 | optional |
| 11 | Output contract | Strict JSON-schema reminder when caller used askJson(); otherwise free-form | 150 | depends |
Total budget target: ≤ 12 000 input tokens in the system message. If a segment exceeds its budget, it is truncated tail-first (newest preserved, oldest cut). Truncation events emit an AgentRunLog row at level=WARN, step='prompt-assembly'.
Order rationale:
- Identity + Role first — the model anchors on "who am I and what do I do" before "how do I do it."
- Tools before Skills — Skills may reference tools; the tool list must be parsed first.
- Skills before scope context — Skills should be domain-agnostic enough to apply across scopes.
- Memory after instructions — memory tunes; instructions shape.
- Output contract last — closest to the model's response, hardest to forget.
2.1 Per-trigger preamble (segment 4)
For non-heartbeat triggers, segment 4 is replaced by a static preamble instead of HEARTBEAT.md:
Task execution preamble:
You are working on a specific Task assigned to you. The Task body
follows. Your output should advance the Task — make progress, ask a
clarifying question in the Task chat, transition the Task status,
or escalate by creating a sub-task. Do NOT take actions outside the
scope of this Task.
Chat reply preamble:
You were mentioned in a Task chat thread. Read the recent messages,
then post a single reply. Do NOT transition the Task status from a
chat reply — use the transition tool only when explicitly asked.
Keep the reply focused on the chat question.
3. User message
Composed of:
| # | Content |
|---|---|
| 1 | The immediate input — for heartbeat, the literal string "What's the next action you should take? Choose ONE."; for task, the Task description; for chat, the new chat message body. |
| 2 | The conversation context — for task, the most recent 20 task_chat_messages of that task (newest last); for chat, the same. For heartbeat, omitted. |
| 3 | Attachments / mentions / KB references rendered inline as fenced blocks the model can read. KB references injected as their instructionsMd body. |
4. Memory model
4.1 Short-term (within a run)
The model sees segments 9-10 (recent activity + recent runs) as part of every run. This is the platform's "short-term memory" — last few hours / runs.
If the run does multiple tool-loop iterations (read a KB doc, then write a file, then post a chat), short-term memory accumulates inside the run as standard tool-loop messages. No additional storage; this lives in the in-flight LangChain conversation.
4.2 Long-term (across runs)
The Agent's SOUL.md / AGENTS.md / HEARTBEAT.md / TOOLS.md are the long-term memory. They are durable, intentional, and editable.
Crucially: long-term memory is not automatically updated. The Agent has to actively decide "this is important enough to remember forever" and use the editAgentFile tool to append a paragraph. This keeps the model from polluting its identity files with conversational noise. See Agent self-editing rules below.
For passive history (last 30 days of activity), the model reads activity_log on demand via a getActivity({since, limit}) tool. Not injected by default — costs tokens.
4.3 Cross-Agent memory
Agents cannot read each other's MD files. Cross-Agent knowledge flows through:
- Tasks — assigning, commenting, mentioning.
- Mission/Work KB — any Agent in scope can read KB documents.
- Activity log —
getActivity()includes other Agents' run summaries (but not their prompts).
This is intentional separation of identity (private) from work product (shared).
5. Token budgeting
The dispatcher's pre-flight check (BudgetGuardService.checkBudget) estimates cost from an upper-bound token count: assembled system message + user message + 1500 reserved for the model's response. If the estimate would exceed remaining budget AND allowOverage = false, the run is short-circuited to failed with errorMessage='Budget exceeded'.
Actual usage is captured post-call by the existing PluginUsageEvent write inside AiFacadeService (research report Topic 1), with the new agentId and optional taskId columns set.