TurnPreparer¶
File: brain/agent/turn_preparer.py
Purpose¶
TurnPreparer assembles the initial message list (messages) for a single agent
turn and returns a frozen TurnPreparation dataclass. It owns all prompt-assembly
logic previously scattered through AgentHarness.
Construction¶
preparer = TurnPreparer(
provider=provider,
system_prompt=system_prompt_override, # None → harness uses SYSTEM_PROMPT default
prompt_id="agent.profile.default",
prompt_version="1.0.0",
prompt_tags=frozenset({"chat"}),
prompt_engine="minimal",
prompt_variables=("user_name",),
run_context_metadata={"source": "chat_api"},
event_log=event_log,
)
prepare() — Public API¶
prep: TurnPreparation = preparer.prepare(
session_id="sess_abc",
history=[{"role": "user", "content": "..."}, ...],
user_message="What are my open loops?",
context_pack={"relevant_facts": [...], "preferences": []},
memory_context_text="User prefers bullet points.",
runtime_provider="anthropic",
runtime_model="claude-sonnet-4-6",
skill_context=None,
system_prompt_override="You are SecondBrain...\n## Thinking Instructions\n...",
)
Message Assembly Order¶
The following messages are assembled in this exact order:
1. System — system_prompt_override (base prompt + optional thinking prefix)
2. System — runtime metadata (session_id, provider, model, today's date)
3. System — memory context (omitted if empty)
4. User/Assistant — history messages
5. System — untrusted_context_block(context_pack)
6. System — skill_context (omitted if None)
7. User — user_message
1. System Prompt¶
Passed in as system_prompt_override (already augmented with thinking
instructions by ThinkingManager.get_thinking_prompt()).
2. Runtime Metadata¶
A system message injected automatically:
Runtime metadata for this chat turn (authoritative):
- session_id: sess_abc
- provider: anthropic
- model: claude-sonnet-4-6
- today: 2026-04-09
- tomorrow: 2026-04-10
Never call tools to find today's date; use the value above.
This prevents the model from calling date-related tools unnecessarily.
3. Memory Context¶
The formatted text from MemoryRetriever.retrieve_for_context(). Injected as a
system message if non-empty.
4. History¶
Previous turn messages passed through from the caller as-is.
5. Context Pack (Prompt-Hardened)¶
The vault context pack is wrapped in untrusted_context_block() from
brain/agent/prompt_hardening.py. This defensive pattern marks the content
as untrusted external input, reducing prompt-injection risk.
6. Skill Context¶
An optional additional system message for active skill context (e.g. "You are acting as a travel planner").
7. User Message¶
The current turn's user message, appended last.
Per-Turn Render Hash¶
from brain.prompts.hashing import stable_render_hash
render_hash = stable_render_hash({"text": system_prompt_override})
The render hash is computed fresh every turn from system_prompt_override.
This means:
- If the system prompt changes mid-session, the hash reflects the new content.
- Each turn gets an independent hash for provenance tracking.
- The hash is stored in TurnPreparation.prompt_render_hash and subsequently
in TurnRuntimeState.prompt_render_hash.
If prompt_id is set, a prompt.rendered event is also written to the event log.
TurnPreparation — Return Type¶
@dataclass(frozen=True)
class TurnPreparation:
context_pack: dict[str, Any]
context_hash: str # SHA-256 hash of context_pack (16-char prefix)
messages: tuple[dict[str, Any], ...] # Immutable; converted to list before mutation
resolved_provider: str
resolved_model: str
prompt_render_hash: str # Per-turn hash of system_prompt_override
The dataclass is frozen: collaborators that receive it cannot accidentally mutate the assembled preparation state.
Provider / Model Resolution¶
_resolve_provider_model(runtime_provider, runtime_model):
- If both are non-empty strings, use them as-is.
- Otherwise, call
_infer_from_provider(): - If the provider is a
ProviderChain, use the first provider in the chain. - Read
provider.nameandprovider.model. - Handle
"name:model"encoding (e.g."anthropic:claude-3-5-sonnet").
This ensures the runtime metadata system message always has accurate values even when a provider chain or custom provider is used.