diff --git a/CONTEXT.md b/CONTEXT.md index 017fada6b8..4264c40107 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,6 +1,6 @@ # DeepSeek Harness -Ubiquitous language for the harness. Started during the 2026-07-05 system-prompt redesign session; grows as terms crystallize. Decisions live in `docs/rfc/` (this repo's ADR equivalent), not here. +Ubiquitous language for the harness; grows as terms crystallize. Decisions live in `docs/rfc/` (this repo's decision log), not here. ## Language — prompt assembly 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 0802e56aac..7f5f4e7afe 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 @@ -62,9 +62,9 @@ Per-tool semantics and when-to-use live in tool DESCRIPTIONS, which already ship - Further variables (`date`, platform, git state) — the registry makes each a one-line contribution by whichever plugin owns the fact; none is claimed here. - A config `cwd` for pre-created stdio agents (would let the stdio persona use `{{cwd}}` and partition persistence by real path) — deferred until the session-cwd story is revisited. -## Acceptance criteria +## Shipped invariants -- `renderPrompt(assemble({agent}))` for the coding-agent example contains the persona FIRST (with the agent's model name interpolated), then fs/bash/web guidance sections; the loop contains no other prompt-composition path. +- `renderPrompt(assemble({ agent }))` for the coding-agent example renders the persona FIRST (with the agent's model name interpolated), then the fs/bash/web guidance sections; the loop has no other prompt-composition path. - The `subagent_fork` schema description says the child inherits the conversation; the `subagent` one says it does not. The tool follows its provider: absent before the backend activates, present after, gone when the backend unloads, re-worded from the fresh provider on reload. - Unknown/valueless/malformed/unbalanced `{{…}}` references throw with the section name in the message; duplicate section, variable, and tool-name registrations all throw. -- The gating runs (`test:coverage`, `test:snapshot`, `doc-sync`, `build`, `hygiene`) are green; no golden re-record is needed (replay never re-verifies the outgoing request). +- Snapshot goldens are prompt-independent by construction: llm-replay keys replay on (turn, step) chunk streams and never re-verifies the outgoing request. diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 357172db9a..383bef0f0d 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -213,6 +213,13 @@ export function apply(ctx: Context, config: Config): void { // Listeners first, then the presence check: both run synchronously, so no // registration can slip between them; the `disposeTool === undefined` guard // makes a same-tick added-event after a successful mount a no-op. + // TODO(subagent-dup-toolname): two WAITING fibers configured with the same + // toolName collide only when their provider finally arrives — the duplicate + // tool-name throw then propagates through `subagent/provider-added` and + // rolls back the PROVIDER registration, so an invalid config blasts the + // backend's fiber instead of the misconfigured tool's. Config-time detection + // would need a cross-fiber registry of intended tool names; revisit if a + // real deployment ever hits it. ctx.on('subagent/provider-added', (provider) => { if (provider.name === config.provider && disposeTool === undefined) mount(provider) }) diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 3756806de3..8799eba06b 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -231,6 +231,29 @@ describe('dsh-tool-subagent', () => { expect(ctx.tools.schemas().find(s => s.name === 'subagent')!.description).toContain('INHERITS this conversation') }) + it('the tool PLUGIN fiber owns its lifecycle listeners: disposal unmounts, and a disposed fiber never zombie-mounts', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + + // Arm 1: a mounted tool dies with its plugin fiber; the provider survives. + await ctx.plugin(mock, { name: 'mock' }) + const mounted = await ctx.plugin(tool, { provider: 'mock' }) + expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(true) + await mounted.dispose() + expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(false) + expect(ctx.subagents.getProvider('mock')).toBeDefined() + + // Arm 2: a fiber disposed while WAITING must not react to the provider + // arriving later — a surviving listener would re-register a tool that no + // live plugin owns (the zombie mount). + const waiting = await ctx.plugin(tool, { provider: 'later', toolName: 'subagent_later' }) + await waiting.dispose() + await ctx.plugin(mock, { name: 'later' }) + expect(ctx.tools.schemas().some(s => s.name === 'subagent_later')).toBe(false) + }) + it('ignores lifecycle events for OTHER providers', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt)