refactor(core): remove owner-final assembly machinery

This commit is contained in:
Tianyi Cui
2026-07-13 13:09:41 +08:00
parent f32cfafa1a
commit e04ec07345
31 changed files with 143 additions and 476 deletions
+7 -8
View File
@@ -1,6 +1,6 @@
# 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 global default `deployment:persona` section — so they remain available regardless of which loop plugin drives an agent. An agent-scoped contribution with the same persona name shadows that default for its agent.
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 global default `deployment:persona` section — so they remain available regardless of which loop plugin drives an agent. An agent-scoped contribution with the same persona name shadows that default for its agent.
## Config
@@ -13,19 +13,19 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool-
### Public API
- `ctx.systemPrompt.section(section: PromptSection): () => 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): () => 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.section(section: PromptSection): () => 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. Disposed with the calling fiber.
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. 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): () => 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.
- `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 and returns its authoritative result. 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](../../../docs/cordis-catalog/events.md). Owner-final restoration applies only after a successful assembly waterfall returns.
`system-prompt/assemble` is an expert cooperative seam: its returned assembly is authoritative, and a listener that replaces or removes entries owns preserving any active Code Mode or structured-output protocol. Prefer [`ToolRegistry.restrict()`](../tools/README.md) when tool filtering must stay aligned across model presentation, lookup, and execution. 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](../../../docs/cordis-catalog/events.md).
### 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 `100–199`.
- `PromptSection` — `{ name, order, text }`. Sections are concatenated in ascending `order`. Order bands: `-100` is the harness identity, `0` the deployment persona, tool guidance uses `100–199`.
- `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.
@@ -36,8 +36,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse
- 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.
- The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller.
### What is NOT here