From 00cf8b693a1f877d76089c9de6843e08a137ab7b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 5 Jul 2026 11:37:32 +0800 Subject: [PATCH] feat(agent-loop): open every prompt with the harness identity section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A static harness:identity section at order -100 — the first occupant of the documented negative band — states that the agent is powered by the DeepSeek Harness SDK before the deployment's persona renders. Harness attribution is a harness fact: it lives on the loop plugin, not in each deployment's persona, so every agent (subagents included) carries it and no YAML can forget it. A deployment that must drop it can remove the section in the system-prompt/assemble waterfall. Order-band docs updated in all five homes (PromptSection JSDoc, the system-prompt and agent-loop READMEs, architecture.md, the RFC). --- docs/architecture.md | 2 +- ...t-variables-and-tool-guidance-ownership.md | 2 +- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/src/index.ts | 21 ++++++++++++------- packages/core/agent-loop/tests/loop.spec.ts | 16 +++++++------- packages/core/system-prompt/README.md | 4 ++-- packages/core/system-prompt/src/index.ts | 6 +++--- 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index c9090c185a..6ad71af75b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,7 +69,7 @@ A `Session` is an append-only log of typed `SessionEvent`s — the single source ## Prompt assembly (dsh-system-prompt) -Plugins contribute `PromptSection`s (named, ordered, static or computed from the per-call `AssembleContext`), tool-schema providers, and named **prompt variables** interpolated as `{{name}}` at render (strict: an unknown or valueless reference throws). `renderPrompt(assemble({ agent }))` IS the full prompt: the loop's `agent:persona` section (order 0) and its `model`/`cwd` variables carry the per-agent facts — no second composition path. Tool schemas are deliberately part of the assembly ([RFC](rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md)); prompt-fact ownership (persona vs description vs section vs variable) is pinned by [the prompt-variables RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). +Plugins contribute `PromptSection`s (named, ordered, static or computed from the per-call `AssembleContext`), tool-schema providers, and named **prompt variables** interpolated as `{{name}}` at render (strict: an unknown or valueless reference throws). `renderPrompt(assemble({ agent }))` IS the full prompt: the loop's `harness:identity` (−100) and `agent:persona` (0) sections plus its `model`/`cwd` variables carry the harness and per-agent facts — no second composition path. Tool schemas are deliberately part of the assembly ([RFC](rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md)); prompt-fact ownership (persona vs description vs section vs variable) is pinned by [the prompt-variables RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). ## Tool pipeline (dsh-tools) diff --git a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md index 7f5f4e7afe..a47e1bc69f 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md +++ b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md @@ -30,7 +30,7 @@ Plugins contribute named values via `ctx.systemPrompt.variable(name, provider)`; ### Persona as the order-0 section -The loop plugin registers ONE section, `agent:persona` at order 0, whose text is `context.agent?.options.systemPrompt ?? ''`. The loop's special-case join is deleted: `fullSystemPrompt ≡ renderPrompt(assembly)`, one ordered pipeline for everything the model sees, and `agent/pre-step` (compaction's token-pressure input) measures exactly the real prompt. Order bands are now convention: persona `0`, tool guidance `100–199`, negative orders render before the persona. `AgentOptions.systemPrompt` keeps its familiar key but is documented as what it is — the persona template fragment, one section of the full prompt, never the whole (see `CONTEXT.md`). +The loop plugin registers ONE section, `agent:persona` at order 0, whose text is `context.agent?.options.systemPrompt ?? ''`. The loop's special-case join is deleted: `fullSystemPrompt ≡ renderPrompt(assembly)`, one ordered pipeline for everything the model sees, and `agent/pre-step` (compaction's token-pressure input) measures exactly the real prompt. Order bands are now convention: harness identity `-100` (the loop's static `harness:identity` section — every agent's prompt opens by stating it is powered by the DeepSeek Harness SDK), persona `0`, tool guidance `100–199`; other negative orders also render before the persona. `AgentOptions.systemPrompt` keeps its familiar key but is documented as what it is — the persona template fragment, one section of the full prompt, never the whole (see `CONTEXT.md`). ### Tool guidance ownership diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index fec733c34e..25480403bd 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -33,7 +33,7 @@ interface Config { } ``` -Agents listed in config are auto-created at startup. The plugin also registers the per-agent prompt pieces on `ctx.systemPrompt`: the `agent:persona` section (order 0 — `AgentOptions.systemPrompt` renders before all tool guidance) and the built-in `model`/`cwd` prompt variables, each resolved per step from the `assemble({ agent })` context. +Agents listed in config are auto-created at startup. The plugin also registers the harness-owned prompt pieces on `ctx.systemPrompt`: the `harness:identity` section (order −100 — every agent's prompt opens by stating it is powered by the DeepSeek Harness SDK), the `agent:persona` section (order 0 — `AgentOptions.systemPrompt` renders after it, before all tool guidance), and the built-in `model`/`cwd` prompt variables, resolved per step from the `assemble({ agent })` context. ### Classes diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 8bc35a8f93..ed7243caf8 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -82,13 +82,20 @@ export class AgentLoop extends Service implements AgentFactory { // Provide the agent-creation factory to the registry (effect-scoped: the // slot is cleared on dispose). ctx.effect(() => this.ctx.agents.setFactory(this), 'agentLoop.setFactory()') - // The per-agent prompt pieces, registered once and resolved per assembly - // from the AssembleContext the loop passes (loop.ts assembles with - // `{ agent }` each step). The persona is the order-0 section — identity - // renders before all tool guidance; `{{model}}`/`{{cwd}}` are the built-in - // prompt variables projecting the agent's configured model and its - // session workspace. A provider returns undefined when the fact is absent - // (renderPrompt then rejects a persona that claims it — fail loud). + // The prompt pieces the harness itself owns, registered once. The + // harness-identity section states what every agent on this loop IS, + // ahead of everything (order −100 — before the deployment's persona); + // the persona is the order-0 section resolved per assembly from the + // AssembleContext the loop passes (loop.ts assembles with `{ agent }` + // each step); `{{model}}`/`{{cwd}}` are the built-in prompt variables + // projecting the agent's configured model and its session workspace. A + // provider returns undefined when the fact is absent (renderPrompt then + // rejects a persona that claims it — fail loud). + ctx.systemPrompt.section({ + name: 'harness:identity', + order: -100, + text: 'You are an AI agent powered by the DeepSeek Harness SDK.', + }) ctx.systemPrompt.section({ name: 'agent:persona', order: 0, diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 6a0763814b..1a0525c0b2 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -141,7 +141,7 @@ describe('agent loop', () => { .toEqual({ diffs: [{ path: 'a.txt', oldText: null, newText: 'x' }] }) }) - it('renders the persona as the order-0 section — before tool guidance — with {{variables}} resolved', async () => { + it('renders harness identity, then the persona, then tool guidance — with {{variables}} resolved', async () => { const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(adapter) ctx.systemPrompt.section({ name: 'tool:noop', order: 100, text: 'Use the noop tool wisely.' }) @@ -161,7 +161,7 @@ describe('agent loop', () => { await waitForIdle(ctx, agent) const request = adapter.requests[0] - expect(request!.system).toBe('You are a test agent on mock.\n\nUse the noop tool wisely.') + expect(request!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are a test agent on mock.\n\nUse the noop tool wisely.') expect(request!.tools?.map(t => t.name)).toEqual(['noop']) }) @@ -179,7 +179,7 @@ describe('agent loop', () => { send(agent, 'hi') await waitForIdle(ctx, agent) - expect(adapter.requests[0]!.system).toBe('Working in /work/space.') + expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nWorking in /work/space.') }) it('contains a strict-variable render failure: the turn errors, the loop keeps serving turns', async () => { @@ -212,7 +212,7 @@ describe('agent loop', () => { await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) - expect(adapter.requests[0]!.system).toBe('In /rescued.') + expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nIn /rescued.') const turnEnds = agent.session.events.filter(e => e.type === 'turn/end') expect(turnEnds).toHaveLength(2) expect(turnEnds[1]?.type === 'turn/end' && turnEnds[1].data.reason.kind).toBe('completed') @@ -426,10 +426,12 @@ describe('agent loop', () => { send(agent, 'go') await waitForIdle(ctx, agent) - // One fire per step, in order, each with the assembled system prompt. + // One fire per step, in order, each with the assembled system prompt + // (here just the loop's own harness-identity section — no persona set). + const HARNESS = 'You are an AI agent powered by the DeepSeek Harness SDK.' expect(fires).toEqual([ - { turn: 1, step: 1, fullSystemPrompt: '' }, - { turn: 1, step: 2, fullSystemPrompt: '' }, + { turn: 1, step: 1, fullSystemPrompt: HARNESS }, + { turn: 1, step: 2, fullSystemPrompt: HARNESS }, ]) }) diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 6207571618..5626d06a1c 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -21,7 +21,7 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool- ### Key types - `AssembleContext` — what one `assemble()` call is FOR. Declared empty here and merge-extensible; `dsh-agent` declares `agent?: Agent`, so providers project per-agent facts. Providers must tolerate absent fields (a bare `assemble()` carries an empty context). -- `PromptSection` — `{ name, order, text: string | ((context) => string) }`. Sections are concatenated in ascending `order`. Order bands: `0` is the per-agent persona (registered by the agent loop), tool guidance uses `100–199`; negative orders render before the persona. +- `PromptSection` — `{ name, order, text: string | ((context) => string) }`. Sections are concatenated in ascending `order`. Order bands: `-100` is the harness identity, `0` the per-agent persona (both registered by the agent loop), tool guidance uses `100–199`; other negative orders also render before the persona. - `PromptAssembly` — `{ sections: AssembledSection[], tools: ToolSchema[], variables: Record }`. 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. @@ -29,7 +29,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse ### Extension points -- Section providers: tool packages own their cross-call guidance (`tool:bash`, `tool:read`, …); the agent loop owns `agent:persona`. +- Section providers: tool packages own their cross-call guidance (`tool:bash`, `tool:read`, …); the agent loop owns `harness:identity` and `agent: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). diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index 62889a2793..4da727f4e5 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -54,9 +54,9 @@ export interface PromptSection { /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */ name: string /** - * Sections are concatenated in ascending order. Convention: `0` is the - * per-agent persona, tool guidance uses 100–199; negative orders render - * before the persona. + * Sections are concatenated in ascending order. Convention: `-100` is the + * harness identity, `0` the per-agent persona, tool guidance uses 100–199; + * other negative orders also render before the persona. */ order: number /**