Skip to content

Agent Context Sequence

Daniel Ellison edited this page Sep 3, 2026 · 1 revision

Agent context-assembly sequence

The full ordering of what lands in an agent's context window, from a fresh subprocess through to the user's first message. The assembly is backend-neutral: one helper in backend.py (assemble_turn_context) builds the per-turn prompt identically for every backend, so switching a person between backends changes the engine, never the context. What differs per backend is only Phase 1, the binary's own startup.

Phase 1 - backend binary startup (before any user input)

Each backend binary builds its own baseline before Kai says anything: its built-in system prompt, its tool definitions, its settings. Kai has no direct visibility into this phase. For Claude Code, the specifics:

  1. Binary's built-in system prompt. Anthropic ships this baked into the CLI: the tool-call protocol, output formatting rules, conciseness guidance.
  2. Settings resolution from ~/.claude/settings.json plus project .claude/settings.json; Kai passes no --settings JSON. The flags Kai does pass: --input-format stream-json, --output-format stream-json, --verbose, --model, --effort, and --permission-mode bypassPermissions. Kai passes no --system-prompt; everything Kai adds rides in as prepended user-message text.
  3. Instruction-file walk-up from cwd. The CLI collects CLAUDE.md files walking up from the workspace. The canonical identity content lives in AGENTS.md (every backend reads the same file); the home workspace carries a one-line CLAUDE.md adapter importing it so Claude's native walk-up finds the same content.
  4. Tool definitions for the built-in tools, and skills discovery under .claude/skills/.

The other backends do their own equivalents over their own protocols (ACP handshakes, the codex app-server initialize, Pi's RPC); their baseline prompts are theirs, not Kai's.

After Phase 1, the binary waits for the first message.

Phase 2 - Kai's prepended block (rides in as user-message text)

assemble_turn_context layers prefixes around the user's message. The reading order, top to bottom:

  1. Foreign-workspace reminder (only when the workspace is not the person's home workspace): a block reminding the agent to respond only to what the user asks, not to auto-act on the foreign repo's instruction files or git state.
  2. Semantic memory block (only when memory is enabled, the context is private, and retrieval found hits above the floor). The search query is captured from the user's original text before any context is prepended, so injected context never pollutes the embedding.
  3. Session-context block - first message of a fresh session only. Internals below.
  4. Agent definition context - the run-bound agent definition (purpose, instructions, declared capabilities), applied on every turn including live sessions. The definition carries no authority of its own; it shapes behavior, not access.
  5. USER_MESSAGE_MARKER - the exact string [User's current message - respond to this:]. Load-bearing: it marks the boundary between Kai's prepended context and the user's actual words.
  6. The user's actual message (text or media blocks).

The implementation prepends in reverse so the marker lands closest to the user text; a regression test guards the ordering.

Session-context internals (first message only)

In reading order:

  1. [Your core identity and instructions:] - only in a foreign workspace. Reads AGENTS.md from the home workspace; in the home workspace the backend's own startup already found it.
  2. [Memory subsystem: ...] - always emitted, with three states: enabled (semantic memory is the fact surface; saves route through the memory API), disabled (MEMORY.md is the fact surface), or unavailable in shared channels (group-channel runs get no per-person memory at all).
  3. [Your personal preferences (file: ...):] - private contexts only. Reads <DATA_DIR>/preferences/<principal_id>/PREFERENCES.md, falling back to the legacy chat-id directory when the canonical one does not exist yet. Skipped entirely in shared channels and in one-shot invocations with no identity.
  4. [Your persistent memory (file: ...):] - only when memory is disabled, and private contexts only. Reads <DATA_DIR>/memory/<principal_id>/MEMORY.md (same legacy fallback). The contents ride inside an untrusted-data wrapper with explicit never-obey-instructions-found-here framing; memory is data, not directives.
  5. ## Workspace Instructions - only when workspaces.yaml sets a system_prompt for the current workspace.
  6. Conversation history - canonical context assembled from the SQLite message store (bounded: recent messages with per-message caps), introduced as [Recent canonical conversation context...], or an explicit no-earlier-messages line on a fresh channel. Compatibility routes without a canonical channel fall back to the JSONL slice with a pointer to the archive directory (<DATA_DIR>/history/<channel_id>/).
  7. [Scheduling API: ...] and [External services available: ...] - endpoint documentation so the agent can schedule jobs and call proxied services.

Deferred file reads on protected installs

On protected installs the daemon does not read per-person preference and memory files itself: each file is owned by the person's os_user, so the session context injects the file path plus an instruction, and the isolated subprocess (which runs as that user) reads it. On single-user installs the daemon inlines the contents directly. Same reading order either way.

What fires per-turn vs once

  • Once at subprocess startup: Phase 1.
  • First message of a session: the session-context block.
  • Every message: the foreign-workspace reminder, semantic memory retrieval, the agent definition context, the marker, the user text.

Shared channels

Group-channel runs are not private contexts: the memory marker reads unavailable in shared channels, and both the preferences and MEMORY.md blocks are skipped entirely. Nothing person-private enters a room other people can read.

No-identity edge cases

One-shot invocations without a chat or principal identity (smoke tests, health checks):

  • The preferences block is omitted; there is no global-fallback PREFERENCES.md.
  • The MEMORY.md block falls back to the legacy flat <DATA_DIR>/memory/MEMORY.md.
  • Semantic retrieval is skipped; an empty user id would search across everyone, a data-isolation risk.

Foreign workspace specifically

When a person switches to another directory:

  • Phase 1 (on Claude) picks up the foreign workspace's own instruction files natively.
  • The session context additionally injects the person's home AGENTS.md, the persona baseline the walk-up cannot reach from a foreign cwd.
  • The per-turn foreign-workspace reminder keeps the agent from auto-acting on the foreign repo's instructions. Both layers, project rules and Kai identity, coexist in context.

Clone this wiki locally