Files
deepseek-harness/packages/core/system-prompt
Tianyi Cui 6967c584d1 Merge remote-tracking branch 'origin/worktree-agent-scope-design' into codex/package-readme-limitations-audit-20260712
# Conflicts:
#	packages/core/scope/README.md
#	packages/session-persistence/session-persistence-jsonl/README.md
#	packages/session-persistence/session-persistence-sqlite/README.md
#	packages/subagent/subagent-acp/README.md
#	packages/subagent/subagent-fork/README.md
#	packages/subagent/subagent-inprocess/README.md
#	packages/subagent/subagent/README.md
#	packages/subagent/tool-subagent/README.md
#	packages/support/invariants/README.md
#	packages/support/subagent-mock/README.md
#	packages/workflow/workflow-workerthread/README.md
#	packages/workflow/workflow/README.md
2026-07-12 23:35:47 +08:00
..

dsh-system-prompt

System prompt assembly registry. Plugins contribute ordered text sections, tool-schema providers, and named prompt variables; contributions that implement required protocol may declare themselves owner-final. The agent loop calls assemble(context) once per step, and renderPrompt(assembly) is the full system prompt the model sees. The plugin registers the harness-owned openers itself — the static harness:identity section and the deployment's deployment:persona section — so they exist for every agent regardless of which loop plugin drives it.

Config

Key Default Meaning
persona '' The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 deployment:persona section unless an agent-scoped contribution shadows it. A template — complete {{…}} groups are interpreted strictly against the registered variables (the shipped loop registers {{model}}/{{cwd}}), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render.
toolOrder Explicit model-facing tool order, as a list of ToolSchema.names with one '<unlisted-tools>' rest entry (TOOL_ORDER_REST): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the system-prompt/assemble waterfall — like the sections' order sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every assemble(); a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: Explicit model-facing tool order.

Service: SystemPrompt (ctx key: systemPrompt)

Public API

  • ctx.systemPrompt.section(section: PromptSection): () => Promise<void> | void Contribute a section. The layer is the calling context's scope: agent.ctx contributes to that agent alone, shadowing a same-named global section there. Duplicate names within one layer and non-finite orders throw. ownerFinal: true restores this section's canonical definition after the complete waterfall and reserves a global section against scoped shadows. Disposed with the calling fiber.
  • ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => Promise<void> | void Contribute tool schemas, evaluated at each assembly with that assembly's context. ToolProviderResult = { schemas, knownNames?, ownerFinalNames? }: schemas is the post-restriction visible set; knownNames is the pre-restriction universe used by toolOrder; ownerFinalNames makes those tools' canonical presence or absence survive the waterfall. A provider must not return a schema named TOOL_ORDER_REST. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
  • ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => Promise<void> | void Contribute a prompt variable, referenced from section text as {{name}}. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; undefined means "no value for this assembly". Disposed with the calling fiber.
  • ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly> Assemble the prompt for one caller: the global layer merged with context.scope's layer, with tool schemas detached before the transform seam. Runs through the scope-filtered system-prompt/assemble waterfall, then restores owner-final contributions from a private pre-waterfall snapshot. Restored entries keep canonical relative order without undoing listener reordering of ordinary entries. Rejects when a configured toolOrder names a tool outside the providers' knownNames universe, or when a provider returns the reserved rest-entry name.

Live events

Prompt assembly is the scope-filtered transformable seam; registry change is the deliberately unfiltered notification that an assembly input changed, possibly for one scope. Exact signatures, dispatch modes, and filtering contracts live in the generated Cordis event catalog. Owner-final restoration applies only after a successful assembly waterfall returns.

Key types

  • AssembleContext — what one assemble() call is FOR. Merge-extensible; declares scope?: ScopeKey (the layer selector) here, and dsh-agent declares agent?: Agent (the typed DX field — never set without scope; use assembleContextFor(agent)). Providers must tolerate absent fields (a bare assemble() carries an empty, scope-less context).
  • PromptSection{ name, order, text, ownerFinal? }. Sections are concatenated in ascending order; ownerFinal is reserved for required protocol instructions. Order bands: -100 is the harness identity, 0 the deployment persona, tool guidance uses 100199.
  • PromptAssembly{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }. Section texts arrive resolved but not yet interpolated; variables holds every registered variable resolved against the context. Tool schemas are part of the assembly by design: "what the model is told it can do" is one coherent thing, even though adapters transmit schemas as a separate wire field.
  • renderPrompt(assembly) — interpolates {{variable}} references in each section, drops empty sections, joins with blank lines. STRICT: an unknown reference (Object.hasOwn lookup — prototype names like {{constructor}} are unknown), a registered-but-valueless reference, a malformed complete {{…}} group, or a {{ that opens no complete group while a }} still follows ({{{model}}}) throws — fail loud beats shipping a malformed prompt. A lone {{ with no }} anywhere after it passes through verbatim; substituted values are never re-scanned.

Merge-extensible: plugins can declare extra fields on PromptAssembly and AssembleContext via declaration merging.

Extension points

  • Section providers: tool packages own their cross-call guidance (tool:bash, tool:read, …); this plugin owns harness:identity and deployment:persona.
  • Variable providers: the agent loop registers model and cwd; any plugin can register the facts it owns (a future date, git state, …).
  • Tool schema providers: ToolRegistry registers itself as a tool provider automatically.
  • The system-prompt/assemble waterfall: mutate or replace the assembly per caller (dynamic tool filtering, extra variables).
  • Owner-final contributions: protocol owners declare finality on the section or tool contribution itself; there is no independent protection registry.

Design rationale: the prompt-variables RFC.

Known Limitations and Deferred Work

  • Deployment-authored prompt text is config/composition only — this plugin owns the global persona default, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API.
  • No prompt compaction here — it belongs on the agent/pre-step seam in dsh-agent (implemented by dsh-compact-basic).
  • No escape syntax for literal {{…}} braces — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one.
  • toolOrder misconfiguration surfaces at prompt assembly (the first turn), not at boot — only shape violations throw at config load.
  • Sections sharing an order value tie-break by registration order — a plugin-load artifact; determinism relies on the distinct-order band convention, unlike the canonicalized tool order.