review: the persona becomes the system-prompt plugin's deployment config

Review round 2 (tianyicui inline comments):

- dsh-system-prompt itself registers the harness:identity (-100) and
  deployment:persona (0) sections — they must survive a swapped loop
  plugin, so they leave dsh-agent-loop; the persona text is the plugin's
  own validated 'persona' config. The model/cwd variables STAY on the
  loop: runtime facts of the agents it drives.
- AgentOptions.systemPrompt is deleted with all its forwarding plumbing:
  the app configs' systemPrompt keys become 'persona' routed through
  dsh-agent-core (schema = z.intersect of the owners'), the ACP bridge
  and tool-subagent stop carrying persona configuration, and subagent
  children now render the deployment persona like every other agent.
- Example personas drop transport/interface trivia (ACP, CLI) — facts
  irrelevant to the model.
- Root CONTEXT.md removed (not idiomatic); its persona definition was
  wrong under the new ownership anyway.
- Docs, READMEs, the prompt-variables RFC, and generated catalogs
  updated; new loop test pins the assemble-waterfall escape valve
  (an emptied assembly sends NO system field).
This commit is contained in:
Tianyi Cui
2026-07-05 23:23:46 +08:00
parent 2304f7a245
commit 3f83a4ee96
55 changed files with 389 additions and 284 deletions
+49 -3
View File
@@ -4,10 +4,16 @@
* collates them through a waterfall that runs once per step, and
* `renderPrompt` interpolates `{{variable}}` references into the final text.
*
* The harness-owned prompt openers live here too: this plugin registers the
* static `harness:identity` section (order −100) and the deployment's
* `deployment:persona` section (order 0, from its `persona` config), so they
* exist for every agent regardless of which loop plugin drives it.
*
* @module @deepseek-ai/dsh-system-prompt
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
declare module 'cordis' {
@@ -55,7 +61,7 @@ export interface PromptSection {
name: string
/**
* Sections are concatenated in ascending order. Convention: `-100` is the
* harness identity, `0` the per-agent persona, tool guidance uses 100–199;
* harness identity, `0` the deployment persona, tool guidance uses 100–199;
* other negative orders also render before the persona.
*/
order: number
@@ -104,6 +110,22 @@ const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/
/** A complete `{{...}}` reference group at the scan position (validated after). */
const GROUP_AT = /^\{\{([^{}]*)\}\}/
export interface Config {
/**
* The deployment's persona — the ONE deployment-authored fragment of the
* system prompt, rendered as the order-0 `deployment:persona` section
* (after the harness identity, before all tool guidance). Every agent in
* the context shares it, subagents included. Template, not free-form text:
* every complete `{{…}}` group is interpreted strictly against the
* registered prompt variables (the shipped agent loop registers `{{model}}`
* and `{{cwd}}`), and there is no escape syntax for literal `{{…}}` prose
* yet (a deliberate deferral; see the prompt-variables RFC). Defaults to
* `''` — the empty section is dropped at render, so a persona-less
* deployment opens with the harness identity alone.
*/
persona?: string
}
/**
* Renders the text part of an assembly: interpolates `{{variable}}`
* references in each section from `assembly.variables`, drops empty sections,
@@ -169,15 +191,39 @@ function interpolate(section: AssembledSection, variables: Record<string, string
/**
* Registry service (`ctx.systemPrompt`): plugins contribute ordered text
* sections, tool-schema providers, and named prompt variables; the agent loop
* calls `assemble(context)` once per step.
* calls `assemble(context)` once per step. Registers the harness-owned
* `harness:identity` and `deployment:persona` sections itself (see
* {@link Config.persona}).
*/
export class SystemPrompt extends Service {
static Config: z<Config> = z.object({
persona: z.string().default(''),
})
private sections: PromptSection[] = []
private toolProviders: (() => ToolSchema[])[] = []
private variableProviders = new Map<string, (context: AssembleContext) => string | undefined>()
constructor(ctx: Context) {
constructor(ctx: Context, public config: Config) {
super(ctx, 'systemPrompt')
// The harness-owned openers. They live HERE (not on the loop plugin) so a
// deployment that swaps in a different loop keeps them: the identity is a
// harness fact stated ahead of everything, and the persona is the
// deployment's config, one section of the full prompt, never the whole.
// An empty persona still RESERVES the section name (one owner — a plugin
// re-registering it throws); renderPrompt drops the empty text.
this.section({
name: 'harness:identity',
order: -100,
text: 'You are an AI agent powered by the DeepSeek Harness SDK.',
})
this.section({
name: 'deployment:persona',
order: 0,
// The schema already defaulted an omitted persona to ''; the ?? only
// narrows the optional-input TYPE, it never supplies a different value.
text: config.persona ?? '',
})
}
/**