refactor(subagent): merge continuation control service

This commit is contained in:
Dudu
2026-07-27 00:00:14 +08:00
committed by imccyu
parent 0a95ad8cc0
commit 88f913a9ae
71 changed files with 543 additions and 673 deletions

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-tool-subagent-control
The globally named `send_message` tool: a thin adapter over `ctx.subagentControl.sendMessage()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers the one shared control tool, so multiple delegation tools never register duplicate global controls.
The optional, globally named `send_message` tool: a thin adapter over `ctx.subagents.sendMessage()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers one shared follow-up tool, so multiple delegation tools never register duplicate global controls. Its presence does not determine whether a delegation tool starts continuable work.
The tool performs no lifecycle routing. It attributes every follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }`; the control service preserves that source while deciding between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child. The tool renders which route was taken and the relevant Task id. A control-service throw becomes an errored tool result stating the message was not delivered.
The tool performs no lifecycle routing. It attributes every follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }`; the subagent service preserves that source while deciding between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child. The tool renders which route was taken and the relevant Task id. A delivery failure becomes an errored tool result stating the message was not delivered.
## Model Experience

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-tool-subagent-control",
"description": "Globally named send_message tool over the continuable-subagent control service",
"description": "Globally named send_message tool over ctx.subagents continuations",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -30,7 +30,7 @@
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-subagent-control": "^0.0.1",
"@deepseek-ai/dsh-subagent": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
@@ -44,7 +44,6 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-control": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",

View File

@@ -1,7 +1,7 @@
/**
* The globally named `send_message` tool: a thin model-facing adapter over
* `ctx.subagentControl.sendMessage()`. It performs no lifecycle routing of its
* own — steer-or-resume orchestration belongs to the control service — and it
* `ctx.subagents.sendMessage()`. It performs no lifecycle routing of its
* own — steer-or-resume orchestration belongs to the subagent service — and it
* lives apart from the provider-bound `@deepseek-ai/dsh-tool-subagent`
* instances so multiple delegation tools share one control tool.
* @module @deepseek-ai/dsh-tool-subagent-control
@@ -11,14 +11,14 @@ import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-subagent-control'
import type {} from '@deepseek-ai/dsh-subagent'
export const name = 'tool-subagent-control'
export const inject = ['tools', 'subagentControl']
export const inject = ['tools', 'subagents']
/**
* Register the `send_message` tool.
* @param ctx - context carrying the tool registry and the control service.
* @param ctx - context carrying the tool registry and subagent service.
*/
export function apply(ctx: Context): void {
ctx.tools.register(defineTool({
@@ -67,7 +67,7 @@ export function apply(ctx: Context): void {
throw new Error('send_message requires a calling agent (exec.agent was undefined)')
}
const message: ContentBlock[] = [{ type: 'text', text: args.message }]
const result = ctx.subagentControl.sendMessage(
const result = ctx.subagents.sendMessage(
parent,
SessionId(args.subagent_id),
message,

View File

@@ -16,7 +16,7 @@ export const inject = ['invariants']
/**
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; delivery
* and activation relations are owned by the control service it calls.
* and activation relations are owned by the subagent service it calls.
*/
const install: InvariantInstaller = () => {}

View File

@@ -9,7 +9,6 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentControlService from '@deepseek-ai/dsh-subagent-control'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
@@ -34,7 +33,6 @@ async function setup(script: ConstructorParameters<typeof MockAdapter>[0]) {
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(LocalTaskService)
await ctx.plugin(ToolTasks, {})
await ctx.plugin(SubagentControlService)
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -68,7 +66,7 @@ describe('dsh-tool-subagent-control', () => {
it('cold-resumes a settled child and renders the started route with its task id', async () => {
const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')])
const started = ctx.subagentControl.startContinuable({
const started = ctx.subagents.startContinuable({
provider: 'spawn',
label: 'work',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
@@ -98,10 +96,10 @@ describe('dsh-tool-subagent-control', () => {
const { ctx, parent } = await setup([])
let steered: string | undefined
let source: unknown
// Reach past the tool into the control service to fake a running route
// Reach past the tool into the subagent service to fake a running route
// deterministically: the tool is a thin adapter, so its steered wording is
// what this test pins.
ctx.subagentControl.sendMessage = async (agent, _childId, message, messageSource) => {
ctx.subagents.sendMessage = async (agent, _childId, message, messageSource) => {
steered = (message[0] as { text: string }).text
source = messageSource
return { route: 'steered', taskId: ctx.tasks.list(agent)[0]?.id ?? ('subagent-9' as never) }
@@ -116,7 +114,7 @@ describe('dsh-tool-subagent-control', () => {
expect(text(result)).toBe('message delivered to running task subagent-9')
})
it('reports a control-service failure as an errored, not-delivered result', async () => {
it('reports a delivery failure as an errored, not-delivered result', async () => {
const { ctx, parent } = await setup([])
const result = await callTool(ctx, 'send_message', {
subagent_id: 'no-such-child',
@@ -148,7 +146,6 @@ describe('dsh-tool-subagent-control', () => {
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(LocalTaskService)
await ctx.plugin(SubagentControlService)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'send_message')).toBe(true)
await fiber.dispose()
@@ -158,7 +155,7 @@ describe('dsh-tool-subagent-control', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-subagent-control')
expect(tool.inject).toEqual(['tools', 'subagentControl'])
expect(tool.inject).toEqual(['tools', 'subagents'])
expect(typeof tool.apply).toBe('function')
})
})

View File

@@ -24,7 +24,7 @@
"path": "../../core/tools"
},
{
"path": "../subagent-control"
"path": "../subagent"
},
{
"path": "../../support/invariants"