mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(agent-loop): open every prompt with the harness identity section
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).
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -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<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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user