refactor(subagent): keep names in a durable alias map
This commit is contained in:
1 parent
769022766f
commit
f84c7371ca
237 files changed
+1895
-2320
No files matched your search
+1
-1
@@ -22,7 +22,7 @@
|
||||
{"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"list-once","name":"list_agents","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"lead [running] — root agent\nlead/legacy:{{sessionId}} [inactive] parent=lead depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [running] — lead: root agent\n{{sessionId}} [inactive] parent={{sessionId}} depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"step/start","data":{"turn":2,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The catalogued subagent is ready. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
|
||||
+3
-3
@@ -2,13 +2,13 @@
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour parent agent id is \"{{sessionId}}\". Use send_message({ name: \"{{sessionId}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your parent agent id is \"{{sessionId}}\". Use send_message({ name: \"{{sessionId}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":17,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"agent-1"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":18,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":19,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":18,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"lead/agent-1"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":19,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}\nname: lead/agent-1"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":21,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":22,"time":0,"data":{"turn":1,"step":2}}}
|
||||
|
||||
@@ -72,7 +72,7 @@ function researcher(messages) {
|
||||
if (!names.includes('list_agents')) return toolChunks([{ name: 'list_agents', args: {} }])
|
||||
const sibling = messages.filter(message => message.role === 'tool')
|
||||
.flatMap(message => message.content.filter(block => block.type === 'text').flatMap(block => block.text.split('\n')))
|
||||
.find(line => line.endsWith('— Own deterministic implementation.'))?.split(' ')[0]
|
||||
.find(line => line.endsWith('Own deterministic implementation.'))?.split(' ')[0]
|
||||
if (sibling === undefined) throw new Error('list_agents did not expose the implementation sibling')
|
||||
if (!names.includes('team_task_create')) {
|
||||
return toolChunks([{ name: 'team_task_create', args: {
|
||||
@@ -179,7 +179,7 @@ function lead(messages) {
|
||||
}
|
||||
if (last.includes('list_agents')) {
|
||||
const inactive = result.split('\n')
|
||||
.filter(row => row.includes('— Own deterministic ') && row.includes('[inactive]')).length
|
||||
.filter(row => row.includes('Own deterministic ') && row.includes('[inactive]')).length
|
||||
if (inactive >= 2) return textChunks('TEAM_WORKFLOW_OK: both teammates and dependent tasks completed.')
|
||||
return toolChunks([{ name: hasNewCoordinationInput(messages) ? 'list_agents' : 'wait_agent',
|
||||
args: hasNewCoordinationInput(messages) ? {} : { timeout_ms: 10000 } }])
|
||||
@@ -202,8 +202,8 @@ class TeamFixtureAdapter extends LlmAdapter {
|
||||
}
|
||||
let chunks
|
||||
if (workflowChild) chunks = textChunks('Fresh workflow child complete.')
|
||||
else if (task?.includes('RESEARCHER_MARK:')) chunks = researcher(options.messages)
|
||||
else if (task?.includes('IMPLEMENTER_MARK:')) chunks = implementer(options.messages)
|
||||
else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages)
|
||||
else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages)
|
||||
else if (role?.includes('You are the Team Lead.')) chunks = lead(options.messages)
|
||||
else throw new Error('Team runtime context is missing a recognized identity')
|
||||
for (const chunk of chunks) {
|
||||
|
||||
@@ -1179,8 +1179,8 @@ describe('headless stream-json snapshots', () => {
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'user')
|
||||
if (initial === undefined) throw new Error('Teammate Session has no initial task')
|
||||
const content = (initial.data as JsonObject).content as JsonObject[]
|
||||
// Named delegations include the task, return guidance, and identity.
|
||||
expect(content).toHaveLength(3)
|
||||
// The task block is followed by the standard return guidance naming the Lead.
|
||||
expect(content).toHaveLength(2)
|
||||
const context = parseJsonl(log.content).find(row => row.type === 'user/message'
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'runtime-context')
|
||||
const source = (context?.data as JsonObject)?.source as JsonObject
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('parent-owned cold child catalog snapshot', () => {
|
||||
if (parent === undefined) throw new Error('missing persisted parent log')
|
||||
|
||||
// Discovery reads child catalogs without requiring activation descriptors.
|
||||
expect(parent).toContain(`lead/legacy:${childId} [inactive] parent=lead depth=1 — descriptorless child`)
|
||||
expect(parent).toContain(`${childId} [inactive] parent=${parentId} depth=1 — descriptorless child`)
|
||||
expect(parent).not.toContain('[diagnostic: corrupt]')
|
||||
|
||||
const context: NormalizeContext = { sessionIds: [parentId, childId], cwd }
|
||||
|
||||
@@ -156,7 +156,9 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl
|
||||
if (notice?.data.source.kind !== 'subagent-settled') throw new Error('Native completion was not delivered to the parent')
|
||||
expect(notice.data.source.senderSessionId).toBe(catalog[0]?.childId)
|
||||
expect(notice.data.content).toContainEqual({ type: 'text', text: RESPONSE })
|
||||
expect(dispatches[0]?.content).toEqual([{ type: 'text', text: 'started subagent lead/native-worker' }])
|
||||
expect(dispatches[0]?.content).toEqual([{
|
||||
type: 'text', text: `started subagent ${notice.data.source.senderSessionId}\nname: lead/native-worker`,
|
||||
}])
|
||||
expect(events.filter(event => event.type === 'turn/end')).toHaveLength(2)
|
||||
expect(server.requests).toHaveLength(1)
|
||||
expect(server.requests[0]?.body).toMatchObject({ model: 'fixture-model' })
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 5ef3da69b4de1d8a
|
||||
/capability-seams-and-core-services:
|
||||
en: 4e6039d7f4fd20ed
|
||||
zh: 69dac723f536eebe
|
||||
en: eb93c7501376985c
|
||||
zh: 178bfc8e12149b47
|
||||
@@ -727,7 +727,7 @@ flowchart LR
|
||||
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
|
||||
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`subagent-names`](../packages/subagent/subagent-names), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | Providers prepare local children or execute external tasks; one activation manager owns both lifecycles. The delegation tool selects a backend, control tools deliver local follow-ups, and workflows await activation results. |
|
||||
| `ctx.subagentNames` | `core` | [`subagent-names`](../packages/subagent/subagent-names) | - | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | - | Owns sibling name reservations, durable bindings, and catalog-backed path lookup. Execution and authorization remain with the subagent service. |
|
||||
| `ctx.subagentNames` | `core` | [`subagent-names`](../packages/subagent/subagent-names) | - | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | - | Owns name reservations and durable name-to-id bindings in the root Session. Execution and authorization remain with the subagent service. |
|
||||
| `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | Routes explicit recognizers; the browser uses the authenticated Remote and keeps transcripts in the draft until submission. |
|
||||
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | Owns the shared task DAG and wait signals for a subagent tree; tool-agent-team contributes task controls. |
|
||||
| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | Loads mods that defineMod plugins add, raises their hook chains from harness extension points, and draws the band above the prompt over its Remote. |
|
||||
|
||||
@@ -729,7 +729,7 @@ flowchart LR
|
||||
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
|
||||
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`subagent-names`](../packages/subagent/subagent-names), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | 提供方准备本地子级或执行外部任务;一个 activation 管理器持有两类生命周期。委派工具选择后端,控制工具投递本地后续消息,工作流等待 activation 结果。 |
|
||||
| `ctx.subagentNames` | `core` | [`subagent-names`](../packages/subagent/subagent-names) | - | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | - | 持有同级名字预留、持久绑定和基于 catalog 的路径查询。执行与授权仍由 subagent 服务负责。 |
|
||||
| `ctx.subagentNames` | `core` | [`subagent-names`](../packages/subagent/subagent-names) | - | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | - | 持有名字预留及根 Session 中持久的名字到 ID 绑定。执行与授权仍由 subagent 服务负责。 |
|
||||
| `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | 路由显式选择的识别器;浏览器使用带认证的 Remote,并在提交前将转写保留在草稿中。 |
|
||||
| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 负责子代理树共享的任务 DAG 和等待信号;tool-agent-team 提供任务控制工具。 |
|
||||
| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | 加载 defineMod 插件添加的模组,从 harness 扩展点触发其钩子链,并通过其 Remote 绘制提示框上方的横幅。 |
|
||||
|
||||
@@ -1327,7 +1327,7 @@ export type Config = Record<never, never>
|
||||
## `@deepseek-ai/dsh-experimental-tool-subagent-oar`
|
||||
|
||||
- `inject`: `tools` · `subagents` · `subagentNames`
|
||||
- `source`: [`packages/experimental/tool-subagent-oar/src/index.ts:13`](../packages/experimental/tool-subagent-oar/src/index.ts)
|
||||
- `source`: [`packages/experimental/tool-subagent-oar/src/index.ts:14`](../packages/experimental/tool-subagent-oar/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned runtime choices exposed by the external delegation tool. */
|
||||
@@ -2105,7 +2105,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
|
||||
|
||||
- `inject`: `llm`
|
||||
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
|
||||
- `source`: [`packages/test-support/llm-replay/src/index.ts:1144`](../packages/test-support/llm-replay/src/index.ts)
|
||||
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
|
||||
@@ -4223,7 +4223,7 @@ export interface PresetSelectionConfig {
|
||||
## `@deepseek-ai/dsh-tool-subagent-control`
|
||||
|
||||
- `inject`: `tools` · `subagents` · `agents` · `systemPrompt`
|
||||
- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:29`](../packages/subagent/tool-subagent-control/src/index.ts)
|
||||
- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Optional subagent control tools. */
|
||||
@@ -4803,7 +4803,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-settings` | `configEditor` · `profileContext` | [`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/experimental/skill-badge/src/index.ts`](../packages/experimental/skill-badge/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-storage` | — | [`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subagent-names` | `agents` · `subagents` · `sessionProjections` | [`packages/subagent/subagent-names/src/index.ts`](../packages/subagent/subagent-names/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subagent-names` | `subagents` · `sessionProjections` | [`packages/subagent/subagent-names/src/index.ts`](../packages/subagent/subagent-names/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
|
||||
|
||||
@@ -1329,7 +1329,7 @@ export type Config = Record<never, never>
|
||||
## `@deepseek-ai/dsh-experimental-tool-subagent-oar`
|
||||
|
||||
- `inject`: `tools` · `subagents` · `subagentNames`
|
||||
- `source`: [`packages/experimental/tool-subagent-oar/src/index.ts:13`](../packages/experimental/tool-subagent-oar/src/index.ts)
|
||||
- `source`: [`packages/experimental/tool-subagent-oar/src/index.ts:14`](../packages/experimental/tool-subagent-oar/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned runtime choices exposed by the external delegation tool. */
|
||||
@@ -2107,7 +2107,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
|
||||
|
||||
- `inject`: `llm`
|
||||
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
|
||||
- `source`: [`packages/test-support/llm-replay/src/index.ts:1144`](../packages/test-support/llm-replay/src/index.ts)
|
||||
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
|
||||
@@ -4225,7 +4225,7 @@ export interface PresetSelectionConfig {
|
||||
## `@deepseek-ai/dsh-tool-subagent-control`
|
||||
|
||||
- `inject`: `tools` · `subagents` · `agents` · `systemPrompt`
|
||||
- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:29`](../packages/subagent/tool-subagent-control/src/index.ts)
|
||||
- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Optional subagent control tools. */
|
||||
@@ -4805,7 +4805,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-settings` | `configEditor` · `profileContext` | [`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/experimental/skill-badge/src/index.ts`](../packages/experimental/skill-badge/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-storage` | — | [`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subagent-names` | `agents` · `subagents` · `sessionProjections` | [`packages/subagent/subagent-names/src/index.ts`](../packages/subagent/subagent-names/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subagent-names` | `subagents` · `sessionProjections` | [`packages/subagent/subagent-names/src/index.ts`](../packages/subagent/subagent-names/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
|
||||
|
||||
@@ -74,7 +74,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
|
||||
@@ -984,8 +984,8 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../p
|
||||
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* A name candidate, usable only while the parent catalog contains its child.
|
||||
* @param data - versioned child identity and sibling name.
|
||||
* A root-owned alias for an accepted subagent identity.
|
||||
* @param data - versioned child identity and complete name.
|
||||
*/
|
||||
'subagent-name/bound': SubagentNameBinding & { readonly version: 1 }
|
||||
```
|
||||
|
||||
@@ -986,8 +986,8 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* A name candidate, usable only while the parent catalog contains its child.
|
||||
* @param data - versioned child identity and sibling name.
|
||||
* A root-owned alias for an accepted subagent identity.
|
||||
* @param data - versioned child identity and complete name.
|
||||
*/
|
||||
'subagent-name/bound': SubagentNameBinding & { readonly version: 1 }
|
||||
```
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 74dd331220011a1b
|
||||
zh: 008acdd3c41a03f4
|
||||
/2026-10-25-subagent-tool-names/summary:
|
||||
en: f6ea2d6660a3e32b
|
||||
zh: a2d6ad218f0dafb2
|
||||
en: bb91739791e437e2
|
||||
zh: a75903b7a3d1c437
|
||||
/2026-10-25-subagent-tool-names/table-of-contents:
|
||||
en: 6cf314a069ceb451
|
||||
zh: 9adff678e9ecab3b
|
||||
@@ -21,8 +21,8 @@
|
||||
en: e50cc33503dc233f
|
||||
zh: 3e08289200786f8b
|
||||
/2026-10-25-subagent-tool-names/verification:
|
||||
en: 926892abb1555020
|
||||
zh: f4adaada56e3232e
|
||||
en: 5e801f6794f1bdee
|
||||
zh: c3313150883bf75e
|
||||
/2026-10-25-subagent-tool-names/dev-note:
|
||||
en: b75c8458928b72b9
|
||||
zh: 003ba4e17b4b5269
|
||||
@@ -9,7 +9,7 @@ English | [中文](2026-10-25-subagent-tool-names.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Adds the parent-owned subagent-name/bound event for optional named subagent creation.
|
||||
Adds the root-Session-owned subagent-name/bound event for persistent name-to-id bindings.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -40,7 +40,7 @@ Existing V4 Sessions remain readable and unnamed catalog children retain their S
|
||||
<a id="verification"></a>
|
||||
## Verification
|
||||
|
||||
The subagent-names package tests cover candidate replay, inherited-fork exclusion, checkpoint recovery, duplicate binding rejection, cold parent lookup, and startup failure. The focused storage-contract.spec.ts and V4 codec.spec.ts run passed 37 tests, including refusal of unknown required events before adoption.
|
||||
The subagent-names package tests cover binding replay, inherited-fork exclusion, checkpoint recovery, and conflicting binding rejection. The storage-contract.spec.ts and V4 codec.spec.ts tests cover refusal of unknown required events before adoption.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## Dev Note
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: persistence-change
|
||||
|
||||
## 概述
|
||||
|
||||
为可选的子智能体命名创建增加由父会话持有的 subagent-name/bound 事件。
|
||||
增加由根 Session 持有的 subagent-name/bound 事件,持久保存名字到 ID 的绑定。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -40,7 +40,7 @@ changes:
|
||||
<a id="verification"></a>
|
||||
## 验证
|
||||
|
||||
subagent-names 包测试覆盖候选记录回放、排除 fork 继承事件、checkpoint 恢复、拒绝重复绑定、冷父节点查询和启动失败。针对 storage-contract.spec.ts 与 V4 codec.spec.ts 的测试共 37 项通过,其中包含在接管前拒绝未知必读事件。
|
||||
subagent-names 包测试覆盖绑定回放、排除 fork 继承事件、checkpoint 恢复及拒绝冲突绑定。storage-contract.spec.ts 与 V4 codec.spec.ts 测试覆盖在接管前拒绝未知必读事件。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## 开发备注
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: e02e1703b183594c
|
||||
zh: 130d475a4304f453
|
||||
/subagents/optional-tool-names:
|
||||
en: 78f0d036ab8b1d4a
|
||||
zh: 1974a1451c350de3
|
||||
en: 44bac31957293e47
|
||||
zh: 1e6c9c38e07bd61a
|
||||
/subagents/results-and-backend-handles:
|
||||
en: 69ea5bed7d9fb64a
|
||||
zh: b187a822b813e80a
|
||||
|
||||
+23
-26
@@ -227,7 +227,7 @@ The parent-owned `subagentCatalog` projection lists all local children and paren
|
||||
|
||||
## Optional tool names
|
||||
|
||||
The [naming plugin](../../packages/subagent/subagent-names/README.md) owns name allocation and path lookup for model-facing tools. It combines its own parent-owned bindings with the existing catalog; execution, authorization, provider requests and runtime notifications continue to use Session ids. Direct service callers do not acquire names automatically.
|
||||
The [naming plugin](../../packages/subagent/subagent-names/README.md) owns name allocation and path lookup for model-facing tools. It stores name-to-id bindings in the root Session; execution, authorization, provider requests and runtime notifications continue to use Session ids. Direct service callers do not acquire names automatically.
|
||||
|
||||
## Results and backend handles
|
||||
|
||||
@@ -413,44 +413,41 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../.
|
||||
|
||||
### `ctx.subagentNames` — `SubagentNames`
|
||||
|
||||
Catalog-backed names that leave subagent execution and authorization with their existing owner.
|
||||
Root-Session aliases shared by named creation, discovery, and control tools.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Start a named child, reserving its sibling name until startup settles.
|
||||
* Parent delivery retains catalog membership. Local names precede catalog publication;
|
||||
* external names follow accepted startup.
|
||||
* @param spec - ordinary activation options and an optional lower-case sibling name.
|
||||
* @returns the existing activation receipt plus its full name.
|
||||
* @throws SubagentNameError for invalid or duplicate names. An external binding failure
|
||||
* includes childId and awaits disposal without undoing completed work.
|
||||
* Reserve a complete name without starting an agent or writing a Session event.
|
||||
* @param parent - exact live caller whose name prefixes the child name.
|
||||
* @param requested - optional sibling segment; omission selects the first free agent-N.
|
||||
* @returns a reservation the caller must dispose after startup and binding.
|
||||
*/
|
||||
async startActivation(spec: NamedSubagentActivationSpec): Promise<SubagentActivation & { readonly name: string }>
|
||||
reserve(parent: Agent, requested?: string): SubagentNameReservation
|
||||
|
||||
/**
|
||||
* Read the full name of an exact live Agent without restoring its ancestors.
|
||||
* @param agent - live root or managed child whose name is requested.
|
||||
* @param signal - cancellation during parent catalog reads.
|
||||
* @returns lead, named path segments, or legacy id segments for unnamed children.
|
||||
* Record an accepted child's full name in the caller's root Session.
|
||||
* Binding failure does not cancel or dispose the child; callers report its id.
|
||||
* @param parent - exact live caller whose root owns the alias.
|
||||
* @param childId - identity returned by the subagent service.
|
||||
* @param name - complete name returned by reserve.
|
||||
*/
|
||||
async nameOf(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
bind(parent: Agent, childId: SessionId, name: string): void
|
||||
|
||||
/**
|
||||
* Translate a canonical path or a tree member's exact id without granting authority.
|
||||
* @param caller - exact live Agent selecting the subagent tree.
|
||||
* @param name - lead, a lead-prefixed path, or an existing member's exact Session id.
|
||||
* @param signal - cancellation during catalog reads.
|
||||
* @returns the identity for the subagent operation to authorize.
|
||||
* Read a bound name from the live root's projection without loading ancestors.
|
||||
* @param caller - exact live caller selecting the tree.
|
||||
* @param targetId - requested identity; omission selects the caller.
|
||||
* @returns lead for the root, a bound full name, or the unchanged id.
|
||||
*/
|
||||
async resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>
|
||||
nameOf(caller: Agent, targetId: SessionId = caller.id): string
|
||||
|
||||
/**
|
||||
* Attach relative names to the existing catalog traversal and its branch diagnostics.
|
||||
* @param rootId - Session whose catalog starts discovery.
|
||||
* @param signal - cancellation while reading descendant and name projections.
|
||||
* @returns existing descendant rows with paths relative to rootId.
|
||||
* Resolve an alias or pass through an id for the subagent service to authorize.
|
||||
* @param caller - exact live caller selecting the root mapping.
|
||||
* @param name - lead, a bound full name, or a raw Session id.
|
||||
* @returns the target identity; an unknown lead/... name throws.
|
||||
*/
|
||||
async listDescendants(rootId: SessionId, signal?: AbortSignal): Promise<(SubagentDescendantListEntry & { readonly path: string })[]>
|
||||
resolveName(caller: Agent, name: string): SessionId
|
||||
```
|
||||
|
||||
Types: [Agent](core.md) · [SessionId](core.md)
|
||||
|
||||
@@ -227,7 +227,7 @@ type SubagentInterruptAuthority =
|
||||
|
||||
## 可选工具名字
|
||||
|
||||
[命名插件](../../packages/subagent/subagent-names/README.zh.md)负责模型工具的名字分配和路径查询。它将自己持有的父级绑定与现有 catalog 结合;执行、授权、provider 请求和运行时通知继续使用 Session id。直接调用服务不会自动分配名字。
|
||||
[命名插件](../../packages/subagent/subagent-names/README.zh.md)负责模型工具的名字分配和路径查询。它在根 Session 中保存名字到 ID 的绑定;执行、授权、provider 请求和运行时通知继续使用 Session id。直接调用服务不会自动分配名字。
|
||||
|
||||
## 结果与后端句柄
|
||||
|
||||
@@ -415,44 +415,41 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../.
|
||||
|
||||
### `ctx.subagentNames` — `SubagentNames`
|
||||
|
||||
Catalog-backed names that leave subagent execution and authorization with their existing owner.
|
||||
Root-Session aliases shared by named creation, discovery, and control tools.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Start a named child, reserving its sibling name until startup settles.
|
||||
* Parent delivery retains catalog membership. Local names precede catalog publication;
|
||||
* external names follow accepted startup.
|
||||
* @param spec - ordinary activation options and an optional lower-case sibling name.
|
||||
* @returns the existing activation receipt plus its full name.
|
||||
* @throws SubagentNameError for invalid or duplicate names. An external binding failure
|
||||
* includes childId and awaits disposal without undoing completed work.
|
||||
* Reserve a complete name without starting an agent or writing a Session event.
|
||||
* @param parent - exact live caller whose name prefixes the child name.
|
||||
* @param requested - optional sibling segment; omission selects the first free agent-N.
|
||||
* @returns a reservation the caller must dispose after startup and binding.
|
||||
*/
|
||||
async startActivation(spec: NamedSubagentActivationSpec): Promise<SubagentActivation & { readonly name: string }>
|
||||
reserve(parent: Agent, requested?: string): SubagentNameReservation
|
||||
|
||||
/**
|
||||
* Read the full name of an exact live Agent without restoring its ancestors.
|
||||
* @param agent - live root or managed child whose name is requested.
|
||||
* @param signal - cancellation during parent catalog reads.
|
||||
* @returns lead, named path segments, or legacy id segments for unnamed children.
|
||||
* Record an accepted child's full name in the caller's root Session.
|
||||
* Binding failure does not cancel or dispose the child; callers report its id.
|
||||
* @param parent - exact live caller whose root owns the alias.
|
||||
* @param childId - identity returned by the subagent service.
|
||||
* @param name - complete name returned by reserve.
|
||||
*/
|
||||
async nameOf(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
bind(parent: Agent, childId: SessionId, name: string): void
|
||||
|
||||
/**
|
||||
* Translate a canonical path or a tree member's exact id without granting authority.
|
||||
* @param caller - exact live Agent selecting the subagent tree.
|
||||
* @param name - lead, a lead-prefixed path, or an existing member's exact Session id.
|
||||
* @param signal - cancellation during catalog reads.
|
||||
* @returns the identity for the subagent operation to authorize.
|
||||
* Read a bound name from the live root's projection without loading ancestors.
|
||||
* @param caller - exact live caller selecting the tree.
|
||||
* @param targetId - requested identity; omission selects the caller.
|
||||
* @returns lead for the root, a bound full name, or the unchanged id.
|
||||
*/
|
||||
async resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>
|
||||
nameOf(caller: Agent, targetId: SessionId = caller.id): string
|
||||
|
||||
/**
|
||||
* Attach relative names to the existing catalog traversal and its branch diagnostics.
|
||||
* @param rootId - Session whose catalog starts discovery.
|
||||
* @param signal - cancellation while reading descendant and name projections.
|
||||
* @returns existing descendant rows with paths relative to rootId.
|
||||
* Resolve an alias or pass through an id for the subagent service to authorize.
|
||||
* @param caller - exact live caller selecting the root mapping.
|
||||
* @param name - lead, a bound full name, or a raw Session id.
|
||||
* @returns the target identity; an unknown lead/... name throws.
|
||||
*/
|
||||
async listDescendants(rootId: SessionId, signal?: AbortSignal): Promise<(SubagentDescendantListEntry & { readonly path: string })[]>
|
||||
resolveName(caller: Agent, name: string): SessionId
|
||||
```
|
||||
|
||||
Types: [Agent](core.zh.md) · [SessionId](core.zh.md)
|
||||
|
||||
+10
-10
@@ -9,8 +9,8 @@
|
||||
en: 6cc2cd9ced70bb09
|
||||
zh: 1c0e6cb4ba2ed637
|
||||
/tool-schema-catalog/tool-package-map:
|
||||
en: 032f2dde75d30202
|
||||
zh: 2ea1f54a96e15461
|
||||
en: 22a4fdb27f864fd3
|
||||
zh: ab6d409bf9191495
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
|
||||
en: e0143916bb634762
|
||||
zh: e0143916bb634762
|
||||
@@ -237,8 +237,8 @@
|
||||
en: 8daafae6faf7898e
|
||||
zh: 8daafae6faf7898e
|
||||
/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-subagent-oar/subagent-oar:
|
||||
en: a29a86479752a3ef
|
||||
zh: 8582b31fc5e99c17
|
||||
en: c39b672aa8258fba
|
||||
zh: 111aa3637f661b2f
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent:
|
||||
en: 841abe0dd5913ea7
|
||||
zh: 841abe0dd5913ea7
|
||||
@@ -246,8 +246,8 @@
|
||||
en: 207228068bc61885
|
||||
zh: 36e9344ee1af8869
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent:
|
||||
en: 510d7dd3afaa98b9
|
||||
zh: 01e3650be0e1acd9
|
||||
en: d331570a9e0219ac
|
||||
zh: 1f39bfe30de418f0
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control:
|
||||
en: ab82e130c3413f04
|
||||
zh: ab82e130c3413f04
|
||||
@@ -255,14 +255,14 @@
|
||||
en: a622b15cc2598d51
|
||||
zh: 48e1b05956d63e26
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents:
|
||||
en: 27354bec67e9f3c1
|
||||
zh: 282ef6ae40681e2b
|
||||
en: 7e5f44383f14d021
|
||||
zh: 8037d7b62ddb33b2
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message:
|
||||
en: aaccc4f194398018
|
||||
zh: d918c1346a87a4b1
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/wait-agent:
|
||||
en: d150b517e88f87f0
|
||||
zh: ba83919e0e13543a
|
||||
en: 560bfc25f9a73740
|
||||
zh: 404626c11e5a936b
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-jobs:
|
||||
en: 9be673b1604d0114
|
||||
zh: 9be673b1604d0114
|
||||
|
||||
@@ -40,8 +40,8 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`, `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `ctx.agents`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. |
|
||||
| `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents`, `ctx.subagentNames` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | The subagent_oar tool selects among configured aliases for external providers. This catalog shows Codex and Claude Code aliases; each deployment chooses its providers. Native model and permission settings belong to the selected provider configuration. |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.sessionProjections`, `optional ctx.subagentNames for names instead of Session ids`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path promptly. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically. |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message`, `wait_agent` | `ctx.tools`, `ctx.subagents`, `ctx.agents`, `ctx.systemPrompt`, `optional ctx.subagentNames for names instead of Session ids` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery returns paths. Without that plugin, controls retain `agent_id` and discovery returns Session ids. |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.sessionProjections`, `optional ctx.subagentNames for names alongside Session ids`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path alongside the child Session id. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically. |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message`, `wait_agent` | `ctx.tools`, `ctx.subagents`, `ctx.agents`, `ctx.systemPrompt`, `optional ctx.subagentNames for names alongside Session ids` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery adds names while retaining Session ids. Without that plugin, controls retain `agent_id` and discovery returns Session ids. |
|
||||
| `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. |
|
||||
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `ctx.subagentNames`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Four task-board tools share the subagent tree’s root Session. The optional Agent Teams profile adds task-board guidance and required Team identity derived from the subagent root, alongside standard subagent delegation and control tools. |
|
||||
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
|
||||
@@ -2113,7 +2113,7 @@ The five read-only tools hide provider cursors and authorize every result from t
|
||||
|
||||
### `subagent_oar`
|
||||
|
||||
Delegate a self-contained task to a configured external agent. Choose its runtime for each call. The child works in its own context and does not see this conversation. This tool returns a full agent path; a later completion notice includes its final answer. The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. When only waiting for their results remains, end your turn instead of polling.
|
||||
Delegate a self-contained task to a configured external agent. Choose its runtime for each call. The child works in its own context and does not see this conversation. This tool returns the child id and full agent path; a later completion notice includes its final answer. The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. When only waiting for their results remains, end your turn instead of polling.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2184,7 +2184,7 @@ Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subag
|
||||
|
||||
### `subagent`
|
||||
|
||||
Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.
|
||||
Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2216,7 +2216,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
|
||||
|
||||
Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path promptly. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically.
|
||||
The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path alongside the child Session id. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically.
|
||||
|
||||
<a id="deepseek-aidsh-tool-subagent-control"></a>
|
||||
|
||||
@@ -2245,7 +2245,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
|
||||
|
||||
### `list_agents`
|
||||
|
||||
List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.
|
||||
List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2300,7 +2300,7 @@ Wait for input at your next step, including agent messages, completion notificat
|
||||
|
||||
Source: [`packages/subagent/tool-subagent-control/src/wait-agent.ts`](../packages/subagent/tool-subagent-control/src/wait-agent.ts)
|
||||
|
||||
The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery returns paths. Without that plugin, controls retain `agent_id` and discovery returns Session ids.
|
||||
The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery adds names while retaining Session ids. Without that plugin, controls retain `agent_id` and discovery returns Session ids.
|
||||
|
||||
<a id="deepseek-aidsh-tool-jobs"></a>
|
||||
|
||||
@@ -2533,7 +2533,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign."
|
||||
"description": "Agent name or Session id of the root or any continuable descendant for root-only reassign; omit to unassign."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
|
||||
| `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents`, `ctx.subagentNames` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | subagent_oar 工具在为外部提供方配置的别名之间选择。本目录展示 Codex 和 Claude Code 别名;每个部署自行选择提供方。原生模型和权限设置由所选提供方的配置决定。 |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于名称替代 Session id 的可选 ctx.subagentNames`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。本目录加载可选的命名插件:委派接受同级名称并立即返回完整路径。没有该插件时,不接受名称,返回 Session id。本地子代理接受后续消息,外部后端自动汇报单次执行结果。 |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message`、`wait_agent` | `ctx.tools`、`ctx.subagents`、`ctx.agents`、`ctx.systemPrompt`、`用于名称替代 Session id 的可选 ctx.subagentNames` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。本目录加载可选命名功能:控制工具通过 `name` 接受名称路径或 Session id,发现结果返回路径。没有该插件时,控制工具保留 `agent_id`,发现结果返回 Session id。 |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于附加名称的可选 ctx.subagentNames`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。本目录加载可选的命名插件:委派接受同级名称,并在子级 Session id 之外返回完整路径。没有该插件时,不接受名称,返回 Session id。本地子代理接受后续消息,外部后端自动汇报单次执行结果。 |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message`、`wait_agent` | `ctx.tools`、`ctx.subagents`、`ctx.agents`、`ctx.systemPrompt`、`用于附加名称的可选 ctx.subagentNames` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。本目录加载可选命名功能:控制工具通过 `name` 接受名称路径或 Session id,发现结果保留 Session id,并附加名称。没有该插件时,控制工具保留 `agent_id`,发现结果返回 Session id。 |
|
||||
| `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 |
|
||||
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `ctx.subagentNames`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 四个任务板工具共享 subagent 树的根 Session。可选 Agent Teams profile 在标准子代理委派和控制工具之外增加任务板指导,以及从 subagent 根派生的必需 Team 身份。 |
|
||||
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
|
||||
@@ -2119,7 +2119,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
|
||||
|
||||
### `subagent_oar`
|
||||
|
||||
将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回完整 Agent 路径,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。
|
||||
将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回子级 id 和完整 Agent 路径,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2190,7 +2190,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录
|
||||
|
||||
### `subagent`
|
||||
|
||||
将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其名称。如果它结束时你仍在线,运行时会自动递送其完成通知。通知包含最终回答,除非子任务在结束前刚给你发过消息。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。
|
||||
将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。如果它结束时你仍在线,运行时会自动递送其完成通知。通知包含最终回答,除非子任务在结束前刚给你发过消息。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2222,7 +2222,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录
|
||||
|
||||
来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。本目录加载可选的命名插件:委派接受同级名称并立即返回完整路径。没有该插件时,不接受名称,返回 Session id。本地子代理接受后续消息,外部后端自动汇报单次执行结果。
|
||||
加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。本目录加载可选的命名插件:委派接受同级名称,并在子级 Session id 之外返回完整路径。没有该插件时,不接受名称,返回 Session id。本地子代理接受后续消息,外部后端自动汇报单次执行结果。
|
||||
|
||||
<a id="deepseek-aidsh-tool-subagent-control"></a>
|
||||
|
||||
@@ -2251,7 +2251,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录
|
||||
|
||||
### `list_agents`
|
||||
|
||||
列出整棵 subagent 树:根代理和所有可达的可继续后代,包括调用方自身。行包含完整 Agent 路径、描述和状态;后代行包含父级路径和相对根的深度。可以向树中列为 running 或 inactive 的任意其他代理发送消息。running 表示正在工作;inactive 表示当前没有工作。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。
|
||||
列出整棵 subagent 树:根代理和所有可达的可继续后代,包括调用方自身。行包含 id、描述和状态;后代行包含父级 id 和相对根的深度。行也会在 Session id 之外提供可用名称。可以向树中列为 running 或 inactive 的任意其他代理发送消息。running 表示正在工作;inactive 表示当前没有工作。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -2306,7 +2306,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录
|
||||
|
||||
来源:[`packages/subagent/tool-subagent-control/src/wait-agent.ts`](../packages/subagent/tool-subagent-control/src/wait-agent.ts)
|
||||
|
||||
共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。本目录加载可选命名功能:控制工具通过 `name` 接受名称路径或 Session id,发现结果返回路径。没有该插件时,控制工具保留 `agent_id`,发现结果返回 Session id。
|
||||
共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。本目录加载可选命名功能:控制工具通过 `name` 接受名称路径或 Session id,发现结果保留 Session id,并附加名称。没有该插件时,控制工具保留 `agent_id`,发现结果返回 Session id。
|
||||
|
||||
<a id="deepseek-aidsh-tool-jobs"></a>
|
||||
|
||||
@@ -2539,7 +2539,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign."
|
||||
"description": "Agent name or Session id of the root or any continuable descendant for root-only reassign; omit to unassign."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 73ec1920e6ebd4bc
|
||||
zh: 1b7688c81f1dfc9a
|
||||
/subagent-tools-use-optional-names/change:
|
||||
en: acaedcb0dfb7db00
|
||||
zh: a105b56d9bac7929
|
||||
en: 7220e2906196bdf2
|
||||
zh: f7a1dc36354d0dd3
|
||||
/subagent-tools-use-optional-names/migration:
|
||||
en: 470fd95ac5394bd2
|
||||
zh: 12fce1ddb43e9af7
|
||||
en: 4505282768738c8e
|
||||
zh: 71b829888a5eacd2
|
||||
@@ -9,13 +9,13 @@ English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Base-backed profiles mount `@deepseek-ai/dsh-subagent-names`. With this plugin, `subagent` and `subagent_fork` accept an optional sibling `name` and return a full path such as `lead/reviewer`. Omission allocates the first unused `agent-N`. `send_message` and `interrupt_agent` use the `name` argument, which accepts a discovered path or an existing member's Session id. `list_agents` returns names and parent paths. Experimental Team tools expose `ownerName`; the OAR tool also returns a name.
|
||||
Base-backed profiles mount `@deepseek-ai/dsh-subagent-names`. With this plugin, `subagent` and `subagent_fork` accept an optional sibling `name` and return `subagentId` plus a full `name` such as `lead/reviewer`. Omission allocates the first unused `agent-N`. `send_message` and `interrupt_agent` use the `name` argument, which accepts a discovered path or an existing member's Session id. `list_agents` adds names and parent paths to its ID fields. Experimental Team tools expose `ownerName`; the OAR tool also returns a name.
|
||||
|
||||
The subagent service, provider requests, Session catalog and SDK handles retain their existing ID interfaces. Runtime messages and completion notices still identify agents by Session id. The naming plugin records its own binding events. Without it, the ordinary delegation and control tools retain their ID interfaces; experimental Team and OAR tools require it.
|
||||
The subagent service, provider requests, Session catalog and SDK handles retain their existing ID interfaces. Runtime messages and completion notices still identify agents by Session id. The naming plugin records name-to-id bindings in the root Session after startup succeeds. A child can begin its first step before its name is bound. Without it, the ordinary delegation and control tools retain their ID interfaces; experimental Team and OAR tools require it.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Update scripted model tool calls from `send_message({ agent_id: id, ... })` to `send_message({ name: id, ... })`, and from `interrupt_agent({ agent_id: id })` to `interrupt_agent({ name: id })`. Use the returned `name` for subsequent calls when available.
|
||||
2. Read `name` from named creation and discovery results, and `ownerName` from experimental Team task results. Keep existing host `startActivation`, `sendMessage`, `interrupt`, and `ownerId` calls unchanged.
|
||||
3. In custom compositions using named tools, mount `@deepseek-ai/dsh-subagent-names` alongside the subagent service and Session query/projection services. The shipped base bundle supplies this plugin.
|
||||
4. Create a child with `name: "reviewer"`, confirm the result is `lead/reviewer`, and confirm `list_agents` and a follow-up message resolve that path. Do not reuse a sibling name whose child remains in the parent catalog.
|
||||
2. Read the optional `name` alongside the existing ID from creation and discovery results, and `ownerName` from experimental Team task results. Keep existing host `startActivation`, `sendMessage`, `interrupt`, and `ownerId` calls unchanged.
|
||||
3. In custom compositions using named tools, mount `@deepseek-ai/dsh-subagent-names` alongside the subagent service and Session projection service. The shipped base bundle supplies this plugin.
|
||||
4. Create a child with `name: "reviewer"`, confirm the result is `lead/reviewer`, and confirm `list_agents` and a follow-up message resolve that path. Names remain reserved for the lifetime of the root Session.
|
||||
@@ -9,13 +9,13 @@ description: "基于 base 的配置通过模型工具中的名字寻址子代理
|
||||
|
||||
## 变更
|
||||
|
||||
基于 base 的配置挂载 `@deepseek-ai/dsh-subagent-names`。启用该插件后,`subagent` 和 `subagent_fork` 接受可选的同级 `name`,返回 `lead/reviewer` 这样的完整路径。省略时分配首个未使用的 `agent-N`。`send_message` 和 `interrupt_agent` 使用 `name` 参数,接受发现到的路径或现有成员的 Session id。`list_agents` 返回名字和父路径。实验性 Team 工具提供 `ownerName`;OAR 工具也返回名字。
|
||||
基于 base 的配置挂载 `@deepseek-ai/dsh-subagent-names`。启用该插件后,`subagent` 和 `subagent_fork` 接受可选的同级 `name`,返回 `subagentId`,以及 `lead/reviewer` 这样的完整 `name`。省略时分配首个未使用的 `agent-N`。`send_message` 和 `interrupt_agent` 使用 `name` 参数,接受发现到的路径或现有成员的 Session id。`list_agents` 在 ID 字段之外附加名字和父路径。实验性 Team 工具提供 `ownerName`;OAR 工具也返回名字。
|
||||
|
||||
subagent 服务、provider 请求、Session catalog 和 SDK 句柄保留现有 ID 接口。运行时消息和完成通知仍通过 Session id 标识代理。命名插件记录自己的绑定事件。未挂载该插件时,普通委派和控制工具保留 ID 接口;实验性 Team 和 OAR 工具需要该插件。
|
||||
subagent 服务、provider 请求、Session catalog 和 SDK 句柄保留现有 ID 接口。运行时消息和完成通知仍通过 Session id 标识代理。命名插件在启动成功后将名字到 ID 的绑定记录在根 Session 中。子代理可能在名字绑定前开始第一步。未挂载该插件时,普通委派和控制工具保留 ID 接口;实验性 Team 和 OAR 工具需要该插件。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 将脚本中的模型工具调用从 `send_message({ agent_id: id, ... })` 改为 `send_message({ name: id, ... })`,从 `interrupt_agent({ agent_id: id })` 改为 `interrupt_agent({ name: id })`。有返回的 `name` 时,后续调用使用该名字。
|
||||
2. 从已命名的创建和发现结果读取 `name`,从实验性 Team 任务结果读取 `ownerName`。现有 Host `startActivation`、`sendMessage`、`interrupt` 和 `ownerId` 调用保持不变。
|
||||
3. 在使用命名工具的自定义装配中,与 subagent 服务及 Session 查询、投影服务一起挂载 `@deepseek-ai/dsh-subagent-names`。随附的 base bundle 已提供该插件。
|
||||
4. 使用 `name: "reviewer"` 创建子代理,确认结果为 `lead/reviewer`,并确认 `list_agents` 和后续消息可以解析该路径。子代理仍在父 catalog 中时,不要复用其同级名字。
|
||||
2. 从创建和发现结果中读取原有 ID 及可选的 `name`,从实验性 Team 任务结果读取 `ownerName`。现有 Host `startActivation`、`sendMessage`、`interrupt` 和 `ownerId` 调用保持不变。
|
||||
3. 在使用命名工具的自定义装配中,与 subagent 服务及 Session 投影服务一起挂载 `@deepseek-ai/dsh-subagent-names`。随附的 base bundle 已提供该插件。
|
||||
4. 使用 `name: "reviewer"` 创建子代理,确认结果为 `lead/reviewer`,并确认 `list_agents` 和后续消息可以解析该路径。名字在根 Session 的整个生命周期内保留。
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 0f971f7ac7c09139
|
||||
zh: ceb866da681c709c
|
||||
/deepseek-ai-dsh-base/use-this-package/what-you-get:
|
||||
en: a232adcce61bc04f
|
||||
zh: 1950dfa23b6d4c7f
|
||||
en: 607df685b0817281
|
||||
zh: 779c7fcb1c1724ba
|
||||
/deepseek-ai-dsh-base/use-this-package/shell-tools-per-platform:
|
||||
en: 2dce12f82d6ee955
|
||||
zh: d4599eb899a3cd88
|
||||
|
||||
@@ -49,7 +49,7 @@ Run `dsh --profile my-profile "your task"` and you get a working agent with mode
|
||||
|
||||
Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval and can reach loopback and private-network hosts. Feedback stays in the Session log. [OTel session upload](../../session/session-telemetry-otel/README.md) defaults to `FEEDBACK_ONLY` for all users, including `deepseek-official`: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context. Later records wait for the next explicit feedback; sending an authorized batch needs no further interaction or model call. `DISABLED` prevents OTel capture. The default-on [DeepSeek session-log contributor](../../session/session-log-deepseek/README.md) remains a separate request path.
|
||||
|
||||
Both `subagent` and `subagent_fork` create managed children and return a full agent path immediately. The bundle mounts [Subagent names](../../subagent/subagent-names/README.md) for optional child names and name-based control tools. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities.
|
||||
Both `subagent` and `subagent_fork` create managed children and return a child id with its full agent path immediately. The bundle mounts [Subagent names](../../subagent/subagent-names/README.md) for optional child names and name-based control tools. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities.
|
||||
|
||||
The built-in `working_directory` tool changes the directory for subsequent operations while existing processes and write permissions remain independent. Current directory information appears in user context. Default file editing uses `read`, `write`, and `edit`. The `str_replace_editor` tool remains available as an explicit opt-in. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch:
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-bundle"
|
||||
|
||||
开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(提供方与模型可配置,你还可以在设置中启用额外提供方)、完整工具集——文件编辑、shell 命令、web 搜索、HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,并且可以访问 loopback 与内网主机。反馈保存在会话日志中。[OTel 会话上传](../../session/session-telemetry-otel/README.zh.md)对所有用户默认使用 `FEEDBACK_ONLY`,包括 `deepseek-official`:新的文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范会话日志前缀,包含上下文。后续记录等待下一次显式反馈;发送已授权批次无需进一步交互或模型调用。`DISABLED` 阻止 OTel 捕获。默认开启的 [DeepSeek 会话日志贡献器](../../session/session-log-deepseek/README.zh.md)仍是独立的请求路径。
|
||||
|
||||
`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回完整的 Agent 路径。bundle 挂载 [Subagent names](../../subagent/subagent-names/README.zh.md),提供可选的子任务名称和按名称寻址的控制工具。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。
|
||||
`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回子任务 ID 及其完整的 Agent 路径。bundle 挂载 [Subagent names](../../subagent/subagent-names/README.zh.md),提供可选的子任务名称和按名称寻址的控制工具。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。
|
||||
|
||||
内置 `working_directory` 工具改变后续操作使用的目录,已有进程与写权限保持独立。当前目录信息出现在用户上下文中。默认文件编辑使用 `read`、`write` 和 `edit`。`str_replace_editor` 工具仍可显式启用。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目:
|
||||
|
||||
|
||||
@@ -21,26 +21,26 @@
|
||||
en: c27d5457e1183675
|
||||
zh: 3c36d513e6400446
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/smallest-working-example:
|
||||
en: 2d034608b7213085
|
||||
zh: 7b395feef826badb
|
||||
en: 73e093d405599d3f
|
||||
zh: c8be9cb03c2376e0
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-the-model-can-do:
|
||||
en: 66f5e2a8cbba3ef0
|
||||
zh: 6c4d7d5a25f2d8ca
|
||||
en: d8294e282ee26b0d
|
||||
zh: f6579be994043294
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-success-and-failure-look-like:
|
||||
en: fb745b864bb4a392
|
||||
zh: d3883a6f56bf4664
|
||||
en: f83d223649e1470d
|
||||
zh: 071db9c5986383ed
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation:
|
||||
en: 3952eb1d9de1f988
|
||||
zh: 789d51b88f41b710
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/design-philosophy:
|
||||
en: 04791b530f48a523
|
||||
zh: 4f36247ea732a364
|
||||
en: 236b044c249412f6
|
||||
zh: b4886ad8eff97b8b
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/source-map:
|
||||
en: 29ff96b23b25b13e
|
||||
zh: 7d2cdf9be86d1ed4
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/policy-and-tools:
|
||||
en: 68956deedf9885ea
|
||||
zh: 1d29d7dc5a6a6866
|
||||
en: 074f6c475a4ee627
|
||||
zh: 4804edb4cc77c355
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/scoped-registration-and-teardown:
|
||||
en: ef8a0940f658d225
|
||||
zh: c13048bcda07d006
|
||||
|
||||
@@ -43,7 +43,7 @@ The plugin takes no configuration. With the [Team domain](../agent-team/README.m
|
||||
|
||||
A row that sets `interrupt`, `freshProvider`, or `forkProvider` does not activate, and dsh names the key in its startup warning. The [upgrade guide](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) maps the removed tools and keys to their replacements, and the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-agent-team) records the empty configuration.
|
||||
|
||||
Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned full path addresses it.
|
||||
Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned name or Session id addresses it.
|
||||
|
||||
### What the model can do
|
||||
|
||||
@@ -53,11 +53,11 @@ The task tools complement the ordinary subagent controls:
|
||||
- **Wait for inbox input** — `wait_agent` belongs to `@deepseek-ai/dsh-tool-subagent-control` and remains available without this package.
|
||||
- **Create and reach teammates** — `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` come from the subagent tool packages and are unchanged by this package.
|
||||
|
||||
Reachable task owners use full agent paths in `owner` arguments and `ownerName` results. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks.
|
||||
Task owners use bound names or Session ids in `owner` arguments and `ownerName` results. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks.
|
||||
|
||||
### What success and failure look like
|
||||
|
||||
Task tools return compact JSON task views or pages. Stale revisions, unauthorized actions, and unready tasks return errors. Updates resolve input and result-owner names before mutation; failed or incomplete name lookup leaves the task unchanged, and a committed result needs no further name lookup. A stored owner absent from a completely read tree renders as `legacy:<id>`; unreadable branches fail the lookup instead of implying absence. This absent-owner marker cannot address a message or a reassignment target.
|
||||
Task tools return compact JSON task views or pages. Stale revisions, unauthorized actions, and unready tasks return errors. Updates resolve input names and capture result-owner names before mutation. An unbound owner displays its original Session id; display names require no Session query or child catalog traversal. Name resolution grants no authority: the Team service still checks every reassignment target.
|
||||
|
||||
-----
|
||||
|
||||
@@ -78,7 +78,7 @@ The adapter is built on four commitments:
|
||||
- **Declared results, compact JSON.** Every tool declares its complete result schema and renders that value as compact JSON, so the compiler checks `execute` against what the model is promised and no result spends tokens on indentation.
|
||||
- **The domain owns authority.** Tools delegate to `ctx.agentTeams`, which enforces Lead authority and revision checks; the adapter adds no weaker path.
|
||||
|
||||
The adapter uses `ctx.subagentNames` to resolve full paths while `ctx.agentTeams` retains Session ids for task ownership and authority. Task listing applies status, readiness, and unowned filters before resolving names. Without an exact owner-name filter, only the requested page needs name resolution; an exact owner-name filter resolves the remaining candidates before pagination.
|
||||
The adapter reads the root Session’s name-to-id mapping synchronously through `ctx.subagentNames`; `ctx.agentTeams` retains Session ids for task ownership and authority. Task listing applies status, readiness, and unowned filters before resolving names. Without an exact owner-name filter, only the requested page needs name resolution; an exact owner-name filter resolves the remaining candidates before pagination.
|
||||
|
||||
The [subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) owns the model-facing and scoping decisions.
|
||||
|
||||
@@ -90,7 +90,7 @@ The [subagent teammates Agent Note](../../../.agents/notes/implemented/architect
|
||||
|
||||
### Policy and tools
|
||||
|
||||
One `team:policy` section, required `team:identity` context, and four task schemas are registered on each Agent scope, including fork children. Tree authority comes from `ctx.subagents.rootOf(agent)`; `ctx.subagentNames` supplies the displayed full path. If the root is unavailable, the context reports that shared tasks, subagent discovery, and messaging are unavailable; it does not infer a parent from Session lineage. Communication and waiting belong to [subagent controls](../../subagent/tool-subagent-control/README.md); the subagent runtime delivers final results.
|
||||
One `team:policy` section, required `team:identity` context, and four task schemas are registered on each Agent scope, including fork children. Tree authority comes from `ctx.subagents.rootOf(agent)`; `ctx.subagentNames` supplies bound display names or original Session ids. If the root is unavailable, the context reports that shared tasks, subagent discovery, and messaging are unavailable; it does not infer a parent from Session lineage. Communication and waiting belong to [subagent controls](../../subagent/tool-subagent-control/README.md); the subagent runtime delivers final results.
|
||||
|
||||
### Scoped registration and teardown
|
||||
|
||||
@@ -126,7 +126,7 @@ Every Team member's system prompt carries one `team:policy` section; the Lead an
|
||||
##### Team policy
|
||||
|
||||
```markdown
|
||||
A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
|
||||
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ kind: "package-reference"
|
||||
|
||||
设置 `interrupt`、`freshProvider` 或 `forkProvider` 的配置行不会激活,dsh 会在启动警告中指出该键。[升级指南](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)把已移除的工具与键对应到替代方式,生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)记录空配置。
|
||||
|
||||
让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的完整路径用于寻址。
|
||||
让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的名称或 Session id 用于寻址。
|
||||
|
||||
### 模型能做什么
|
||||
|
||||
@@ -53,11 +53,11 @@ kind: "package-reference"
|
||||
- **等待收件箱输入** — `wait_agent` 属于 `@deepseek-ai/dsh-tool-subagent-control`,无需此包即可使用。
|
||||
- **创建和联系队友**——`subagent`、`subagent_fork`、`send_message`、`list_agents` 和 `interrupt_agent` 来自子代理工具包,本包不修改它们。
|
||||
|
||||
可达任务负责人在 `owner` 参数和 `ownerName` 结果中使用完整 agent 路径。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。
|
||||
任务负责人在 `owner` 参数和 `ownerName` 结果中使用已绑定名称或 Session id。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。
|
||||
|
||||
### 成功与失败的表现
|
||||
|
||||
任务工具返回紧凑的 JSON 任务视图或分页。过期修订、未授权操作和未就绪任务返回错误。更新在修改前解析输入与结果中的负责人名称;名称查询失败或不完整时任务保持不变,已提交结果无需再查询名称。完整读取树后仍不存在的已存储负责人显示为 `legacy:<id>`;不可读分支使查询失败,不表示负责人不存在。这个缺席负责人标记不能用于发送消息或指定重新分配的目标。
|
||||
任务工具返回紧凑的 JSON 任务视图或分页。过期修订、未授权操作和未就绪任务返回错误。更新在修改前解析输入名称并保存结果中的负责人名称。未绑定名称的负责人显示原始 Session id;显示名称无需查询 Session 或遍历子级目录。名称解析不授予权限:Team 服务仍检查每个重新分配的目标。
|
||||
|
||||
-----
|
||||
|
||||
@@ -78,7 +78,7 @@ kind: "package-reference"
|
||||
- **声明式结果,紧凑 JSON。** 每个工具都声明完整结果 schema,并把该值渲染为紧凑 JSON,因此编译器会对照向模型承诺的结果检查 `execute`,任何结果都不会在缩进上消耗 token。
|
||||
- **领域掌握裁决权。** 工具委托给 `ctx.agentTeams`,后者强制执行 Lead 权限与 revision 校验;适配器不添加更弱的路径。
|
||||
|
||||
适配器使用 `ctx.subagentNames` 解析完整路径,`ctx.agentTeams` 继续使用 Session id 处理任务归属和权限。任务列表先按状态、就绪情况和未分配条件筛选,再解析名称。没有精确负责人名称筛选时,只解析请求页中的名称;按精确负责人名称筛选时,在分页前解析剩余候选任务的负责人名称。
|
||||
适配器通过 `ctx.subagentNames` 同步读取根 Session 中的名称与 id 映射;`ctx.agentTeams` 继续使用 Session id 处理任务归属和权限。任务列表先按状态、就绪情况和未分配条件筛选,再解析名称。没有精确负责人名称筛选时,只解析请求页中的名称;按精确负责人名称筛选时,在分页前解析剩余候选任务的负责人名称。
|
||||
|
||||
[subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责模型侧与 scoping 决策。
|
||||
|
||||
@@ -90,7 +90,7 @@ kind: "package-reference"
|
||||
|
||||
### 策略与工具
|
||||
|
||||
每个 Agent 作用域均注册一个 `team:policy` 段落、必需的 `team:identity` 上下文和四个任务 schema,包括 fork 子级。树权限从 `ctx.subagents.rootOf(agent)` 派生;`ctx.subagentNames` 提供显示的完整路径。根不可用时,上下文说明共享任务、subagent 发现与通信不可用,不从 Session 创建来源推断父代理。通信和等待属于 [subagent 控制工具](../../subagent/tool-subagent-control/README.zh.md);subagent 运行时负责投递最终结果。
|
||||
每个 Agent 作用域均注册一个 `team:policy` 段落、必需的 `team:identity` 上下文和四个任务 schema,包括 fork 子级。树权限从 `ctx.subagents.rootOf(agent)` 派生;`ctx.subagentNames` 提供已绑定的显示名称或原始 Session id。根不可用时,上下文说明共享任务、subagent 发现与通信不可用,不从 Session 创建来源推断父代理。通信和等待属于 [subagent 控制工具](../../subagent/tool-subagent-control/README.zh.md);subagent 运行时负责投递最终结果。
|
||||
|
||||
### 按作用域注册与拆除
|
||||
|
||||
@@ -126,7 +126,7 @@ kind: "package-reference"
|
||||
##### Team 策略
|
||||
|
||||
```markdown
|
||||
A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
|
||||
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Config: z<Config> = z.object({})
|
||||
const REMOVED_CONFIG_KEYS = ['interrupt', 'freshProvider', 'forkProvider'] as const
|
||||
|
||||
/** Model-facing collaboration guidance shared by Lead and teammates. */
|
||||
const TEAM_POLICY = `A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
const TEAM_POLICY = `A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
|
||||
|
||||
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
|
||||
|
||||
@@ -77,34 +77,22 @@ function jsonOutput<const S extends ValueSchemaSpec>(schema: S): {
|
||||
|
||||
type NamedTaskView = Omit<TeamTaskView, 'ownerId'> & { readonly ownerName?: string }
|
||||
|
||||
/** Read immutable owner names before a mutation or for the requested result rows. */
|
||||
async function taskOwnerNames(
|
||||
ctx: Context, caller: Agent, owners: readonly (SessionId | undefined)[], signal: AbortSignal,
|
||||
): Promise<ReadonlyMap<SessionId, string>> {
|
||||
const root = ctx.subagents.rootOf(caller)
|
||||
const names = new Map<SessionId, string>([[root.id, 'lead']])
|
||||
const missing = new Set<SessionId>()
|
||||
/** Capture owner display names before a mutation or for the requested result rows. */
|
||||
function taskOwnerNames(
|
||||
ctx: Context, caller: Agent, owners: readonly (SessionId | undefined)[],
|
||||
): ReadonlyMap<SessionId, string> {
|
||||
const names = new Map<SessionId, string>()
|
||||
for (const ownerId of owners) {
|
||||
if (ownerId === undefined || names.has(ownerId)) continue
|
||||
const agent = ctx.agents.get(ownerId)
|
||||
if (agent === undefined) missing.add(ownerId)
|
||||
else names.set(ownerId, await ctx.subagentNames.nameOf(agent, signal))
|
||||
}
|
||||
if (missing.size > 0) {
|
||||
const descendants = await ctx.subagentNames.listDescendants(root.id, signal)
|
||||
for (const entry of descendants) names.set(entry.id, `lead/${entry.path}`)
|
||||
if ([...missing].some(id => !names.has(id)) && descendants.some(entry => entry.kind === 'diagnostic')) {
|
||||
throw new Error('Task owner names are unavailable because subagent discovery is incomplete; restore the unreadable branch and retry.')
|
||||
}
|
||||
if (ownerId !== undefined && !names.has(ownerId)) names.set(ownerId, ctx.subagentNames.nameOf(caller, ownerId))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/** Format a task without querying mutable services after its commit. */
|
||||
/** Format a task from names captured for each possible result owner before its commit. */
|
||||
function namedTask({ ownerId, ...view }: TeamTaskView, names: ReadonlyMap<SessionId, string>): NamedTaskView {
|
||||
return {
|
||||
...view,
|
||||
...ownerId === undefined ? {} : { ownerName: names.get(ownerId) ?? `legacy:${ownerId}` },
|
||||
...ownerId === undefined ? {} : { ownerName: names.get(ownerId) as string },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +172,7 @@ function install(agent: Agent, ctx: Context): () => void {
|
||||
limit: { type: 'integer', description: 'Number of rows, 1 through 100. Defaults to 50.' },
|
||||
},
|
||||
output: jsonOutput(TASK_LIST_VALUE_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
execute(args, exec) {
|
||||
const cursor = args.cursor ?? 0
|
||||
const limit = args.limit ?? 50
|
||||
if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error('cursor must be a non-negative safe integer')
|
||||
@@ -196,12 +184,12 @@ function install(agent: Agent, ctx: Context): () => void {
|
||||
&& (args.ready === undefined || task.ready === args.ready))
|
||||
const filterOwner = args.owner !== undefined && args.owner !== 'unowned'
|
||||
const candidates = filterOwner ? filtered : filtered.slice(cursor, cursor + limit)
|
||||
const names = await taskOwnerNames(ctx, caller, candidates.map(task => task.ownerId), exec.signal)
|
||||
const names = taskOwnerNames(ctx, caller, candidates.map(task => task.ownerId))
|
||||
if (filterOwner) filtered = filtered.filter(task => namedTask(task, names).ownerName === args.owner)
|
||||
return {
|
||||
return Promise.resolve({
|
||||
tasks: filtered.slice(cursor, cursor + limit).map(task => namedTask(task, names)),
|
||||
...(cursor + limit < filtered.length ? { nextCursor: cursor + limit } : {}),
|
||||
}
|
||||
})
|
||||
},
|
||||
})))
|
||||
|
||||
@@ -212,11 +200,11 @@ function install(agent: Agent, ctx: Context): () => void {
|
||||
task_id: { type: 'string', required: true, description: 'Shared task id.' },
|
||||
},
|
||||
output: jsonOutput(TASK_VIEW_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
execute(args, exec) {
|
||||
const caller = callingAgent(exec.agent, 'team_task_get')
|
||||
const task = ctx.agentTeams.getTask(caller, TeamTaskId(args.task_id))
|
||||
const names = await taskOwnerNames(ctx, caller, [task.ownerId], exec.signal)
|
||||
return namedTask(task, names)
|
||||
const names = taskOwnerNames(ctx, caller, [task.ownerId])
|
||||
return Promise.resolve(namedTask(task, names))
|
||||
},
|
||||
})))
|
||||
|
||||
@@ -236,7 +224,7 @@ function install(agent: Agent, ctx: Context): () => void {
|
||||
description: { type: 'string', description: 'Replacement details for edit.' },
|
||||
blocked_by: { type: 'array', items: { type: 'string' }, description: 'Complete blocker list for set_dependencies.' },
|
||||
write_scopes: { type: 'array', items: { type: 'string' }, description: 'Replacement advisory write scopes for edit.' },
|
||||
owner: { type: 'string', description: 'Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign.' },
|
||||
owner: { type: 'string', description: 'Agent name or Session id of the root or any continuable descendant for root-only reassign; omit to unassign.' },
|
||||
},
|
||||
output: jsonOutput(TASK_VIEW_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
@@ -244,12 +232,12 @@ function install(agent: Agent, ctx: Context): () => void {
|
||||
const taskId = TeamTaskId(args.task_id)
|
||||
const current = ctx.agentTeams.getTask(caller, taskId)
|
||||
const owner = args.owner === undefined ? undefined
|
||||
: await ctx.subagentNames.resolveName(caller, args.owner, exec.signal)
|
||||
: ctx.subagentNames.resolveName(caller, args.owner)
|
||||
const keepsOwner = !['claim', 'release', 'reopen', 'reassign'].includes(args.action)
|
||||
// Names are immutable; the service still checks the revision and authority after these reads.
|
||||
const names = await taskOwnerNames(ctx, caller, [
|
||||
const names = taskOwnerNames(ctx, caller, [
|
||||
caller.id, owner, keepsOwner ? current.ownerId : undefined,
|
||||
], exec.signal)
|
||||
])
|
||||
const task = await ctx.agentTeams.updateTask(caller, {
|
||||
taskId,
|
||||
expectedRevision: args.expected_revision,
|
||||
|
||||
@@ -82,7 +82,7 @@ async function setup(script: ConstructorParameters<typeof MockAdapter>[0]) {
|
||||
const queryFiber = await ctx.plugin(TestSessionQuery)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentNames)
|
||||
const namesFiber = await ctx.plugin(SubagentNames)
|
||||
await ctx.plugin(ToolSubagentControl)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
await ctx.plugin(SubagentFork, { providerName: 'fork' })
|
||||
@@ -91,7 +91,7 @@ async function setup(script: ConstructorParameters<typeof MockAdapter>[0]) {
|
||||
const adapter = new MockAdapter(script)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const lead = await ctx.agentLoop.create(SessionId('tool-team-lead'), { provider: 'mock', model: 'mock' })
|
||||
return { ctx, lead, fiber, adapter, queryFiber }
|
||||
return { ctx, lead, fiber, adapter, queryFiber, namesFiber }
|
||||
}
|
||||
|
||||
function execute(
|
||||
@@ -128,16 +128,22 @@ async function startWait(ctx: Context, agent: Agent) {
|
||||
return { result }
|
||||
}
|
||||
|
||||
/** Start a continuable child of the Lead with a task description. */
|
||||
/** Start a continuable child and record its display name after acceptance. */
|
||||
async function spawn(ctx: Context, lead: Agent, description: string | undefined, prompt: string, provider = 'spawn'): Promise<SubagentActivation & { readonly name: string }> {
|
||||
const started = await ctx.subagentNames.startActivation({ name: `child-${randomUUID()}`,
|
||||
provider,
|
||||
label: `${description ?? 'shared'} task`,
|
||||
prompt: [{ type: 'text', text: prompt }], parent: lead,
|
||||
signal: SIGNAL,
|
||||
delivery: 'parent',
|
||||
})
|
||||
return started
|
||||
const reservation = ctx.subagentNames.reserve(lead, `child-${randomUUID()}`)
|
||||
try {
|
||||
const started = await ctx.subagents.startActivation({
|
||||
provider,
|
||||
label: `${description ?? 'shared'} task`,
|
||||
prompt: [{ type: 'text', text: prompt }], parent: lead,
|
||||
signal: SIGNAL,
|
||||
delivery: 'parent',
|
||||
})
|
||||
ctx.subagentNames.bind(lead, started.childId, reservation.name)
|
||||
return { ...started, name: reservation.name }
|
||||
} finally {
|
||||
reservation.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
async function assembly(ctx: Context, agent: Agent) {
|
||||
@@ -171,25 +177,9 @@ async function waitNoAgent(ctx: Context, id: SessionId): Promise<void> {
|
||||
await vi.waitFor(() => { expect(ctx.agents.get(id)).toBeUndefined() }, { timeout: 5_000 })
|
||||
}
|
||||
|
||||
/** Publish the catalog membership required by named discovery for synthetic child Agents. */
|
||||
function bindTeamChild(ctx: Context, child: Agent, root: Agent, ancestors: readonly SessionId[] = [root.id]): void {
|
||||
bindSubagentActivation(ctx, child, root, ancestors)
|
||||
const parentId = ancestors.at(-1)
|
||||
const parent = parentId === undefined ? undefined : ctx.agents.get(parentId)
|
||||
if (parent === undefined) throw new Error('expected a live fixture creator')
|
||||
parent.session.append('subagent/catalog', {
|
||||
version: 0, childId: child.id, childCreatedAt: child.session.header.createdAt,
|
||||
mode: 'continuable', label: 'Fixture child',
|
||||
})
|
||||
}
|
||||
|
||||
/** Assign a task to a child whose name must be read from its durable catalog. */
|
||||
/** Assign a task to a settled child with a persisted name binding. */
|
||||
async function taskWithSettledOwner(ctx: Context, lead: Agent) {
|
||||
const activation = await ctx.subagentNames.startActivation({
|
||||
name: 'worker', provider: 'spawn', label: 'Review',
|
||||
parent: lead, prompt: [{ type: 'text', text: 'Review the change' }],
|
||||
signal: SIGNAL, delivery: 'parent',
|
||||
})
|
||||
const activation = await spawn(ctx, lead, 'worker', 'Review the change')
|
||||
try {
|
||||
await activation.result
|
||||
} finally {
|
||||
@@ -220,7 +210,7 @@ describe('dsh-tool-team', () => {
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const result = execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: task.revision, action: 'reassign', owner: await ctx.subagentNames.nameOf(ctx.agents.get(childId)!),
|
||||
task_id: task.id, expected_revision: task.revision, action: 'reassign', owner: ctx.subagentNames.nameOf(ctx.agents.get(childId)!),
|
||||
}, controller.signal)
|
||||
try {
|
||||
await entered.promise
|
||||
@@ -321,15 +311,15 @@ describe('dsh-tool-team', () => {
|
||||
const child = await ctx.agents.create({
|
||||
sessionId: SessionId('identity-child'),
|
||||
meta: { parentSession: lead.id, origin: 'subagent' },
|
||||
setup: (_ctx, agent) => { bindTeamChild(ctx, agent, lead) },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, lead) },
|
||||
})
|
||||
const nested = await ctx.agents.create({
|
||||
sessionId: SessionId('identity-nested'),
|
||||
meta: { parentSession: child.agent.id, origin: 'subagent' },
|
||||
setup: (_ctx, agent) => { bindTeamChild(ctx, agent, lead, [lead.id, child.agent.id]) },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, lead, [lead.id, child.agent.id]) },
|
||||
})
|
||||
expect(renderContextSnapshot(await assembly(ctx, nested.agent))).toContain(teammateIdentity(lead))
|
||||
expect(renderContextSnapshot(await assembly(ctx, nested.agent))).not.toContain(`Team led by ${JSON.stringify(await ctx.subagentNames.nameOf(child.agent))}`)
|
||||
expect(renderContextSnapshot(await assembly(ctx, nested.agent))).not.toContain(`Team led by ${JSON.stringify(ctx.subagentNames.nameOf(child.agent))}`)
|
||||
})
|
||||
|
||||
it.each([undefined, 'in-history'] as const)('appends current Lead identity to a teammate fork with update mode %s', async (systemPromptUpdate) => {
|
||||
@@ -444,7 +434,7 @@ describe('dsh-tool-team', () => {
|
||||
expected_revision: task.revision,
|
||||
action: 'claim',
|
||||
})
|
||||
expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerName: await ctx.subagentNames.nameOf(ctx.agents.get(childId)!) })
|
||||
expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerName: ctx.subagentNames.nameOf(ctx.agents.get(childId)!) })
|
||||
const stale = await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id,
|
||||
expected_revision: task.revision,
|
||||
@@ -520,7 +510,7 @@ describe('dsh-tool-team', () => {
|
||||
task_id: first.id,
|
||||
expected_revision: dependency.revision,
|
||||
action: 'reassign',
|
||||
owner: await ctx.subagentNames.nameOf(ctx.agents.get(childId)!),
|
||||
owner: ctx.subagentNames.nameOf(ctx.agents.get(childId)!),
|
||||
})).isError).toBe(true)
|
||||
|
||||
const child = ctx.agents.get(childId)!
|
||||
@@ -534,7 +524,7 @@ describe('dsh-tool-team', () => {
|
||||
await waitNoAgent(ctx, childId)
|
||||
})
|
||||
|
||||
it('retains a settled task owner’s catalog name and rejects its legacy alias', async () => {
|
||||
it('retains a settled task owner’s bound name and rejects its legacy alias', async () => {
|
||||
const { ctx, lead } = await setup([textResponse('done')])
|
||||
const { childId: id, name } = await spawn(ctx, lead, 'reviewer', 'review')
|
||||
await waitNoAgent(ctx, id)
|
||||
@@ -550,7 +540,7 @@ describe('dsh-tool-team', () => {
|
||||
})).isError).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves a task unchanged when its owner name cannot be read before an edit', async () => {
|
||||
it('edits named task owners without Session queries', async () => {
|
||||
const { ctx, lead, queryFiber } = await setup([textResponse('done')])
|
||||
const { task } = await taskWithSettledOwner(ctx, lead)
|
||||
await queryFiber.dispose()
|
||||
@@ -558,20 +548,20 @@ describe('dsh-tool-team', () => {
|
||||
const result = await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: task.revision, action: 'edit', subject: 'Edited',
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('listing subagents requires the sessionQuery service')
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toEqual(task)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(JSON.parse(text(result))).toMatchObject({ subject: 'Edited', ownerName: ctx.subagentNames.nameOf(lead, task.ownerId) })
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toMatchObject({ subject: 'Edited', revision: task.revision + 1 })
|
||||
await using stored = await ctx.sessionPersistence.open(lead.id, 'read')
|
||||
expect((await stored.read()).events.filter(event => event.type === 'team/task')).toHaveLength(2)
|
||||
expect((await stored.read()).events.filter(event => event.type === 'team/task')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('returns the committed task when Session queries unload during the commit', async () => {
|
||||
const { ctx, lead, queryFiber } = await setup([textResponse('done')])
|
||||
it('returns captured owner names when the naming plugin unloads during the commit', async () => {
|
||||
const { ctx, lead, namesFiber } = await setup([textResponse('done')])
|
||||
const { task, ownerName } = await taskWithSettledOwner(ctx, lead)
|
||||
const flush = ctx.sessions.flush.bind(ctx.sessions)
|
||||
vi.spyOn(ctx.sessions, 'flush').mockImplementationOnce(async (session) => {
|
||||
const flushed = await flush(session)
|
||||
await queryFiber.dispose()
|
||||
await namesFiber.dispose()
|
||||
return flushed
|
||||
})
|
||||
|
||||
@@ -583,42 +573,26 @@ describe('dsh-tool-team', () => {
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toMatchObject({ subject: 'Edited', revision: task.revision + 1 })
|
||||
})
|
||||
|
||||
it('rejects an edit if another writer changes the task while its owner name is being read', async () => {
|
||||
const { ctx, lead } = await setup([textResponse('done')])
|
||||
const { task } = await taskWithSettledOwner(ctx, lead)
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const discover = ctx.subagentNames.listDescendants.bind(ctx.subagentNames)
|
||||
vi.spyOn(ctx.subagentNames, 'listDescendants').mockImplementationOnce(async (...args) => {
|
||||
const entries = await discover(...args)
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
return entries
|
||||
it('does not grant task authority through a name binding for an unrelated Agent', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
const unrelated = await ctx.agents.create({
|
||||
sessionId: SessionId('unrelated-task-owner'), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const result = execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: task.revision, action: 'edit', subject: 'Delayed edit',
|
||||
})
|
||||
try {
|
||||
await entered.promise
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toEqual(task)
|
||||
await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'edit', subject: 'Concurrent edit',
|
||||
})
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
}
|
||||
expect((await result).isError).toBe(true)
|
||||
expect(text(await result)).toContain('stale team task')
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toMatchObject({
|
||||
subject: 'Concurrent edit', revision: task.revision + 1,
|
||||
ctx.subagentNames.bind(lead, unrelated.agent.id, 'lead/not-a-teammate')
|
||||
const task = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Requires a teammate' })
|
||||
const result = await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: task.revision, action: 'reassign', owner: 'lead/not-a-teammate',
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toEqual(task)
|
||||
await unrelated.dispose()
|
||||
})
|
||||
|
||||
it('reads owner names only for rows surviving filters and pagination', async () => {
|
||||
const { ctx, lead } = await setup([textResponse('done')])
|
||||
const unowned = await ctx.agentTeams.createTask(lead, { subject: 'First', description: 'Unowned' })
|
||||
const { task, ownerName } = await taskWithSettledOwner(ctx, lead)
|
||||
const observe = vi.spyOn(ctx.sessionQuery, 'observeSession')
|
||||
const observe = vi.spyOn(ctx.subagentNames, 'nameOf')
|
||||
for (const filter of [{ owner: 'unowned' }, { status: 'pending' }, { ready: true }, { limit: 1 }]) {
|
||||
const result = await execute(ctx, lead, 'team_task_list', filter)
|
||||
expect(result.isError).toBe(false)
|
||||
@@ -648,16 +622,18 @@ describe('dsh-tool-team', () => {
|
||||
? await ctx.agentTeams.updateTask(lead, { taskId: task.id, expectedRevision: task.revision, action: 'complete' })
|
||||
: task
|
||||
await queryFiber.dispose()
|
||||
const nameOf = vi.spyOn(ctx.subagentNames, 'nameOf')
|
||||
const result = await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: current.revision, action,
|
||||
...action === 'reassign' ? { owner: 'lead' } : {},
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(nameOf.mock.calls.some(([, id]) => id === task.ownerId)).toBe(false)
|
||||
if (action === 'reassign') expect(JSON.parse(text(result))).toMatchObject({ ownerName: 'lead' })
|
||||
else expect(JSON.parse(text(result))).not.toHaveProperty('ownerName')
|
||||
})
|
||||
|
||||
it('retains an uncataloged historical task owner as a legacy id', async () => {
|
||||
it('displays an unnamed historical task owner by its Session id', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
const task = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Read old ownership' })
|
||||
const { ready: _ready, writeScopeWarnings: _warnings, ...saved } = task
|
||||
@@ -666,10 +642,10 @@ describe('dsh-tool-team', () => {
|
||||
task: { ...saved, revision: 2, ownerId: SessionId('missing-worker') },
|
||||
})
|
||||
expect(JSON.parse(text(await execute(ctx, lead, 'team_task_get', { task_id: task.id }))))
|
||||
.toMatchObject({ ownerName: 'legacy:missing-worker' })
|
||||
.toMatchObject({ ownerName: 'missing-worker' })
|
||||
})
|
||||
|
||||
it('rejects a task edit when an unreadable branch may contain its owner', async () => {
|
||||
it('edits an unnamed task owner without traversing unreadable child catalogs', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
const created = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Read old ownership' })
|
||||
const { ready: _ready, writeScopeWarnings: _warnings, ...saved } = created
|
||||
@@ -685,10 +661,10 @@ describe('dsh-tool-team', () => {
|
||||
const result = await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: task.id, expected_revision: task.revision, action: 'edit', subject: 'Edited',
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('subagent discovery is incomplete')
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toEqual(task)
|
||||
expect(lead.session.ownEvents().filter(event => event.type === 'team/task')).toHaveLength(2)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(JSON.parse(text(result))).toMatchObject({ subject: 'Edited', ownerName: 'unobserved-worker' })
|
||||
expect(ctx.agentTeams.getTask(lead, task.id)).toMatchObject({ subject: 'Edited', revision: task.revision + 1 })
|
||||
expect(lead.session.ownEvents().filter(event => event.type === 'team/task')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.each([false, true])('rejects historical roster assignments without a readable continuable target (cataloged: %s)', async (cataloged) => {
|
||||
@@ -702,11 +678,11 @@ describe('dsh-tool-team', () => {
|
||||
const created = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Review history' })
|
||||
expect((await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: created.id, expected_revision: created.revision, action: 'reassign',
|
||||
owner: cataloged ? 'lead/legacy:historical-worker' : 'legacy:historical-worker',
|
||||
owner: id,
|
||||
})).isError).toBe(true)
|
||||
expect((await execute(ctx, lead, 'send_message', { name: 'legacy:historical-worker', message: 'hello' })).isError).toBe(true)
|
||||
expect((await execute(ctx, lead, 'send_message', { name: id, message: 'hello' })).isError).toBe(true)
|
||||
expect((await execute(ctx, lead, 'team_task_update', {
|
||||
task_id: created.id, expected_revision: 2, action: 'reassign', owner: 'legacy:missing',
|
||||
task_id: created.id, expected_revision: 2, action: 'reassign', owner: 'missing',
|
||||
})).isError).toBe(true)
|
||||
})
|
||||
|
||||
@@ -748,7 +724,7 @@ describe('dsh-tool-team', () => {
|
||||
const child = await ctx.agents.create({
|
||||
sessionId: id, agentOptions: { provider: 'mock', model: 'mock' },
|
||||
meta: { origin: 'subagent', parentSession: lead.id },
|
||||
setup: (_ctx, agent) => { bindTeamChild(ctx, agent, lead) },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, lead) },
|
||||
})
|
||||
expect(teamTools(await assembly(ctx, child.agent))).toEqual(TEAM_TOOLS)
|
||||
const created = await execute(ctx, child.agent, 'team_task_create', {
|
||||
@@ -767,7 +743,7 @@ describe('dsh-tool-team', () => {
|
||||
const child = await ctx.agents.create({
|
||||
sessionId: SessionId('waiting-child'), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
meta: { origin: 'subagent', parentSession: lead.id },
|
||||
setup: (_ctx, agent) => { bindTeamChild(ctx, agent, lead) },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, lead) },
|
||||
})
|
||||
const unrelated = await ctx.agents.create({
|
||||
sessionId: SessionId('unrelated-root'), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 520d9dc51e2b0142
|
||||
zh: 8f332776243107a9
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/summary:
|
||||
en: c04d4c7d89864380
|
||||
zh: 9c1724f34943c24a
|
||||
en: 2d63ab94ccdafa8f
|
||||
zh: 6372823aea399513
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 8d1e20b826450ffe
|
||||
@@ -24,11 +24,11 @@
|
||||
en: 7777b50b62aa53c0
|
||||
zh: ff2a53de5dcbf518
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/understand-the-implementation:
|
||||
en: cbfa18477278437a
|
||||
zh: c7135dccdc611892
|
||||
en: e168a1ffb8497b1a
|
||||
zh: e9085b004a2d2c55
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/further-exploration:
|
||||
en: f85bd24e012ceb8b
|
||||
zh: a6c09ed212b52745
|
||||
en: 8aeb6cf566b48838
|
||||
zh: cc0f5bb71ee7a9fe
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
@@ -36,8 +36,8 @@
|
||||
en: 8febbb031aa0452d
|
||||
zh: da3f9011631652da
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/what-the-model-sees:
|
||||
en: 9fd8beb18063ef0d
|
||||
zh: 2fea6cefb5bcf19f
|
||||
en: 4b2fc7303598e643
|
||||
zh: efaa45f0c42befd2
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/token-effect:
|
||||
en: 60d7ffec7f4dc858
|
||||
zh: 09f286978df6b4ec
|
||||
@@ -45,8 +45,8 @@
|
||||
en: 7c1ac0b007b2f0ce
|
||||
zh: 2332a07c6f8bcbdc
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/known-limitations-and-deferred-work:
|
||||
en: 5157ca8c4aa1a865
|
||||
zh: 47ad253d87a02838
|
||||
en: e0ed82ba366c0f17
|
||||
zh: c7fd3d1b48bb3c11
|
||||
/deepseek-ai-dsh-experimental-tool-subagent-oar/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 8ec3e73528ddb1a6
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns a full child path, with a later completion notice delivered to the parent.
|
||||
Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns its child id and full name, with a later completion notice delivered to the parent.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -59,7 +59,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
[`src/index.ts`](src/index.ts) builds the runtime enum from the configured aliases and registers one tool. Each call resolves its provider and starts a named activation through `ctx.subagentNames`, which wraps `ctx.subagents.startActivation()`. The subagent service owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children.
|
||||
[`src/index.ts`](src/index.ts) registers the configured runtime choices and calls `ctx.subagents.startActivation()` directly. The naming service reserves a sibling name and maps the accepted child id to its full name; the child's prompt is the submitted task. The subagent service owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -69,7 +69,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
|
||||
## Further Exploration
|
||||
|
||||
- [Subagent service](../../subagent/subagent/README.md) — provider registration and activation ownership.
|
||||
- [Subagent names](../../subagent/subagent-names/README.md) — sibling names and full-path results.
|
||||
- [Subagent names](../../subagent/subagent-names/README.md) — sibling names and full-name lookup.
|
||||
- [OAR provider](../subagent-oar/README.md) — native runtime setup and permission configuration.
|
||||
- [Subagent subsystem](../../../docs/subsystems/subagent.md) — parent completion notices and retained results.
|
||||
|
||||
@@ -82,7 +82,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent <name>`, where `<name>` is the full child path. The shared subagent service later supplies the child's answer or failure notice, identifying the external execution by its child id.
|
||||
The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the result contains `kind: "activation"`, `subagentId`, and `name`. The model sees `started subagent <ID>` followed by `name: <full name>` on the next line. The shared subagent service later supplies the child's answer or failure notice, identifying the external execution by the same child id.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -100,6 +100,7 @@ The tool's choices and execution capabilities are fixed by its configured provid
|
||||
|
||||
- **One-shot providers only:** child context is independent, and the tool exposes no follow-up, resume, structured output, or per-call native permission override.
|
||||
- **Provider availability:** a selected provider that is absent or unloaded fails at call time; its configured alias remains in the schema until this tool's configuration changes.
|
||||
- **Name binding failure:** if the accepted child's name cannot be recorded, the error reports its id and leaves execution managed by the subagent service. Check that child's outcome before retrying; the reserved name is released.
|
||||
- **Provider-owned work:** runtime setup, startup cancellation, process cleanup, and answer selection follow the selected provider's documented behavior. Removing this tool does not revoke accepted activations.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子级完整路径,完成通知稍后送达父智能体。
|
||||
使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务 id 和完整名称,完成通知稍后送达父智能体。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -59,7 +59,7 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现内部细节 — 点击展开</summary>
|
||||
|
||||
[`src/index.ts`](src/index.ts)根据配置的别名构建运行时枚举,并注册一个工具。每次调用解析其提供方,并通过 `ctx.subagentNames` 启动具名激活;命名服务包装 `ctx.subagents.startActivation()`。subagent 服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。
|
||||
[`src/index.ts`](src/index.ts)注册配置的运行时选项,并直接调用 `ctx.subagents.startActivation()`。命名服务预留同级名称,并将已接受的子任务 id 映射到完整名称;子任务的提示就是提交的任务。subagent 服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -69,7 +69,7 @@ kind: "package-reference"
|
||||
## 延伸阅读
|
||||
|
||||
- [子智能体服务](../../subagent/subagent/README.zh.md) — 提供方注册和激活所有权。
|
||||
- [Subagent 命名](../../subagent/subagent-names/README.zh.md) — 同级名字和完整路径结果。
|
||||
- [Subagent 命名](../../subagent/subagent-names/README.zh.md) — 同级名称和完整名称查询。
|
||||
- [OAR 提供方](../subagent-oar/README.zh.md) — 原生运行时配置和权限配置。
|
||||
- [子智能体子系统](../../../docs/subsystems/subagent.zh.md) — 父智能体完成通知和保留结果。
|
||||
|
||||
@@ -82,7 +82,7 @@ kind: "package-reference"
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent <name>`,其中 `<name>` 是子级完整路径。共享子智能体服务稍后提供子任务的答复或失败通知,以子任务 id 标识外部执行。
|
||||
[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,结果包含 `kind: "activation"`、`subagentId` 和 `name`。模型看到 `started subagent <ID>`,下一行是 `name: <full name>`。共享子智能体服务稍后提供子任务的答复或失败通知,以相同的子任务 id 标识外部执行。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -100,6 +100,7 @@ kind: "package-reference"
|
||||
|
||||
- **仅支持单次提供方:** 子任务上下文独立,工具不提供后续输入、恢复、结构化输出或按调用覆盖原生权限的功能。
|
||||
- **提供方可用性:** 选择的提供方不存在或已卸载时,调用会失败;其配置的别名会保留在 schema 中,直到此工具的配置改变。
|
||||
- **名称绑定失败:** 如果已接受的子任务名称无法记录,错误会报告其 id,执行仍由 subagent 服务管理。重试前应检查该子任务的结果;预留的名称会被释放。
|
||||
- **提供方负责的工作:** 运行时配置、启动取消、进程清理和答复选择遵循所选提供方的文档行为。移除此工具不会撤销已接受的激活。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-names": "workspace:*",
|
||||
"@deepseek-ai/dsh-tools": "workspace:*"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-subagent-names'
|
||||
@@ -37,7 +38,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
name: toolName,
|
||||
description: 'Delegate a self-contained task to a configured external agent. Choose its runtime for each call. '
|
||||
+ 'The child works in its own context and does not see this conversation. '
|
||||
+ 'This tool returns a full agent path; a later completion notice includes its final answer. '
|
||||
+ 'This tool returns the child id and full agent path; a later completion notice includes its final answer. '
|
||||
+ 'The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. '
|
||||
+ 'When only waiting for their results remains, end your turn instead of polling.',
|
||||
parameters: {
|
||||
@@ -74,10 +75,11 @@ export function apply(ctx: Context, config: Config): void {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, const: 'activation' },
|
||||
subagentId: { type: 'string', required: true },
|
||||
name: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.name}` }],
|
||||
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}\nname: ${value.name}` }],
|
||||
},
|
||||
/* jscpd:ignore-end */
|
||||
isConcurrencySafe: () => true,
|
||||
@@ -90,17 +92,31 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (provider !== undefined && (provider.prepareContinuable !== undefined || provider.inheritsParentContext)) {
|
||||
throw new Error('subagent_oar requires a one-shot provider with independent conversation context')
|
||||
}
|
||||
const activation = await ctx.subagentNames.startActivation({
|
||||
...args.name === undefined ? {} : { name: args.name },
|
||||
provider: providerName,
|
||||
label: args.description,
|
||||
parent,
|
||||
prompt: [{ type: 'text', text: args.prompt }],
|
||||
...args.cwd === undefined ? {} : { cwd: args.cwd },
|
||||
signal: exec.signal,
|
||||
delivery: 'parent',
|
||||
})
|
||||
return { kind: 'activation' as const, name: activation.name }
|
||||
const names = ctx.subagentNames
|
||||
const reservation = names.reserve(parent, args.name)
|
||||
try {
|
||||
const activation = await ctx.subagents.startActivation({
|
||||
provider: providerName,
|
||||
label: args.description,
|
||||
parent,
|
||||
prompt: [{ type: 'text', text: args.prompt }],
|
||||
...args.cwd === undefined ? {} : { cwd: args.cwd },
|
||||
signal: exec.signal,
|
||||
delivery: 'parent',
|
||||
})
|
||||
try {
|
||||
names.bind(parent, activation.childId, reservation.name)
|
||||
} catch (cause: unknown) {
|
||||
throw new HarnessError(
|
||||
`Subagent ${activation.childId} started, but its name ${reservation.name} could not be recorded. `
|
||||
+ 'Its execution is still managed by the subagent service. Check this child before retrying.',
|
||||
'SUBAGENT_NAME_BIND_FAILED', { cause },
|
||||
)
|
||||
}
|
||||
return { kind: 'activation' as const, subagentId: activation.childId, name: reservation.name }
|
||||
} finally {
|
||||
reservation.dispose()
|
||||
}
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: args.description, rawInput: args }),
|
||||
}))
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SubagentRuntime, { NO_START_CAPABILITIES } from '@deepseek-ai/dsh-subagent'
|
||||
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
|
||||
import type { ResolvedSubagentStartRequest } from '@deepseek-ai/dsh-subagent'
|
||||
import { expect, it } from 'vitest'
|
||||
import type { ResolvedSubagentStartRequest, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
|
||||
import * as externalTool from '../src/index.ts'
|
||||
|
||||
@@ -20,14 +20,15 @@ async function setup() {
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentNames)
|
||||
const calls: { provider: string; request: ResolvedSubagentStartRequest }[] = []
|
||||
const calls: { id: SessionId; provider: string; request: ResolvedSubagentStartRequest }[] = []
|
||||
for (const name of ['codex-provider', 'claude-provider', 'future-provider']) {
|
||||
ctx.subagents.registerProvider({
|
||||
name, capabilities: NO_START_CAPABILITIES, inheritsParentContext: false,
|
||||
start: async (request) => {
|
||||
calls.push({ provider: name, request })
|
||||
const id = SessionId(randomUUID())
|
||||
calls.push({ id, provider: name, request })
|
||||
return {
|
||||
id: SessionId(randomUUID()),
|
||||
id,
|
||||
result: Promise.resolve({ output: [{ type: 'text' as const, text: 'child result' }], stopReason: 'completed' as const }),
|
||||
dispose: () => Promise.resolve(),
|
||||
}
|
||||
@@ -55,15 +56,12 @@ it('selects one configured provider per call and exposes only task inputs', asyn
|
||||
arguments: { runtime, description: `Use ${runtime}`, prompt: 'independent task' },
|
||||
})
|
||||
expect(result.isError, JSON.stringify(result.content)).toBe(false)
|
||||
expect(result.content).toHaveLength(1)
|
||||
const content = result.content[0]
|
||||
expect(content?.type).toBe('text')
|
||||
if (content?.type === 'text') expect(content.text).toBe(`started subagent lead/agent-${index + 1}`)
|
||||
expect(result.value).toEqual({ kind: 'activation', subagentId: calls[index]?.id, name: `lead/agent-${index + 1}` })
|
||||
expect(result.content).toEqual([{ type: 'text', text: `started subagent ${calls[index]?.id}\nname: lead/agent-${index + 1}` }])
|
||||
}
|
||||
expect(calls.map(call => call.provider)).toEqual(['codex-provider', 'claude-provider', 'future-provider'])
|
||||
expect(calls[0]?.request.prompt).toEqual([
|
||||
{ type: 'text', text: 'independent task' },
|
||||
{ type: 'text', text: 'Your name is "lead/agent-1". Your parent agent name is "lead".' },
|
||||
])
|
||||
expect(calls[0]?.request.parent).toBe(parent)
|
||||
expect(calls[0]?.request.cwd).toBe(process.cwd())
|
||||
@@ -142,7 +140,8 @@ it('passes an explicit child directory and presents the task description', async
|
||||
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'started subagent lead/inspector' }])
|
||||
expect(result.content).toEqual([{ type: 'text', text: `started subagent ${calls[0]?.id}\nname: lead/inspector` }])
|
||||
expect(result.value).toEqual({ kind: 'activation', subagentId: calls[0]?.id, name: 'lead/inspector' })
|
||||
expect(calls[0]?.request.cwd).toBe(join(process.cwd(), 'packages'))
|
||||
})
|
||||
|
||||
@@ -152,3 +151,69 @@ it('supports direct composition with the declared optional defaults', async (tes
|
||||
externalTool.apply(ctx, { providers: { codex: 'codex-provider' } })
|
||||
expect(ctx.tools.schemas().some(tool => tool.name === 'subagent_oar')).toBe(true)
|
||||
})
|
||||
|
||||
it('releases the requested name when provider startup fails', async (test) => {
|
||||
const { ctx, parent } = await setup()
|
||||
test.onTestFinished(() => ctx.fiber.dispose())
|
||||
const childId = SessionId(randomUUID())
|
||||
const start = vi.fn<() => Promise<SubagentRun>>()
|
||||
.mockRejectedValueOnce(new Error('provider startup failed'))
|
||||
.mockResolvedValueOnce({
|
||||
id: childId,
|
||||
result: Promise.resolve({ output: [], stopReason: 'completed' }),
|
||||
dispose: async () => {},
|
||||
})
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'retry-provider', capabilities: NO_START_CAPABILITIES, inheritsParentContext: false, start,
|
||||
})
|
||||
await ctx.plugin(externalTool, { providers: { retry: 'retry-provider' } })
|
||||
const args = { name: 'inspector', runtime: 'retry', description: 'task', prompt: 'task' }
|
||||
const failed = await executeToolFixture(ctx, {
|
||||
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
|
||||
})
|
||||
expect(failed.isError).toBe(true)
|
||||
expect(failed.error?.message).toContain('provider startup failed')
|
||||
const retried = await executeToolFixture(ctx, {
|
||||
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
|
||||
})
|
||||
expect(retried.isError, JSON.stringify(retried.content)).toBe(false)
|
||||
expect(retried.value).toEqual({ kind: 'activation', subagentId: childId, name: 'lead/inspector' })
|
||||
expect(start).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('reports the accepted child id without stopping it when name binding fails', async (test) => {
|
||||
const { ctx, parent } = await setup()
|
||||
const childId = SessionId(randomUUID())
|
||||
const childResult = Promise.withResolvers<SubagentResult>()
|
||||
let childRequest: ResolvedSubagentStartRequest | undefined
|
||||
const dispose = vi.fn(async () => { childResult.resolve({ output: [], stopReason: 'aborted' }) })
|
||||
const bind = vi.spyOn(ctx.subagentNames, 'bind').mockImplementationOnce(() => { throw new Error('session append failed') })
|
||||
test.onTestFinished(async () => {
|
||||
bind.mockRestore()
|
||||
childResult.resolve({ output: [], stopReason: 'completed' })
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'pending-provider', capabilities: NO_START_CAPABILITIES, inheritsParentContext: false,
|
||||
start: async (request) => {
|
||||
childRequest = request
|
||||
return { id: childId, result: childResult.promise, dispose }
|
||||
},
|
||||
})
|
||||
await ctx.plugin(externalTool, { providers: { pending: 'pending-provider' } })
|
||||
const result = await executeToolFixture(ctx, {
|
||||
agent: parent, name: 'subagent_oar', callId: ToolCallId(randomUUID()), signal: test.signal,
|
||||
arguments: { name: 'inspector', runtime: 'pending', description: 'task', prompt: 'task' },
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error?.info?.code).toBe('SUBAGENT_NAME_BIND_FAILED')
|
||||
expect(result.error?.message).toContain(`Subagent ${childId} started, but its name lead/inspector could not be recorded.`)
|
||||
expect(result.error?.message).toContain('Check this child before retrying.')
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
expect(childRequest?.signal.aborted).toBe(false)
|
||||
const reservation = ctx.subagentNames.reserve(parent, 'inspector')
|
||||
expect(reservation.name).toBe('lead/inspector')
|
||||
reservation.dispose()
|
||||
childResult.resolve({ output: [{ type: 'text', text: 'child finished' }], stopReason: 'completed' })
|
||||
await expect.poll(() => dispose.mock.calls.length).toBe(1)
|
||||
})
|
||||
@@ -17,6 +17,9 @@
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent"
|
||||
},
|
||||
|
||||
@@ -3147,33 +3147,31 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
key: 'subagentNames',
|
||||
summary: 'Catalog-backed names that leave subagent execution and authorization with their existing owner.',
|
||||
description: 'Catalog-backed names that leave subagent execution and authorization with their existing owner.',
|
||||
summary: 'Root-Session aliases shared by named creation, discovery, and control tools.',
|
||||
description: 'Root-Session aliases shared by named creation, discovery, and control tools.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'async startActivation(spec: NamedSubagentActivationSpec): Promise<SubagentActivation & { readonly name: string }>',
|
||||
description: 'Start a named child, reserving its sibling name until startup settles. Parent delivery retains catalog membership. Local names precede catalog publication; external names follow accepted startup.',
|
||||
parameters: [{ name: 'spec', description: 'ordinary activation options and an optional lower-case sibling name.' }],
|
||||
returns: 'the existing activation receipt plus its full name.',
|
||||
throws: ['SubagentNameError for invalid or duplicate names. An external binding failure includes childId and awaits disposal without undoing completed work.'],
|
||||
signature: 'reserve(parent: Agent, requested?: string): SubagentNameReservation',
|
||||
description: 'Reserve a complete name without starting an agent or writing a Session event.',
|
||||
parameters: [{ name: 'parent', description: 'exact live caller whose name prefixes the child name.' }, { name: 'requested', description: 'optional sibling segment; omission selects the first free agent-N.' }],
|
||||
returns: 'a reservation the caller must dispose after startup and binding.',
|
||||
},
|
||||
{
|
||||
signature: 'async nameOf(agent: Agent, signal?: AbortSignal): Promise<string>',
|
||||
description: 'Read the full name of an exact live Agent without restoring its ancestors.',
|
||||
parameters: [{ name: 'agent', description: 'live root or managed child whose name is requested.' }, { name: 'signal', description: 'cancellation during parent catalog reads.' }],
|
||||
returns: 'lead, named path segments, or legacy id segments for unnamed children.',
|
||||
signature: 'bind(parent: Agent, childId: SessionId, name: string): void',
|
||||
description: 'Record an accepted child\'s full name in the caller\'s root Session. Binding failure does not cancel or dispose the child; callers report its id.',
|
||||
parameters: [{ name: 'parent', description: 'exact live caller whose root owns the alias.' }, { name: 'childId', description: 'identity returned by the subagent service.' }, { name: 'name', description: 'complete name returned by reserve.' }],
|
||||
},
|
||||
{
|
||||
signature: 'async resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>',
|
||||
description: 'Translate a canonical path or a tree member\'s exact id without granting authority.',
|
||||
parameters: [{ name: 'caller', description: 'exact live Agent selecting the subagent tree.' }, { name: 'name', description: 'lead, a lead-prefixed path, or an existing member\'s exact Session id.' }, { name: 'signal', description: 'cancellation during catalog reads.' }],
|
||||
returns: 'the identity for the subagent operation to authorize.',
|
||||
signature: 'nameOf(caller: Agent, targetId: SessionId = caller.id): string',
|
||||
description: 'Read a bound name from the live root\'s projection without loading ancestors.',
|
||||
parameters: [{ name: 'caller', description: 'exact live caller selecting the tree.' }, { name: 'targetId', description: 'requested identity; omission selects the caller.' }],
|
||||
returns: 'lead for the root, a bound full name, or the unchanged id.',
|
||||
},
|
||||
{
|
||||
signature: 'async listDescendants(rootId: SessionId, signal?: AbortSignal): Promise<(SubagentDescendantListEntry & { readonly path: string })[]>',
|
||||
description: 'Attach relative names to the existing catalog traversal and its branch diagnostics.',
|
||||
parameters: [{ name: 'rootId', description: 'Session whose catalog starts discovery.' }, { name: 'signal', description: 'cancellation while reading descendant and name projections.' }],
|
||||
returns: 'existing descendant rows with paths relative to rootId.',
|
||||
signature: 'resolveName(caller: Agent, name: string): SessionId',
|
||||
description: 'Resolve an alias or pass through an id for the subagent service to authorize.',
|
||||
parameters: [{ name: 'caller', description: 'exact live caller selecting the root mapping.' }, { name: 'name', description: 'lead, a bound full name, or a raw Session id.' }],
|
||||
returns: 'the target identity; an unknown lead/... name throws.',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -6567,10 +6565,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ModTier',
|
||||
declaration: 'export type ModTier = \'prepend\' | \'user\' | \'append\' | \'builtin\' | \'core\';',
|
||||
},
|
||||
{
|
||||
name: 'NamedSubagentActivationSpec',
|
||||
declaration: 'export type NamedSubagentActivationSpec = Omit<SubagentActivationSpec, \'childId\' | \'delivery\'> & {\n readonly delivery: \'parent\';\n readonly name?: string;\n};',
|
||||
},
|
||||
{
|
||||
name: 'NativeFileApplication',
|
||||
declaration: 'export interface NativeFileApplication {\n readonly id: string;\n readonly name: string;\n readonly default: boolean;\n readonly icon: string | null;\n}',
|
||||
@@ -8211,6 +8205,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SubagentListEntry',
|
||||
declaration: 'export type SubagentListEntry = SubagentCatalogRow & {\n readonly kind: \'child\';\n readonly hasChildren: boolean;\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};',
|
||||
},
|
||||
{
|
||||
name: 'SubagentNameReservation',
|
||||
declaration: 'export interface SubagentNameReservation {\n readonly name: string;\n dispose(): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubagentPromptReceipt',
|
||||
declaration: 'export interface SubagentPromptReceipt {\n readonly messageId: MessageId;\n}',
|
||||
|
||||
+1
-1
@@ -462,7 +462,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
|
||||
| `@deepseek-ai/dsh-subagent-codex` | yes | One-shot Codex subagent provider over the official app-server protocol |
|
||||
| `@deepseek-ai/dsh-subagent-dsh-sdk` | yes | Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client |
|
||||
| `@deepseek-ai/dsh-subagent-fork-in-process` | yes | In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log |
|
||||
| `@deepseek-ai/dsh-subagent-names` | no | Optional catalog-backed names for subagent tools |
|
||||
| `@deepseek-ai/dsh-subagent-names` | no | Durable name mappings for subagent tools |
|
||||
| `@deepseek-ai/dsh-subagent-spawn-in-process` | yes | In-process spawn subagent backend: runs a fresh child agent on ctx.agents |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | yes | Model-facing subagent delegation tool over the ctx.subagents seam |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | yes | Shared messaging, interruption, and child discovery tools for subagents |
|
||||
|
||||
@@ -43,7 +43,7 @@ function settlementText(events: SessionEvent[]): string[] {
|
||||
const notice = notices[0]!
|
||||
if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice')
|
||||
expect(results[0]!.data.message.content).toEqual([
|
||||
{ type: 'text', text: 'started subagent lead/cwd-probe' },
|
||||
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}\nname: lead/cwd-probe` },
|
||||
])
|
||||
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ function settlementText(events: SessionEvent[]): string[] {
|
||||
const notice = notices[0]!
|
||||
if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice')
|
||||
expect(results[0]!.data.message.content).toEqual([
|
||||
{ type: 'text', text: 'started subagent lead/route-probe' },
|
||||
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}\nname: lead/route-probe` },
|
||||
])
|
||||
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-names/README.md
|
||||
/:
|
||||
en: ac134a941d5a4791
|
||||
zh: 2e78de95e7019137
|
||||
en: 26564f09306cf08f
|
||||
zh: b8933154b4911a0c
|
||||
/deepseek-ai-dsh-subagent-names:
|
||||
en: 622989d6dcd8611a
|
||||
zh: 6f0a3beb8e665332
|
||||
/deepseek-ai-dsh-subagent-names/summary:
|
||||
en: dce79dec57405de4
|
||||
zh: 322c420f5779baaa
|
||||
en: 1af1d0f9ab2e446e
|
||||
zh: 4846168336979ca6
|
||||
/deepseek-ai-dsh-subagent-names/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: c614dc45fe9577a5
|
||||
@@ -18,41 +18,29 @@
|
||||
en: 15e2ab025baa3f2e
|
||||
zh: eab7ed264c78a4e4
|
||||
/deepseek-ai-dsh-subagent-names/use-this-package/minimal-configuration:
|
||||
en: 12034dd48a7e0c1b
|
||||
zh: a84690d6356ffbd3
|
||||
en: 46eb3758e96fa2a9
|
||||
zh: d85a69812bb4ed77
|
||||
/deepseek-ai-dsh-subagent-names/use-this-package/choosing-and-finding-names:
|
||||
en: 3f36d041164a1e04
|
||||
zh: 97418063aec0a08c
|
||||
/deepseek-ai-dsh-subagent-names/use-this-package/creation-and-failure:
|
||||
en: f5ee74db9a397c18
|
||||
zh: 3c9acad39ef8127d
|
||||
en: d7f5ce2da602deb1
|
||||
zh: 9862710f0cd113b9
|
||||
/deepseek-ai-dsh-subagent-names/use-this-package/binding-after-startup:
|
||||
en: 9c0853a5c5b3f776
|
||||
zh: 8d63108d8fb65f6e
|
||||
/deepseek-ai-dsh-subagent-names/understand-the-implementation:
|
||||
en: 4b0f042e1512b764
|
||||
zh: 65329202d8ce93b3
|
||||
en: e7ead7723e3e5d29
|
||||
zh: 30a1b369fe8df0a5
|
||||
/deepseek-ai-dsh-subagent-names/further-exploration:
|
||||
en: 8509edc3d9146efa
|
||||
zh: 09a6e3ebd1a75011
|
||||
en: ed0fb57dc13736e6
|
||||
zh: acd0cd0756510e65
|
||||
/deepseek-ai-dsh-subagent-names/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/initial-child-identity:
|
||||
en: 22f7737ddb8bb24b
|
||||
zh: 497b7190cf316a0d
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/initial-child-identity/what-the-model-sees:
|
||||
en: d05d767ea412d8bd
|
||||
zh: 031080dcce3211d6
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/initial-child-identity/what-the-model-sees/identity-message:
|
||||
en: d032a01e1b229d26
|
||||
zh: 05dd1a8dc54e9109
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/initial-child-identity/token-effect:
|
||||
en: 63fa3c1fab95b75a
|
||||
zh: 9805b73493dddb66
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/initial-child-identity/kv-cache-effect:
|
||||
en: 248a19e7e4b73b1d
|
||||
zh: 6d360af936b15bb5
|
||||
en: 40f9481ebd3e077d
|
||||
zh: 118c16ec3fed9652
|
||||
/deepseek-ai-dsh-subagent-names/model-experience/kv-cache-effect:
|
||||
en: f1456be695a842d8
|
||||
zh: ba3a1608efd6100b
|
||||
/deepseek-ai-dsh-subagent-names/known-limitations-and-deferred-work:
|
||||
en: 9b06e33292c3ff6d
|
||||
zh: 654cd7f7b49bfc2f
|
||||
en: 81988fdd0823cffa
|
||||
zh: 96bacb5621c1e874
|
||||
/deepseek-ai-dsh-subagent-names/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 8ec3e73528ddb1a6
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Create subagents with durable sibling names and resolve their tree paths for delegation tools without changing Session identities."
|
||||
description: "Bind readable subagent names to Session ids and recover the mappings from the root Session."
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Give delegated agents readable names such as `lead/research` and `lead/research/checker`. Names survive Session reloads, and lookup can read parents that are no longer live. Choose this package for named delegation tools while retaining Session ids for execution and authorization. Names identify existing catalog children; a failed creation does not permanently occupy its requested name.
|
||||
Address delegated agents by readable names such as `lead/research` and `lead/research/checker`. Bound names survive root Session reloads and keep the same spelling after agents finish. Choose this package when delegation tools need names while preserving Session ids for execution and authorization. Agents without a bound name remain addressable by id.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -29,27 +29,25 @@ Mount this plugin with the naming-aware [delegation tool](../tool-subagent/READM
|
||||
|
||||
### Minimal configuration
|
||||
|
||||
Add this row to a composition providing `agents`, `subagents`, and `sessionProjections`:
|
||||
Add this row to a composition providing `subagents` and `sessionProjections`:
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-subagent-names'
|
||||
```
|
||||
|
||||
This plugin has no configuration fields. It exposes `ctx.subagentNames`; the existing subagent service continues to own providers, activations, messages, and authorization. Mount `sessionQuery` for parent reads used by name lookup and discovery; starting a root's child and reading the root's name do not require it.
|
||||
This plugin has no configuration fields. It exposes `ctx.subagentNames`; the subagent service owns execution, messages, discovery, and authorization.
|
||||
|
||||
### Choosing and finding names
|
||||
|
||||
A requested sibling name starts with a lower-case letter and contains only lower-case letters, digits, underscores, and hyphens. Omission selects the first available `agent-N` name, starting at `agent-1`. Names are immutable and unique under their direct parent, including concurrent starts. A committed child keeps its name after execution settles.
|
||||
A requested name segment starts with a lower-case letter and contains only lower-case letters, digits, underscores, and hyphens. Omission selects the first available `agent-N` under the parent's name, starting at `agent-1`. Reservations prevent concurrent starts from choosing the same full name. A bound name remains occupied after execution settles.
|
||||
|
||||
The live tree root is `lead`; child paths append one segment per parent. An unnamed catalog child uses `legacy:<sessionId>` as its segment. Lookup accepts a full `lead/...` path, `lead`, or a tree member's exact Session id, including ids in existing runtime notices. It resolves identity only; the subsequent subagent operation still authorizes the caller.
|
||||
The root is `lead`. `nameOf` returns a bound full name, or the original Session id when no binding exists. `resolveName` accepts `lead` and bound full names; it rejects an unknown `lead/...` name and passes other ids through. The subsequent subagent operation still authorizes the caller.
|
||||
|
||||
### Creation and failure
|
||||
### Binding after startup
|
||||
|
||||
Call `ctx.subagentNames.startActivation` with `delivery: 'parent'` so the existing catalog retains the child identity. For caller-owned delivery, use `ctx.subagents.startActivation`; that operation retains its id-based behavior.
|
||||
Call `reserve(parent, requested?)`, start the child through `ctx.subagents.startActivation`, then call `bind(parent, childId, reservation.name)` with the successful receipt. Release the reservation with `dispose()` in `finally`, whether startup and binding succeed or fail.
|
||||
|
||||
Local creation records a candidate name before starting the child. The name becomes discoverable only when the existing parent catalog records that child. A candidate left by failed creation or a crash neither resolves nor occupies the name while its child is absent from the catalog. Once the catalog contains the child, a later startup failure does not free its name.
|
||||
|
||||
External providers allocate their own ids, so their names are recorded after startup returns. If that recording fails, the operation attempts and awaits disposal, then reports the started child id. Completed external work cannot be undone; if disposal also fails, execution may still be active. Check that child before retrying.
|
||||
Binding records the full name in the root Session. If binding fails after startup succeeds, the caller must report the already-started child id; the child remains managed by the subagent service. A crash before binding can likewise leave an unnamed child that remains addressable by id.
|
||||
|
||||
-----
|
||||
|
||||
@@ -59,14 +57,14 @@ External providers allocate their own ids, so their names are recorded after sta
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The plugin owns parent-local name records and joins them with the existing subagent catalog. Its projection excludes inherited fork records. Pending sibling reservations belong to the plugin instance; unloading cancels and awaits in-flight named starts before releasing their state.
|
||||
Each root Session stores its own name-to-id bindings. A projection recovers those bindings and excludes inherited fork records. Lookups read this root mapping without observing ancestor Sessions or joining child catalogs. Callers own transient reservations and release them after their operation.
|
||||
|
||||
Name reads traverse creation ancestry through Session observations without restoring parent agents. Discovery adds relative paths to the subagent service's existing descendant rows. Missing projections and inconsistent catalog/name records fail explicitly instead of supplying an unverified alias.
|
||||
The mapping retains one binding per named child: storage is O(n), a lookup or binding is O(n), and replaying n distinct bindings is O(n²) in the worst case because each event searches the binding array.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | Named startup, reservation lifetime, path lookup, and discovery |
|
||||
| [`src/projection.ts`](src/projection.ts) | Durable candidate records and replay validation |
|
||||
| [`src/index.ts`](src/index.ts) | Name reservations, binding, and lookup |
|
||||
| [`src/projection.ts`](src/projection.ts) | Durable bindings and replay validation |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -75,48 +73,34 @@ Name reads traverse creation ancestry through Session observations without resto
|
||||
<a id="further-exploration"></a>
|
||||
## Further Exploration
|
||||
|
||||
These pages own the execution behavior and model-facing consumers that use names.
|
||||
These pages own execution and the model-facing tools that use names.
|
||||
|
||||
- [Subagent service](../subagent/README.md) — activation ownership, catalogs, and authorization.
|
||||
- [Delegation tool](../tool-subagent/README.md) — named creation and start acknowledgements.
|
||||
- [Control tools](../tool-subagent-control/README.md) — discovery, messaging, and interruption.
|
||||
- [Session projections](../../session/session-projection/README.md) — incremental recovery and observation.
|
||||
- [Session projections](../../session/session-projection/README.md) — incremental recovery.
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
### Initial child identity
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Named startup appends the child's full name and its parent's full name to the task prompt. Local children receive this text in their durable initial user message; external providers receive it in the delegated prompt. For a child named `research` under the root:
|
||||
|
||||
##### Identity message
|
||||
|
||||
```markdown
|
||||
Your name is "lead/research". Your parent agent name is "lead".
|
||||
```
|
||||
|
||||
#### Token effect
|
||||
|
||||
One short identity sentence is added to each named child's initial prompt; longer ancestry adds path text. This plugin registers no tool schema or system-prompt section.
|
||||
Indirectly, through the [delegation tool](../tool-subagent/README.md) and [control tools](../tool-subagent-control/README.md), which render names alongside Session ids and accept names as addresses.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The identity joins the new task prompt after any inherited history. Existing history remains unchanged; each child receives its own identity text.
|
||||
This plugin adds no prompt text or tool schema. Its consumers own any model-visible name text and its cache effect.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
Names supplement the existing execution identities with these limits:
|
||||
Name bindings have these limits:
|
||||
|
||||
- **Direct subagent callers remain unnamed** — starts that bypass `ctx.subagentNames` use the `legacy:<sessionId>` path representation.
|
||||
- **External name binding follows execution startup** — a recording failure can occur after external work begins or completes.
|
||||
- **Runtime notices retain Session ids** — names do not rewrite messages or settlement notices owned by the subagent service.
|
||||
- **Names cannot be renamed or reused for cataloged children** — settling or disposing an activation does not remove its durable catalog membership.
|
||||
- **Names are available after binding** — the child's first request may precede its binding; this plugin adds no initial identity message.
|
||||
- **Names are immutable** — bound names and ids cannot be reassigned or reused after an activation ends.
|
||||
- **Paths capture the parent's name at reservation time** — an unnamed parent uses `lead/legacy:<parentId>` as its prefix; a later parent binding does not rewrite the child's stored path.
|
||||
- **Naming is explicit** — callers that do not bind a name continue to use Session ids.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "为 subagent 创建持久的同级名称,并为委派工具解析其树路径,同时保留 Session 身份。"
|
||||
description: "将可读的 subagent 名称绑定到 Session id,并从根 Session 恢复映射。"
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
为委派的 agent(智能体)赋予 `lead/research` 和 `lead/research/checker` 这样的可读名称。名称在 Session 重新加载后仍然保留,查询也能读取已不在运行中的父 agent。需要命名委派工具,同时保留 Session id 用于执行和授权时,可选择此包。名称标识目录中已有的子 agent;创建失败不会永久占用所请求的名称。
|
||||
通过 `lead/research` 和 `lead/research/checker` 这样的可读名称寻址委派的 agent(智能体)。绑定名称在根 Session 重新加载后仍然保留,agent 执行结束后也保持原名称。委派工具需要名称,同时仍以 Session id 执行操作和检查授权时,可选择此包。未绑定名称的 agent 仍可通过 id 寻址。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -29,27 +29,25 @@ kind: "package-reference"
|
||||
|
||||
### 最小配置
|
||||
|
||||
在提供 `agents`、`subagents` 和 `sessionProjections` 的组合中添加此配置项:
|
||||
在提供 `subagents` 和 `sessionProjections` 的组合中添加此配置项:
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-subagent-names'
|
||||
```
|
||||
|
||||
此插件没有配置字段。它提供 `ctx.subagentNames`;现有 subagent 服务继续负责提供方、激活、消息和授权。名称查询和发现所需的父级读取要求挂载 `sessionQuery`;为根启动子 agent 以及读取根名称不需要它。
|
||||
此插件没有配置字段。它提供 `ctx.subagentNames`;subagent 服务负责执行、消息、发现和授权。
|
||||
|
||||
### 选择和查找名称
|
||||
|
||||
请求的同级名称以小写字母开头,只能包含小写字母、数字、下划线和连字符。省略名称时,从 `agent-1` 开始选择第一个可用的 `agent-N` 名称。名称不可变,且在直属父 agent 下唯一,并发启动也遵守此规则。已提交的子 agent 在执行结算后仍保留其名称。
|
||||
请求的名称段以小写字母开头,只能包含小写字母、数字、下划线和连字符。省略时,从 `agent-1` 开始选择父名称下第一个可用的 `agent-N`。预留可防止并发启动选择相同的完整名称。执行结算后,绑定名称仍被占用。
|
||||
|
||||
运行中树的根名称为 `lead`;子路径按父级逐段追加。目录中未命名的子 agent 使用 `legacy:<sessionId>` 作为路径段。查询接受完整的 `lead/...` 路径、`lead`,或树成员的确切 Session id,包括现有运行时通知中的 id。查询只解析身份;后续 subagent 操作仍会对调用方进行授权检查。
|
||||
根名称为 `lead`。`nameOf` 返回已绑定的完整名称;没有绑定时,返回原 Session id。`resolveName` 接受 `lead` 和已绑定的完整名称;它拒绝未知的 `lead/...` 名称,并透传其他 id。后续 subagent 操作仍会对调用方进行授权检查。
|
||||
|
||||
### 创建与失败
|
||||
### 启动后绑定
|
||||
|
||||
调用 `ctx.subagentNames.startActivation` 时使用 `delivery: 'parent'`,让现有目录保留子 agent 身份。需要由调用方接收结果时,使用 `ctx.subagents.startActivation`;该操作保留基于 id 的行为。
|
||||
调用 `reserve(parent, requested?)`,通过 `ctx.subagents.startActivation` 启动子 agent,再根据成功回执调用 `bind(parent, childId, reservation.name)`。无论启动和绑定成功还是失败,都要在 `finally` 中调用 `dispose()` 释放预留。
|
||||
|
||||
本地创建会在启动子 agent 前记录候选名称。只有现有父目录记录该子 agent 后,名称才可被发现。创建失败或崩溃遗留的候选记录,只要其子 agent 不在目录中,就既不可解析,也不占用名称。目录包含子 agent 后,即使启动随后失败,也不会释放其名称。
|
||||
|
||||
外部提供方分配自己的 id,因此在启动返回后才记录名称。如果记录失败,操作会尝试并等待 dispose(资源释放),然后报告已启动的子 agent id。已完成的外部工作无法撤销;若 dispose 也失败,执行可能仍在继续。重试前请检查该子 agent。
|
||||
绑定将完整名称记录到根 Session。若启动成功后绑定失败,调用方必须报告已启动的子 agent id;该子 agent 仍由 subagent 服务管理。绑定前崩溃也可能留下未命名的子 agent,它仍可通过 id 寻址。
|
||||
|
||||
-----
|
||||
|
||||
@@ -59,14 +57,14 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现内部细节 — 点击展开</summary>
|
||||
|
||||
插件拥有父级本地的名称记录,并将其与现有 subagent 目录关联。其投影排除 fork 继承的记录。待提交的同级名称预留属于插件实例;卸载时会先取消并等待进行中的命名启动,再释放其状态。
|
||||
每个根 Session 保存自己的名称到 id 绑定。投影恢复这些绑定,并排除 fork 继承的记录。查询读取根映射,无需观察祖先 Session 或关联子 agent 目录。调用方拥有临时预留,并在操作后释放它们。
|
||||
|
||||
名称读取通过 Session 观察沿创建祖先关系遍历,无需恢复父 agent。发现操作为 subagent 服务已有的后代条目附加相对路径。投影缺失或目录与名称记录不一致时会明确失败,不会提供未经确认的别名。
|
||||
映射为每个具名子 agent 保留一条绑定:空间为 O(n),单次查询或绑定为 O(n);由于每个事件都会搜索绑定数组,回放 n 条不同绑定的最坏时间复杂度为 O(n²)。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | 命名启动、预留生命周期、路径查询与发现 |
|
||||
| [`src/projection.ts`](src/projection.ts) | 持久候选记录与回放校验 |
|
||||
| [`src/index.ts`](src/index.ts) | 名称预留、绑定与查询 |
|
||||
| [`src/projection.ts`](src/projection.ts) | 持久绑定与回放校验 |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -75,48 +73,34 @@ kind: "package-reference"
|
||||
<a id="further-exploration"></a>
|
||||
## 进一步探索
|
||||
|
||||
以下页面负责执行行为以及使用名称的模型侧消费方。
|
||||
以下页面负责执行行为以及使用名称的模型侧工具。
|
||||
|
||||
- [Subagent 服务](../subagent/README.zh.md) — 激活所有权、目录与授权。
|
||||
- [委派工具](../tool-subagent/README.zh.md) — 命名创建与启动确认。
|
||||
- [控制工具](../tool-subagent-control/README.zh.md) — 发现、消息与中断。
|
||||
- [Session 投影](../../session/session-projection/README.zh.md) — 增量恢复与观察。
|
||||
- [Session 投影](../../session/session-projection/README.zh.md) — 增量恢复。
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
### 子 agent 初始身份
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
命名启动会在任务提示词后加入子 agent 及其父 agent 的完整名称。本地子 agent 在持久的初始用户消息中接收此文本;外部提供方在委派提示词中接收它。根下名为 `research` 的子 agent 会收到:
|
||||
|
||||
##### 身份消息
|
||||
|
||||
```markdown
|
||||
Your name is "lead/research". Your parent agent name is "lead".
|
||||
```
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个命名子 agent 的初始提示词增加一条简短的身份句;祖先路径越长,路径文本越多。此插件不注册工具 schema 或系统提示词段落。
|
||||
通过[委派工具](../tool-subagent/README.zh.md)和[控制工具](../tool-subagent-control/README.zh.md)间接影响模型;这些工具在 Session id 旁展示名称,并接受名称作为地址。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
身份文本作为新任务提示词的一部分,位于继承历史之后。已有历史保持不变;每个子 agent 接收各自的身份文本。
|
||||
此插件不添加提示词文本或工具 schema。消费方负责模型可见的名称文本及其缓存影响。
|
||||
|
||||
## 已知限制与待办工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
名称在现有执行身份之外提供补充,并存在以下限制:
|
||||
名称绑定存在以下限制:
|
||||
|
||||
- **直接调用 subagent 服务的创建仍未命名** — 绕过 `ctx.subagentNames` 的启动使用 `legacy:<sessionId>` 路径表示。
|
||||
- **外部名称绑定发生在执行启动之后** — 记录失败时,外部工作可能已经开始或完成。
|
||||
- **运行时通知保留 Session id** — 名称不会改写由 subagent 服务负责的消息或结算通知。
|
||||
- **目录中的子 agent 不可改名或复用名称** — 激活结算或 dispose 不会移除其持久目录成员记录。
|
||||
- **绑定后才可查询名称** — 子 agent 的首个请求可能早于绑定;此插件不添加初始身份消息。
|
||||
- **名称不可变** — 激活结束后,绑定的名称和 id 也不能重新分配或复用。
|
||||
- **路径采用预留时的父名称** — 未命名的父 agent 使用 `lead/legacy:<parentId>` 作为前缀;后续父级绑定不会改写子 agent 已存储的路径。
|
||||
- **命名需要显式绑定** — 不绑定名称的调用方继续使用 Session id。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-subagent-names",
|
||||
"description": "Optional catalog-backed names for subagent tools",
|
||||
"description": "Durable name mappings for subagent tools",
|
||||
"version": "0.2.1-rc.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -35,7 +35,6 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,14 +47,8 @@
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-session-query": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
/** Optional names for tool-owned subagent creation and discovery. @module */
|
||||
/** Durable aliases for subagent tools; execution remains owned by subagents. @module */
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import type { ProjectionHandle, ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
|
||||
import type {} from '@deepseek-ai/dsh-session-query'
|
||||
import type {
|
||||
SubagentActivation, SubagentActivationSpec, SubagentCatalogEntry, SubagentDescendantListEntry,
|
||||
} from '@deepseek-ai/dsh-subagent'
|
||||
import { nameSchema, subagentNamesProjection } from './projection.ts'
|
||||
import type { SubagentNameBinding, SubagentNamesState } from './projection.ts'
|
||||
import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import { fullNameSchema, nameSchema, subagentNamesProjection } from './projection.ts'
|
||||
import type { SubagentNamesState } from './projection.ts'
|
||||
|
||||
export type { SubagentNameBinding } from './projection.ts'
|
||||
|
||||
@@ -22,302 +17,117 @@ declare module '@deepseek-ai/cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Named children use parent delivery so the existing catalog retains their identities. */
|
||||
export type NamedSubagentActivationSpec = Omit<SubagentActivationSpec, 'childId' | 'delivery'> & {
|
||||
readonly delivery: 'parent'
|
||||
readonly name?: string
|
||||
}
|
||||
/** Stable categories for alias validation and lookup failures. */
|
||||
export type SubagentNameErrorCode = 'INVALID_AGENT_NAME' | 'DUPLICATE_AGENT_NAME' | 'AGENT_NAME_NOT_FOUND'
|
||||
|
||||
/** Stable categories for name validation, discovery, and accepted-start binding failures. */
|
||||
export type SubagentNameErrorCode =
|
||||
| 'INVALID_AGENT_NAME'
|
||||
| 'DUPLICATE_AGENT_NAME'
|
||||
| 'AGENT_NAME_NOT_FOUND'
|
||||
| 'CORRUPT_AGENT_NAMES'
|
||||
| 'AGENT_NAMES_UNAVAILABLE'
|
||||
| 'DUPLICATE_CHILD'
|
||||
| 'SUBAGENT_NAME_BIND_FAILED'
|
||||
|
||||
/** A name operation failed; an external binding failure identifies the already-started child. */
|
||||
/** An alias could not be reserved, bound, or resolved. */
|
||||
export class SubagentNameError extends HarnessError {
|
||||
constructor(message: string, code: SubagentNameErrorCode, options?: ErrorOptions, readonly childId?: SessionId) {
|
||||
super(message, code, options)
|
||||
constructor(message: string, code: SubagentNameErrorCode) {
|
||||
super(message, code)
|
||||
this.name = 'SubagentNameError'
|
||||
}
|
||||
}
|
||||
|
||||
interface ParentNames {
|
||||
readonly children: readonly SubagentCatalogEntry[]
|
||||
readonly bindings: readonly SubagentNameBinding[]
|
||||
/** A caller-owned name reservation; release it after startup and binding settle. */
|
||||
export interface SubagentNameReservation {
|
||||
readonly name: string
|
||||
/** Release the reservation; repeated disposal is harmless. */
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
interface Startup {
|
||||
readonly controller: AbortController
|
||||
readonly settled: Promise<void>
|
||||
}
|
||||
|
||||
/** Catalog-backed names that leave subagent execution and authorization with their existing owner. */
|
||||
/** Root-Session aliases shared by named creation, discovery, and control tools. */
|
||||
export class SubagentNames extends Service {
|
||||
static inject = ['agents', 'subagents', 'sessionProjections']
|
||||
static inject = ['subagents', 'sessionProjections']
|
||||
|
||||
private projection!: ProjectionHandle<SubagentNamesState>
|
||||
private readonly pending = new WeakMap<Agent, Set<string>>()
|
||||
private readonly initializing = new Map<SessionId, { readonly root: Agent; readonly name: string }>()
|
||||
private readonly startups = new Set<Startup>()
|
||||
private closing = false
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'subagentNames')
|
||||
}
|
||||
|
||||
protected async [Service.init](): Promise<void> {
|
||||
const ctx = this.ctx
|
||||
const registration = ctx.sessionProjections.register(subagentNamesProjection)
|
||||
const drain = async (): Promise<void> => {
|
||||
this.closing = true
|
||||
for (const startup of this.startups) startup.controller.abort(new Error('subagent naming is unloading'))
|
||||
await Promise.all([...this.startups].map(startup => startup.settled))
|
||||
}
|
||||
ctx.effect(function* () {
|
||||
// The registry's documented callable registration is its exact effect disposer.
|
||||
// A synchronous yield adopts it without awaiting its handle or replacing its identity.
|
||||
yield registration as typeof registration & (() => Promise<void>)
|
||||
yield ctx.effect(() => drain, 'subagentNames.startups()')
|
||||
}, 'subagentNames.lifetime()')
|
||||
this.projection = await registration
|
||||
this.projection = await this.ctx.sessionProjections.register(subagentNamesProjection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a named child, reserving its sibling name until startup settles.
|
||||
* Parent delivery retains catalog membership. Local names precede catalog publication;
|
||||
* external names follow accepted startup.
|
||||
* @param spec - ordinary activation options and an optional lower-case sibling name.
|
||||
* @returns the existing activation receipt plus its full name.
|
||||
* @throws SubagentNameError for invalid or duplicate names. An external binding failure
|
||||
* includes childId and awaits disposal without undoing completed work.
|
||||
* Reserve a complete name without starting an agent or writing a Session event.
|
||||
* @param parent - exact live caller whose name prefixes the child name.
|
||||
* @param requested - optional sibling segment; omission selects the first free agent-N.
|
||||
* @returns a reservation the caller must dispose after startup and binding.
|
||||
*/
|
||||
async startActivation(spec: NamedSubagentActivationSpec): Promise<SubagentActivation & { readonly name: string }> {
|
||||
this.assertOpen()
|
||||
const controller = new AbortController()
|
||||
const signal = AbortSignal.any([controller.signal, spec.signal])
|
||||
const settled = Promise.withResolvers<void>()
|
||||
const startup = { controller, settled: settled.promise }
|
||||
this.startups.add(startup)
|
||||
try {
|
||||
return await this.startNamed(spec, signal)
|
||||
} finally {
|
||||
this.startups.delete(startup)
|
||||
settled.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private async startNamed(
|
||||
spec: NamedSubagentActivationSpec,
|
||||
signal: AbortSignal,
|
||||
): Promise<SubagentActivation & { readonly name: string }> {
|
||||
const { name: requested, ...request } = spec
|
||||
reserve(parent: Agent, requested?: string): SubagentNameReservation {
|
||||
if (requested !== undefined && !nameSchema.safeParse(requested).success) {
|
||||
throw new SubagentNameError('agent name must start with a lower-case letter and contain only lower-case letters, digits, underscores, or hyphens', 'INVALID_AGENT_NAME')
|
||||
}
|
||||
const parent = spec.parent
|
||||
const root = this.ctx.subagents.rootOf(parent)
|
||||
const parentName = await this.nameOf(parent, signal)
|
||||
this.assertOpen()
|
||||
signal.throwIfAborted()
|
||||
this.ctx.subagents.rootOf(parent)
|
||||
const provider = this.ctx.subagents.getProvider(spec.provider)
|
||||
if (provider === undefined) throw new SubagentNameError(`subagent provider "${spec.provider}" is unavailable`, 'AGENT_NAMES_UNAVAILABLE')
|
||||
const pending = this.pending.get(parent) ?? new Set<string>()
|
||||
this.pending.set(parent, pending)
|
||||
const names = this.liveParentNames(parent.session)
|
||||
const occupied = new Set([...names.children.map(child => this.segment(names, child.id)), ...pending])
|
||||
const bindings = this.projection.stateOf(root.session).bindings
|
||||
const prefix = parent === root ? 'lead'
|
||||
: bindings.find(binding => binding.childId === parent.id)?.name ?? `lead/legacy:${parent.id}`
|
||||
const pending = this.pending.get(root) ?? new Set<string>()
|
||||
this.pending.set(root, pending)
|
||||
const occupied = new Set([...bindings.map(binding => binding.name), ...pending])
|
||||
let segment = requested
|
||||
if (segment === undefined) {
|
||||
let ordinal = 1
|
||||
while (occupied.has(`agent-${ordinal}`)) ordinal++
|
||||
while (occupied.has(`${prefix}/agent-${ordinal}`)) ordinal++
|
||||
segment = `agent-${ordinal}`
|
||||
}
|
||||
if (occupied.has(segment)) throw new SubagentNameError(`agent name "${segment}" is already in use under ${parentName}; choose another name`, 'DUPLICATE_AGENT_NAME')
|
||||
const name = `${parentName}/${segment}`
|
||||
pending.add(segment)
|
||||
const childId = provider.prepareContinuable === undefined ? undefined : SessionId(randomUUID())
|
||||
try {
|
||||
if (childId !== undefined) {
|
||||
parent.session.append('subagent-name/bound', { version: 1, childId, name: segment })
|
||||
this.initializing.set(childId, { root, name })
|
||||
}
|
||||
const activation = await this.ctx.subagents.startActivation({
|
||||
...request,
|
||||
...childId === undefined ? {} : { childId },
|
||||
signal,
|
||||
prompt: [
|
||||
...request.prompt,
|
||||
{ type: 'text', text: `Your name is ${JSON.stringify(name)}. Your parent agent name is ${JSON.stringify(parentName)}.` },
|
||||
],
|
||||
})
|
||||
if (childId === undefined) {
|
||||
try {
|
||||
signal.throwIfAborted()
|
||||
this.ctx.subagents.rootOf(parent)
|
||||
if (this.projection.stateOf(parent.session).bindings.some(binding => binding.childId === activation.childId)) {
|
||||
throw new SubagentNameError(`subagent identity ${activation.childId} already has a name candidate`, 'DUPLICATE_CHILD')
|
||||
}
|
||||
parent.session.append('subagent-name/bound', { version: 1, childId: activation.childId, name: segment })
|
||||
} catch (error: unknown) {
|
||||
let cause = error
|
||||
let disposition = 'Its activation has been disposed; completed work cannot be undone.'
|
||||
try {
|
||||
await activation.dispose()
|
||||
} catch (cleanupError: unknown) {
|
||||
cause = new AggregateError([error, cleanupError], 'External subagent name binding and disposal failed', { cause: error })
|
||||
disposition = 'Its disposal also failed; execution may still be active.'
|
||||
}
|
||||
throw new SubagentNameError(
|
||||
`Subagent ${activation.childId} started but its name ${name} could not be recorded. ${disposition} Check this child before retrying.`,
|
||||
'SUBAGENT_NAME_BIND_FAILED', { cause }, activation.childId,
|
||||
)
|
||||
}
|
||||
}
|
||||
return { ...activation, name }
|
||||
} finally {
|
||||
pending.delete(segment)
|
||||
if (childId !== undefined) this.initializing.delete(childId)
|
||||
}
|
||||
const name = `${prefix}/${segment}`
|
||||
if (occupied.has(name)) throw new SubagentNameError(`agent name "${name}" is already in use; choose another name`, 'DUPLICATE_AGENT_NAME')
|
||||
pending.add(name)
|
||||
let active = true
|
||||
return { name, dispose: () => {
|
||||
if (!active) return
|
||||
active = false
|
||||
pending.delete(name)
|
||||
} }
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the full name of an exact live Agent without restoring its ancestors.
|
||||
* @param agent - live root or managed child whose name is requested.
|
||||
* @param signal - cancellation during parent catalog reads.
|
||||
* @returns lead, named path segments, or legacy id segments for unnamed children.
|
||||
* Record an accepted child's full name in the caller's root Session.
|
||||
* Binding failure does not cancel or dispose the child; callers report its id.
|
||||
* @param parent - exact live caller whose root owns the alias.
|
||||
* @param childId - identity returned by the subagent service.
|
||||
* @param name - complete name returned by reserve.
|
||||
*/
|
||||
async nameOf(agent: Agent, signal?: AbortSignal): Promise<string> {
|
||||
this.assertOpen()
|
||||
signal?.throwIfAborted()
|
||||
const pending = this.initializing.get(agent.id)
|
||||
if (pending !== undefined && this.ctx.agents.get(agent.id) === agent
|
||||
&& this.ctx.agents.get(pending.root.id) === pending.root) return pending.name
|
||||
const root = this.ctx.subagents.rootOf(agent)
|
||||
if (root === agent) return 'lead'
|
||||
const segments: string[] = []
|
||||
const visited = new Set<SessionId>([agent.id])
|
||||
let childId = agent.id
|
||||
let parentId = agent.session.header.parentSession
|
||||
while (parentId !== undefined) {
|
||||
if (visited.has(parentId)) throw new SubagentNameError('agent naming found cyclic creation ancestry', 'CORRUPT_AGENT_NAMES')
|
||||
visited.add(parentId)
|
||||
using parent = await this.query().observeSession(parentId, signal === undefined ? {} : { signal })
|
||||
signal?.throwIfAborted()
|
||||
segments.unshift(this.segment(this.observedNames(parent.projections?.values), childId))
|
||||
if (parentId === root.id) {
|
||||
this.ctx.subagents.rootOf(agent)
|
||||
return ['lead', ...segments].join('/')
|
||||
}
|
||||
childId = parentId
|
||||
parentId = parent.header.parentSession
|
||||
bind(parent: Agent, childId: SessionId, name: string): void {
|
||||
if (!fullNameSchema.safeParse(name).success) throw new SubagentNameError('a bound agent name must be a complete lead/... path', 'INVALID_AGENT_NAME')
|
||||
const root = this.ctx.subagents.rootOf(parent)
|
||||
const bindings = this.projection.stateOf(root.session).bindings
|
||||
const existing = bindings.find(binding => binding.childId === childId || binding.name === name)
|
||||
if (existing !== undefined) {
|
||||
if (existing.childId === childId && existing.name === name) return
|
||||
throw new SubagentNameError(`agent name "${name}" or identity ${childId} is already bound`, 'DUPLICATE_AGENT_NAME')
|
||||
}
|
||||
throw new SubagentNameError('agent is absent from the root creation ancestry', 'AGENT_NAMES_UNAVAILABLE')
|
||||
root.session.append('subagent-name/bound', { version: 1, childId, name })
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a canonical path or a tree member's exact id without granting authority.
|
||||
* @param caller - exact live Agent selecting the subagent tree.
|
||||
* @param name - lead, a lead-prefixed path, or an existing member's exact Session id.
|
||||
* @param signal - cancellation during catalog reads.
|
||||
* @returns the identity for the subagent operation to authorize.
|
||||
* Read a bound name from the live root's projection without loading ancestors.
|
||||
* @param caller - exact live caller selecting the tree.
|
||||
* @param targetId - requested identity; omission selects the caller.
|
||||
* @returns lead for the root, a bound full name, or the unchanged id.
|
||||
*/
|
||||
async resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId> {
|
||||
this.assertOpen()
|
||||
signal?.throwIfAborted()
|
||||
nameOf(caller: Agent, targetId: SessionId = caller.id): string {
|
||||
const root = this.ctx.subagents.rootOf(caller)
|
||||
if (name === 'lead' || name === root.id) return root.id
|
||||
if (!name.startsWith('lead/')) {
|
||||
const entries = await this.ctx.subagents.listDescendants(root.id, signal)
|
||||
const entry = entries.find(candidate => candidate.id === name)
|
||||
if (entry !== undefined) return entry.id
|
||||
} else {
|
||||
let parentId = root.id
|
||||
const segments = name.slice('lead/'.length).split('/')
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
const parent = await this.readParentNames(parentId, signal)
|
||||
const child = parent.children.find(child => this.segment(parent, child.id) === segment)
|
||||
if (child === undefined) break
|
||||
if (index === segments.length - 1) return child.id
|
||||
if (child.mode === 'external') break
|
||||
parentId = child.id
|
||||
}
|
||||
}
|
||||
throw new SubagentNameError(`agent "${name}" was not found; use list_agents to discover names or ids`, 'AGENT_NAME_NOT_FOUND')
|
||||
if (targetId === root.id) return 'lead'
|
||||
return this.projection.stateOf(root.session).bindings.find(binding => binding.childId === targetId)?.name ?? targetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach relative names to the existing catalog traversal and its branch diagnostics.
|
||||
* @param rootId - Session whose catalog starts discovery.
|
||||
* @param signal - cancellation while reading descendant and name projections.
|
||||
* @returns existing descendant rows with paths relative to rootId.
|
||||
* Resolve an alias or pass through an id for the subagent service to authorize.
|
||||
* @param caller - exact live caller selecting the root mapping.
|
||||
* @param name - lead, a bound full name, or a raw Session id.
|
||||
* @returns the target identity; an unknown lead/... name throws.
|
||||
*/
|
||||
async listDescendants(rootId: SessionId, signal?: AbortSignal): Promise<(SubagentDescendantListEntry & { readonly path: string })[]> {
|
||||
this.assertOpen()
|
||||
const entries = await this.ctx.subagents.listDescendants(rootId, signal)
|
||||
const paths = new Map<SessionId, string>([[rootId, '']])
|
||||
const parents = new Map<SessionId, ParentNames>()
|
||||
const result: (SubagentDescendantListEntry & { readonly path: string })[] = []
|
||||
for (const entry of entries) {
|
||||
let parent = parents.get(entry.parentId)
|
||||
if (parent === undefined) {
|
||||
parent = await this.readParentNames(entry.parentId, signal)
|
||||
parents.set(entry.parentId, parent)
|
||||
}
|
||||
// The subagent traversal emits parents before their descendants.
|
||||
const prefix = paths.get(entry.parentId) as string
|
||||
const segment = this.segment(parent, entry.id)
|
||||
const path = prefix.length === 0 ? segment : `${prefix}/${segment}`
|
||||
paths.set(entry.id, path)
|
||||
result.push({ ...entry, path })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private liveParentNames(session: Session): ParentNames {
|
||||
// The required subagents service registers this projection before becoming available.
|
||||
const children = this.ctx.sessionProjections.snapshot(session, ['subagentCatalog']).values.subagentCatalog as SubagentCatalogEntry[]
|
||||
return { children, bindings: this.projection.stateOf(session).bindings }
|
||||
}
|
||||
|
||||
private async readParentNames(id: SessionId, signal?: AbortSignal): Promise<ParentNames> {
|
||||
signal?.throwIfAborted()
|
||||
using parent = await this.query().observeSession(id, signal === undefined ? {} : { signal })
|
||||
signal?.throwIfAborted()
|
||||
return this.observedNames(parent.projections?.values)
|
||||
}
|
||||
|
||||
private observedNames(values: ProjectionSnapshot['values'] | undefined): ParentNames {
|
||||
if (values?.subagentCatalog === undefined || values.subagentNames === undefined) {
|
||||
throw new SubagentNameError('subagent catalog or name projection is unavailable', 'AGENT_NAMES_UNAVAILABLE')
|
||||
}
|
||||
return { children: values.subagentCatalog, bindings: values.subagentNames }
|
||||
}
|
||||
|
||||
private segment(parent: ParentNames, childId: SessionId): string {
|
||||
if (!parent.children.some(child => child.id === childId)) {
|
||||
throw new SubagentNameError('agent is absent from its parent catalog', 'AGENT_NAMES_UNAVAILABLE')
|
||||
}
|
||||
const name = parent.bindings.find(binding => binding.childId === childId)?.name
|
||||
if (name !== undefined && parent.bindings.some(binding => binding.name === name && binding.childId !== childId
|
||||
&& parent.children.some(child => child.id === binding.childId))) {
|
||||
throw new SubagentNameError(`agent name "${name}" refers to multiple catalog children`, 'CORRUPT_AGENT_NAMES')
|
||||
}
|
||||
return name ?? `legacy:${childId}`
|
||||
}
|
||||
|
||||
private assertOpen(): void {
|
||||
if (this.closing) throw new SubagentNameError('subagent naming is unavailable while its plugin unloads', 'AGENT_NAMES_UNAVAILABLE')
|
||||
}
|
||||
|
||||
private query(): Context['sessionQuery'] {
|
||||
const query = this.ctx.get('sessionQuery')
|
||||
if (query === undefined) throw new SubagentNameError('agent naming requires the sessionQuery service for parent reads', 'AGENT_NAMES_UNAVAILABLE')
|
||||
return query
|
||||
resolveName(caller: Agent, name: string): SessionId {
|
||||
const root = this.ctx.subagents.rootOf(caller)
|
||||
if (name === 'lead') return root.id
|
||||
const binding = this.projection.stateOf(root.session).bindings.find(binding => binding.name === name)
|
||||
if (binding !== undefined) return binding.childId
|
||||
if (name.startsWith('lead/')) throw new SubagentNameError(`agent "${name}" was not found; use list_agents to discover names or ids`, 'AGENT_NAME_NOT_FOUND')
|
||||
return SessionId(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** Parent-owned alias candidates; the subagent catalog establishes usable membership. @module */
|
||||
/** Root-Session aliases restored independently of subagent execution. @module */
|
||||
|
||||
import { z } from 'zod'
|
||||
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
/** One immutable sibling name requested for a child identity. */
|
||||
/** One immutable full name bound to an accepted child identity. */
|
||||
export interface SubagentNameBinding {
|
||||
readonly childId: SessionId
|
||||
readonly name: string
|
||||
@@ -14,8 +14,8 @@ export interface SubagentNameBinding {
|
||||
declare module '@deepseek-ai/dsh-session/types' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* A name candidate, usable only while the parent catalog contains its child.
|
||||
* @param data - versioned child identity and sibling name.
|
||||
* A root-owned alias for an accepted subagent identity.
|
||||
* @param data - versioned child identity and complete name.
|
||||
*/
|
||||
'subagent-name/bound': SubagentNameBinding & { readonly version: 1 }
|
||||
}
|
||||
@@ -27,24 +27,28 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Names use one lower-case path segment; legacy ids have a separate prefix. */
|
||||
/** Requested sibling names use one lower-case path segment. */
|
||||
export const nameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/)
|
||||
|
||||
const bindingSchema = z.object({ childId: z.string().transform(SessionId), name: nameSchema }).strict()
|
||||
/** Bound paths include the root prefix and may include an unnamed parent's id. */
|
||||
export const fullNameSchema = z.string().startsWith('lead/').min(6)
|
||||
|
||||
const bindingSchema = z.object({ childId: z.string().transform(SessionId), name: fullNameSchema }).strict()
|
||||
const boundSchema = bindingSchema.extend({ version: z.literal(1) })
|
||||
const bindingsSchema = z.array(bindingSchema).refine(
|
||||
bindings => new Set(bindings.map(binding => binding.childId)).size === bindings.length,
|
||||
'each child identity must have one name binding',
|
||||
bindings => new Set(bindings.map(binding => binding.childId)).size === bindings.length
|
||||
&& new Set(bindings.map(binding => binding.name)).size === bindings.length,
|
||||
'child identities and names must each be unique',
|
||||
)
|
||||
const stateSchema = z.object({
|
||||
inheritedEventCount: z.number().int().nonnegative().transform(SessionLogOffset),
|
||||
bindings: bindingsSchema,
|
||||
}).strict()
|
||||
|
||||
/** Recoverable direct-parent alias candidates, excluding inherited fork records. */
|
||||
/** Recoverable root aliases, excluding inherited fork records. */
|
||||
export type SubagentNamesState = z.infer<typeof stateSchema>
|
||||
|
||||
/** Alias recovery is pure; catalog membership is checked when a name is read. */
|
||||
/** Alias recovery does not start agents or read other Sessions. */
|
||||
export const subagentNamesProjection = {
|
||||
key: 'subagentNames',
|
||||
stateVersion: 1,
|
||||
@@ -56,9 +60,9 @@ export const subagentNamesProjection = {
|
||||
if (event.seq < state.inheritedEventCount) return state
|
||||
if (event.type === 'subagent-name/bound') {
|
||||
const { version: _version, ...binding } = boundSchema.parse(event.data)
|
||||
const previous = state.bindings.find(entry => entry.childId === binding.childId)
|
||||
const previous = state.bindings.find(entry => entry.childId === binding.childId || entry.name === binding.name)
|
||||
if (previous !== undefined) {
|
||||
if (previous.name !== binding.name) throw new Error(`subagent name for ${binding.childId} is immutable`)
|
||||
if (previous.name !== binding.name || previous.childId !== binding.childId) throw new Error('subagent names and identities must have one immutable binding')
|
||||
return state
|
||||
}
|
||||
return { ...state, bindings: [...state.bindings, binding] }
|
||||
|
||||
@@ -2,18 +2,15 @@ import { randomUUID } from 'node:crypto'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
|
||||
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { mountLocalActivations } from '../../subagent/tests/local-activation.ts'
|
||||
import { bindSubagentActivation } from '../../subagent/tests/manager-internals.ts'
|
||||
import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts'
|
||||
import { TestSessionQuery } from '../../subagent/tests/test-session-query.ts'
|
||||
import SubagentNames from '../src/index.ts'
|
||||
import type { NamedSubagentActivationSpec } from '../src/index.ts'
|
||||
|
||||
async function setup(script: ConstructorParameters<typeof MockAdapter>[0] = []) {
|
||||
const ctx = new Context()
|
||||
@@ -21,382 +18,155 @@ async function setup(script: ConstructorParameters<typeof MockAdapter>[0] = [])
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await mountLocalActivations(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
const queryFiber = await ctx.plugin(TestSessionQuery)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
const namesFiber = await ctx.plugin(SubagentNames)
|
||||
const adapter = new MockAdapter(script)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const parent = await ctx.agentLoop.create(SessionId(randomUUID()), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/pre-step', async ({ agent }, next) => agent === parent ? { kind: 'reject' } : next())
|
||||
const start = (overrides: Partial<NamedSubagentActivationSpec> = {}) => ctx.subagentNames.startActivation({
|
||||
provider: 'spawn', label: 'Review', parent, delivery: 'parent',
|
||||
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
|
||||
const parentHandle = await ctx.agents.create({
|
||||
sessionId: SessionId(randomUUID()), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const parent = parentHandle.agent
|
||||
ctx.on('agent/pre-step', async ({ agent }, next) => agent.id === parent.id ? { kind: 'reject' } : next())
|
||||
const start = (creator: Agent = parent) => ctx.subagents.startActivation({
|
||||
provider: 'spawn', label: 'Review', parent: creator, delivery: 'parent',
|
||||
prompt: [{ type: 'text', text: 'Review the change' }], signal: new AbortController().signal,
|
||||
...overrides,
|
||||
})
|
||||
const catalog = () => ctx.sessionProjections.snapshot(parent.session, ['subagentCatalog']).values.subagentCatalog
|
||||
return { ctx, parent, namesFiber, queryFiber, adapter, start, catalog }
|
||||
return { ctx, parent, parentHandle, namesFiber, start }
|
||||
}
|
||||
|
||||
function localProvider(ctx: Context) {
|
||||
const provider = ctx.subagents.getProvider('spawn')
|
||||
if (provider?.prepareContinuable === undefined) throw new Error('spawn preparation is unavailable')
|
||||
return provider
|
||||
}
|
||||
describe('root Session name mappings', () => {
|
||||
it('binds an accepted child without changing its id, prompt, or execution', async () => {
|
||||
const { ctx, parent, start } = await setup([textResponse('done')])
|
||||
const reservation = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
const activation = await start()
|
||||
expect(ctx.subagentNames.nameOf(parent, activation.childId)).toBe(activation.childId)
|
||||
ctx.subagentNames.bind(parent, activation.childId, reservation.name)
|
||||
reservation.dispose()
|
||||
await activation.result
|
||||
|
||||
function externalRun() {
|
||||
const result = Promise.withResolvers<SubagentResult>()
|
||||
const dispose = vi.fn(async () => { result.resolve({ output: [], stopReason: 'aborted' }) })
|
||||
const run: SubagentRun = { id: SessionId(randomUUID()), result: result.promise, dispose }
|
||||
return { result, dispose, run }
|
||||
}
|
||||
|
||||
function registerExternal(ctx: Context, run: SubagentRun): void {
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'external', inheritsParentContext: false,
|
||||
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
|
||||
start: async () => run,
|
||||
})
|
||||
}
|
||||
|
||||
describe('catalog-backed subagent names', () => {
|
||||
it('allocates names, preserves explicit names, and resolves the same identities by name or id', async () => {
|
||||
const { ctx, parent, start } = await setup([textResponse('first'), textResponse('second'), textResponse('third')])
|
||||
expect(await ctx.subagentNames.nameOf(parent)).toBe('lead')
|
||||
const first = await start()
|
||||
await first.result
|
||||
const second = await start({ name: 'reviewer' })
|
||||
await second.result
|
||||
const third = await start()
|
||||
await third.result
|
||||
|
||||
expect([first.name, second.name, third.name]).toEqual(['lead/agent-1', 'lead/reviewer', 'lead/agent-2'])
|
||||
for (const activation of [first, second, third]) {
|
||||
expect(await ctx.subagentNames.resolveName(parent, activation.name)).toBe(activation.childId)
|
||||
expect(await ctx.subagentNames.resolveName(parent, activation.childId)).toBe(activation.childId)
|
||||
}
|
||||
expect(await ctx.subagentNames.resolveName(parent, 'lead')).toBe(parent.id)
|
||||
expect(await ctx.subagentNames.resolveName(parent, parent.id)).toBe(parent.id)
|
||||
expect(await ctx.subagentNames.listDescendants(parent.id)).toMatchObject([
|
||||
{ id: first.childId, parentId: parent.id, path: 'agent-1' },
|
||||
{ id: second.childId, parentId: parent.id, path: 'reviewer' },
|
||||
{ id: third.childId, parentId: parent.id, path: 'agent-2' },
|
||||
])
|
||||
const stored = await loadStoredSession(ctx.sessionPersistence, first.childId)
|
||||
const initialMessage = stored.events.find(event => event.type === 'user/message')
|
||||
expect(initialMessage?.data.content.slice(0, 2)).toEqual([
|
||||
{ type: 'text', text: 'Review the change' },
|
||||
{ type: 'text', text: 'Your name is "lead/agent-1". Your parent agent name is "lead".' },
|
||||
])
|
||||
expect(ctx.subagentNames.nameOf(parent)).toBe('lead')
|
||||
expect(ctx.subagentNames.nameOf(parent, activation.childId)).toBe('lead/reviewer')
|
||||
expect(ctx.subagentNames.resolveName(parent, 'lead')).toBe(parent.id)
|
||||
expect(ctx.subagentNames.resolveName(parent, 'lead/reviewer')).toBe(activation.childId)
|
||||
expect(ctx.subagentNames.resolveName(parent, activation.childId)).toBe(activation.childId)
|
||||
expect(ctx.subagentNames.resolveName(parent, 'unmapped-id')).toBe('unmapped-id')
|
||||
expect(() => ctx.subagentNames.resolveName(parent, 'lead/misspelled')).toThrow('was not found')
|
||||
const stored = await loadStoredSession(ctx.sessionPersistence, activation.childId)
|
||||
const message = stored.events.find(event => event.type === 'user/message')
|
||||
expect(message?.data.content[0]).toEqual({ type: 'text', text: 'Review the change' })
|
||||
expect(JSON.stringify(message)).not.toContain('Your name is')
|
||||
expect(stored.events.some(event => event.type === 'subagent-name/bound')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps unnamed SDK children discoverable by their legacy path and exact id', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('legacy')])
|
||||
const legacy = await ctx.subagents.startActivation({
|
||||
provider: 'spawn', label: 'SDK child', parent, delivery: 'caller',
|
||||
prompt: [{ type: 'text', text: 'SDK work' }], signal: new AbortController().signal,
|
||||
})
|
||||
await legacy.result
|
||||
expect(await ctx.subagentNames.resolveName(parent, `lead/legacy:${legacy.childId}`)).toBe(legacy.childId)
|
||||
expect(await ctx.subagentNames.resolveName(parent, legacy.childId)).toBe(legacy.childId)
|
||||
expect(await ctx.subagentNames.listDescendants(parent.id)).toMatchObject([{ id: legacy.childId, path: `legacy:${legacy.childId}` }])
|
||||
})
|
||||
|
||||
it('keeps root naming and creation available without query while failing parent reads explicitly', async () => {
|
||||
const { ctx, parent, queryFiber, start } = await setup([textResponse('child')])
|
||||
await queryFiber.dispose()
|
||||
expect(await ctx.subagentNames.nameOf(parent)).toBe('lead')
|
||||
const child = await start({ name: 'reviewer' })
|
||||
await child.result
|
||||
await expect(ctx.subagentNames.resolveName(parent, child.name)).rejects.toMatchObject({ code: 'AGENT_NAMES_UNAVAILABLE' })
|
||||
})
|
||||
|
||||
it('rejects observations that do not supply the naming projections', async () => {
|
||||
const { ctx, parent, start } = await setup([textResponse('child')])
|
||||
const child = await start({ name: 'reviewer' })
|
||||
await child.result
|
||||
const observe = ctx.sessionQuery.observeSession.bind(ctx.sessionQuery)
|
||||
vi.spyOn(ctx.sessionQuery, 'observeSession').mockImplementation((id, options) =>
|
||||
observe(id, { ...options, projectionMode: 'none' }),
|
||||
)
|
||||
await expect(ctx.subagentNames.resolveName(parent, child.name)).rejects.toMatchObject({ code: 'AGENT_NAMES_UNAVAILABLE' })
|
||||
})
|
||||
|
||||
it.each(['cyclic', 'missing-ancestry', 'uncatalogued'] as const)(
|
||||
'rejects %s creation ancestry instead of inventing a name', async (kind) => {
|
||||
const { ctx, parent } = await setup()
|
||||
const id = SessionId(randomUUID())
|
||||
const child = await ctx.agents.create({
|
||||
sessionId: id,
|
||||
meta: { origin: 'subagent', ...kind === 'missing-ancestry' ? {} : { parentSession: kind === 'cyclic' ? id : parent.id } },
|
||||
})
|
||||
bindSubagentActivation(ctx, child.agent, parent)
|
||||
await expect(ctx.subagentNames.nameOf(child.agent)).rejects.toMatchObject({
|
||||
code: kind === 'cyclic' ? 'CORRUPT_AGENT_NAMES' : 'AGENT_NAMES_UNAVAILABLE',
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
it('rejects two cataloged children that claim the same sibling name', async () => {
|
||||
const { ctx, parent, start } = await setup()
|
||||
for (let index = 0; index < 2; index++) {
|
||||
const childId = SessionId(randomUUID())
|
||||
parent.session.append('subagent-name/bound', { version: 1, childId, name: 'reviewer' })
|
||||
parent.session.append('subagent/catalog', { version: 2, childId, childCreatedAt: 1, mode: 'external' })
|
||||
}
|
||||
await expect(ctx.subagentNames.resolveName(parent, 'lead/reviewer')).rejects.toMatchObject({ code: 'CORRUPT_AGENT_NAMES' })
|
||||
await expect(start()).rejects.toMatchObject({ code: 'CORRUPT_AGENT_NAMES' })
|
||||
it('reserves distinct defaults, rejects overlap, and reuses released unbound names', async () => {
|
||||
const { ctx, parent } = await setup()
|
||||
const first = ctx.subagentNames.reserve(parent)
|
||||
const second = ctx.subagentNames.reserve(parent)
|
||||
expect([first.name, second.name]).toEqual(['lead/agent-1', 'lead/agent-2'])
|
||||
expect(() => ctx.subagentNames.reserve(parent, 'agent-1')).toThrow('already in use')
|
||||
first.dispose()
|
||||
first.dispose()
|
||||
const retry = ctx.subagentNames.reserve(parent)
|
||||
expect(retry.name).toBe('lead/agent-1')
|
||||
first.dispose()
|
||||
expect(() => ctx.subagentNames.reserve(parent, 'agent-1')).toThrow('already in use')
|
||||
ctx.subagentNames.bind(parent, SessionId('accepted-child'), retry.name)
|
||||
retry.dispose()
|
||||
expect(() => ctx.subagentNames.reserve(parent, 'agent-1')).toThrow('already in use')
|
||||
second.dispose()
|
||||
const available = ctx.subagentNames.reserve(parent)
|
||||
expect(available.name).toBe('lead/agent-2')
|
||||
available.dispose()
|
||||
})
|
||||
|
||||
it.each(['', 'Reviewer', 'lead/reviewer', 'two words', '1reviewer', 'legacy:child'])(
|
||||
'rejects invalid sibling name %j before preparing a child', async (name) => {
|
||||
const { ctx, start, catalog } = await setup()
|
||||
const prepare = vi.spyOn(localProvider(ctx), 'prepareContinuable')
|
||||
await expect(start({ name })).rejects.toMatchObject({ code: 'INVALID_AGENT_NAME' })
|
||||
expect(prepare).not.toHaveBeenCalled()
|
||||
expect(catalog()).toEqual([])
|
||||
'rejects invalid requested segment %j before reserving it', async (name) => {
|
||||
const { ctx, parent } = await setup()
|
||||
expect(() => ctx.subagentNames.reserve(parent, name)).toThrow('agent name must')
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toEqual([])
|
||||
},
|
||||
)
|
||||
|
||||
it('holds the sibling reservation throughout overlapping provider preparation', async () => {
|
||||
const { ctx, start, catalog } = await setup([textResponse('first'), textResponse('second')])
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const prepare = vi.spyOn(localProvider(ctx), 'prepareContinuable').mockImplementationOnce(async () => {
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
return {}
|
||||
})
|
||||
const first = start({ name: 'reviewer' })
|
||||
try {
|
||||
await entered.promise
|
||||
await expect(start({ name: 'reviewer' })).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' })
|
||||
expect(catalog()).toEqual([])
|
||||
expect(prepare).toHaveBeenCalledTimes(1)
|
||||
const independent = await start({ name: 'writer' })
|
||||
await independent.result
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
}
|
||||
const accepted = await first
|
||||
await accepted.result
|
||||
expect(accepted.name).toBe('lead/reviewer')
|
||||
await expect(start({ name: 'reviewer' })).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' })
|
||||
it('keeps exact bindings idempotent while rejecting name and identity conflicts', async () => {
|
||||
const { ctx, parent } = await setup()
|
||||
const id = SessionId('accepted-child')
|
||||
expect(() => { ctx.subagentNames.bind(parent, id, 'reviewer') }).toThrow('complete lead/... path')
|
||||
ctx.subagentNames.bind(parent, id, 'lead/reviewer')
|
||||
const seq = parent.session.seq
|
||||
ctx.subagentNames.bind(parent, id, 'lead/reviewer')
|
||||
expect(parent.session.seq).toBe(seq)
|
||||
expect(() => { ctx.subagentNames.bind(parent, id, 'lead/renamed') }).toThrow('already bound')
|
||||
expect(() => { ctx.subagentNames.bind(parent, SessionId('another-child'), 'lead/reviewer') }).toThrow('already bound')
|
||||
})
|
||||
|
||||
it('does not occupy a name when its candidate append fails', async () => {
|
||||
const { ctx, parent, start, catalog } = await setup([textResponse('retry')])
|
||||
const failure = new Error('name append failed')
|
||||
const append = vi.spyOn(parent.session, 'append').mockImplementationOnce(() => { throw failure })
|
||||
await expect(start({ name: 'reviewer' })).rejects.toBe(failure)
|
||||
append.mockRestore()
|
||||
expect(catalog()).toEqual([])
|
||||
const retry = await start({ name: 'reviewer' })
|
||||
await retry.result
|
||||
expect(await ctx.subagentNames.resolveName(parent, retry.name)).toBe(retry.childId)
|
||||
})
|
||||
|
||||
it('ignores a candidate without a catalog child and reuses its name after failed preparation', async () => {
|
||||
const { ctx, parent, start, catalog } = await setup([textResponse('retry')])
|
||||
const failure = new Error('provider preparation failed')
|
||||
vi.spyOn(localProvider(ctx), 'prepareContinuable').mockRejectedValueOnce(failure)
|
||||
await expect(start({ name: 'reviewer' })).rejects.toBe(failure)
|
||||
expect(catalog()).toEqual([])
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toHaveLength(1)
|
||||
await expect(ctx.subagentNames.resolveName(parent, 'lead/reviewer')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
|
||||
const retry = await start({ name: 'reviewer' })
|
||||
await retry.result
|
||||
expect(await ctx.subagentNames.resolveName(parent, retry.name)).toBe(retry.childId)
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('retains the name when catalog publication commits before startup reports failure', async () => {
|
||||
const { ctx, parent, start, catalog } = await setup(['hang'])
|
||||
const failure = new Error('catalog acknowledgement failed')
|
||||
const append = parent.session.append.bind(parent.session)
|
||||
const spy = vi.spyOn(parent.session, 'append').mockImplementation((...args) => {
|
||||
const event = Reflect.apply(append, parent.session, args) as ReturnType<typeof append>
|
||||
if (args[0] === 'subagent/catalog') throw failure
|
||||
return event
|
||||
})
|
||||
await expect(start({ name: 'reviewer' })).rejects.toBe(failure)
|
||||
spy.mockRestore()
|
||||
const childId = catalog()?.[0]?.id
|
||||
expect(childId).toBeDefined()
|
||||
expect(await ctx.subagentNames.resolveName(parent, 'lead/reviewer')).toBe(childId)
|
||||
await expect(start({ name: 'reviewer' })).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' })
|
||||
})
|
||||
|
||||
it('reads a resumed descendant name through its cold parent without restoring the parent', async () => {
|
||||
const { ctx, parent, adapter, start } = await setup(['hang', 'hang', 'hang'])
|
||||
const creator = await start({ name: 'reviewer' })
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
const creatorAgent = ctx.agents.get(creator.childId)
|
||||
expect(creatorAgent).toBeDefined()
|
||||
const leaf = await start({ parent: creatorAgent!, name: 'facts' })
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(2) })
|
||||
const leafAgent = ctx.agents.get(leaf.childId)
|
||||
expect(await ctx.subagentNames.nameOf(leafAgent!)).toBe('lead/reviewer/facts')
|
||||
await leaf.dispose()
|
||||
it('records nested aliases only on the root and restores them without ancestor reads', async () => {
|
||||
const { ctx, parent, parentHandle, start } = await setup(['hang', 'hang'])
|
||||
const creator = await start()
|
||||
ctx.subagentNames.bind(parent, creator.childId, 'lead/reviewer')
|
||||
const creatorAgent = ctx.agents.get(creator.childId)!
|
||||
const reservation = ctx.subagentNames.reserve(creatorAgent, 'facts')
|
||||
const leaf = await start(creatorAgent)
|
||||
ctx.subagentNames.bind(creatorAgent, leaf.childId, reservation.name)
|
||||
reservation.dispose()
|
||||
expect(ctx.subagentNames.nameOf(creatorAgent)).toBe('lead/reviewer')
|
||||
expect(ctx.sessionProjections.snapshot(creatorAgent.session).values.subagentNames).toEqual([])
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toEqual([
|
||||
{ childId: creator.childId, name: 'lead/reviewer' },
|
||||
{ childId: leaf.childId, name: 'lead/reviewer/facts' },
|
||||
])
|
||||
await creator.dispose()
|
||||
expect(ctx.agents.get(creator.childId)).toBeUndefined()
|
||||
expect(ctx.agents.get(leaf.childId)).toBeUndefined()
|
||||
|
||||
await ctx.subagents.sendMessage(parent, leaf.childId, [{ type: 'text', text: 'Check again' }], { signal: new AbortController().signal })
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(3) })
|
||||
const restored = ctx.agents.get(leaf.childId)
|
||||
expect(restored).toBeDefined()
|
||||
expect(await ctx.subagentNames.nameOf(restored!)).toBe('lead/reviewer/facts')
|
||||
expect(await ctx.subagentNames.resolveName(parent, 'lead/reviewer/facts')).toBe(leaf.childId)
|
||||
expect(await ctx.subagentNames.listDescendants(parent.id)).toMatchObject([
|
||||
{ id: creator.childId, path: 'reviewer' }, { id: leaf.childId, path: 'reviewer/facts' },
|
||||
])
|
||||
expect(ctx.agents.get(creator.childId)).toBeUndefined()
|
||||
expect(ctx.subagentNames.nameOf(parent, leaf.childId)).toBe('lead/reviewer/facts')
|
||||
await parentHandle.dispose()
|
||||
const restored = await ctx.agents.resume({ resumeSessionId: parent.id })
|
||||
expect(ctx.subagentNames.resolveName(restored.agent, 'lead/reviewer/facts')).toBe(leaf.childId)
|
||||
expect(ctx.get('sessionQuery')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('binds an external child only after the accepted provider identity is known', async () => {
|
||||
const { ctx, parent, start } = await setup()
|
||||
const backend = externalRun()
|
||||
registerExternal(ctx, backend.run)
|
||||
const activation = await start({ provider: 'external', name: 'reviewer', delivery: 'parent' })
|
||||
expect(activation.childId).toBe(backend.run.id)
|
||||
expect(await ctx.subagentNames.resolveName(parent, activation.name)).toBe(backend.run.id)
|
||||
await expect(ctx.subagentNames.resolveName(parent, `${activation.name}/child`)).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
|
||||
await activation.dispose()
|
||||
expect(backend.dispose).toHaveBeenCalledTimes(1)
|
||||
it('uses a stable id prefix for children whose parent has no name', async () => {
|
||||
const { ctx, parent, start } = await setup(['hang'])
|
||||
const creator = await start()
|
||||
const creatorAgent = ctx.agents.get(creator.childId)!
|
||||
expect(ctx.subagentNames.nameOf(creatorAgent)).toBe(creator.childId)
|
||||
const reservation = ctx.subagentNames.reserve(creatorAgent, 'facts')
|
||||
expect(reservation.name).toBe(`lead/legacy:${creator.childId}/facts`)
|
||||
const childId = SessionId('accepted-leaf')
|
||||
ctx.subagentNames.bind(creatorAgent, childId, reservation.name)
|
||||
reservation.dispose()
|
||||
ctx.subagentNames.bind(parent, creator.childId, 'lead/reviewer')
|
||||
expect(ctx.subagentNames.nameOf(parent, childId)).toBe(`lead/legacy:${creator.childId}/facts`)
|
||||
})
|
||||
|
||||
it('reports both binding and disposal failures for an accepted external child', async () => {
|
||||
const { ctx, parent, start } = await setup()
|
||||
const backend = externalRun()
|
||||
registerExternal(ctx, backend.run)
|
||||
const bindingFailure = new Error('binding failed')
|
||||
const cleanupFailure = new Error('cleanup failed')
|
||||
backend.dispose.mockImplementationOnce(async () => {
|
||||
backend.result.resolve({ output: [], stopReason: 'aborted' })
|
||||
throw cleanupFailure
|
||||
})
|
||||
const append = parent.session.append.bind(parent.session)
|
||||
vi.spyOn(parent.session, 'append').mockImplementation((...args) => {
|
||||
if (args[0] === 'subagent-name/bound') throw bindingFailure
|
||||
return Reflect.apply(append, parent.session, args) as ReturnType<typeof append>
|
||||
})
|
||||
const error = await start({ provider: 'external', name: 'reviewer' }).catch((failure: unknown) => failure)
|
||||
expect(error).toMatchObject({ code: 'SUBAGENT_NAME_BIND_FAILED', childId: backend.run.id })
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
if (!(error instanceof Error)) throw new Error('expected named-start failure')
|
||||
expect(error.message).toContain('execution may still be active')
|
||||
expect(error.cause).toBeInstanceOf(AggregateError)
|
||||
if (!(error.cause instanceof AggregateError)) throw new Error('expected combined failure')
|
||||
expect(error.cause.errors).toHaveLength(2)
|
||||
expect(error.cause.errors[0]).toBe(bindingFailure)
|
||||
expect(error.cause.errors[1]).toMatchObject({ code: 'ACTIVATION_TEARDOWN_FAILED', cause: cleanupFailure })
|
||||
it('does not stop an accepted child when the alias append fails', async () => {
|
||||
const { ctx, parent, start } = await setup(['hang'])
|
||||
const reservation = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
const child = await start()
|
||||
const failure = new Error('binding append failed')
|
||||
const append = vi.spyOn(parent.session, 'append').mockImplementationOnce(() => { throw failure })
|
||||
expect(() => { ctx.subagentNames.bind(parent, child.childId, reservation.name) }).toThrow(failure)
|
||||
append.mockRestore()
|
||||
reservation.dispose()
|
||||
expect(ctx.agents.get(child.childId)).toBeDefined()
|
||||
expect(ctx.subagentNames.nameOf(parent, child.childId)).toBe(child.childId)
|
||||
const retry = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
retry.dispose()
|
||||
await child.dispose()
|
||||
})
|
||||
|
||||
it('reports the started external child and awaits that receipt disposal when binding fails', async () => {
|
||||
const { ctx, parent, start } = await setup()
|
||||
const backend = externalRun()
|
||||
const unrelated = externalRun()
|
||||
registerExternal(ctx, backend.run)
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'unrelated', inheritsParentContext: false,
|
||||
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
|
||||
start: async () => unrelated.run,
|
||||
})
|
||||
const existing = await start({ provider: 'unrelated', name: 'writer', delivery: 'parent' })
|
||||
const cleaning = Promise.withResolvers<undefined>()
|
||||
const cleaned = Promise.withResolvers<undefined>()
|
||||
backend.dispose.mockImplementation(async () => {
|
||||
backend.result.resolve({ output: [], stopReason: 'aborted' })
|
||||
cleaning.resolve(undefined)
|
||||
await cleaned.promise
|
||||
})
|
||||
const failure = new Error('external binding append failed')
|
||||
const append = parent.session.append.bind(parent.session)
|
||||
const spy = vi.spyOn(parent.session, 'append').mockImplementation((...args) => {
|
||||
if (args[0] === 'subagent-name/bound') throw failure
|
||||
return Reflect.apply(append, parent.session, args) as ReturnType<typeof append>
|
||||
})
|
||||
const starting = start({ provider: 'external', name: 'reviewer', delivery: 'parent' })
|
||||
const observed = vi.fn()
|
||||
void starting.then(observed, observed)
|
||||
const rejected = expect(starting).rejects.toMatchObject({
|
||||
code: 'SUBAGENT_NAME_BIND_FAILED', childId: backend.run.id, cause: failure,
|
||||
})
|
||||
try {
|
||||
await cleaning.promise
|
||||
expect(backend.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(unrelated.dispose).not.toHaveBeenCalled()
|
||||
expect(observed).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
cleaned.resolve(undefined)
|
||||
}
|
||||
await rejected
|
||||
spy.mockRestore()
|
||||
await existing.dispose()
|
||||
})
|
||||
|
||||
it('refuses to overwrite a historical name when an external provider reuses its child id', async () => {
|
||||
const { ctx, parent, start } = await setup()
|
||||
const first = externalRun()
|
||||
const next = externalRun()
|
||||
const reused = { ...next.run, id: first.run.id }
|
||||
const providerStart = vi.fn().mockResolvedValueOnce(first.run).mockResolvedValueOnce(reused)
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'external', inheritsParentContext: false,
|
||||
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
|
||||
start: providerStart,
|
||||
})
|
||||
const original = await start({ provider: 'external', name: 'reviewer', delivery: 'parent' })
|
||||
await original.dispose()
|
||||
await expect(start({ provider: 'external', name: 'writer', delivery: 'parent' })).rejects.toMatchObject({
|
||||
code: 'SUBAGENT_NAME_BIND_FAILED', childId: first.run.id,
|
||||
})
|
||||
expect(first.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(next.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toEqual([
|
||||
{ childId: first.run.id, name: 'reviewer' },
|
||||
])
|
||||
expect(await ctx.subagentNames.resolveName(parent, original.name)).toBe(first.run.id)
|
||||
await expect(ctx.subagentNames.resolveName(parent, 'lead/writer')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
|
||||
})
|
||||
|
||||
it('aborts pending preparation and drains it before plugin disposal completes', async () => {
|
||||
const { ctx, parent, namesFiber, start, catalog } = await setup()
|
||||
it('unregisters only its projection and leaves accepted execution with its owner', async () => {
|
||||
const { ctx, parent, namesFiber, start } = await setup(['hang'])
|
||||
const names = ctx.subagentNames
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const aborted = Promise.withResolvers<undefined>()
|
||||
const cleanup = Promise.withResolvers<undefined>()
|
||||
vi.spyOn(localProvider(ctx), 'prepareContinuable').mockImplementationOnce(async ({ signal }) => {
|
||||
entered.resolve(signal)
|
||||
signal.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true })
|
||||
await aborted.promise
|
||||
await cleanup.promise
|
||||
signal.throwIfAborted()
|
||||
return {}
|
||||
})
|
||||
const starting = start({ name: 'reviewer' })
|
||||
const rejected = expect(starting).rejects.toThrow('unloading')
|
||||
const signal = await entered.promise
|
||||
const disposal = namesFiber.dispose()
|
||||
const disposed = vi.fn()
|
||||
void disposal.then(disposed, disposed)
|
||||
try {
|
||||
await aborted.promise
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(disposed).not.toHaveBeenCalled()
|
||||
expect(catalog()).toEqual([])
|
||||
const bindings = ctx.sessionProjections.snapshot(parent.session).values.subagentNames
|
||||
expect(bindings).toHaveLength(1)
|
||||
expect(bindings?.[0]?.childId).toEqual(expect.any(String))
|
||||
expect(bindings?.[0]?.name).toBe('reviewer')
|
||||
await expect(names.nameOf(parent)).rejects.toMatchObject({ code: 'AGENT_NAMES_UNAVAILABLE' })
|
||||
} finally {
|
||||
cleanup.resolve(undefined)
|
||||
}
|
||||
await rejected
|
||||
await disposal
|
||||
const reservation = names.reserve(parent, 'reviewer')
|
||||
const child = await start()
|
||||
await namesFiber.dispose()
|
||||
expect(ctx.get('subagentNames')).toBeUndefined()
|
||||
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toBeUndefined()
|
||||
expect(ctx.agents.get(child.childId)).toBeDefined()
|
||||
// Accepted callers retain their projection handle while completing a binding.
|
||||
names.bind(parent, child.childId, reservation.name)
|
||||
reservation.dispose()
|
||||
await ctx.plugin(SubagentNames)
|
||||
expect(ctx.subagentNames.nameOf(parent, child.childId)).toBe('lead/reviewer')
|
||||
await child.dispose()
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,7 @@ const header: SessionHeader = {
|
||||
isSeeded: false,
|
||||
}
|
||||
|
||||
function bound(seq: number, childId = 'child', name = 'worker'): SessionEvent<'subagent-name/bound'> {
|
||||
function bound(seq: number, childId = 'child', name = 'lead/worker'): SessionEvent<'subagent-name/bound'> {
|
||||
return {
|
||||
type: 'subagent-name/bound', seq: SessionSeq(seq), time: seq,
|
||||
data: { version: 1, childId: SessionId(childId), name },
|
||||
@@ -32,19 +32,21 @@ describe('subagent names projection', () => {
|
||||
it('keeps each child binding immutable without mutating earlier states', () => {
|
||||
const initial = subagentNamesProjection.init(header, SessionLogOffset(0))
|
||||
const first = subagentNamesProjection.apply(initial, bound(0))
|
||||
const second = subagentNamesProjection.apply(first, bound(1, 'other-child', 'reviewer'))
|
||||
const second = subagentNamesProjection.apply(first, bound(1, 'other-child', 'lead/reviewer'))
|
||||
|
||||
expect(initial.bindings).toEqual([])
|
||||
expect(first.bindings).toEqual([{ childId: 'child', name: 'worker' }])
|
||||
expect(first.bindings).toEqual([{ childId: 'child', name: 'lead/worker' }])
|
||||
expect(second.bindings).toEqual([
|
||||
{ childId: 'child', name: 'worker' },
|
||||
{ childId: 'other-child', name: 'reviewer' },
|
||||
{ childId: 'child', name: 'lead/worker' },
|
||||
{ childId: 'other-child', name: 'lead/reviewer' },
|
||||
])
|
||||
expect(() => subagentNamesProjection.apply(second, bound(2, 'child', 'renamed')))
|
||||
.toThrow('subagent name for child is immutable')
|
||||
expect(() => subagentNamesProjection.apply(second, bound(2, 'child', 'lead/renamed')))
|
||||
.toThrow('one immutable binding')
|
||||
expect(() => subagentNamesProjection.apply(second, bound(2, 'different-child', 'lead/worker')))
|
||||
.toThrow('one immutable binding')
|
||||
expect(second.bindings).toEqual([
|
||||
{ childId: 'child', name: 'worker' },
|
||||
{ childId: 'other-child', name: 'reviewer' },
|
||||
{ childId: 'child', name: 'lead/worker' },
|
||||
{ childId: 'other-child', name: 'lead/reviewer' },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -65,22 +67,22 @@ describe('subagent names projection', () => {
|
||||
|
||||
it('excludes inherited bindings and admits the event at the exact inherited cut', () => {
|
||||
const initial = subagentNamesProjection.init(header, SessionLogOffset(2))
|
||||
expect(subagentNamesProjection.apply(initial, bound(0, 'child', 'inherited'))).toBe(initial)
|
||||
expect(subagentNamesProjection.apply(initial, bound(1, 'other-child', 'inherited-too'))).toBe(initial)
|
||||
expect(subagentNamesProjection.apply(initial, bound(0, 'child', 'lead/inherited'))).toBe(initial)
|
||||
expect(subagentNamesProjection.apply(initial, bound(1, 'other-child', 'lead/inherited-too'))).toBe(initial)
|
||||
const own = subagentNamesProjection.apply(initial, bound(2))
|
||||
|
||||
expect(own.bindings).toEqual([{ childId: 'child', name: 'worker' }])
|
||||
expect(own.bindings).toEqual([{ childId: 'child', name: 'lead/worker' }])
|
||||
expect(own.inheritedEventCount).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ version: 2, childId: 'child', name: 'worker' },
|
||||
{ childId: 'child', name: 'worker' },
|
||||
{ version: 1, childId: 1, name: 'worker' },
|
||||
{ version: 1, name: 'worker' },
|
||||
{ version: 2, childId: 'child', name: 'lead/worker' },
|
||||
{ childId: 'child', name: 'lead/worker' },
|
||||
{ version: 1, childId: 1, name: 'lead/worker' },
|
||||
{ version: 1, name: 'lead/worker' },
|
||||
{ version: 1, childId: 'child' },
|
||||
{ version: 1, childId: 'child', name: 1 },
|
||||
{ version: 1, childId: 'child', name: 'worker', extra: true },
|
||||
{ version: 1, childId: 'child', name: 'lead/worker', extra: true },
|
||||
...['', 'Worker', '1worker', 'parent/worker', 'legacy:child', 'worker name'].map(name => ({
|
||||
version: 1, childId: 'child', name,
|
||||
})),
|
||||
@@ -99,10 +101,11 @@ describe('subagent names projection', () => {
|
||||
{ inheritedEventCount: '0', bindings: [] },
|
||||
{ inheritedEventCount: 0, bindings: {} },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 1, name: 'worker' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 1, name: 'lead/worker' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'parent/worker' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'worker', version: 1 }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'worker' }, { childId: 'child', name: 'reviewer' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker', version: 1 }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker' }, { childId: 'child', name: 'lead/reviewer' }] },
|
||||
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker' }, { childId: 'other', name: 'lead/worker' }] },
|
||||
{ inheritedEventCount: 0, bindings: [], extra: true },
|
||||
])('rejects malformed checkpoint state %j', (state) => {
|
||||
expect(subagentNamesProjection.stateSchema.safeParse(state).success).toBe(false)
|
||||
@@ -112,16 +115,16 @@ describe('subagent names projection', () => {
|
||||
const projections = await registry()
|
||||
const inheritedEventCount = SessionLogOffset(1)
|
||||
const events: SessionEvent[] = [
|
||||
bound(0, 'ancestor-child', 'inherited'),
|
||||
bound(0, 'ancestor-child', 'lead/inherited'),
|
||||
bound(1),
|
||||
bound(2),
|
||||
bound(3, 'other-child', 'reviewer'),
|
||||
bound(3, 'other-child', 'lead/reviewer'),
|
||||
{ type: 'turn/start', seq: SessionSeq(4), time: 4, data: { turn: 1 } },
|
||||
]
|
||||
const prefix = projections.restore({}, events.slice(0, 2), SessionLogOffset(0), header, inheritedEventCount)
|
||||
expect(prefix.checkpoint.subagentNames).toEqual({
|
||||
ver: 1, seq: 1,
|
||||
val: { inheritedEventCount: 1, bindings: [{ childId: 'child', name: 'worker' }] },
|
||||
val: { inheritedEventCount: 1, bindings: [{ childId: 'child', name: 'lead/worker' }] },
|
||||
})
|
||||
const resumed = projections.restore(
|
||||
prefix.checkpoint, events.slice(2), SessionLogOffset(2), header, inheritedEventCount,
|
||||
@@ -132,8 +135,8 @@ describe('subagent names projection', () => {
|
||||
expect(resumed.snapshot).toEqual({
|
||||
asOfSeq: 4,
|
||||
values: { subagentNames: [
|
||||
{ childId: 'child', name: 'worker' },
|
||||
{ childId: 'other-child', name: 'reviewer' },
|
||||
{ childId: 'child', name: 'lead/worker' },
|
||||
{ childId: 'other-child', name: 'lead/reviewer' },
|
||||
] },
|
||||
})
|
||||
expect(projections.viewCheckpoint(resumed.checkpoint)).toEqual(resumed.snapshot.values)
|
||||
@@ -150,11 +153,11 @@ describe('subagent names projection', () => {
|
||||
|
||||
expect(projections.viewCheckpoint(incompatible)).toEqual({})
|
||||
expect(() => projections.restore(
|
||||
incompatible, [bound(1, 'other-child', 'reviewer')], SessionLogOffset(1), header, SessionLogOffset(0),
|
||||
incompatible, [bound(1, 'other-child', 'lead/reviewer')], SessionLogOffset(1), header, SessionLogOffset(0),
|
||||
)).toThrow(/re-read from seq 0/)
|
||||
expect(projections.restore(
|
||||
incompatible, [bound(0)], SessionLogOffset(0), header, SessionLogOffset(0),
|
||||
).snapshot.values.subagentNames).toEqual([{ childId: 'child', name: 'worker' }])
|
||||
).snapshot.values.subagentNames).toEqual([{ childId: 'child', name: 'lead/worker' }])
|
||||
})
|
||||
|
||||
it('rejects a version-matching checkpoint with invalid binding fields', async () => {
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
{
|
||||
"path": "../../session/session-projection"
|
||||
},
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../subagent"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
|
||||
import { TestSessionQuery } from '../../subagent/tests/test-session-query.ts'
|
||||
import * as Spawn from '../src/index.ts'
|
||||
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
|
||||
|
||||
@@ -42,7 +41,6 @@ export async function spawnHarness(workdir: string): Promise<Context> {
|
||||
await ctx.plugin(JsonlSessionPersistence, { root: join(workdir, '.sessions') })
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(TestSessionQuery)
|
||||
await ctx.plugin(SubagentNames)
|
||||
await ctx.plugin(Spawn, { providerName: 'spawn' })
|
||||
// The model-facing subagent tool, bound to the spawn backend.
|
||||
|
||||
@@ -24,20 +24,20 @@
|
||||
en: a30b863b9a9bca72
|
||||
zh: e8c5168f98eaf88a
|
||||
/deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent:
|
||||
en: b0eceb2ffde56e6c
|
||||
zh: b47bc05cf400a429
|
||||
en: 4116c0be76097afd
|
||||
zh: ffe3beac5025344a
|
||||
/deepseek-ai-dsh-tool-subagent-control/use-this-package/wait-agent:
|
||||
en: 41ed5ec6e82e0282
|
||||
zh: 0545e0719d5565d8
|
||||
/deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents:
|
||||
en: 79d2508e6231c00b
|
||||
zh: 3923348b97d77bd0
|
||||
en: bfc34a440678540a
|
||||
zh: f83e5519fa2761cb
|
||||
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation:
|
||||
en: c5cdf01a78c990f2
|
||||
zh: 53c180d726ce4502
|
||||
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/design-concept:
|
||||
en: ca2f40711df98a11
|
||||
zh: 9a1d1ef5d0fb7d5d
|
||||
en: 27f73437315a97ca
|
||||
zh: 47999ba2c9c8c394
|
||||
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership:
|
||||
en: 65137f5fe14ebb7f
|
||||
zh: ce00e4f32709246a
|
||||
@@ -63,26 +63,26 @@
|
||||
en: 5eb1d46a94b7ec1b
|
||||
zh: 5670760ca695d4ad
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/kv-cache-effect:
|
||||
en: 89619e31f3ea7585
|
||||
zh: 5681bc8096cd8e32
|
||||
en: c399622fde875897
|
||||
zh: 11e0b3bcd71d8d8e
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context:
|
||||
en: 95d76d1aba983086
|
||||
zh: b645a3837ec17ef4
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/what-the-model-sees:
|
||||
en: 9a8ddc3e8e466aeb
|
||||
zh: 7df44948893bfde0
|
||||
en: adb5b38af8f1cfb1
|
||||
zh: 2a7f2779ed5e8a9f
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/token-effect:
|
||||
en: 24d9c15986cb4d3d
|
||||
zh: 6fb66cf84f8bc812
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/kv-cache-effect:
|
||||
en: de3af33c2e6949ef
|
||||
zh: 0f57b7f40419fcad
|
||||
en: be04fa8fa539cad0
|
||||
zh: 797fd37af7d32f90
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result:
|
||||
en: d64e936e83e89a2b
|
||||
zh: b9905c33af6cca45
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/what-the-model-sees:
|
||||
en: 0465c4407ae55795
|
||||
zh: 1a9e1623afb92a29
|
||||
en: 00d6ca03eab6e7fa
|
||||
zh: 202e242f06ea8818
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/token-effect:
|
||||
en: 5d1bcef28b5f18da
|
||||
zh: 027d67d93d660e76
|
||||
@@ -105,8 +105,8 @@
|
||||
en: 094e55d258f2ec2e
|
||||
zh: f364bc9046530bff
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/what-the-model-sees:
|
||||
en: 031b07c564bf3e6e
|
||||
zh: 80040e0c55d158a9
|
||||
en: 0e68be81042a70d0
|
||||
zh: 4ed29257cd12c5f4
|
||||
/deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/token-effect:
|
||||
en: 027aaeb808238388
|
||||
zh: 5b974abc429d9e23
|
||||
|
||||
@@ -49,7 +49,7 @@ In named mode, `name` selects a full path or Session id of another agent in the
|
||||
|
||||
### interrupt_agent
|
||||
|
||||
In named mode, `name` identifies a descendant by full path or Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. Cancellation during name resolution leaves the target running. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results.
|
||||
In named mode, `name` identifies a descendant by full path or Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results.
|
||||
|
||||
<a id="wait_agent"></a>
|
||||
### wait_agent
|
||||
@@ -58,7 +58,7 @@ Waits for the exact caller’s next-step inbox. Pending input returns immediatel
|
||||
|
||||
### list_agents
|
||||
|
||||
Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Named rows carry full paths, with `lead` for the root; descendants carry their direct parent path and depth from the root. Unnamed children use `legacy:<SessionId>` path segments. Each child’s label comes from the `description` supplied at creation. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate subagent trees.
|
||||
Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Rows retain Session ids, parent ids, and depth from the root. The optional naming plugin adds `name` and `parentName` when a mapping is available; the root is named `lead`. Unnamed children retain their ids. Each child’s label comes from the `description` supplied at creation. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate subagent trees.
|
||||
|
||||
-----
|
||||
|
||||
@@ -72,7 +72,7 @@ This section explains what the tools delegate to the subagent service; the obser
|
||||
|
||||
### Design concept
|
||||
|
||||
The optional `ctx.subagentNames` resolves names; messaging, interruption, and catalog traversal delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. The wait tool subscribes to the exact caller’s inbox and wait signals before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement.
|
||||
The optional `ctx.subagentNames` resolves names synchronously; messaging, interruption, and catalog traversal delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. The wait tool subscribes to the exact caller’s inbox and wait signals before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement.
|
||||
|
||||
### Delivery and signal ownership
|
||||
|
||||
@@ -120,13 +120,13 @@ Fixed schema cost per request, independent of the number or descriptions of chil
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Prefix-stable; the schemas do not change at runtime, and a parent and its continuable children, including fork children, receive identical definitions.
|
||||
The schema prefix stays stable while the naming service’s availability is unchanged. A parent and its continuable children, including fork children, inherit the same definitions.
|
||||
|
||||
### Communication context
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The logged `subagent:communication` runtime context tells delegated children their own and direct creator’s JSON-encoded names (or the parent id without naming) and how to send findings that change the parent’s next action. Sending a message does not end the turn. Root Agents and ordinary Session forks receive no communication context; delegated children receive it only when the standard messaging definition is admitted in the current scope and step. Automatic result delivery remains owned by the subagent service.
|
||||
The logged `subagent:communication` runtime context tells delegated children their direct creator’s JSON-encoded Session id and how to send findings that change the parent’s next action. Named controls also accept this id. Sending a message does not end the turn. Root Agents and ordinary Session forks receive no communication context; delegated children receive it only when the standard messaging definition is admitted in the current scope and step. Automatic result delivery remains owned by the subagent service.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -134,13 +134,13 @@ One short context entry per delegated child, independent of tree size.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Communication guidance is retained as runtime context after inherited history, leaving the shared system prompt and tool definitions unchanged between parent and child. Cold resume derives the parent address again from durable records. If the runtime root is unavailable, guidance retains the parent id; corrupt naming records reject assembly.
|
||||
Communication guidance is retained as runtime context after inherited history, leaving the shared system prompt and tool definitions unchanged between parent and child. The parent id comes from the child Session header; prompt assembly does not wait for a name binding or read the name map.
|
||||
|
||||
### Interrupt result
|
||||
|
||||
#### What the model sees
|
||||
|
||||
`interrupt requested for agent <address>` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; a known settled target still renders the acceptance line. Named mode rejects unknown addresses; id mode retains an accepted no-op for absent targets.
|
||||
`interrupt requested for agent <address>` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; a known settled target still renders the acceptance line. Unknown name paths are rejected; an absent raw Session id retains an accepted no-op.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -168,7 +168,7 @@ Append-only; newly visible content follows the reusable request prefix and does
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The first row is `<address> [<status>] — root agent`. Each continuable descendant follows as `<address> [<status>] parent=<address> depth=<n> — <label>`. The root has `kind: root`; descendants have `kind: child` relative to that root. Diagnostics render `<address> [diagnostic: <reason>] parent=<address> depth=<n>`. Status describes turn activity, not task completion.
|
||||
The first row is `<id> [<status>] — root agent`. Each continuable descendant follows as `<id> [<status>] parent=<parent-id> depth=<n> — <label>`. When a name is available, the text prefixes the label with `<name>: `; canonical rows retain the original `label` and add `name` separately. The root has `kind: root`; descendants have `kind: child` relative to that root. Diagnostics retain the id, reason, parent, and depth, with an optional name after ` — `. Status describes turn activity, not task completion.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-reference"
|
||||
|
||||
### interrupt_agent
|
||||
|
||||
命名模式的 `name` 使用完整路径或 Session id 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。名称解析期间取消调用会让目标继续运行。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
|
||||
命名模式的 `name` 使用完整路径或 Session id 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
|
||||
|
||||
<a id="wait_agent"></a>
|
||||
### wait_agent
|
||||
@@ -58,7 +58,7 @@ kind: "package-reference"
|
||||
|
||||
### list_agents
|
||||
|
||||
按稳定前序列出根与所有可达的可继续后代,包括调用方。命名模式的行包含完整路径,根为 `lead`;后代行包含直接父级路径与相对根的深度。未命名子级使用 `legacy:<SessionId>` 路径片段。每个子级的标签来自创建时提供的 `description`。在线状态为 `running` 或 `inactive`。外部条目被省略,且不读取其子 Session;可读的一次性子级仍作为遍历节点。未知模式与不可读目录产生诊断。普通 Session 分叉形成独立的 subagent 树。
|
||||
按稳定前序列出根与所有可达的可继续后代,包括调用方。行保留 Session id、父级 id 与相对根的深度。可选命名插件在映射可用时增加 `name` 与 `parentName`;根的名称为 `lead`。未命名子级保留其 id。每个子级的标签来自创建时提供的 `description`。在线状态为 `running` 或 `inactive`。外部条目被省略,且不读取其子 Session;可读的一次性子级仍作为遍历节点。未知模式与不可读目录产生诊断。普通 Session 分叉形成独立的 subagent 树。
|
||||
|
||||
-----
|
||||
|
||||
@@ -72,7 +72,7 @@ kind: "package-reference"
|
||||
|
||||
### 设计理念
|
||||
|
||||
命名由可选的 `ctx.subagentNames` 解析;通信、中断和目录遍历委派给 `ctx.subagents`;驻留、冷恢复和授权属于该服务。等待工具在启动定时器并检查待处理下一步输入前订阅当前调用者的收件箱和等待信号,并在每次结束时移除监听器和定时器。
|
||||
命名由可选的 `ctx.subagentNames` 同步解析;通信、中断和目录遍历委派给 `ctx.subagents`;驻留、冷恢复和授权属于该服务。等待工具在启动定时器并检查待处理下一步输入前订阅当前调用者的收件箱和等待信号,并在每次结束时移除监听器和定时器。
|
||||
|
||||
### 投递与信号所有权
|
||||
|
||||
@@ -120,13 +120,13 @@ kind: "package-reference"
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
前缀保持稳定;schema 不会在运行时改变,父级与其可继续子级(包括 fork 子级)收到相同的定义。
|
||||
命名服务的可用性不变时,schema 前缀保持稳定。父级与其可继续子级(包括 fork 子级)继承相同的定义。
|
||||
|
||||
### 通信上下文
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
持久记录的 `subagent:communication` 运行时上下文向委派子级提供自身及直接创建者经 JSON 编码的名称(无命名服务时为父级 id),并说明如何发送会改变父级下一步行动的发现。发送消息不会结束轮次。根 Agent 和普通 Session fork 不接收通信上下文;委派子级仅在标准通信定义于当前作用域和 step 中可用时接收。自动结果交付仍由 subagent 服务负责。
|
||||
持久记录的 `subagent:communication` 运行时上下文向委派子级提供其直接创建者经 JSON 编码的 Session id,并说明如何发送会改变父级下一步行动的发现。命名控制工具也接受该 id。发送消息不会结束轮次。根 Agent 和普通 Session fork 不接收通信上下文;委派子级仅在标准通信定义于当前作用域和 step 中可用时接收。自动结果交付仍由 subagent 服务负责。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -134,13 +134,13 @@ kind: "package-reference"
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
通信指导作为运行时上下文保留在继承历史之后,父子级共享的系统提示词和工具定义不变。冷恢复会再次从持久记录推导父级地址。根运行时不可用时,通信指导保留父级 id;命名记录损坏则使组装失败。
|
||||
通信指导作为运行时上下文保留在继承历史之后,父子级共享的系统提示词和工具定义不变。父级 id 来自子级 Session 头;提示组装不等待名称绑定,也不读取名称映射。
|
||||
|
||||
### 中断结果
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
接受时返回 `interrupt requested for agent <address>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;已知目标已结算时仍渲染接受行;命名模式拒绝未知地址,id 模式对不存在的目标保留成功空操作。
|
||||
接受时返回 `interrupt requested for agent <address>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;已知目标已结算时仍渲染接受行;未知名称路径会被拒绝;不存在的原始 Session id 保留成功空操作。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -168,7 +168,7 @@ kind: "package-reference"
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
首行为 `<address> [<status>] — root agent`。各可继续后代随后显示为 `<address> [<status>] parent=<address> depth=<n> — <label>`。根使用 `kind: root`;后代相对该根使用 `kind: child`。诊断显示为 `<address> [diagnostic: <reason>] parent=<address> depth=<n>`。状态描述轮次活动,不表示任务完成。
|
||||
首行为 `<id> [<status>] — root agent`。各可继续后代随后显示为 `<id> [<status>] parent=<parent-id> depth=<n> — <label>`。名称可用时,文本在标签前增加 `<name>: `;规范结果行保留原始 `label`,并另加 `name`。根使用 `kind: root`;后代相对该根使用 `kind: child`。诊断保留 id、原因、父级与深度,并可在 ` — ` 后显示名称。状态描述轮次活动,不表示任务完成。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -8,14 +8,12 @@
|
||||
* @module @deepseek-ai/dsh-tool-subagent-control
|
||||
*/
|
||||
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SubagentError } from '@deepseek-ai/dsh-subagent'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-subagent-names'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
@@ -50,7 +48,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
dispose = registerControls(ctx, config.interrupt !== false)
|
||||
}
|
||||
ctx.on('internal/service', (service) => {
|
||||
if (service !== 'subagentNames' || names === ctx.get('subagentNames')) return
|
||||
if (service !== 'subagentNames' || ctx.fiber.uid === null || ctx.fiber.state === FiberState.UNLOADING
|
||||
|| names === ctx.get('subagentNames')) return
|
||||
names = ctx.get('subagentNames')
|
||||
install()
|
||||
})
|
||||
@@ -93,19 +92,12 @@ function registerControls(ctx: Context, interrupt: boolean): () => void {
|
||||
const address = args[parameter] as string
|
||||
const message: ContentBlock[] = [{ type: 'text', text: args.message as string }]
|
||||
const target = names === undefined ? SessionId(address)
|
||||
: await names.resolveName(sender, address, exec.signal)
|
||||
: names.resolveName(sender, address)
|
||||
const messageId = await ctx.subagents.sendMessage(sender, target, message, { signal: exec.signal })
|
||||
return { messageId }
|
||||
},
|
||||
})
|
||||
disposers.push(ctx.tools.register(sendMessage))
|
||||
const communication = new WeakMap<Agent, string>()
|
||||
const legacyGuidance = (agent: Agent): string => {
|
||||
const parentId = JSON.stringify(agent.session.header.parentSession)
|
||||
return `Your parent agent id is ${parentId}. Use `
|
||||
+ `send_message({ ${parameter}: ${parentId}, message: "<finding>" }) when a finding changes what the `
|
||||
+ 'parent should do next; sending a message does not end your turn.'
|
||||
}
|
||||
disposers.push(ctx.systemPrompt.context({
|
||||
name: 'subagent:communication',
|
||||
order: ctx.systemPrompt.getContextOrder('SUBAGENT_DELEGATION'),
|
||||
@@ -114,31 +106,11 @@ function registerControls(ctx: Context, interrupt: boolean): () => void {
|
||||
text: ({ agent, scope, toolSet }) => {
|
||||
if (agent === undefined || agent.session.header.origin !== 'subagent'
|
||||
|| ctx.tools.get(sendMessage.name, scope, toolSet) !== sendMessage) return ''
|
||||
return names === undefined ? legacyGuidance(agent) : communication.get(agent) ?? ''
|
||||
},
|
||||
}))
|
||||
if (names !== undefined) disposers.push(ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
|
||||
const assembly = await next()
|
||||
const { agent, scope, toolSet, signal } = context
|
||||
if (agent === undefined || agent.session.header.origin !== 'subagent'
|
||||
|| ctx.tools.get(sendMessage.name, scope, toolSet) !== sendMessage) return assembly
|
||||
communication.delete(agent)
|
||||
let text: string
|
||||
try {
|
||||
const name = await names.nameOf(agent, signal)
|
||||
const parentName = JSON.stringify(name.slice(0, name.lastIndexOf('/')))
|
||||
text = `Your name is ${JSON.stringify(name)}. Your parent agent name is ${parentName}. Use `
|
||||
+ `send_message({ name: ${parentName}, message: "<finding>" }) when a finding changes what the `
|
||||
const parentId = JSON.stringify(agent.session.header.parentSession)
|
||||
return `Your parent agent id is ${parentId}. Use `
|
||||
+ `send_message({ ${parameter}: ${parentId}, message: "<finding>" }) when a finding changes what the `
|
||||
+ 'parent should do next; sending a message does not end your turn.'
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof SubagentError && error.code === 'UNAUTHORIZED')
|
||||
&& !(error instanceof HarnessError && error.code === 'AGENT_NAMES_UNAVAILABLE')) throw error
|
||||
text = legacyGuidance(agent)
|
||||
}
|
||||
// Admission refresh reads the registered provider after this asynchronous assembly.
|
||||
communication.set(agent, text)
|
||||
return { ...assembly, contexts: assembly.contexts.map(entry => entry.name === 'subagent:communication'
|
||||
? { ...entry, text } : entry) }
|
||||
},
|
||||
}))
|
||||
if (interrupt) disposers.push(ctx.tools.register(defineTool({
|
||||
name: 'interrupt_agent',
|
||||
@@ -161,16 +133,16 @@ function registerControls(ctx: Context, interrupt: boolean): () => void {
|
||||
},
|
||||
render: (args, _value) => [{ type: 'text', text: `interrupt requested for agent ${args[parameter]}` }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
execute(args, exec) {
|
||||
const caller = exec.agent
|
||||
if (!caller) throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)')
|
||||
// The schema requires the dynamic address key before execution.
|
||||
const address = args[parameter] as string
|
||||
const target = names === undefined ? SessionId(address)
|
||||
: await names.resolveName(caller, address, exec.signal)
|
||||
: names.resolveName(caller, address)
|
||||
exec.signal.throwIfAborted()
|
||||
ctx.subagents.interrupt(target, { kind: 'ancestor', agent: caller })
|
||||
return { accepted: true }
|
||||
return Promise.resolve({ accepted: true })
|
||||
},
|
||||
})))
|
||||
return dispose
|
||||
|
||||
@@ -8,25 +8,32 @@ import type { SubagentDescendantListEntry } from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-subagent-names'
|
||||
|
||||
|
||||
type ListAgentsEntry = ({ readonly id: SessionId } | { readonly name: string }) & (
|
||||
type ListAgentsEntry =
|
||||
| {
|
||||
readonly kind: 'root'
|
||||
readonly id: SessionId
|
||||
readonly name?: string
|
||||
readonly status: 'running' | 'inactive'
|
||||
}
|
||||
| {
|
||||
readonly kind: 'child'
|
||||
readonly id: SessionId
|
||||
readonly name?: string
|
||||
readonly label: string
|
||||
readonly status: 'running' | 'inactive'
|
||||
readonly parent?: string
|
||||
readonly parent?: SessionId
|
||||
readonly parentName?: string
|
||||
readonly depth?: number
|
||||
}
|
||||
| {
|
||||
readonly kind: 'diagnostic'
|
||||
readonly id: SessionId
|
||||
readonly name?: string
|
||||
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
|
||||
readonly parent?: string
|
||||
readonly parent?: SessionId
|
||||
readonly parentName?: string
|
||||
readonly depth?: number
|
||||
}
|
||||
)
|
||||
|
||||
/** Report turn activity without exposing whether the child is loaded. */
|
||||
function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: SessionId): 'running' | 'inactive' {
|
||||
@@ -36,20 +43,23 @@ function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: Session
|
||||
/** Project one service row into the model-facing entry, or omit a one-shot child. */
|
||||
function project(
|
||||
agents: { get(id: SessionId): Agent | undefined },
|
||||
entry: SubagentDescendantListEntry & { readonly path?: string },
|
||||
entry: SubagentDescendantListEntry,
|
||||
nameOf: (id: SessionId) => string | undefined,
|
||||
): ListAgentsEntry | undefined {
|
||||
const name = entry.path === undefined ? undefined : `lead/${entry.path}`
|
||||
const address = name === undefined ? { id: entry.id } : { name }
|
||||
const at = { parent: name === undefined ? entry.parentId : name.slice(0, name.lastIndexOf('/')), depth: entry.depth }
|
||||
const name = nameOf(entry.id)
|
||||
const parentName = nameOf(entry.parentId)
|
||||
const at = { parent: entry.parentId, depth: entry.depth,
|
||||
...name === undefined ? {} : { name },
|
||||
...parentName === undefined ? {} : { parentName } }
|
||||
if (entry.kind === 'diagnostic') {
|
||||
return { kind: 'diagnostic', ...address, reason: entry.reason, ...at }
|
||||
return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at }
|
||||
}
|
||||
// One-shot children cannot be continued by send_message, so the model
|
||||
// never selects them; discovery still traversed them for descendants.
|
||||
if (entry.mode !== 'continuable') return undefined
|
||||
return {
|
||||
kind: 'child',
|
||||
...address,
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
status: statusOf(agents, entry.id),
|
||||
...at,
|
||||
@@ -63,14 +73,12 @@ function project(
|
||||
*/
|
||||
export function registerListAgents(ctx: Context): () => void {
|
||||
const names = ctx.get('subagentNames')
|
||||
const parameter = names === undefined ? 'id' : 'name'
|
||||
return ctx.tools.register(defineTool({
|
||||
name: 'list_agents',
|
||||
description:
|
||||
'List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. '
|
||||
+ (names === undefined
|
||||
? 'Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. '
|
||||
: 'Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. ')
|
||||
+ 'Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. '
|
||||
+ (names === undefined ? '' : 'Rows also include available names alongside Session ids. ')
|
||||
+ 'You can message any other agent in the subagent tree listed as running or inactive. '
|
||||
+ 'running means it is working; inactive means it is not currently working. '
|
||||
+ 'While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. '
|
||||
@@ -86,7 +94,8 @@ export function registerListAgents(ctx: Context): () => void {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, enum: ['root'] },
|
||||
[parameter]: { type: 'string', required: true },
|
||||
id: { type: 'string', required: true },
|
||||
...names === undefined ? {} : { name: { type: 'string' as const } },
|
||||
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
|
||||
},
|
||||
},
|
||||
@@ -95,10 +104,12 @@ export function registerListAgents(ctx: Context): () => void {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, enum: ['child'] },
|
||||
[parameter]: { type: 'string', required: true },
|
||||
id: { type: 'string', required: true },
|
||||
...names === undefined ? {} : { name: { type: 'string' as const } },
|
||||
label: { type: 'string', required: true },
|
||||
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
|
||||
parent: { type: 'string' },
|
||||
...names === undefined ? {} : { parentName: { type: 'string' as const } },
|
||||
depth: { type: 'number' },
|
||||
},
|
||||
},
|
||||
@@ -107,9 +118,11 @@ export function registerListAgents(ctx: Context): () => void {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, enum: ['diagnostic'] },
|
||||
[parameter]: { type: 'string', required: true },
|
||||
id: { type: 'string', required: true },
|
||||
...names === undefined ? {} : { name: { type: 'string' as const } },
|
||||
reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
|
||||
parent: { type: 'string' },
|
||||
...names === undefined ? {} : { parentName: { type: 'string' as const } },
|
||||
depth: { type: 'number' },
|
||||
},
|
||||
},
|
||||
@@ -120,12 +133,12 @@ export function registerListAgents(ctx: Context): () => void {
|
||||
return [{
|
||||
type: 'text',
|
||||
text: entries.map((entry) => {
|
||||
const address = 'name' in entry ? entry.name : entry.id
|
||||
if (entry.kind === 'root') return `${address} [${entry.status}] — root agent`
|
||||
const prefix = entry.name === undefined ? '' : `${entry.name}: `
|
||||
if (entry.kind === 'root') return `${entry.id} [${entry.status}] — ${prefix}root agent`
|
||||
const at = ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
|
||||
return entry.kind === 'diagnostic'
|
||||
? `${address} [diagnostic: ${entry.reason}]${at}`
|
||||
: `${address} [${entry.status}]${at} — ${entry.label}`
|
||||
? `${entry.id} [diagnostic: ${entry.reason}]${at}` + (entry.name === undefined ? '' : ` — ${entry.name}`)
|
||||
: `${entry.id} [${entry.status}]${at} — ${prefix}${entry.label}`
|
||||
}).join('\n'),
|
||||
}]
|
||||
},
|
||||
@@ -137,11 +150,17 @@ export function registerListAgents(ctx: Context): () => void {
|
||||
throw new Error('list_agents requires a calling agent (exec.agent was undefined)')
|
||||
}
|
||||
const root = ctx.subagents.rootOf(parent)
|
||||
const entries = await (names ?? ctx.subagents).listDescendants(root.id, exec.signal)
|
||||
const entries = await ctx.subagents.listDescendants(root.id, exec.signal)
|
||||
const nameOf = (id: SessionId): string | undefined => {
|
||||
const name = names?.nameOf(parent, id)
|
||||
return name === id ? undefined : name
|
||||
}
|
||||
const name = nameOf(root.id)
|
||||
return [
|
||||
{ kind: 'root' as const, ...names === undefined ? { id: root.id } : { name: 'lead' }, status: statusOf(ctx.agents, root.id) },
|
||||
{ kind: 'root' as const, id: root.id, status: statusOf(ctx.agents, root.id),
|
||||
...name === undefined ? {} : { name } },
|
||||
...entries
|
||||
.map(entry => project(ctx.agents, entry))
|
||||
.map(entry => project(ctx.agents, entry, nameOf))
|
||||
.filter(entry => entry !== undefined),
|
||||
]
|
||||
},
|
||||
|
||||
@@ -69,7 +69,6 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentNames)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
await ctx.plugin(tool)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
@@ -125,13 +124,51 @@ describe('list_agents', () => {
|
||||
expect(result.isError).toBe(false)
|
||||
if (result.isError) throw new Error(text(result))
|
||||
expect(result.value).toEqual([
|
||||
{ kind: 'root', name: 'lead', status: 'inactive' },
|
||||
{
|
||||
kind: 'child', name: `lead/legacy:${started.childId}`, label: 'workflow child',
|
||||
parent: 'lead', depth: 1, status: 'inactive',
|
||||
},
|
||||
{ kind: 'root', id: parent.id, status: 'inactive' },
|
||||
{ kind: 'child', id: started.childId, label: 'workflow child', parent: parent.id, depth: 1, status: 'inactive' },
|
||||
])
|
||||
expect(text(result)).toBe(`lead [inactive] — root agent\nlead/legacy:${started.childId} [inactive] parent=lead depth=1 — workflow child`)
|
||||
expect(text(result)).toBe(`${parent.id} [inactive] — root agent\n${started.childId} [inactive] parent=${parent.id} depth=1 — workflow child`)
|
||||
})
|
||||
|
||||
it('annotates the original child rows with names while retaining Session ids and labels', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('named result'), textResponse('unnamed result')])
|
||||
await ctx.plugin(SubagentNames)
|
||||
const reservation = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
const child = await ctx.subagents.startActivation({
|
||||
delivery: 'parent', provider: 'spawn', label: 'review implementation',
|
||||
prompt: [{ type: 'text', text: 'review' }], parent, signal: testToolSignal,
|
||||
})
|
||||
ctx.subagentNames.bind(parent, child.childId, reservation.name)
|
||||
reservation.dispose()
|
||||
await child.result
|
||||
const unnamed = await ctx.subagents.startActivation({
|
||||
delivery: 'parent', provider: 'spawn', label: 'unnamed task',
|
||||
prompt: [{ type: 'text', text: 'check' }], parent, signal: testToolSignal,
|
||||
})
|
||||
await unnamed.result
|
||||
const missing = SessionId('missing-child')
|
||||
parent.session.append('subagent/catalog', {
|
||||
version: 0, childId: missing, childCreatedAt: 1, mode: 'continuable', label: 'missing child',
|
||||
})
|
||||
const missingName = ctx.subagentNames.reserve(parent, 'missing')
|
||||
ctx.subagentNames.bind(parent, missing, missingName.name)
|
||||
missingName.dispose()
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError, text(result)).toBe(false)
|
||||
if (result.isError) throw new Error(text(result))
|
||||
expect(result.value).toEqual([
|
||||
{ kind: 'root', id: parent.id, name: 'lead', status: 'inactive' },
|
||||
{ kind: 'child', id: child.childId, name: 'lead/reviewer', label: 'review implementation',
|
||||
parent: parent.id, parentName: 'lead', depth: 1, status: 'inactive' },
|
||||
{ kind: 'child', id: unnamed.childId, label: 'unnamed task',
|
||||
parent: parent.id, parentName: 'lead', depth: 1, status: 'inactive' },
|
||||
{ kind: 'diagnostic', id: missing, name: 'lead/missing', reason: 'unavailable',
|
||||
parent: parent.id, parentName: 'lead', depth: 1 },
|
||||
])
|
||||
expect(text(result)).toBe('parent [inactive] — lead: root agent\n'
|
||||
+ `${child.childId} [inactive] parent=parent depth=1 — lead/reviewer: review implementation\n`
|
||||
+ `${unnamed.childId} [inactive] parent=parent depth=1 — unnamed task\n`
|
||||
+ 'missing-child [diagnostic: unavailable] parent=parent depth=1 — lead/missing')
|
||||
})
|
||||
|
||||
it('omits external leaves without diagnostics', async () => {
|
||||
@@ -142,7 +179,7 @@ describe('list_agents', () => {
|
||||
})
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe('lead [inactive] — root agent')
|
||||
expect(text(result)).toBe(`${parent.id} [inactive] — root agent`)
|
||||
})
|
||||
|
||||
it('registers list_agents once, globally, without parameters', async () => {
|
||||
@@ -163,7 +200,7 @@ describe('list_agents', () => {
|
||||
await ctx.sessions.flush(parent.session)
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe('lead [inactive] — root agent')
|
||||
expect(text(result)).toBe(`${parent.id} [inactive] — root agent`)
|
||||
})
|
||||
|
||||
it('renders direct children in array order with registry statuses', async () => {
|
||||
@@ -204,9 +241,9 @@ describe('list_agents', () => {
|
||||
mode: 'continuable',
|
||||
},
|
||||
] satisfies SubagentCatalogEntry[]
|
||||
ctx.subagentNames.listDescendants = () => Promise.resolve(entries.map(entry => ({
|
||||
ctx.subagents.listDescendants = () => Promise.resolve(entries.map(entry => ({
|
||||
...entry, kind: 'child' as const, activity: 'inactive' as const, hasChildren: false,
|
||||
parentId: parent.id, depth: 1, path: `legacy:${entry.id}`,
|
||||
parentId: parent.id, depth: 1,
|
||||
})))
|
||||
const agents = new Map<string, {
|
||||
status: 'running' | 'idle'
|
||||
@@ -218,17 +255,17 @@ describe('list_agents', () => {
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(
|
||||
'lead [inactive] — root agent\n'
|
||||
+ `lead/legacy:${started.childId} [inactive] parent=lead depth=1 — real child\n`
|
||||
+ 'lead/legacy:running-child [running] parent=lead depth=1 — still working\n'
|
||||
+ 'lead/legacy:waiting-child [inactive] parent=lead depth=1 — waiting on descendants',
|
||||
`${parent.id} [inactive] — root agent\n`
|
||||
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — real child\n`
|
||||
+ `running-child [running] parent=${parent.id} depth=1 — still working\n`
|
||||
+ `waiting-child [inactive] parent=${parent.id} depth=1 — waiting on descendants`,
|
||||
)
|
||||
})
|
||||
|
||||
it('always lists descendants and forwards the caller id and cancellation signal', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
const signal = new AbortController().signal
|
||||
const listDescendants = vi.spyOn(ctx.subagentNames, 'listDescendants').mockResolvedValue([])
|
||||
const listDescendants = vi.spyOn(ctx.subagents, 'listDescendants').mockResolvedValue([])
|
||||
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent, signal)
|
||||
|
||||
@@ -252,9 +289,9 @@ describe('list_agents', () => {
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(
|
||||
'lead [inactive] — root agent\n'
|
||||
+ 'lead/legacy:historical-one-shot [diagnostic: unavailable] parent=lead depth=1\n'
|
||||
+ `lead/legacy:${started.childId} [inactive] parent=lead depth=1 — summarize the doc`,
|
||||
`${parent.id} [inactive] — root agent\n`
|
||||
+ `historical-one-shot [diagnostic: unavailable] parent=${parent.id} depth=1\n`
|
||||
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — summarize the doc`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -286,7 +323,6 @@ describe('list_agents', () => {
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentNames)
|
||||
const fiber = await ctx.plugin(tool)
|
||||
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(true)
|
||||
await fiber.dispose()
|
||||
@@ -332,9 +368,9 @@ describe('list_agents', () => {
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(
|
||||
'lead [inactive] — root agent\n'
|
||||
+ `lead/legacy:${started.childId} [inactive] parent=lead depth=1 — waiting branch\n`
|
||||
+ `lead/legacy:${started.childId}/legacy:${grandchild.childId} [running] parent=lead/legacy:${started.childId} depth=2 — nested leaf`,
|
||||
`${parent.id} [inactive] — root agent\n`
|
||||
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — waiting branch\n`
|
||||
+ `${grandchild.childId} [running] parent=${started.childId} depth=2 — nested leaf`,
|
||||
)
|
||||
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, ctx.agents.get(grandchild.childId)))).toBe(text(result))
|
||||
@@ -348,11 +384,10 @@ describe('list_agents', () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
// Deterministic service rows: a one-shot intermediate owning a continuable
|
||||
// leaf, plus a positioned diagnostic. The tool filters only the one-shot.
|
||||
ctx.subagentNames.listDescendants = () => Promise.resolve([
|
||||
ctx.subagents.listDescendants = () => Promise.resolve([
|
||||
{
|
||||
kind: 'child',
|
||||
id: SessionId('one-shot-mid'),
|
||||
path: 'legacy:one-shot-mid',
|
||||
label: 'one-shot intermediate',
|
||||
mode: 'one-shot',
|
||||
activity: 'inactive',
|
||||
@@ -363,7 +398,6 @@ describe('list_agents', () => {
|
||||
{
|
||||
kind: 'child',
|
||||
id: SessionId('deep-leaf'),
|
||||
path: 'legacy:one-shot-mid/legacy:deep-leaf',
|
||||
label: 'deep leaf',
|
||||
mode: 'continuable',
|
||||
activity: 'inactive',
|
||||
@@ -374,7 +408,6 @@ describe('list_agents', () => {
|
||||
{
|
||||
kind: 'diagnostic',
|
||||
id: SessionId('broken-node'),
|
||||
path: 'legacy:broken-node',
|
||||
reason: 'unavailable',
|
||||
parentId: parent.id,
|
||||
depth: 1,
|
||||
@@ -383,9 +416,9 @@ describe('list_agents', () => {
|
||||
const result = await callTool(ctx, 'list_agents', {}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(
|
||||
'lead [inactive] — root agent\n'
|
||||
+ 'lead/legacy:one-shot-mid/legacy:deep-leaf [inactive] parent=lead/legacy:one-shot-mid depth=2 — deep leaf\n'
|
||||
+ 'lead/legacy:broken-node [diagnostic: unavailable] parent=lead depth=1',
|
||||
`${parent.id} [inactive] — root agent\n`
|
||||
+ 'deep-leaf [inactive] parent=one-shot-mid depth=2 — deep leaf\n'
|
||||
+ `broken-node [diagnostic: unavailable] parent=${parent.id} depth=1`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -413,11 +446,11 @@ describe('list_agents', () => {
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(4) })
|
||||
|
||||
const expected = [
|
||||
'lead [inactive] — root agent',
|
||||
`lead/legacy:${alpha} [running] parent=lead depth=1 — alpha task`,
|
||||
`lead/legacy:${beta} [inactive] parent=lead depth=1 — beta task`,
|
||||
`lead/legacy:${gamma} [inactive] parent=lead depth=1 — gamma task`,
|
||||
`lead/legacy:${delta} [running] parent=lead depth=1 — delta task`,
|
||||
`${parent.id} [inactive] — root agent`,
|
||||
`${alpha} [running] parent=${parent.id} depth=1 — alpha task`,
|
||||
`${beta} [inactive] parent=${parent.id} depth=1 — beta task`,
|
||||
`${gamma} [inactive] parent=${parent.id} depth=1 — gamma task`,
|
||||
`${delta} [running] parent=${parent.id} depth=1 — delta task`,
|
||||
].join('\n')
|
||||
for (const caller of [parent, ctx.agents.get(alpha), ctx.agents.get(delta)]) {
|
||||
const result = await callTool(ctx, 'list_agents', {}, caller)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import SubagentNames, { SubagentNameError } from '@deepseek-ai/dsh-subagent-names'
|
||||
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
|
||||
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
|
||||
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
@@ -61,7 +61,7 @@ afterEach(async () => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
|
||||
})
|
||||
|
||||
async function setupWith(adapter: MockAdapter | GatedAdapter, park = true, withNames = true) {
|
||||
async function setupWith(adapter: MockAdapter | GatedAdapter, park = true) {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
@@ -72,7 +72,6 @@ async function setupWith(adapter: MockAdapter | GatedAdapter, park = true, withN
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
if (withNames) await ctx.plugin(SubagentNames)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
await ctx.plugin(SubagentFork, { providerName: 'fork' })
|
||||
const controlFiber = await ctx.plugin(tool)
|
||||
@@ -115,50 +114,84 @@ async function waitNoActivation(ctx: Context, childId: SessionId): Promise<void>
|
||||
}
|
||||
|
||||
describe('dsh-tool-subagent-control', () => {
|
||||
it('retains id-addressed controls without naming and refreshes on naming service changes', async () => {
|
||||
const { ctx, parent } = await setupWith(new MockAdapter([]), true, false)
|
||||
it.each(['root disposal', 'plugin restart'] as const)('does not reinstall controls while %s drains', async (mode) => {
|
||||
const { ctx, controlFiber } = await setup([])
|
||||
await controlFiber.dispose()
|
||||
const names = await ctx.plugin(SubagentNames)
|
||||
const controls = await ctx.plugin(tool)
|
||||
const cleanupEntered = Promise.withResolvers<undefined>()
|
||||
const releaseCleanup = Promise.withResolvers<undefined>()
|
||||
let cleaned = false
|
||||
controls.ctx.effect(() => async () => {
|
||||
cleanupEntered.resolve(undefined)
|
||||
await releaseCleanup.promise
|
||||
cleaned = true
|
||||
})
|
||||
const register = vi.spyOn(ctx.tools, 'register')
|
||||
let settled = false
|
||||
const stopping = mode === 'root disposal' ? ctx.fiber.dispose() : controls.restart()
|
||||
const removing = mode === 'root disposal' ? Promise.resolve() : names.dispose()
|
||||
const drained = Promise.allSettled([stopping, removing]).then((results) => { settled = true; return results })
|
||||
try {
|
||||
await cleanupEntered.promise
|
||||
expect(settled).toBe(false)
|
||||
expect(cleaned).toBe(false)
|
||||
expect(register).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
releaseCleanup.resolve(undefined)
|
||||
}
|
||||
const results = await drained
|
||||
expect(results.every(result => result.status === 'fulfilled')).toBe(true)
|
||||
expect(cleaned).toBe(true)
|
||||
})
|
||||
|
||||
it('removes partially registered controls when another tool already owns send_message', async () => {
|
||||
const { ctx, controlFiber } = await setup([])
|
||||
await controlFiber.dispose()
|
||||
const existing = defineTool({
|
||||
name: 'send_message', description: 'Another messaging tool.', parameters: {},
|
||||
output: { schema: { type: 'object', additionalProperties: false, properties: {} }, render: () => [] },
|
||||
execute: () => Promise.resolve({}),
|
||||
})
|
||||
ctx.tools.register(existing)
|
||||
await expect(ctx.plugin(tool)).rejects.toThrow('already registered')
|
||||
expect(ctx.tools.get('send_message')).toBe(existing)
|
||||
expect(ctx.tools.get('list_agents')).toBeUndefined()
|
||||
expect(ctx.tools.get('wait_agent')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('updates address schemas when the naming plugin loads and unloads', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
expect(ctx.tools.get('send_message')!.parameters.properties).toHaveProperty('agent_id')
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, parent))).toBe('parent [inactive] — root agent')
|
||||
const names = await ctx.plugin(SubagentNames)
|
||||
expect(ctx.tools.get('send_message')!.parameters.properties).toHaveProperty('name')
|
||||
expect(ctx.tools.get('send_message')!.parameters.properties).not.toHaveProperty('agent_id')
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, parent))).toBe('lead [inactive] — root agent')
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, parent))).toBe('parent [inactive] — lead: root agent')
|
||||
await names.dispose()
|
||||
expect(ctx.tools.get('send_message')!.parameters.properties).toHaveProperty('agent_id')
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, parent))).toBe('parent [inactive] — root agent')
|
||||
})
|
||||
|
||||
it('discovers, messages, and interrupts children by Session id without the naming plugin', async () => {
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([
|
||||
{ chunks: textResponse('held'), gate: release.promise },
|
||||
{ chunks: textResponse('continued') },
|
||||
])
|
||||
const { ctx, parent } = await setupWith(adapter, true, false)
|
||||
const started = await ctx.subagents.startActivation({
|
||||
provider: 'spawn', label: 'legacy child', delivery: 'parent', parent,
|
||||
prompt: [{ type: 'text', text: 'work' }], signal: testToolSignal,
|
||||
it('continues a named child by path while keeping raw ID message attribution', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('first result'), textResponse('continued result')])
|
||||
await ctx.plugin(SubagentNames)
|
||||
const reservation = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
const child = await ctx.subagents.startActivation({
|
||||
provider: 'spawn', label: 'review', delivery: 'parent', parent,
|
||||
prompt: [{ type: 'text', text: 'review' }], signal: testToolSignal,
|
||||
})
|
||||
try {
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
const child = ctx.agents.get(started.childId)!
|
||||
const guidance = renderContextSnapshot(await ctx.systemPrompt.assemble({ agent: child, scope: child }))
|
||||
expect(guidance).toContain('send_message({ agent_id: "parent"')
|
||||
expect(text(await callTool(ctx, 'list_agents', {}, parent)))
|
||||
.toContain(`${child.id} [running] parent=parent depth=1 — legacy child`)
|
||||
const cancel = vi.spyOn(child, 'cancel')
|
||||
const interrupted = await callTool(ctx, 'interrupt_agent', { agent_id: child.id }, parent)
|
||||
expect(interrupted.isError, text(interrupted)).toBe(false)
|
||||
expect(cancel).toHaveBeenCalledExactlyOnceWith({ kind: 'parent' }, { keepInbox: true })
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
await started.result
|
||||
}
|
||||
const sent = await callTool(ctx, 'send_message', { agent_id: started.childId, message: 'continue' }, parent)
|
||||
expect(sent.isError, text(sent)).toBe(false)
|
||||
expect(text(sent)).toBe(`message delivered to agent ${started.childId}`)
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
ctx.subagentNames.bind(parent, child.childId, reservation.name)
|
||||
reservation.dispose()
|
||||
await child.result
|
||||
const result = await callTool(ctx, 'send_message', { name: reservation.name, message: 'check once more' }, parent)
|
||||
expect(result.isError, text(result)).toBe(false)
|
||||
expect(text(result)).toBe('message delivered to agent lead/reviewer')
|
||||
await waitNoActivation(ctx, child.childId)
|
||||
const interrupted = await callTool(ctx, 'interrupt_agent', { name: reservation.name }, parent)
|
||||
expect(interrupted.isError, text(interrupted)).toBe(false)
|
||||
const stored = await loadStoredSession(ctx.sessionPersistence, child.childId)
|
||||
expect(stored.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'agent-message' && event.data.source.senderSessionId === parent.id
|
||||
&& text(event.data).includes(`Agent ${parent.id} sent a message: `))).toBe(true)
|
||||
})
|
||||
|
||||
it('registers send_message once, globally, with the two required parameters', async () => {
|
||||
@@ -166,51 +199,16 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const schemas = ctx.tools.schemas().filter(schema => schema.name === 'send_message')
|
||||
expect(schemas).toHaveLength(1)
|
||||
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
|
||||
expect(Object.keys(props).sort()).toEqual(['message', 'name'])
|
||||
expect(Object.keys(props).sort()).toEqual(['agent_id', 'message'])
|
||||
// The continuable path has no Task, so the schema must not promise one.
|
||||
expect(schemas[0]!.description).not.toContain('job_output')
|
||||
expect(schemas[0]!.description).not.toContain('job id')
|
||||
expect(schemas[0]!.description).toContain('receives it at its next step')
|
||||
expect(props.name).toMatchObject({
|
||||
description: 'The full agent path (such as lead/reviewer) or Session id of another member of your subagent tree.',
|
||||
expect(props.agent_id).toMatchObject({
|
||||
description: 'The agent id of another member of your subagent tree, as shown by list_agents.',
|
||||
})
|
||||
})
|
||||
|
||||
it('continues a named child by path while preserving ID attribution in the delivered message', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('first result'), textResponse('continued result')])
|
||||
const child = await ctx.subagentNames.startActivation({
|
||||
name: 'reviewer', provider: 'spawn', label: 'review', delivery: 'parent',
|
||||
parent, prompt: [{ type: 'text', text: 'review the change' }], signal: testToolSignal,
|
||||
})
|
||||
await child.result
|
||||
const result = await callTool(ctx, 'send_message', { name: child.name, message: 'check one more case' }, parent)
|
||||
expect(result.isError, text(result)).toBe(false)
|
||||
expect(text(result)).toBe('message delivered to agent lead/reviewer')
|
||||
await waitNoActivation(ctx, child.childId)
|
||||
const stored = await loadStoredSession(ctx.sessionPersistence, child.childId)
|
||||
expect(stored.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'agent-message' && event.data.source.senderSessionId === parent.id
|
||||
&& text(event.data).includes(`Agent ${parent.id} sent a message: `))).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to parent IDs when names are unavailable and rejects corrupt names without retaining stale context', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
const { agent: child } = await ctx.agents.create({
|
||||
sessionId: SessionId('identity-child'), meta: { origin: 'subagent', parentSession: parent.id },
|
||||
})
|
||||
const nameOf = vi.spyOn(ctx.subagentNames, 'nameOf').mockResolvedValue('lead/reviewer')
|
||||
const context = { agent: child, scope: child }
|
||||
const first = await ctx.systemPrompt.assemble(context)
|
||||
expect(renderContextSnapshot(first)).toContain('Your name is "lead/reviewer"')
|
||||
nameOf.mockRejectedValue(new SubagentNameError('root unavailable', 'AGENT_NAMES_UNAVAILABLE'))
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble(context))).toContain('Your parent agent id is "parent"')
|
||||
nameOf.mockRejectedValue(new SubagentNameError('corrupt binding', 'CORRUPT_AGENT_NAMES'))
|
||||
await expect(ctx.systemPrompt.assemble(context)).rejects.toThrow('corrupt binding')
|
||||
expect(renderContextSnapshot(ctx.systemPrompt.refreshContext(first, context))).not.toContain('lead/reviewer')
|
||||
nameOf.mockRestore()
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble(context))).toContain('Your parent agent id is "parent"')
|
||||
})
|
||||
|
||||
it('keeps the send_message definition and ordering byte-identical in a fork child', async () => {
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const { ctx, parent, adapter } = await setupWith(new GatedAdapter([
|
||||
@@ -252,13 +250,13 @@ describe('dsh-tool-subagent-control', () => {
|
||||
expect(texts[0]).toBe('fork task')
|
||||
expect(texts[1]).toContain('Your final answer is delivered automatically')
|
||||
expect(texts[1]).not.toContain('send_message')
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('Your parent agent name is')
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('Your parent agent id is')
|
||||
expect(loaded.events.some(event => event.type === 'user/message' && event.data.source.kind === 'runtime-context'
|
||||
&& JSON.stringify(event.data.content).includes('Your parent agent name is'))).toBe(true)
|
||||
&& JSON.stringify(event.data.content).includes('Your parent agent id is'))).toBe(true)
|
||||
expect(adapter.requests[1]!.messages[0]).toEqual(adapter.requests[0]!.messages[0])
|
||||
})
|
||||
|
||||
it('uses names in logged communication context independently of the parent Session id', async () => {
|
||||
it('JSON-encodes communication ids in logged runtime context', async () => {
|
||||
const { ctx } = await setup([textResponse('child done')])
|
||||
const parent = await ctx.agentLoop.create(SessionId('parent"\nagent'), { provider: 'mock', model: 'mock' })
|
||||
parkParent(ctx, parent)
|
||||
@@ -272,11 +270,11 @@ describe('dsh-tool-subagent-control', () => {
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
|
||||
const prompt = loaded.events.find(event => event.type === 'user/message' && event.data.source.kind === 'runtime-context'
|
||||
&& event.data.content.some(block => block.type === 'text' && block.text.includes('Your parent agent name is')))
|
||||
&& event.data.content.some(block => block.type === 'text' && block.text.includes('Your parent agent id is')))
|
||||
if (prompt?.type !== 'user/message') throw new Error('expected logged communication context')
|
||||
const guidance = text(prompt.data)
|
||||
|
||||
expect(guidance).toContain('Your parent agent name is "lead"')
|
||||
expect(guidance).toContain(`Your parent agent id is ${JSON.stringify(parent.id)}`)
|
||||
expect(guidance).not.toContain(parent.id)
|
||||
})
|
||||
|
||||
@@ -287,28 +285,27 @@ describe('dsh-tool-subagent-control', () => {
|
||||
meta: { origin: 'subagent', parentSession: parent.id },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, parent) },
|
||||
})
|
||||
parent.session.append('subagent/catalog', { version: 0, childId: child.id, childCreatedAt: 1, mode: 'continuable', label: 'communication child' })
|
||||
const { agent: fork } = await ctx.agents.create({
|
||||
sessionId: SessionId('ordinary-fork'), meta: { parentSession: parent.id },
|
||||
})
|
||||
const communication = async () => renderContextSnapshot(await ctx.systemPrompt.assemble({ agent: child, scope: child }))
|
||||
expect(await communication()).toContain('Your parent agent name is "lead"')
|
||||
expect(await communication()).toContain(`Your parent agent id is ${JSON.stringify(parent.id)}`)
|
||||
for (const agent of [undefined, parent, fork]) {
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble(agent === undefined ? {} : { agent, scope: agent })))
|
||||
.not.toContain('Your parent agent name')
|
||||
.not.toContain('Your parent agent id')
|
||||
}
|
||||
const off = child.ctx.tools.register(defineTool({
|
||||
name: 'send_message', description: 'A different tool.', parameters: {},
|
||||
output: { schema: { type: 'object', additionalProperties: false, properties: {} }, render: () => [] },
|
||||
execute: () => Promise.resolve({}),
|
||||
}))
|
||||
expect(await communication()).not.toContain('Your parent agent name')
|
||||
expect(await communication()).not.toContain('Your parent agent id')
|
||||
off()
|
||||
expect(await communication()).toContain('Your parent agent name')
|
||||
expect(await communication()).toContain('Your parent agent id')
|
||||
child.ctx.tools.restrict({ deny: ['send_message'] })
|
||||
const toolSet = await ctx.tools.resolve({ agent: child, modelSelection: { provider: 'mock', model: 'mock' }, signal: testToolSignal })
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ agent: child, scope: child, toolSet })))
|
||||
.not.toContain('Your parent agent name')
|
||||
.not.toContain('Your parent agent id')
|
||||
toolSet.dispose()
|
||||
})
|
||||
|
||||
@@ -324,7 +321,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const childRequest = JSON.stringify(adapter.requests[0]!.messages)
|
||||
expect(childRequest.includes('Your final answer is delivered automatically')).toBe(delivery === 'parent')
|
||||
expect(childRequest).not.toContain('send_message')
|
||||
expect(childRequest).not.toContain('Your parent agent name')
|
||||
expect(childRequest).not.toContain('Your parent agent id')
|
||||
expect(followup.mock.calls.some(([message]) => message.source.kind === 'subagent-settled')).toBe(delivery === 'parent')
|
||||
})
|
||||
|
||||
@@ -349,7 +346,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
})
|
||||
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
name: parent.id,
|
||||
agent_id: parent.id,
|
||||
message: 'CHILD_FINDING',
|
||||
}, child)
|
||||
|
||||
@@ -382,7 +379,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
name: started.childId,
|
||||
agent_id: started.childId,
|
||||
message: 'and then?',
|
||||
}, parent)
|
||||
|
||||
@@ -416,7 +413,7 @@ describe('dsh-tool-subagent-control', () => {
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
name: started.childId,
|
||||
agent_id: started.childId,
|
||||
message: 'also consider Y',
|
||||
}, parent)
|
||||
expect(result.isError).toBe(false)
|
||||
@@ -439,11 +436,11 @@ describe('dsh-tool-subagent-control', () => {
|
||||
it('reports a delivery failure as an errored, not-delivered result', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
name: 'no-such-child',
|
||||
agent_id: 'no-such-child',
|
||||
message: 'hello?',
|
||||
}, parent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('was not found')
|
||||
expect(text(result)).toContain('unavailable')
|
||||
})
|
||||
|
||||
it('rejects a caller outside the child\'s subagent tree', async () => {
|
||||
@@ -459,16 +456,16 @@ describe('dsh-tool-subagent-control', () => {
|
||||
const stranger = await ctx.agentLoop.create(SessionId('stranger'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
const result = await callTool(ctx, 'send_message', {
|
||||
name: started.childId,
|
||||
agent_id: started.childId,
|
||||
message: 'mine now',
|
||||
}, stranger)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('was not found')
|
||||
expect(text(result)).toContain('not a member of this subagent tree')
|
||||
})
|
||||
|
||||
it('fails loud when invoked without a calling agent', async () => {
|
||||
const { ctx } = await setup([])
|
||||
const result = await callTool(ctx, 'send_message', { name: 'x', message: 'y' })
|
||||
const result = await callTool(ctx, 'send_message', { agent_id: 'x', message: 'y' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('requires a calling agent')
|
||||
})
|
||||
@@ -480,8 +477,6 @@ describe('dsh-tool-subagent-control', () => {
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentNames)
|
||||
await ctx.plugin(TestSessionQuery)
|
||||
const fiber = await ctx.plugin(tool, config)
|
||||
const parent = await ctx.agentLoop.create(SessionId('hmr-parent'), { provider: 'mock', model: 'mock' })
|
||||
const { agent: child } = await ctx.agents.create({
|
||||
@@ -489,9 +484,8 @@ describe('dsh-tool-subagent-control', () => {
|
||||
meta: { origin: 'subagent', parentSession: parent.id },
|
||||
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, parent) },
|
||||
})
|
||||
parent.session.append('subagent/catalog', { version: 0, childId: child.id, childCreatedAt: 1, mode: 'continuable', label: 'communication child' })
|
||||
const communication = async () => renderContextSnapshot(await ctx.systemPrompt.assemble({ agent: child, scope: child }))
|
||||
expect(await communication()).toContain('Your parent agent name')
|
||||
expect(await communication()).toContain('Your parent agent id')
|
||||
const names = ['send_message', 'list_agents', 'wait_agent']
|
||||
if (config.interrupt !== false) names.push('interrupt_agent')
|
||||
for (const name of names) expect(ctx.tools.schemas().filter(schema => schema.name === name)).toHaveLength(1)
|
||||
@@ -503,9 +497,9 @@ describe('dsh-tool-subagent-control', () => {
|
||||
expect(ctx.tools.get('list_subagent_models')).toBeUndefined()
|
||||
await fiber.dispose()
|
||||
for (const name of names) expect(ctx.tools.get(name)).toBeUndefined()
|
||||
expect(await communication()).not.toContain('Your parent agent name')
|
||||
expect(await communication()).not.toContain('Your parent agent id')
|
||||
const reloaded = await ctx.plugin(tool)
|
||||
expect(await communication()).toContain('Your parent agent name')
|
||||
expect(await communication()).toContain('Your parent agent id')
|
||||
for (const name of ['send_message', 'interrupt_agent', 'list_agents', 'wait_agent']) {
|
||||
expect(ctx.tools.schemas().filter(schema => schema.name === name)).toHaveLength(1)
|
||||
}
|
||||
@@ -527,44 +521,16 @@ describe('dsh-tool-subagent-control', () => {
|
||||
})
|
||||
|
||||
describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
it('registers interrupt_agent with the single name parameter and current-turn wording', async () => {
|
||||
it('registers interrupt_agent with the single agent_id parameter and current-turn wording', async () => {
|
||||
const { ctx } = await setup([])
|
||||
const schemas = ctx.tools.schemas().filter(schema => schema.name === 'interrupt_agent')
|
||||
expect(schemas).toHaveLength(1)
|
||||
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
|
||||
expect(Object.keys(props)).toEqual(['name'])
|
||||
expect(Object.keys(props)).toEqual(['agent_id'])
|
||||
expect(schemas[0]!.description).toContain('stop its current work')
|
||||
expect(schemas[0]!.description).toContain('send_message')
|
||||
})
|
||||
|
||||
it('leaves the child running when cancellation arrives during name resolution', async () => {
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([{ chunks: textResponse('finished'), gate: release.promise }])
|
||||
const { ctx, parent } = await setupWith(adapter)
|
||||
const child = await ctx.subagentNames.startActivation({
|
||||
name: 'reviewer', provider: 'spawn', label: 'review', delivery: 'parent',
|
||||
parent, prompt: [{ type: 'text', text: 'review' }], signal: testToolSignal,
|
||||
})
|
||||
try {
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
const cancel = vi.spyOn(ctx.agents.get(child.childId)!, 'cancel')
|
||||
const controller = new AbortController()
|
||||
const resolve = ctx.subagentNames.resolveName.bind(ctx.subagentNames)
|
||||
vi.spyOn(ctx.subagentNames, 'resolveName').mockImplementation(async (...args) => {
|
||||
const id = await resolve(...args)
|
||||
controller.abort()
|
||||
return id
|
||||
})
|
||||
const result = await callTool(ctx, 'interrupt_agent', { name: child.name }, parent, controller.signal)
|
||||
expect(result.isError, text(result)).toBe(true)
|
||||
expect(text(result)).toMatch(/abort/i)
|
||||
expect(cancel).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
await child.result
|
||||
}
|
||||
})
|
||||
|
||||
it('interrupts a running direct child with the parent cause, parking its queue', async () => {
|
||||
const releaseFirst = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([
|
||||
@@ -583,13 +549,13 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
|
||||
const child = ctx.agents.get(started.childId)!
|
||||
const queued = await callTool(ctx, 'send_message', {
|
||||
name: started.childId,
|
||||
agent_id: started.childId,
|
||||
message: 'parked follow-up',
|
||||
}, parent)
|
||||
expect(queued.isError).toBe(false)
|
||||
const cancelSpy = vi.spyOn(child, 'cancel')
|
||||
|
||||
const result = await callTool(ctx, 'interrupt_agent', { name: started.childId }, parent)
|
||||
const result = await callTool(ctx, 'interrupt_agent', { agent_id: started.childId }, parent)
|
||||
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toBe(`interrupt requested for agent ${started.childId}`)
|
||||
@@ -601,7 +567,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
expect(child.inbox.nextStep).toHaveLength(1)
|
||||
|
||||
const waking = await callTool(ctx, 'send_message', {
|
||||
name: started.childId,
|
||||
agent_id: started.childId,
|
||||
message: 'wake up',
|
||||
}, parent)
|
||||
expect(waking.isError).toBe(false)
|
||||
@@ -650,7 +616,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
const grandchildAgent = ctx.agents.get(grandchild.childId)!
|
||||
const cancelSpy = vi.spyOn(grandchildAgent, 'cancel')
|
||||
|
||||
const result = await callTool(ctx, 'interrupt_agent', { name: grandchild.childId }, parent)
|
||||
const result = await callTool(ctx, 'interrupt_agent', { agent_id: grandchild.childId }, parent)
|
||||
|
||||
expect(result.isError).toBe(false)
|
||||
expect(cancelSpy).toHaveBeenCalledExactlyOnceWith({ kind: 'parent' }, { keepInbox: true })
|
||||
@@ -689,15 +655,15 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
const stranger = await ctx.agentLoop.create(SessionId('stranger'), { provider: 'mock', model: 'mock' })
|
||||
const cancelSpy = vi.spyOn(targetAgent, 'cancel')
|
||||
|
||||
const self = await callTool(ctx, 'interrupt_agent', { name: target.childId }, targetAgent)
|
||||
const self = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, targetAgent)
|
||||
expect(self.isError).toBe(true)
|
||||
expect(text(self)).toContain('cannot interrupt itself')
|
||||
const fromSibling = await callTool(ctx, 'interrupt_agent', { name: target.childId }, siblingAgent)
|
||||
const fromSibling = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, siblingAgent)
|
||||
expect(fromSibling.isError).toBe(true)
|
||||
expect(text(fromSibling)).toContain('not a live descendant')
|
||||
const fromStranger = await callTool(ctx, 'interrupt_agent', { name: target.childId }, stranger)
|
||||
const fromStranger = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, stranger)
|
||||
expect(fromStranger.isError).toBe(true)
|
||||
expect(text(fromStranger)).toContain('was not found')
|
||||
expect(text(fromStranger)).toContain('not a live descendant')
|
||||
expect(cancelSpy).not.toHaveBeenCalled()
|
||||
|
||||
releaseA.resolve(undefined)
|
||||
@@ -706,7 +672,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
await waitNoActivation(ctx, sibling.childId)
|
||||
})
|
||||
|
||||
it('accepts settled targets without cold resume and rejects unknown addresses', async () => {
|
||||
it('accepts an absent target as a no-op without cold-resuming it', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('done')])
|
||||
const started = await ctx.subagents.startActivation({
|
||||
delivery: 'parent',
|
||||
@@ -717,19 +683,18 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
|
||||
})
|
||||
await waitNoActivation(ctx, started.childId)
|
||||
|
||||
const settled = await callTool(ctx, 'interrupt_agent', { name: started.childId }, parent)
|
||||
const settled = await callTool(ctx, 'interrupt_agent', { agent_id: started.childId }, parent)
|
||||
expect(settled.isError).toBe(false)
|
||||
expect(text(settled)).toBe(`interrupt requested for agent ${started.childId}`)
|
||||
const unknown = await callTool(ctx, 'interrupt_agent', { name: 'no-such-agent' }, parent)
|
||||
expect(unknown.isError).toBe(true)
|
||||
expect(text(unknown)).toContain('was not found')
|
||||
const unknown = await callTool(ctx, 'interrupt_agent', { agent_id: 'no-such-agent' }, parent)
|
||||
expect(unknown.isError).toBe(false)
|
||||
// No cold resume: the settled target never rematerialized.
|
||||
expect(ctx.agents.get(started.childId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails loud when invoked without a calling agent', async () => {
|
||||
const { ctx } = await setup([])
|
||||
const result = await callTool(ctx, 'interrupt_agent', { name: 'x' })
|
||||
const result = await callTool(ctx, 'interrupt_agent', { agent_id: 'x' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('requires a calling agent')
|
||||
})
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
en: 686fb5136bef0a3b
|
||||
zh: d25d6569174a01b5
|
||||
/deepseek-ai-dsh-tool-subagent/summary:
|
||||
en: f3bc11c938886906
|
||||
zh: 097d6ac20c4a083e
|
||||
en: 7e130a0ae9710707
|
||||
zh: c0064a555e8e82c6
|
||||
/deepseek-ai-dsh-tool-subagent/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-tool-subagent/use-this-package:
|
||||
en: d7f6f5c27d5ce6db
|
||||
zh: 167aab86ebaf45b7
|
||||
en: 4afa29cd0dd88929
|
||||
zh: cf663e26c625000b
|
||||
/deepseek-ai-dsh-tool-subagent/use-this-package/minimal-configuration:
|
||||
en: 1a23808ca32b8f2d
|
||||
zh: e637a35220da0564
|
||||
/deepseek-ai-dsh-tool-subagent/use-this-package/managed-delegation:
|
||||
en: f40a519a0da50df7
|
||||
zh: da4433f82d61ca85
|
||||
en: 47f5712ca5b830b7
|
||||
zh: 821c2af8d0139027
|
||||
/deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-preset:
|
||||
en: 1950ef92bab0f46e
|
||||
zh: 593f5f64ed9b5631
|
||||
@@ -36,11 +36,11 @@
|
||||
en: 485cbed262dadc83
|
||||
zh: f08a8cceae5fd442
|
||||
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership:
|
||||
en: 5f810162a8484d3e
|
||||
zh: 7e3b95a4fe6eb9de
|
||||
en: b8779dbccc6122b7
|
||||
zh: 715f8aa6040bd6d9
|
||||
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/result-acknowledgement:
|
||||
en: 6953ade5ef96e038
|
||||
zh: e9d3d86cb1a0ecff
|
||||
en: 04a59f91aceafd91
|
||||
zh: 748c6bcb77655d8d
|
||||
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/context-sensitive-wording:
|
||||
en: 1733728ac5aa6bb0
|
||||
zh: 341ffc6e21a289af
|
||||
@@ -111,8 +111,8 @@
|
||||
en: 45854c8f4da73af7
|
||||
zh: 7a9803ca8c03c40f
|
||||
/deepseek-ai-dsh-tool-subagent/model-experience/start-result/what-the-model-sees:
|
||||
en: 78c3b1c834f85311
|
||||
zh: 1f419e17c51b8b66
|
||||
en: c5627d1b5fbd5bfd
|
||||
zh: 48fc158716e9a4d0
|
||||
/deepseek-ai-dsh-tool-subagent/model-experience/start-result/token-effect:
|
||||
en: 807d71626c81c858
|
||||
zh: ba4b85709fa46c84
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Use this package to give an agent a named tool that delegates work to a configured backend. Every call starts a managed activation and returns a child address; the runtime owns completion notices and cleanup. Local children support messages and restoration, while external backends execute one task and return their final answer through the completion notice. Supported backends can expose approved child LLM routes. Each instance can select a child preset and depth limit.
|
||||
Use this package to give an agent a named tool that delegates work to a configured backend. Every call starts a managed activation and returns a child Session id; the runtime owns completion notices and cleanup. Local children support messages and restoration, while external backends execute one task and return their final answer through the completion notice. Supported backends can expose approved child LLM routes. Each instance can select a child preset and depth limit.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -29,7 +29,7 @@ The optional `cwd` argument selects the child's initial working directory. Relat
|
||||
|
||||
Mount one instance per delegation target, each with a distinct `toolName`. The tool exists exactly while its provider does, so sibling load order and provider reloads never strand it.
|
||||
|
||||
With [subagent-names](../subagent-names/README.md) loaded, the optional `name` argument chooses a unique sibling name; omission selects the first unused `agent-N`. The result contains a full path such as `lead/reviewer`. Without the naming plugin, the tool retains its Session-id delegation interface.
|
||||
With [subagent-names](../subagent-names/README.md) loaded, the optional `name` argument chooses a unique sibling name; omission selects the first unused `agent-N`. The result keeps the child Session id and adds a full path such as `lead/reviewer`. Without the naming plugin, the tool retains its Session-id delegation interface.
|
||||
|
||||
### Minimal configuration
|
||||
|
||||
@@ -60,7 +60,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
||||
|
||||
### Managed delegation
|
||||
|
||||
Every call returns `started subagent <address>` after the runtime accepts the child. It does not wait for the child's result. The activation owns the work independently of the completed tool call; the runtime sends a completion notice and releases execution resources.
|
||||
Every call returns `started subagent <id>` after the runtime accepts the child; named results add a second line, `name: <path>`. It does not wait for the child's result. The activation owns the work independently of the completed tool call; the runtime sends a completion notice and releases execution resources.
|
||||
|
||||
For every backend, the runtime automatically delivers the completion notice if the calling Agent is still online when the child finishes. The notice includes the final answer unless the child sent that Agent a message just before finishing. Local Spawn and Fork children can also send messages, accept more work through `send_message` while active, or resume after settling. Codex, Claude Code, ACP, and DSH SDK backends do not accept follow-up messages.
|
||||
|
||||
@@ -97,11 +97,11 @@ One instance is one provider plus one tool name. The plugin registers its defini
|
||||
|
||||
### Activation ownership
|
||||
|
||||
With naming available, the tool binds the name through `ctx.subagentNames.startActivation()`; otherwise it calls `ctx.subagents.startActivation()` directly. Both select parent delivery. The runtime owns startup cancellation, execution, outcome reporting, and cleanup. A startup failure is an errored tool result; failures after publication arrive through the completion notice, with diagnostics and partial output preserved by the runtime.
|
||||
The tool calls `ctx.subagents.startActivation()` with parent delivery. With naming available, it reserves the requested name before startup, binds the returned child id afterward, and releases the reservation when the call settles. A failed binding reports the already-started child id without stopping its work; callers must check that child before retrying. The runtime owns startup cancellation, execution, outcome reporting, and cleanup. Failures after publication arrive through the completion notice, with diagnostics and partial output preserved by the runtime.
|
||||
|
||||
### Result acknowledgement
|
||||
|
||||
The named tool result is `{ kind: 'activation', name }`; without the naming plugin it is `{ kind: 'activation', subagentId }`. It contains no execution Promise or final output; programmatic consumers that need to await results use the subagent service directly.
|
||||
The result is `{ kind: 'activation', subagentId }`, with an additional `name` when naming succeeds. It contains no execution Promise or final output; programmatic consumers that need to await results use the subagent service directly.
|
||||
|
||||
### Context-sensitive wording
|
||||
|
||||
@@ -210,7 +210,7 @@ Depth changes do not alter the system prompt or tool schemas. Updated runtime co
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The call retains the task description and prompt. Success returns `started subagent <address>`; startup rejection returns an error. Child execution details are delivered separately from this acknowledgement.
|
||||
The call retains the task description and prompt. Success returns `started subagent <id>` and an optional `name: <path>` line; startup rejection returns an error. Child execution details are delivered separately from this acknowledgement.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
使用本包可为 agent 提供具名工具,把工作委派给配置的后端。每次调用都会启动受管理的 activation 并返回子任务地址;运行时负责完成通知与清理。本地子任务支持消息和恢复,外部后端执行一次任务,并通过完成通知返回最终回答。受支持的后端可以公开获准的子级 LLM 路由。每个实例可选择子级 preset 和深度限制。
|
||||
使用本包可为 agent 提供具名工具,把工作委派给配置的后端。每次调用都会启动受管理的 activation 并返回子任务 Session id;运行时负责完成通知与清理。本地子任务支持消息和恢复,外部后端执行一次任务,并通过完成通知返回最终回答。受支持的后端可以公开获准的子级 LLM 路由。每个实例可选择子级 preset 和深度限制。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -29,7 +29,7 @@ kind: "package-reference"
|
||||
|
||||
每个委派目标挂载一个实例,且每个实例的 `toolName` 必须不同。工具与其提供方同时存在、同时消失,因此同级加载顺序与提供方重新加载都不会让工具悬空。
|
||||
|
||||
加载 [subagent-names](../subagent-names/README.zh.md) 后,可选 `name` 参数选择同级唯一名称,省略时分配首个未使用的 `agent-N`。工具返回完整路径,例如 `lead/reviewer`。未加载命名插件时,工具保留按 Session id 委派的接口。
|
||||
加载 [subagent-names](../subagent-names/README.zh.md) 后,可选 `name` 参数选择同级唯一名称,省略时分配首个未使用的 `agent-N`。工具保留子任务 Session id,并附加完整路径,例如 `lead/reviewer`。未加载命名插件时,工具保留按 Session id 委派的接口。
|
||||
|
||||
### 最小配置
|
||||
|
||||
@@ -60,7 +60,7 @@ kind: "package-reference"
|
||||
|
||||
### 受管理的委派
|
||||
|
||||
运行时接受子任务后,每次调用都会返回 `started subagent <address>`,不等待子任务结果。activation 独立于已完成的工具调用拥有这项工作;运行时发送完成通知并释放执行资源。
|
||||
运行时接受子任务后,每次调用都会返回 `started subagent <id>`;命名结果增加第二行 `name: <path>`。调用不等待子任务结果。activation 独立于已完成的工具调用拥有这项工作;运行时发送完成通知并释放执行资源。
|
||||
|
||||
所有后端都由运行时自动递送完成通知,前提是子任务结束时调用方 Agent 仍在线。通知包含最终回答,除非子任务在结束前刚向该 Agent 发过消息。本地 Spawn 和 Fork 子任务还可以发送消息,在活跃期间通过 `send_message` 接受更多工作,或在结束后恢复。Codex、Claude Code、ACP 与 DSH SDK 后端不接受后续消息。
|
||||
|
||||
@@ -97,11 +97,11 @@ kind: "package-reference"
|
||||
|
||||
### Activation 所有权
|
||||
|
||||
命名插件可用时,工具通过 `ctx.subagentNames.startActivation()` 绑定名称;否则直接调用 `ctx.subagents.startActivation()`。两种方式都选择向父任务交付。运行时拥有启动取消、执行、结果汇报与清理。启动失败产生出错的工具结果;发布后的失败通过完成通知到达,运行时保留诊断和部分输出。
|
||||
工具调用 `ctx.subagents.startActivation()`,并选择向父任务交付。命名插件可用时,先预留请求的名称,启动后绑定返回的子任务 id,在调用结束时释放预留。绑定失败会报告已启动的子任务 id,但不会停止其工作;调用方必须先检查该子任务再重试。运行时拥有启动取消、执行、结果汇报与清理。发布后的失败通过完成通知到达,运行时保留诊断和部分输出。
|
||||
|
||||
### 结果确认
|
||||
|
||||
命名模式的工具结果为 `{ kind: 'activation', name }`;未加载命名插件时为 `{ kind: 'activation', subagentId }`。其中没有执行 Promise 或最终输出;需要等待结果的程序调用方直接使用 subagent 服务。
|
||||
结果为 `{ kind: 'activation', subagentId }`,命名成功时附加 `name`。其中没有执行 Promise 或最终输出;需要等待结果的程序调用方直接使用 subagent 服务。
|
||||
|
||||
### 随上下文变化的措辞
|
||||
|
||||
@@ -210,7 +210,7 @@ Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do no
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
调用保留任务描述与提示词。成功返回 `started subagent <address>`;启动被拒绝则返回错误。子任务执行详情独立于这条确认交付。
|
||||
调用保留任务描述与提示词。成功返回 `started subagent <id>`,并可附带 `name: <path>` 行;启动被拒绝则返回错误。子任务执行详情独立于这条确认交付。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
* @module @deepseek-ai/dsh-tool-subagent
|
||||
*/
|
||||
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { scopeChainOf, scopeOf } from '@deepseek-ai/dsh-scope'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import { HarnessError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
assertSubagentMaxDepth,
|
||||
@@ -203,7 +204,6 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
const mount = (subagentProvider: SubagentProvider): void => {
|
||||
assertSubagentProviderConfiguration(subagentProvider)
|
||||
const names = runtimeCtx.get('subagentNames')
|
||||
const resultKey = names === undefined ? 'subagentId' : 'name'
|
||||
const wording = providerWording(subagentProvider.inheritsParentContext)
|
||||
const providerRouteDefaults = subagentProvider.agentRouteDefaults
|
||||
const selectionDescription = providerRouteDefaults !== undefined
|
||||
@@ -212,7 +212,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
const definition = defineTool({
|
||||
name: toolName,
|
||||
description: wording.description
|
||||
+ ` This tool starts an independently managed subagent and immediately returns its ${names === undefined ? 'id' : 'name'}. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing.`
|
||||
+ ' This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing.'
|
||||
+ (subagentProvider.prepareContinuable !== undefined
|
||||
? ' Use `send_message` to steer it while running or continue its conversation after it finishes.'
|
||||
: ' This backend does not accept follow-up messages.')
|
||||
@@ -267,10 +267,12 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, const: 'activation' },
|
||||
[resultKey]: { type: 'string', required: true },
|
||||
subagentId: { type: 'string', required: true },
|
||||
...names === undefined ? {} : { name: { type: 'string' as const, required: true as const } },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `started subagent ${value[resultKey]}` }],
|
||||
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}`
|
||||
+ (value.name === undefined ? '' : `\nname: ${value.name}`) }],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
@@ -340,8 +342,22 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
const started = await runtimeCtx.subagents.startActivation(spec)
|
||||
return { kind: 'activation' as const, subagentId: started.childId }
|
||||
}
|
||||
const started = await names.startActivation({ ...spec, ...args.name === undefined ? {} : { name: args.name } })
|
||||
return { kind: 'activation' as const, name: started.name }
|
||||
const reservation = names.reserve(parent, args.name)
|
||||
try {
|
||||
const started = await runtimeCtx.subagents.startActivation(spec)
|
||||
try {
|
||||
names.bind(parent, started.childId, reservation.name)
|
||||
} catch (cause: unknown) {
|
||||
throw new HarnessError(
|
||||
`Subagent ${started.childId} started, but its name ${reservation.name} could not be recorded. `
|
||||
+ 'Its execution is still managed by the subagent service. Check this child before retrying.',
|
||||
'SUBAGENT_NAME_BIND_FAILED', { cause },
|
||||
)
|
||||
}
|
||||
return { kind: 'activation' as const, subagentId: started.childId, name: reservation.name }
|
||||
} finally {
|
||||
reservation.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
const disposeTool = runtimeCtx.effect(() => {
|
||||
@@ -352,11 +368,13 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
unregister()
|
||||
}
|
||||
})
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous.
|
||||
mounted = { subagentProvider, definition, disposeTool }
|
||||
}
|
||||
|
||||
runtimeCtx.on('internal/service', (name) => {
|
||||
if (name !== 'subagentNames' || mounted === undefined) return
|
||||
if (name !== 'subagentNames' || mounted === undefined
|
||||
|| runtimeCtx.fiber.uid === null || runtimeCtx.fiber.state === FiberState.UNLOADING) return
|
||||
const { subagentProvider } = mounted
|
||||
mounted.disposeTool()
|
||||
mounted = undefined
|
||||
|
||||
@@ -16,7 +16,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
|
||||
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
|
||||
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentActivation, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
|
||||
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts'
|
||||
@@ -65,7 +65,7 @@ describe('dsh-tool-subagent', () => {
|
||||
)).rejects.toThrow('does not support child agentOptions')
|
||||
})
|
||||
|
||||
it('starts a managed subagent and returns its name without collecting its output', async () => {
|
||||
it('starts a managed subagent and returns its ID and name without collecting its output', async () => {
|
||||
const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' })
|
||||
const result = await callSubagent(ctx, {
|
||||
description: 'do a thing',
|
||||
@@ -78,6 +78,73 @@ describe('dsh-tool-subagent', () => {
|
||||
expect(text(result)).toMatch(/^started subagent /)
|
||||
})
|
||||
|
||||
it('rejects invalid names before starting a child', async () => {
|
||||
const ctx = await setup({ provider: 'mock' })
|
||||
const start = vi.spyOn(ctx.subagents, 'startActivation')
|
||||
const result = await callSubagent(ctx, { name: 'bad/name', description: 'invalid name', prompt: 'work' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(start).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a binding failure with the accepted child ID and leaves its work running', async () => {
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const ctx = await setup({ provider: 'mock' }, { onStart: () => release.promise })
|
||||
const parent = modelSelectionSetupAgent(ctx)
|
||||
const accepted = Promise.withResolvers<SubagentActivation>()
|
||||
const start = ctx.subagents.startActivation.bind(ctx.subagents)
|
||||
vi.spyOn(ctx.subagents, 'startActivation').mockImplementation(async (spec) => {
|
||||
const child = await start(spec)
|
||||
accepted.resolve(child)
|
||||
return child
|
||||
})
|
||||
vi.spyOn(ctx.subagentNames, 'bind').mockImplementationOnce(() => { throw new Error('name storage unavailable') })
|
||||
try {
|
||||
const result = await callSubagent(ctx, { name: 'reviewer', description: 'review code', prompt: 'work' })
|
||||
const child = await accepted.promise
|
||||
expect(result.error?.info?.code).toBe('SUBAGENT_NAME_BIND_FAILED')
|
||||
expect(text(result)).toContain(child.childId)
|
||||
const retry = ctx.subagentNames.reserve(parent, 'reviewer')
|
||||
retry.dispose()
|
||||
release.resolve(undefined)
|
||||
await expect(child.result).resolves.toMatchObject({ stopReason: 'completed' })
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['root disposal', 'plugin restart'] as const)('does not reinstall delegation while %s drains', async (mode) => {
|
||||
const ctx = await projectedContext()
|
||||
await mountWorkingDirectoryFixture(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await mock.mountScriptedProvider(ctx, { name: 'mock' })
|
||||
const names = await ctx.plugin(SubagentNames)
|
||||
const delegation = await ctx.plugin(tool, { provider: 'mock' })
|
||||
expect(ctx.tools.get('subagent')).toBeDefined()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
let cleanupDone = false
|
||||
delegation.ctx.effect(() => async () => {
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
cleanupDone = true
|
||||
})
|
||||
const register = vi.spyOn(ctx.tools, 'register')
|
||||
let settled = false
|
||||
const stop = mode === 'root disposal' ? ctx.fiber.dispose() : delegation.restart()
|
||||
const removeNames = mode === 'root disposal' ? Promise.resolve() : names.dispose()
|
||||
const drained = Promise.allSettled([stop, removeNames]).then((results) => { settled = true; return results })
|
||||
try {
|
||||
await entered.promise
|
||||
expect(settled).toBe(false)
|
||||
expect(cleanupDone).toBe(false)
|
||||
expect(register).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
}
|
||||
expect((await drained).every(result => result.status === 'fulfilled')).toBe(true)
|
||||
expect(cleanupDone).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps id-based delegation without naming and refreshes the schema when naming loads or unloads', async () => {
|
||||
const ctx = await setup({ provider: 'mock', withNames: false })
|
||||
const properties = () => ctx.tools.get('subagent')!.parameters.properties
|
||||
@@ -91,7 +158,8 @@ describe('dsh-tool-subagent', () => {
|
||||
const named = await callSubagent(ctx, { name: 'reviewer', description: 'named task', prompt: 'review' })
|
||||
expect(named.isError).toBe(false)
|
||||
if (named.isError) throw new Error('expected named subagent success')
|
||||
expect(named.value).toEqual({ kind: 'activation', name: 'lead/reviewer' })
|
||||
expect(named.value).toMatchObject({ kind: 'activation', name: 'lead/reviewer' })
|
||||
expect(named.value).toHaveProperty('subagentId', expect.any(String))
|
||||
await names.dispose()
|
||||
expect(properties()).not.toHaveProperty('name')
|
||||
})
|
||||
@@ -737,7 +805,7 @@ describe('dsh-tool-subagent local activation', () => {
|
||||
})).toEqual({ kind: 'parallel' })
|
||||
})
|
||||
|
||||
it('starts a local activation and returns only its full name', async () => {
|
||||
it('starts a local activation and keeps its Session id alongside its full name', async () => {
|
||||
const { ctx, parent } = await continuableSetup()
|
||||
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')!
|
||||
// Continuable delegation has no Task, so the schema promises no collection.
|
||||
@@ -746,7 +814,7 @@ describe('dsh-tool-subagent local activation', () => {
|
||||
expect(schema.description).toContain('send_message')
|
||||
expect(schema.description).toContain('steer it while running')
|
||||
expect(schema.description).not.toContain('send_message` starts a later turn')
|
||||
expect(schema.description).toContain('immediately returns its name')
|
||||
expect(schema.description).toContain('immediately returns its id')
|
||||
expect(schema.description).not.toContain('never poll or wait on it')
|
||||
const properties = (schema.parameters as {
|
||||
properties: Record<string, { description?: string }>
|
||||
@@ -762,12 +830,12 @@ describe('dsh-tool-subagent local activation', () => {
|
||||
{ agent: parent },
|
||||
)
|
||||
expect(started.isError, text(started)).toBe(false)
|
||||
const match = /^started subagent (\S+)$/.exec(text(started))
|
||||
const match = /^started subagent (\S+)\nname: lead\/agent-1$/.exec(text(started))
|
||||
expect(match).not.toBeNull()
|
||||
expect(match?.[1]).toBe('lead/agent-1')
|
||||
const catalog = ctx.sessionProjections.snapshot(parent.session, ['subagentCatalog']).values.subagentCatalog!
|
||||
expect(catalog).toHaveLength(1)
|
||||
const childId = catalog[0]!.id
|
||||
expect(match?.[1]).toBe(childId)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(ctx.agents.get(childId)).toBeUndefined()
|
||||
@@ -839,6 +907,7 @@ describe('dsh-tool-subagent local activation', () => {
|
||||
|
||||
expect(succeeded.isError ? undefined : succeeded.value).toEqual({
|
||||
kind: 'activation',
|
||||
subagentId: survivingChildId,
|
||||
name: 'lead/agent-2',
|
||||
})
|
||||
await vi.waitFor(() => {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: d7dd048cd69e723d
|
||||
zh: 9f5dfa5186549cd0
|
||||
/deepseek-ai-dsh-llm-replay/use-this-package/nested-agents:
|
||||
en: b36583992187fd87
|
||||
zh: 994a9d4fbc5cb783
|
||||
en: 7745c49d10a63b0e
|
||||
zh: 937b954c664f1973
|
||||
/deepseek-ai-dsh-llm-replay/use-this-package/failure-modes-and-overrides:
|
||||
en: 319e95521a4a06fe
|
||||
zh: 884c8299028df8a3
|
||||
|
||||
@@ -73,8 +73,6 @@ The fixture is a projection of one selected persisted Session generation produce
|
||||
|
||||
A scenario where a parent agent delegates to in-process subagents records one role per Session: parent `session[.vN].jsonl`, then contiguous children `session.<ordinal>[.vN].jsonl`. The snapshot harness supplies only the highest generation of each role. Live Session ids are freshly random each run, so replay binds each live Session to a recorded script by first-call order: the first live Session to make a model call claims the parent script, the next new Session the next child script, and so on, with each Session advancing its own cursor. More distinct live Sessions than recorded scripts fails loud.
|
||||
|
||||
Recorded `{{session:N}}` tokens resolve to the live Session id at the same ordinal. A `started subagent` receipt can bind that id before a background child makes its first model call. Raw-id receipts work without naming services; `lead/...` receipts require the live calling Agent and `subagentNames.resolveName`, so unresolved names fail instead of becoming Session ids.
|
||||
|
||||
### Failure modes and overrides
|
||||
|
||||
When replay serves `deepseek-official` with `ctx.deepseekLlmApiExtensions`, it prepares and accepts those fields after selecting a valid script entry and before yielding the first chunk. This mirrors the live adapter's post-2xx commit point, so durable acceptance watermarks and SDK event notifications behave the same in recording and replay. Replay supplies a synthetic `{ messages: [] }` base body: it proves acceptance side effects, not prepared field bytes.
|
||||
|
||||
@@ -73,8 +73,6 @@ fixture 是运行一次真实 agent 所产生的一份选定持久化 Session ge
|
||||
|
||||
parent agent 委托给进程内 subagent 的场景会为每个 Session 记录一个角色:parent 为 `session[.vN].jsonl`,随后是连续 child `session.<ordinal>[.vN].jsonl`。snapshot harness 只提供每个角色的最高 generation。live Session id 每次运行都会重新随机生成,因此 replay 按首次调用顺序把每个 live Session 绑定到已记录脚本:第一个发起模型调用的 live Session 取得 parent 脚本,下一个新 Session 取得下一条 child 脚本,依此类推,每个 Session 分别推进自己的 cursor。不同 live Session 数量超过已记录脚本数时会明确报错。
|
||||
|
||||
已记录的 `{{session:N}}` token 会解析为相同序号的 live Session id。`started subagent` 回执可以在后台 child 首次调用模型之前绑定该 id。原始 id 回执无需命名服务即可使用;`lead/...` 回执需要存活的调用方 Agent 与 `subagentNames.resolveName`,因此无法解析的名称会报错,不会被当作 Session id。
|
||||
|
||||
### 失败模式与覆盖
|
||||
|
||||
当回放在带有 `ctx.deepseekLlmApiExtensions` 的组合中服务 `deepseek-official` 时,它会在选择有效脚本条目后、产生首个分片前准备并接受这些字段。这与实时适配器的 2xx 后提交点一致,因此持久接受水位与 SDK 事件通知在录制和回放中行为相同。回放提供合成 `{ messages: [] }` 基础 body:它证明接受副作用,而非准备后的字段字节。
|
||||
|
||||
@@ -28,35 +28,22 @@
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-names": "workspace:*"
|
||||
"@deepseek-ai/dsh-session": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-agent": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-subagent-names": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-names": "workspace:*"
|
||||
"@deepseek-ai/dsh-session": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-session-format-catalog": "workspace:*",
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { delimiter as pathDelimiter } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-compaction'
|
||||
import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
|
||||
import { SESSION_FORMAT_VERSION, SessionLogOffset, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-subagent-names'
|
||||
import {
|
||||
createSessionFormatCatalogWithChildren,
|
||||
SessionFormatUnsupportedMigrationError,
|
||||
@@ -647,33 +645,20 @@ function materializeSessionTokens(entry: ReplayEntry, liveSessionIds: readonly (
|
||||
return replace(entry) as ReplayEntry
|
||||
}
|
||||
|
||||
/** Resolve background-child receipts to Session ids before those children reach their first model calls. */
|
||||
async function inferStartedSubagents(
|
||||
ctx: Context,
|
||||
options: GenerateOptions,
|
||||
/** Learn a background child id from the stable tool-result text before that child reaches its first model call. */
|
||||
function inferStartedSubagents(
|
||||
messages: GenerateOptions['messages'],
|
||||
liveSessionIds: (string | undefined)[],
|
||||
): Promise<void> {
|
||||
): void {
|
||||
const leaves: string[] = []
|
||||
collectStrings(options.messages, leaves)
|
||||
collectStrings(messages, leaves)
|
||||
for (const leaf of leaves) {
|
||||
for (const match of leaf.matchAll(/started subagent ([^\s"'<>]+)/g)) {
|
||||
const receipt = match[1]
|
||||
const id = match[1]
|
||||
/* v8 ignore next -- the fixed regular expression always has capture group 1. */
|
||||
if (receipt === undefined) continue
|
||||
let index = liveSessionIds.findIndex((value, candidate) => candidate > 0 && value === undefined)
|
||||
if (id === undefined || liveSessionIds.includes(id)) continue
|
||||
const index = liveSessionIds.findIndex((value, candidate) => candidate > 0 && value === undefined)
|
||||
if (index < 0) return
|
||||
let id = receipt
|
||||
if (receipt.startsWith('lead/')) {
|
||||
const names = ctx.get('subagentNames')
|
||||
const caller = options.sessionId === undefined ? undefined : ctx.get('agents')?.get(options.sessionId)
|
||||
if (names === undefined || caller === undefined) {
|
||||
throw new Error(`llm-replay: named subagent receipt ${receipt} requires subagentNames and its live calling Agent`)
|
||||
}
|
||||
id = await names.resolveName(caller, receipt, options.signal)
|
||||
index = liveSessionIds.findIndex((value, candidate) => candidate > 0 && value === undefined)
|
||||
if (index < 0) return
|
||||
}
|
||||
if (liveSessionIds.includes(id)) continue
|
||||
liveSessionIds[index] = id
|
||||
}
|
||||
}
|
||||
@@ -1094,7 +1079,7 @@ export function installLlmReplay(ctx: Context, config: ReplayConfig): ReplayHand
|
||||
+ `but its script has only ${boundState.entries.length}; re-record the scenario`,
|
||||
)
|
||||
}
|
||||
await inferStartedSubagents(ctx, options, liveSessionIds)
|
||||
inferStartedSubagents(options.messages, liveSessionIds)
|
||||
const resolved = resolveScriptedEntry(materializeSessionTokens(entry, liveSessionIds), options.messages)
|
||||
if (options.provider === 'deepseek-official' && providerAccepted(resolved)) {
|
||||
const extensions = ctx.get('deepseekLlmApiExtensions')
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
import { SESSION_FORMAT_VERSION, SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
|
||||
import { CompactionId } from '@deepseek-ai/dsh-compaction'
|
||||
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
|
||||
import LlmRuntime, {
|
||||
@@ -39,7 +35,6 @@ import {
|
||||
prepareSessionSnapshotFixtureForComparison,
|
||||
resolveScriptedEntry,
|
||||
} from '../src/index.ts'
|
||||
import { TestSessionQuery } from '../../../subagent/subagent/tests/test-session-query.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-deepseek-llm-api-extensions/types' {
|
||||
interface DeepSeekLlmApiExtensionMap {
|
||||
@@ -1998,31 +1993,6 @@ describe('installLlmReplay (per-session keying)', () => {
|
||||
const live = (id: string): GenerateOptions =>
|
||||
({ provider: 'm', model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
|
||||
async function namedParent() {
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await mountAgentLoopTestDependencies(ctx, { workingDirectory: true })
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(TestSessionQuery)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(SubagentNames)
|
||||
const parent = await ctx.agentLoop.create(SessionId('live-parent'))
|
||||
const childId = SessionId('live-child-before-call')
|
||||
parent.session.append('subagent-name/bound', { version: 1, childId, name: 'reviewer' })
|
||||
parent.session.append('subagent/catalog', {
|
||||
version: 0, childId, childCreatedAt: 2, mode: 'continuable', label: 'Review',
|
||||
})
|
||||
return { ctx, parent, childId }
|
||||
}
|
||||
|
||||
function namedReceipt(name = 'lead/reviewer'): GenerateOptions['messages'] {
|
||||
return [createToolResultMessage({
|
||||
callId: ToolCallId('start-reviewer'),
|
||||
isError: false,
|
||||
content: [{ type: 'text', text: `started subagent ${name}` }],
|
||||
})]
|
||||
}
|
||||
|
||||
it('routes each live session to its own script by FIRST-CALL order', async () => {
|
||||
const parentFile = writeSession('session.jsonl', { id: 'rec-parent', createdAt: 100 }, [TEXT_CHUNKS])
|
||||
const childFile = writeSession('session.1.jsonl', { id: 'rec-child', createdAt: 200 }, [second])
|
||||
@@ -2105,126 +2075,28 @@ describe('installLlmReplay (per-session keying)', () => {
|
||||
|
||||
it('learns a background child id from its started-subagent tool result', async () => {
|
||||
const reference: StreamChunk[] = [
|
||||
{ type: 'text-delta', index: 0, text: '{{session:2}} {{session:3}}' },
|
||||
{ type: 'text-delta', index: 0, text: '{{session:2}}' },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [reference])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const otherChildFile = writeSession('session.2.jsonl', { id: '{{session:3}}', createdAt: 3 }, [second])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmRuntime)
|
||||
installLlmReplay(ctx, { file: parentFile, childFiles: [childFile, otherChildFile] })
|
||||
installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
const options: GenerateOptions = {
|
||||
...live('live-parent'),
|
||||
messages: [createUserMessage({
|
||||
content: [{ type: 'text', text: 'started subagent live-child-before-call started subagent live-child-before-call started subagent live-other-child' }],
|
||||
content: [{ type: 'text', text: 'started subagent live-child-before-call started subagent live-child-before-call' }],
|
||||
source: { kind: 'user' },
|
||||
})],
|
||||
}
|
||||
|
||||
expect(await drain(ctx.llm.stream(options))).toEqual([
|
||||
{ type: 'text-delta', index: 0, text: 'live-child-before-call live-other-child' },
|
||||
{ type: 'text-delta', index: 0, text: 'live-child-before-call' },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('resolves a named receipt to the catalog child before its first model call', async () => {
|
||||
const reference: StreamChunk[] = [
|
||||
{ type: 'text-delta', index: 0, text: '{{session:2}}' },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [reference])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const { ctx, parent, childId } = await namedParent()
|
||||
const replay = installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
|
||||
expect(await drain(ctx.llm.stream({
|
||||
...live(parent.id),
|
||||
messages: namedReceipt(),
|
||||
}))).toEqual([
|
||||
{ type: 'text-delta', index: 0, text: childId },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
])
|
||||
expect(await drain(ctx.llm.stream(live(childId)))).toEqual(second)
|
||||
replay.assertConsumed()
|
||||
})
|
||||
|
||||
it('retains a child binding made while its receipt name is resolving', async () => {
|
||||
const reference: StreamChunk[] = [
|
||||
{ type: 'text-delta', index: 0, text: '{{session:2}}' },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [reference])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const { ctx, parent, childId } = await namedParent()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const resolveName = ctx.subagentNames.resolveName.bind(ctx.subagentNames)
|
||||
vi.spyOn(ctx.subagentNames, 'resolveName').mockImplementationOnce(async (...args) => {
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
return resolveName(...args)
|
||||
})
|
||||
const replay = installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
const pending = drain(ctx.llm.stream({ ...live(parent.id), messages: namedReceipt() }))
|
||||
try {
|
||||
await entered.promise
|
||||
expect(await drain(ctx.llm.stream(live(childId)))).toEqual(second)
|
||||
} finally {
|
||||
release.resolve(undefined)
|
||||
await pending
|
||||
}
|
||||
expect(await pending).toEqual([
|
||||
{ type: 'text-delta', index: 0, text: childId },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
])
|
||||
replay.assertConsumed()
|
||||
})
|
||||
|
||||
it('rejects a named receipt absent from the caller catalog', async () => {
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [TEXT_CHUNKS])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const { ctx, parent } = await namedParent()
|
||||
installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
|
||||
await expect(drain(ctx.llm.stream({
|
||||
...live(parent.id), messages: namedReceipt('lead/missing'),
|
||||
}))).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
|
||||
})
|
||||
|
||||
it('rejects a named receipt when its calling Agent is unavailable', async () => {
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [TEXT_CHUNKS])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const { ctx } = await namedParent()
|
||||
installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
|
||||
await expect(drain(ctx.llm.stream({
|
||||
...live('unavailable-parent'), messages: namedReceipt(),
|
||||
}))).rejects.toThrow('named subagent receipt lead/reviewer requires subagentNames and its live calling Agent')
|
||||
})
|
||||
|
||||
it.each([undefined, 'live-parent'])('rejects a named receipt without its naming context (%s)', async (sessionId) => {
|
||||
const reference: StreamChunk[] = [
|
||||
{ type: 'text-delta', index: 0, text: '{{session:2}}' },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [reference])
|
||||
const childFile = writeSession('session.1.jsonl', { id: '{{session:2}}', createdAt: 2 }, [second])
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await ctx.plugin(LlmRuntime)
|
||||
installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] })
|
||||
|
||||
await expect(drain(ctx.llm.stream({
|
||||
provider: 'm', model: 'm',
|
||||
...sessionId === undefined ? {} : { sessionId: SessionId(sessionId) },
|
||||
messages: [createUserMessage({
|
||||
content: [{ type: 'text', text: 'started subagent lead/reviewer' }],
|
||||
source: { kind: 'user' },
|
||||
})],
|
||||
}))).rejects.toThrow('named subagent receipt lead/reviewer requires subagentNames and its live calling Agent')
|
||||
})
|
||||
|
||||
it('ignores started-subagent text after every recorded session has bound', async () => {
|
||||
const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [TEXT_CHUNKS])
|
||||
const ctx = new Context()
|
||||
|
||||
@@ -28,12 +28,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../../session/session-format-catalog"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent-names"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
-18
@@ -15727,9 +15727,6 @@ importers:
|
||||
'@deepseek-ai/dsh-session-projection':
|
||||
specifier: workspace:*
|
||||
version: link:../../session/session-projection
|
||||
'@deepseek-ai/dsh-session-query':
|
||||
specifier: workspace:*
|
||||
version: link:../../session-query/session-query
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:*
|
||||
version: link:../subagent
|
||||
@@ -16250,15 +16247,6 @@ importers:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:~
|
||||
version: link:../../../vendor/cordis
|
||||
'@deepseek-ai/dsh-agent':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/agent
|
||||
'@deepseek-ai/dsh-agent-loop':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/agent-loop
|
||||
'@deepseek-ai/dsh-agent-loop-testkit':
|
||||
specifier: workspace:*
|
||||
version: link:../agent-loop-testkit
|
||||
'@deepseek-ai/dsh-compaction':
|
||||
specifier: workspace:*
|
||||
version: link:../../compaction/compaction
|
||||
@@ -16271,12 +16259,6 @@ importers:
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:*
|
||||
version: link:../../subagent/subagent
|
||||
'@deepseek-ai/dsh-subagent-names':
|
||||
specifier: workspace:*
|
||||
version: link:../../subagent/subagent-names
|
||||
|
||||
packages/test-support/loader-smoke:
|
||||
dependencies:
|
||||
|
||||
@@ -651,7 +651,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ContinuableCreateSpec: 'subagent.md',
|
||||
SubagentActivation: 'subagent.md',
|
||||
SubagentActivationSpec: 'subagent.md',
|
||||
NamedSubagentActivationSpec: 'subagent.md',
|
||||
SubagentNameReservation: 'subagent.md',
|
||||
SubagentNameBinding: 'subagent.md',
|
||||
SubagentNameError: 'subagent.md',
|
||||
SubagentNameErrorCode: 'subagent.md',
|
||||
|
||||
@@ -808,7 +808,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Optional subagent names',
|
||||
mode: 'core',
|
||||
consumers: ['tool-subagent', 'tool-subagent-control', 'experimental-tool-agent-team', 'experimental-tool-subagent-oar'],
|
||||
note: 'Owns sibling name reservations, durable bindings, and catalog-backed path lookup. Execution and authorization remain with the subagent service.',
|
||||
note: 'Owns name reservations and durable name-to-id bindings in the root Session. Execution and authorization remain with the subagent service.',
|
||||
},
|
||||
{
|
||||
key: 'speechToText',
|
||||
|
||||
@@ -575,7 +575,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
list_subagent_models: 'packages/subagent/tool-subagent/src/list-models.ts',
|
||||
subagent: 'packages/subagent/tool-subagent/src/index.ts',
|
||||
},
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.sessionProjections', 'optional ctx.subagentNames for names instead of Session ids', 'ctx.llm for model discovery and selected-route validation'],
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.sessionProjections', 'optional ctx.subagentNames for names alongside Session ids', 'ctx.llm for model discovery and selected-route validation'],
|
||||
writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
|
||||
shippedNames: ['subagent', 'subagent_fork'],
|
||||
async mount(ctx) {
|
||||
@@ -587,7 +587,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolSubagent, { provider: 'mock', modelDiscovery: true })
|
||||
},
|
||||
note:
|
||||
'The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path promptly. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically.',
|
||||
'The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. This catalog includes the optional naming plugin: delegation accepts a sibling name and returns its full path alongside the child Session id. Without that plugin it accepts no name and returns a Session id. Local children accept follow-ups, while external backends report their single execution automatically.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-subagent-control',
|
||||
@@ -598,7 +598,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
send_message: 'packages/subagent/tool-subagent-control/src/index.ts',
|
||||
wait_agent: 'packages/subagent/tool-subagent-control/src/wait-agent.ts',
|
||||
},
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.agents', 'ctx.systemPrompt', 'optional ctx.subagentNames for names instead of Session ids'],
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.agents', 'ctx.systemPrompt', 'optional ctx.subagentNames for names alongside Session ids'],
|
||||
writes: ['tool/call', 'tool/result', 'child session events through ctx.subagents'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
@@ -609,7 +609,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolSubagentControl)
|
||||
},
|
||||
note:
|
||||
'The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery returns paths. Without that plugin, controls retain `agent_id` and discovery returns Session ids.',
|
||||
'The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. This catalog includes optional naming: controls accept a name path or Session id through `name`, and discovery adds names while retaining Session ids. Without that plugin, controls retain `agent_id` and discovery returns Session ids.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-jobs',
|
||||
|
||||
@@ -885,19 +885,9 @@
|
||||
"arguments": "{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "subagent-name/bound",
|
||||
"seq": 37,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 1,
|
||||
"childId": "{{child-1}}",
|
||||
"name": "agent-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "subagent/catalog",
|
||||
"seq": 38,
|
||||
"seq": 37,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 0,
|
||||
@@ -907,6 +897,16 @@
|
||||
"label": "Check direct child"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "subagent-name/bound",
|
||||
"seq": 38,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 1,
|
||||
"childId": "{{child-1}}",
|
||||
"name": "lead/agent-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool/result",
|
||||
"seq": 39,
|
||||
@@ -924,7 +924,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "started subagent lead/agent-1"
|
||||
"text": "started subagent {{child-1}}\nname: lead/agent-1"
|
||||
}
|
||||
],
|
||||
"isError": false,
|
||||
@@ -2583,22 +2583,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{parent}}",
|
||||
"event": {
|
||||
"type": "subagent-name/bound",
|
||||
"seq": 37,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 1,
|
||||
"childId": "{{child-1}}",
|
||||
"name": "agent-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "subagent.started",
|
||||
"payload": {
|
||||
@@ -2718,10 +2702,6 @@
|
||||
"type": "text",
|
||||
"text": "Reply with exactly DIRECT_CHILD_OK and nothing else."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."
|
||||
@@ -2799,7 +2779,7 @@
|
||||
"sessionId": "{{parent}}",
|
||||
"event": {
|
||||
"type": "subagent/catalog",
|
||||
"seq": 38,
|
||||
"seq": 37,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 0,
|
||||
@@ -2811,6 +2791,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
"sessionId": "{{parent}}",
|
||||
"event": {
|
||||
"type": "subagent-name/bound",
|
||||
"seq": 38,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"version": 1,
|
||||
"childId": "{{child-1}}",
|
||||
"name": "lead/agent-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "session.event",
|
||||
"payload": {
|
||||
@@ -2832,7 +2828,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "started subagent lead/agent-1"
|
||||
"text": "started subagent {{child-1}}\nname: lead/agent-1"
|
||||
}
|
||||
],
|
||||
"isError": false,
|
||||
@@ -2975,10 +2971,6 @@
|
||||
"type": "text",
|
||||
"text": "Reply with exactly DIRECT_CHILD_OK and nothing else."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":33}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{child-1}}","name":"agent-1"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"continuable","label":"Check direct child"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-direct-child"},"toolCallId":"advanced-direct-child","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[36],"surfaceOp":"append"}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{child-1}}","name":"lead/agent-1"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"advanced-direct-child"},"toolCallId":"advanced-direct-child","content":[{"type":"text","text":"started subagent {{child-1}}\nname: lead/agent-1"}],"isError":false,"id":"{{messageId}}"}},"sourceEventSeqs":[36],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","data":{"turn":1,"step":4}}
|
||||
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":41}}
|
||||
|
||||
@@ -48,6 +48,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
* blocks. A package moves on or off this list with its context behavior.
|
||||
*/
|
||||
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/subagent/subagent-names': { kind: 'indirect', reason: 'Stores durable name-to-id bindings; delegation and control tools own their model-facing rendering.' },
|
||||
'packages/client/ui-entry-customization': { kind: 'none', reason: 'Controls browser launchers and prepares a reviewable Creator draft without submitting model input.' },
|
||||
'packages/experimental/client-ui-cot-translation': { kind: 'indirect', reason: 'The translator owns optional paid queries; normal Session activation delegates startup context to the preset.' },
|
||||
'packages/experimental/cot-translation-bundle': { kind: 'indirect', reason: 'The GUI consumer delegates paid query prompts to the translator and normal activation context to the Session preset.' },
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -677,7 +677,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -706,7 +706,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -343,7 +343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -677,7 +677,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -706,7 +706,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -150,7 +150,7 @@ interface ToolArgsMap {
|
||||
/** Max wait in milliseconds with wait: true. Defaults to and is capped by configuration. */
|
||||
timeout_ms?: number;
|
||||
} & Record<string, JsonValue>;
|
||||
/** List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. */
|
||||
/** List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. */
|
||||
list_agents: Record<string, JsonValue>;
|
||||
/** Declare existing files as final deliverables for the user. Hosts may show presented files only after your turn ends, so call it once the files are final. If the user needs to review files earlier, such as before a tool call that waits for their answer, link them in your message instead. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */
|
||||
present: {
|
||||
@@ -275,7 +275,7 @@ interface ToolArgsMap {
|
||||
/** The exact skill name from the available skills list. */
|
||||
name: string;
|
||||
} & Record<string, JsonValue>;
|
||||
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. */
|
||||
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. */
|
||||
subagent: {
|
||||
/** Optional name unique among your direct children. Omission selects the first unused agent-N, starting at agent-1. Use one segment starting with a lower-case letter, followed by lower-case letters, digits, underscores, or hyphens. Names remain taken after agents finish. The result returns the full path, such as lead/reviewer. */
|
||||
name?: string;
|
||||
@@ -286,7 +286,7 @@ interface ToolArgsMap {
|
||||
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
|
||||
prompt: string;
|
||||
} & Record<string, JsonValue>;
|
||||
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. */
|
||||
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. */
|
||||
subagent_fork: {
|
||||
/** Optional name unique among your direct children. Omission selects the first unused agent-N, starting at agent-1. Use one segment starting with a lower-case letter, followed by lower-case letters, digits, underscores, or hyphens. Names remain taken after agents finish. The result returns the full path, such as lead/reviewer. */
|
||||
name?: string;
|
||||
@@ -517,20 +517,25 @@ interface ToolOutputMap {
|
||||
};
|
||||
list_agents: ({
|
||||
kind: "root";
|
||||
name: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
status: "running" | "inactive";
|
||||
} | {
|
||||
kind: "child";
|
||||
name: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
label: string;
|
||||
status: "running" | "inactive";
|
||||
parent?: string;
|
||||
parentName?: string;
|
||||
depth?: number;
|
||||
} | {
|
||||
kind: "diagnostic";
|
||||
name: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
reason: "corrupt" | "unsupported" | "unavailable";
|
||||
parent?: string;
|
||||
parentName?: string;
|
||||
depth?: number;
|
||||
})[];
|
||||
present: {
|
||||
@@ -880,10 +885,12 @@ interface ToolOutputMap {
|
||||
};
|
||||
subagent: {
|
||||
kind: "activation";
|
||||
subagentId: string;
|
||||
name: string;
|
||||
};
|
||||
subagent_fork: {
|
||||
kind: "activation";
|
||||
subagentId: string;
|
||||
name: string;
|
||||
};
|
||||
todo_write: {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_OK."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_OK."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:12}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_OK."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_OK."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly CHILD_OK. Your","messageSeqs":[9],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -12,20 +12,19 @@
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Start one background subagent that","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_first","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900412202,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900412202,"index":0,"dt":[],"id":"call_first","name":"subagent","args":["{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"]},{"type":"chunk","time":1792900412202,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_first","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}},{"type":"chunk","time":1792900412202,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900412202,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_first","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905843032,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905843032,"index":0,"dt":[],"id":"call_first","name":"subagent","args":["{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"]},{"type":"chunk","time":1792905843032,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_first","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}},{"type":"chunk","time":1792905843032,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905843032,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_first","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"agent-1"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1789000001000,"mode":"continuable","label":"Reply CHILD_OK"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_first"},"toolCallId":"call_first","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"lead/agent-1"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_first"},"toolCallId":"call_first","content":[{"type":"text","text":"started subagent {{session:2}}\nname: lead/agent-1"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_over_capacity","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900412232,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900412232,"index":0,"dt":[],"id":"call_over_capacity","name":"subagent","args":["{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"]},{"type":"chunk","time":1792900412232,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_over_capacity","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}},{"type":"chunk","time":1792900412232,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900412232,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_over_capacity","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905843064,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905843064,"index":0,"dt":[],"id":"call_over_capacity","name":"subagent","args":["{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"]},{"type":"chunk","time":1792905843064,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_over_capacity","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}},{"type":"chunk","time":1792905843064,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905843064,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_over_capacity","name":"subagent","arguments":"{\"description\":\"Reply CHILD_OK\",\"prompt\":\"Reply with exactly CHILD_OK.\"}"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{id:1}}","name":"agent-2"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_over_capacity"},"toolCallId":"call_over_capacity","content":[{"type":"text","text":"Error: subagent limit reached (active child limit: 1); wait for an existing child to finish or complete this work with the current agents"}],"isError":true,"id":"{{message:7}}"},"error":{"name":"SubagentError","code":"ACTIVATION_LIMIT_REACHED"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900412255,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792900412255,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1792900412255,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1792900412255,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900412255,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"ROOT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905843092,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792905843092,"index":0,"dt":[],"texts":["ROOT_DONE"]},{"type":"chunk","time":1792905843092,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}},{"type":"chunk","time":1792905843092,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905843092,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:9}}"}]}}
|
||||
@@ -33,6 +32,6 @@
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"LIMIT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900412297,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792900412297,"index":0,"dt":[],"texts":["LIMIT_OK"]},{"type":"chunk","time":1792900412297,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"LIMIT_OK"}}},{"type":"chunk","time":1792900412297,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900412297,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"LIMIT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905843140,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792905843140,"index":0,"dt":[],"texts":["LIMIT_OK"]},{"type":"chunk","time":1792905843140,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"LIMIT_OK"}}},{"type":"chunk","time":1792905843140,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905843140,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -343,7 +343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Reply with CHILD_OK","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"read-only","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:9}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:10}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:10}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[9],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900414106,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900414106,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":1792900414106,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":1792900414106,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900414106,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905845182,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905845182,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":1792905845182,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":1792905845182,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905845182,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\":\"Reply with CHILD_OK\",\"prompt\":\"Reply with exactly the word CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"agent-1"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1789000001000,"mode":"continuable","label":"Reply with CHILD_OK"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"lead/agent-1"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:5}}"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_bg_start"},"toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_bg_start"},"toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}\nname: lead/agent-1"}],"isError":false,"id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:5}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900414174,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792900414174,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1792900414174,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1792900414174,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900414174,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905845272,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792905845272,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1792905845272,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1792905845272,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905845272,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -343,7 +343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:15}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:15}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:1}} sent a message: "},{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:16}}"}]}}
|
||||
@@ -11,8 +11,8 @@
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":2,"inserted":[{"content":[{"type":"text","text":"Human priority: keep the requested exact reply."}],"source":{"kind":"user","rpcId":"{{rpc:1}}"},"role":"user","id":"{{message:18}}"}]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:19}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\"."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:15}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your name is \"lead/agent-1\". Your parent agent name is \"lead\". Use send_message({ name: \"lead\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:20}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{message:15}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYour parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:communication","text":"Your parent agent id is \"{{session:1}}\". Use send_message({ name: \"{{session:1}}\", message: \"<finding>\" }) when a finding changes what the parent should do next; sending a message does not end your turn."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:20}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[12],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -12,29 +12,29 @@
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Follow these steps exactly, then","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413022,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900413022,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1792900413022,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1792900413022,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413022,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844083,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905844084,"index":0,"dt":[],"id":"call_bg_start","name":"subagent","args":["{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"]},{"type":"chunk","time":1792905844084,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}},{"type":"chunk","time":1792905844084,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844084,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"agent-1"}}
|
||||
{"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1789000001000,"mode":"continuable","label":"Reply with CHILD_OK"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_bg_start"},"toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{session:2}}","name":"lead/agent-1"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_bg_start"},"toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}\nname: lead/agent-1"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413060,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900413060,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1792900413060,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1792900413060,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413060,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844115,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905844115,"index":0,"dt":[],"id":"call_followup_1","name":"send_message","args":["{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"]},{"type":"chunk","time":1792905844115,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}},{"type":"chunk","time":1792905844115,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844115,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_followup_1"},"toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to agent lead/agent-1"}],"isError":false,"id":"{{message:7}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413090,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900413090,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1792900413090,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1792900413090,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413090,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844141,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905844141,"index":0,"dt":[],"id":"call_followup_2","name":"send_message","args":["{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"]},{"type":"chunk","time":1792905844141,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}},{"type":"chunk","time":1792905844141,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844141,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"name\": \"lead/agent-1\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"call_followup_2"},"toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to agent lead/agent-1"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413116,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900413116,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1792900413116,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1792900413116,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413116,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844162,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792905844162,"index":0,"dt":[],"id":"call_followup_unknown","name":"send_message","args":["{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"]},{"type":"chunk","time":1792905844162,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}},{"type":"chunk","time":1792905844162,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844162,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"name\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"role":"tool","source":{"kind":"tool","callId":"call_followup_unknown"},"toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: agent \"22222222-2222-4222-8222-222222222222\" was not found; use list_agents to discover names or ids"}],"isError":true,"id":"{{message:11}}"},"error":{"name":"SubagentNameError","code":"AGENT_NAME_NOT_FOUND"}},"sourceEventSeqs":[31],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"role":"tool","source":{"kind":"tool","callId":"call_followup_unknown"},"toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true,"id":"{{message:11}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[31],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","data":{"turn":1,"step":5}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413136,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792900413136,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792900413136,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792900413136,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413136,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844183,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792905844183,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792905844183,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792905844183,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844183,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":5}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:13}}"}]}}
|
||||
@@ -42,6 +42,6 @@
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900413208,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792900413208,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1792900413208,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1792900413208,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900413208,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792905844247,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792905844247,"index":0,"dt":[],"texts":["SUBAGENT_SETTLED_NOTED"]},{"type":"chunk","time":1792905844247,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}},{"type":"chunk","time":1792905844247,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792905844247,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -343,7 +343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","args":["{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":16,"time":0,"data":{"version":2,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"external","label":"Observe first DSH SDK failure"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":17,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"agent-1"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":17,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"lead/agent-1"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":18,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"partial child loader answer"},{"type":"text","text":"Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} failed before it finished.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":19,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_first"},"toolCallId":"call_dsh_sdk_first","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":19,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_first"},"toolCallId":"call_dsh_sdk_first","content":[{"type":"text","text":"started subagent {{sessionId}}\nname: lead/agent-1"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":1}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":21,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":22,"time":0,"data":{"turn":1,"step":2}}}}
|
||||
@@ -24,9 +24,9 @@
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","args":["{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":26,"time":0,"data":{"turn":1,"step":2,"callId":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":27,"time":0,"data":{"version":2,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"external","label":"Observe second DSH SDK failure"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":28,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"agent-2"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"subagent-name/bound","seq":28,"time":0,"data":{"version":1,"childId":"{{sessionId}}","name":"lead/agent-2"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":29,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"partial child loader answer"},{"type":"text","text":"Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} failed before it finished.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":30,"time":0,"data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_second"},"toolCallId":"call_dsh_sdk_second","content":[{"type":"text","text":"started subagent lead/agent-2"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":30,"time":0,"data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_second"},"toolCallId":"call_dsh_sdk_second","content":[{"type":"text","text":"started subagent {{sessionId}}\nname: lead/agent-2"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":31,"time":0,"data":{"turn":1,"step":2}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":32,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":33,"time":0,"data":{"turn":1,"step":3}}}}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900414876,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900414876,"index":0,"dt":[],"id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","args":["{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"]},{"type":"chunk","time":1792900414876,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}},{"type":"chunk","time":1792900414876,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900414876,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_dsh_sdk_first","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe first DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}
|
||||
{"type":"subagent/catalog","data":{"version":2,"childId":"{{id:1}}","childCreatedAt":1792900415547,"mode":"external","label":"Observe first DSH SDK failure"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{id:1}}","name":"agent-1"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{id:1}}","name":"lead/agent-1"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{id:1}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"partial child loader answer"},{"type":"text","text":"Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{id:1}} failed before it finished.","senderSessionId":"{{id:1}}"},"role":"user","id":"{{message:5}}"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_first"},"toolCallId":"call_dsh_sdk_first","content":[{"type":"text","text":"started subagent lead/agent-1"}],"isError":false,"id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_first"},"toolCallId":"call_dsh_sdk_first","content":[{"type":"text","text":"started subagent {{id:1}}\nname: lead/agent-1"}],"isError":false,"id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
@@ -27,9 +27,9 @@
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792900415681,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792900415681,"index":0,"dt":[],"id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","args":["{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"]},{"type":"chunk","time":1792900415681,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}},{"type":"chunk","time":1792900415681,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792900415681,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dsh_sdk_second","name":"subagent_dsh_sdk_second","arguments":"{\"description\":\"Observe second DSH SDK failure\",\"prompt\":\"Return the scripted DSH SDK failure.\"}"}}
|
||||
{"type":"subagent/catalog","data":{"version":2,"childId":"{{id:2}}","childCreatedAt":1792900416291,"mode":"external","label":"Observe second DSH SDK failure"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{id:2}}","name":"agent-2"}}
|
||||
{"type":"subagent-name/bound","data":{"version":1,"childId":"{{id:2}}","name":"lead/agent-2"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{id:2}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"partial child loader answer"},{"type":"text","text":"Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{id:2}} failed before it finished.","senderSessionId":"{{id:2}}"},"role":"user","id":"{{message:8}}"}]}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_second"},"toolCallId":"call_dsh_sdk_second","content":[{"type":"text","text":"started subagent lead/agent-2"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dsh_sdk_second"},"toolCallId":"call_dsh_sdk_second","content":[{"type":"text","text":"started subagent {{id:2}}\nname: lead/agent-2"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "list_agents",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"description": "List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. Rows also include available names alongside Session ids. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -343,7 +343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_dsh_sdk",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. This backend does not accept follow-up messages.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. This backend does not accept follow-up messages.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -372,7 +372,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_dsh_sdk_second",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. This backend does not accept follow-up messages.",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. This backend does not accept follow-up messages.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -401,7 +401,7 @@
|
||||
},
|
||||
{
|
||||
"name": "subagent_fork",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Loaded 100 of 237 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user