Prompt assembly
Every system prompt Harn sends to a model is the deterministic reduction of an
ordered list of fragments. Host-provided parts (system_preamble,
system_prefix, system_context, system_prompt_parts, system_appendix,
system_suffix), the agent's primary system text, capability-gated tool
guidance, project context profiles, and rendered
system reminders all flow through one reducer. There is
no parallel string-concatenation path, and there is no place where an
instruction is glued on by hand and silently drifts from the rest of the prompt.
Because assembly is a single reduction, it is also fully auditable: the runtime can answer "why is this sentence in the prompt?" and "what would the prompt look like without tool X?" without anyone reverse-engineering a concatenation.
Fragments
A fragment is one contributor to the system string:
| field | meaning |
|---|---|
id | stable identifier, e.g. host:system_preamble, primary:active_skills, tool:todo.guidance |
source | who contributed it (host:*, primary, reminder, tool:<name>) |
bucket | before (preamble … primary … reminders) or after (appendix/suffix and tail recitations) |
requires_tools | included only when every named tool is in the active tool set |
requires_caps | included only when every named capability flag is set |
body | the already-rendered text; trimmed, and skipped if empty |
The reducer emits all included before fragments in declaration order, then all
included after fragments, joined by a blank line.
The primary block is decomposed
The agent's per-turn primary system text is itself a composite — the base system
prompt, MCP advisory context, active skills, the skill catalog, the progress-tool
nudge, and the loop/tool contracts. Rather than glue these into one opaque
primary string, agent_loop hands the assembler each part as its
own fragment through the internal _system_fragments channel, so every part is
traced on its own (primary:system, primary:active_skills,
primary:loop_contract, …) and can be gated with requires_tools independently.
Those internal fragments may also set bucket: "after" when a live recitation
must land at the prompt tail.
Joining the fragment bodies with a blank line reproduces the single string the
legacy path produced, so the assembled prompt is unchanged — only its provenance
is finer-grained. agent_build_turn_system_fragments is the stdlib helper that
emits the list; agent_build_turn_system is the thin wrapper that joins it.
Tool guidance rides with the tool
The drift-proof way to attach an instruction to a tool is to co-locate it with
the tool definition. Any tool that declares a guidance string auto-contributes
a fragment gated on that tool's own presence:
tool_define(registry, "todo", "Add, update, and complete plan items", {
parameters: { /* … */ },
handler: todo_handler,
guidance: "When working from a plan or task list, always update the TODO tracker after each item.",
})
When todo is in the active tool set, the instruction appears. Drop the tool and
the instruction vanishes — the two share one source of truth and cannot drift.
This is the canonical answer to "tell the model to use the TODO tracker, but only
when a TODO tool is actually available." No if, no hand-maintained list of tool
names in prose.
Guidance is prompt-side metadata: it is rendered into the system prompt but is never sent to the provider as part of the tool's schema.
Project profiles are fragments too
project_context_profile resolves project
signals such as Git remotes, language/build files, supplied code-librarian
signals, and available credential aliases into reducer-ready fragments. Agent
preflight forwards context_profile.prompt_fragments through _system_fragments;
direct llm_call and prompt_explain users can pass context_profile or
project_context_profile in options.
Each profile fragment is capability-gated. For example, a Rust profile fragment
carries requires_caps: ["language.rust"]; prompt_explain(...) then records
the profile:rust trace with the capability reason instead of making the
project guidance an opaque always-on paragraph.
Inspecting the assembled prompt
prompt_explain(options) assembles the system prompt from the same options you
would hand agent_loop and returns the final string plus a
provenance record for every fragment — included or excluded, with the reason and
byte count:
let explained = prompt_explain({
system: "You are a pragmatic engineering partner.",
tools: [
{ name: "todo", description: "Track plan items", guidance: "Always update the TODO tracker." },
{ name: "read", description: "Read a file" },
],
})
// explained.system → the assembled string
// explained.fragments → [{ id, source, bucket, included, reason, bytes }, …]
// explained.included / explained.excluded → counts
Each fragment's reason tells you exactly why it is or isn't present — for
example tool(s) present: todo for a guidance fragment that made it in, or
requires tool \todo` (not available)for one gated out. The bundledharn demo prompt-guidance` scenario assembles a prompt with and without a tool
and prints the provenance side by side.
Session reminders are layered at live call time from the active session, so
prompt_explain shows the static system prompt (host parts + primary +
capability-gated tool guidance). See System reminders
for the dynamic, turn-boundary layer that composes into the same reducer at call
time.
Relationship to other primitives
- System reminders are the dynamic subset of fragments: they carry a lifecycle (TTL, dedupe, preserve-on-compact) and are injected at turn boundaries, but they reduce through the same assembler.
- Prompt templating (
.harn.prompt) renders a fragment'sbody; assembly decides whether and where that body appears. - Capability-adaptive rendering chooses the wire format of the assembled prompt per model capability; fragment assembly chooses which content is present.