Skip to content

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):

  1. If both are non-empty strings, use them as-is.
  2. Otherwise, call _infer_from_provider():
  3. If the provider is a ProviderChain, use the first provider in the chain.
  4. Read provider.name and provider.model.
  5. 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.