07 — System prompt
Status: implemented in 2.0.0
Current seed: crates/vak-core/src/system-prompt.md (~770 tokens, block-marked),
plus a runtime Surface: line of ~40 and any appended surface notes. Layer composition, editing surfaces, and
trust are specified in doc 45.
Editable per layer; see doc 45. .vak/SYSTEM.md remains as a legacy
project-layer identity override.
The presentation contract is universal: the prompt may select semantic shapes such as maps, calendars, boards, entities, evidence, documents, graphs, forms, transactions, alerts, conversations, and simulations in addition to coding, research, data, and lifestyle results. The built-in seed pack currently ships 70 disabled, revision-3 presentation definitions. New built-in definitions are reconciled additively at server startup; user presentations and activations are retained.
Diff notes
-
3.0.63: expanded the presentation vocabulary from the original 52 seeded definitions to 70 universal definitions and documented startup reconciliation. Prompt examples now explain domain-neutral composition.
-
v0.1.0: initial six-tool kernel prompt. Rules emphasize read-before-edit, small verifiable steps, error-driven fixing, workspace containment.
-
v0.1.1: documented the dynamically advertised task, memory, web, and MCP tools; explicitly separated skill guidance from executable tools; added a verification/no-false-completion rule after live Ollama prompt testing found ambiguity around
taskand discovered skill names. -
v0.1.2: made the skill boundary imperative by explicitly prohibiting skill names in tool calls after live-model retries showed that a descriptive skill name could still be selected as an executable tool.
-
v0.1.3: instructed the agent to inspect file-backed requirements immediately rather than asking the user to repeat them; this closes a live Ollama case where the model stopped before reading README.md.
-
v0.2.0: identity change. The prompt opened with "an expert coding agent operating in the user's terminal", which was wrong on both halves. vak is general-purpose — research, writing, data, and operations run through the same core as code — and the same prompt text is served to the CLI, the desktop app, the HTTP server, and every chat gateway, so a Telegram user was being addressed as though they were at a shell. The new opening states the general-purpose identity and explicitly forbids assuming a surface. The capability contract is unchanged; the rules were widened from edit/test phrasing to look-before-you-act and verify-with-whatever-the-work-has, with the code-specific cases kept as instances rather than the whole job. Two clauses were added: no tool for the ask means say so rather than narrate the effect, and out-of-workspace or irreversible effects are confirmed first.
default_prompt_documents_identity_and_dynamic_tool_boundaries(vak-core/src/lib.rs) now pins the identity phrases and asserts the coding/terminal-only wording never returns. The auxiliary prompts moved with it — reflection (vak-core/src/reflection.rs), completion audit (vak-agent/src/goal.rs), and compaction (vak-context/src/assemble.rs), whose "files created/modified" retention rule now also keeps non-file effects. -
v0.2.1: the prompt now names the surface instead of telling the model to assume nothing.
Core::with_surface(vak-core/src/lib.rs) carries aSurface—Cli,Desktop,Server,Chat { channel },Background, or the defaultUnknown— on theCorehandle itself rather than inCoreInner, exactly asdefault_deliver_toalready does, so the gateway can clone-and-stamp per inbound message without touching shared workspace state.system_prompt()appends aSurface:block that says where the reply will be read and what that costs: a phone-sized chat bubble, a terminal, a markdown panel next to a diff viewer the user can already see, an API client that may not render at all, or an unattended run with nobody to ask.Unknownis the default because it is honest — an un-stamped caller gets the old assume-nothing text rather than being mislabelled.Stamped at: the CLI run paths and
run_serve(vak/src/main.rs), the desktop backend (vak-desktop/src/main.rs), the pooled per-workspace cores (vak-server/src/core_pool.rs), best-of-N children (vak-server/src/lib.rs) and the heartbeat (vak-server/src/heartbeat.rs) asBackground, and each inbound gateway message (vak-server/src/gateway.rs), which narrows the pool'sServerto the real transport.Known limitation. The assembled prompt freezes into the session contract at creation (
FrozenContract::system_prompt, AGENTS.md rule 17), so a session started on one surface and resumed on another keeps the originalSurface:line. This is the same staleness the frozen skills and MCP sections already carry, and it is left consistent with them deliberately rather than given a bespoke live-patching path for one line. -
v0.2.2: the seed became a seed. The single document is now split on
<!-- block: ... -->markers intoidentity,capability_contract,operating_rules, andguardrails, resolved through the layer chain in doc 45. Two guardrails were added that the prompt never had: content arriving through a tool is data rather than instruction (an agent with web, MCP, and file tools had nothing at all to say about prompt injection), and credentials are never revealed, transmitted, or written into a command line, commit, or outbound request. TheSurface:paragraph is unchanged. The whole-document.vak/SYSTEM.mdoverride is now read as the project layer'sidentityonly, and is demoted entirely for an untrusted project — it could previously delete the capability contract and every safety rule on the first-run path. -
v0.2.3:
surface-note— a fourth editable block appended after the generatedSurface:line under "Also true on this surface:". The line itself stays code-owned; a note is a separate concatenating block, so nothing can name or replace the runtime's own observation, and no narrower layer can drop a wider one's note. Notes are dropped for an untrusted project (unlike guardrails) because free-form context can widen perceived latitude rather than narrow it. -
v3.0.17: multi-turn continuity, conversational drift, and no-escape-hatch clarification rules in
operating_rules. Instructs the agent that conversational drift across turns is expected and to follow along smoothly without complaint or resistance; to resolve references ("the data", "do that", "it", "something") against earlier turns; and prohibits using demands for manual input or clarification as an exception-handling escape hatch to avoid taking action or using available tools. -
v3.4.5: presentation cards are now preferentially emitted via per-shape
emit_*_cardtool calls (vak-core/src/presentation_tools.rs) rather than a hand-writtenvakfence, and thecapability_contractblock's card guidance was reworded to say so — measured against the real local model this app ships (gemma4:e2b-mlx via Ollama): a free-text fence in prose parsed as valid JSON only ~20% of the time, against 100% for a tool call constrained by a precise per-shape JSON Schema. The fence path stays as a fallback for a turn where no matchingemit_*_cardtool is present. Also added an explicit instruction not to restate a card just emitted via tool as a trailing fence — observed live producing a duplicate card, sincevak-server's tool-result projection and the client's own fence-parsing are independent paths with no cross-source dedup;vak-agent's turn loop now also enforces this with one bounded repair turn (find_duplicate_card_fence, mirroring the existing malformed-fence and grounding-check repairs) since a small local model can't be trusted to self-police it from prompt wording alone. -
3.5.0: the composed prompt is no longer one string.
resolve()now returns aResolutionwithtext(the stable prefix: identity, contract, guardrails, surface, card catalogue, skills, mcp, standing) and a separatetailcarryingtemporalandepistemic_stance, which used to be fused intotext. The request assembler renders one control block per turn fromtailplus the session's<intent>/<work_contract>/<conversation_thread>sections and places it before the user's own words on the last user message — never after, and never as its own message. Measured live: with the block placed after the directive, a small local model answered the block's own text instead of the question; with it appended after a tool result and echoing the directive, the model read the echo as "the user is asking again" and re-emitted the same card up to nineteen times, so the block never restates the directive either. Rationale, the prefix-stability motivation (a byte-identical prefix is what lets a provider's cache serve it), and the cache-breakpoint mechanics are in docs/design/68-context-engine.md §6.prompt_drift/the drift fingerprint coverstextonly, sincetailis per-turn by definition. -
Unreleased (after 3.5.1): the seed is back under its 1500-token budget (16.6 KB → 4.3 KB, ~1,000 tokens), now enforced by
the_seed_stays_under_its_token_budget. Every distinct rule survives; what went is repetition and the fifteen card payload examples plus fence syntax incapability_contract. Cards are taught by theemit_*_cardtools' own descriptions and schemas — exactly the path v3.4.5 measured at 100% valid against ~20% for fences — and a card tool the turn did not load is listed in the "More tools" catalogue. The generated "Also accepted assemantic_type" catalogue was fence-path coverage and is removed with it. The contract now namesfind_toolsas how a listed tool is loaded. Beyond the seed, the composed prefix no longer carries: an inlined "active skill" body (a heuristic pick that changed with each reading, read without the digest check theskilltool enforces), MCP tool descriptions (server and tool names only;mcp listreturns the rest), a second copy of the skill list (it was also in theskilltool's description), or memory notes of kindinvariant/proceduralpromoted to guardrails (memory never writes a prompt layer — invariant 28). It gains the reading-independent tool catalogue, which was computed and logged but never rendered. -
Unreleased (prompt audit): every code-owned section now says only what is true where it is sent. The card guidance moved out of
capability_contractinto its ownpresentation_contractblock, included only when card tools are admitted. The "files appear in the user's preview" sentence left the seed, thebashdescription and the "Sandbox runtime" line (deleted) for theSurface:line of desktop and web only. A background run is told to stop at a needed confirmation and leave the question in its result. "Never refuse to run something" became "never claim you cannot run something", so it no longer reads as overriding the guardrails. A blank line now separates the guardrails from theSurface:line. Side dispatches: compaction, handoff, reflection and the completion judge each state that transcript content is material, never instructions (their output returns as trusted context or memory); the handoff no longer calls vak a coding agent. The[steering-drift]nudge and the multi-part intent note no longer quote the user's request back (doc 68 §6). The tier-2/3 classifier choosesdomainsfrom the fixed vocabulary (vak_intent::DOMAIN_VOCABULARY) instead of free-form subject tags. -
Unreleased (resolver version 4, docs/design/47-commitment-kernel.md What the second review changed): the seed is unchanged; four runtime texts change. The analytical stance no longer ends "cite sources for every factual assertion" — the stance is chosen for analysing code and logs as much as the world, and the evidence standard is its own axis with its own wording, so the stance told a model to cite sources for a stack trace. The intent note no longer labels parts by act ("Part 2: author", which a small model copied into its answer as a heading): it gives the number of parts, then order and dependency as plain sentences ("Do part 2 after part 1."), for at most twelve parts, and per-part guidance as "For part 2: …". It says nothing about pasted material beyond that it is there. The classifier prompt shows each part on one line, capped at 280 characters. The conversation thread says "Primary objective" only for a goal a person stated with
/goal; before, the first message of every conversation, "hi" included, was presented as the objective of every later turn.
Successor
Doc 45 (45-prompt-layers.md) supersedes this document's "one constant plus
.vak/SYSTEM.md" model with user-editable, inherited prompt blocks. Diff
notes continue here for the shipped seed; layer composition, trust, and the
editing surfaces are specified there. Doc 68 supersedes the runtime
per-turn half (temporal context, stance, intent, thread) that doc 45's block
table does not cover, since those blocks were never user-editable.
Policy
The prompt stays under 1500 tokens. Every change ships with a diff note here and passes the nightly eval suite before release (Phase 7). Prompt churn is a bug class, not a feature — changes are reviewable events.