mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Review follow-up (#196): a listed name with no registered tool was silently ignored; misconfiguration must block work instead. The check lives in the assembly — the earliest moment the registered tool set exists (tool plugins register after the service constructs) and the only universal one (cordis has no "all plugins loaded" event; registrations change at any time). assemble() is now async so the throw surfaces as a rejection rather than a synchronous escape from a Promise-returning method. Blast radius, pinned by a loop-level test: the rejection reaches the turn's outer catch — the turn closes balanced with an `error` reason, agent/error mirrors it, no step opens, no request/header is logged, no request reaches the adapter, and the agent returns to idle; every turn fails identically until the config is fixed. A boot-time validation pass was considered and rejected (recorded in the RFC). The general principle — misconfiguration fails loud, never a silent skip — is added to AGENTS.md.
dsh-system-prompt
System prompt assembly registry. Plugins contribute ordered text sections, tool-schema providers, and named prompt variables; 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 deployment persona: the ONE deployment-authored prompt fragment, rendered as the order-0 deployment:persona section and shared by every agent in the context (subagents included). 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() — 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): () => voidContribute a section. Duplicate names throw. Disposed with the calling fiber.ctx.systemPrompt.tools(provider: () => ToolSchema[]): () => voidContribute tool schemas (evaluated at each assembly). Disposed with the calling fiber.ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => voidContribute a prompt variable, referenced from section text as{{name}}. Duplicate or unreferenceable names throw;undefinedmeans "no value for this assembly". Disposed with the calling fiber.ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>Assemble the prompt for one caller. Runs through thesystem-prompt/assemblewaterfall. Rejects when a configuredtoolOrdernames a tool no provider contributed.
Events
| Event | Mode | Purpose |
|---|---|---|
system-prompt/assemble |
waterfall | Mutate/extend the assembly (with the caller's context) before it reaches the model |
system-prompt/change |
emit | A section, tool provider, or variable was registered or unregistered |
Key types
AssembleContext— what oneassemble()call is FOR. Declared empty here and merge-extensible;dsh-agentdeclaresagent?: Agent, so providers project per-agent facts. Providers must tolerate absent fields (a bareassemble()carries an empty context).PromptSection—{ name, order, text: string | ((context) => string) }. Sections are concatenated in ascendingorder. Order bands:-100is the harness identity,0the deployment persona (both registered by this plugin), tool guidance uses100–199; other negative orders also render before the persona.PromptAssembly—{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }. Section texts arrive resolved but not yet interpolated;variablesholds 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.hasOwnlookup — 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 ownsharness:identityanddeployment:persona. - Variable providers: the agent loop registers
modelandcwd; any plugin can register the facts it owns (a futuredate, git state, …). - Tool schema providers:
ToolRegistryregisters itself as a tool provider automatically. - The
system-prompt/assemblewaterfall: mutate or replace the assembly per caller (dynamic tool filtering, extra variables).
What is NOT here
- Any deployment-authored prompt text outside config — the persona is this plugin's
personaconfig, and every other section comes from the plugin that owns the fact. (Theharness:identityline is deliberately a code literal: a harness fact, not a deployment choice; thesystem-prompt/assemblewaterfall is the escape valve for a deployment that must drop it.) - Prompt compaction (belongs on the
agent/pre-stepseam indsh-agent).
Design rationale: the prompt-variables RFC.