refactor(subagent): unify service errors
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md
|
||||
2026-07-26-merge-subagent-control-service.md: a5ce673997502ef6fbd7c66ff4e94e301d4192ba
|
||||
2026-07-26-merge-subagent-control-service.zh.md: cf867ab444438b7ee62cde68a3d53e83c3d049d1
|
||||
2026-07-26-merge-subagent-control-service.md: eb8a76dd4dfc5f06deb67608a67c12e061819286
|
||||
2026-07-26-merge-subagent-control-service.zh.md: 6599606634a1933790949e8a66df906a0bb9def0
|
||||
@@ -12,6 +12,8 @@ Continuable-child orchestration originally lived in a separate `ctx.subagentCont
|
||||
|
||||
`SubagentService` is the only public service. It retains raw `start(name, request)` and `resume(name, request)` for callers that own run collection, and exposes `startContinuable(spec)` and `sendMessage(...)` for durable Task-backed activations. The standalone `@deepseek-ai/dsh-subagent-control` package and `ctx.subagentControl` key are deleted; the optional `@deepseek-ai/dsh-tool-subagent-control` package injects `ctx.subagents` directly.
|
||||
|
||||
The merged service and its providers expose one `SubagentError` taxonomy. Stable codes distinguish provider lookup and capability failures from continuation routing, authorization, cancellation, persistence, and delivery failures; the removed service does not retain a separate error class.
|
||||
|
||||
The continuation implementation remains an internal manager rather than expanding the provider registry's core state. `SubagentService` creates it through `ctx.inject(['tasks', 'agents'], ...)`, so the injected Cordis child fiber owns its Task completion listener and teardown effects. Loading the provider registry does not require Tasks or persistence. The manager exists only while Tasks and Agents are available, and each continuation operation resolves session persistence at the point it needs durability. Disposing that fiber cancels and settles active continuations before releasing their associations.
|
||||
|
||||
`startContinuable` remains distinct from raw `start` because it has a different ownership and timing contract: it allocates the durable child id, creates the Task, and returns both ids synchronously while startup continues inside the Task. Raw `start` instead awaits provider publication and transfers a holder-owned run. Folding the method onto `start` through flags or return unions would broaden the low-level contract and create more change than keeping the existing explicit entry.
|
||||
|
||||
+2
@@ -12,6 +12,8 @@ Status: implemented
|
||||
|
||||
`SubagentService` 是唯一的公开服务。它为自行收集 run 的调用方保留底层 `start(name, request)` 和 `resume(name, request)`,并公开 `startContinuable(spec)` 与 `sendMessage(...)`,用于具备持久性、由 Task 支撑的激活。系统删除独立的 `@deepseek-ai/dsh-subagent-control` 包(package)和 `ctx.subagentControl` 键;可选的 `@deepseek-ai/dsh-tool-subagent-control` 包则直接注入 `ctx.subagents`。
|
||||
|
||||
合并后的服务及其提供方公开一套 `SubagentError` 分类体系。稳定错误码把提供方查找失败和功能检查失败,与继续执行路由、鉴权、取消、持久化和送达失败区分开来;已移除的服务不保留单独的错误类。
|
||||
|
||||
继续执行的实现仍是内部管理器,不会扩展提供方注册表的核心状态。`SubagentService` 通过 `ctx.inject(['tasks', 'agents'], ...)` 创建该管理器,因此注入的 Cordis child fiber 拥有自身的 Task 完成监听器和拆卸 effect。加载提供方注册表不要求 Task 或持久化。只有 Task 和 Agent 可用时,该管理器才会存在;每项继续执行操作都在需要持久性时解析会话持久化服务。dispose(资源释放)该 fiber 会先取消并结算活跃的继续执行,再释放其关联。
|
||||
|
||||
`startContinuable` 与底层 `start` 保持分离,因为二者的所有权与时序契约不同:前者分配持久化 child id、创建 Task,并同步返回两个 id,而启动过程继续在 Task 内运行;底层 `start` 则等待提供方发布,并移交一个由持有方负责的 run。若通过标志或返回值联合类型将该方法并入 `start`,会扩大底层契约,改动反而多于保留现有的显式入口。
|
||||
|
||||
@@ -2030,7 +2030,7 @@ async resume(name: string, request: SubagentResumeRequest): Promise<SubagentRun>
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageSource](../core-data-structures/core.md) · [SendMessageResult](../core-data-structures/subagent.md) · [SessionId](../core-data-structures/core.md) · [SubagentProvider](../core-data-structures/subagent.md) · [SubagentResumeRequest](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:206`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:198`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `ctx.subprocess` — `SubprocessService` (abstract seam)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{"type":"tool/call","seq":33,"time":1785517567431,"data":{"turn":1,"step":3,"callId":"call_follow_up","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}
|
||||
{"type":"tool/result","seq":34,"time":1785517567438,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_follow_up"},"content":[{"type":"tool-result","toolCallId":"call_follow_up","content":[{"type":"text","text":"message started task subagent-2 continuing subagent 22222222-2222-4222-8222-222222222222"}],"isError":false}],"role":"user","id":"6a7a5d22-1172-4a10-9230-ec12aed58e5e"}},"sourceEventSeqs":[33],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":35,"time":1785517567438,"data":{"turn":1,"step":3}}
|
||||
{"type":"user/message","seq":36,"time":1785517567444,"data":{"content":[{"type":"text","text":"background task subagent-2 (subagent: Please continue.) finished [status: failed, SubagentControlError: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks"},"role":"user","id":"32644e35-5ea1-4d29-8ef6-e09eb813781c"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":36,"time":1785517567444,"data":{"content":[{"type":"text","text":"background task subagent-2 (subagent: Please continue.) finished [status: failed, SubagentError: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks"},"role":"user","id":"32644e35-5ea1-4d29-8ef6-e09eb813781c"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":37,"time":1785517567444,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1789000000037,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1789000000038,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"call_collect_2","name":"task_output","argumentsDelta":"{\"task_id\": \"subagent-2\", \"wait\": true}"}}}
|
||||
@@ -44,7 +44,7 @@
|
||||
{"type":"assistant/chunk","seq":42,"time":1785517567453,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":43,"time":1785517567453,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_collect_2","name":"task_output","arguments":"{\"task_id\": \"subagent-2\", \"wait\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"31137fd0-a07c-4d5f-b847-6dbb33e86305"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":44,"time":1785517567454,"data":{"turn":1,"step":4,"callId":"call_collect_2","name":"task_output","arguments":"{\"task_id\": \"subagent-2\", \"wait\": true}"}}
|
||||
{"type":"tool/result","seq":45,"time":1785517567460,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_collect_2"},"content":[{"type":"tool-result","toolCallId":"call_collect_2","content":[{"type":"text","text":"(no new output)\n[status: failed, SubagentControlError: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable]"}],"isError":false}],"role":"user","id":"21807217-0a28-4369-868c-c2480398e883"}},"sourceEventSeqs":[44],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":45,"time":1785517567460,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_collect_2"},"content":[{"type":"tool-result","toolCallId":"call_collect_2","content":[{"type":"text","text":"(no new output)\n[status: failed, SubagentError: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable]"}],"isError":false}],"role":"user","id":"21807217-0a28-4369-868c-c2480398e883"}},"sourceEventSeqs":[44],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":46,"time":1785517567460,"data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","seq":47,"time":1785517567467,"data":{"turn":1,"step":5}}
|
||||
{"type":"assistant/chunk","seq":48,"time":1789000000047,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor
|
||||
import type { SubagentResult, SubagentRun, SubagentStartRequest } from './types.ts'
|
||||
import type { SubagentService } from './index.ts'
|
||||
import type { TaskHooks, TaskId, TaskOutcome } from '@deepseek-ai/dsh-tasks'
|
||||
import { SubagentError } from './error.ts'
|
||||
|
||||
/** Attribution for a model coordinator's follow-up to one of its children. */
|
||||
export interface CoordinatorMessageSource {
|
||||
@@ -38,14 +39,6 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Typed error for continuation routing, authorization, and delivery failures. */
|
||||
export class SubagentControlError extends HarnessError {
|
||||
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||
super(message, code, options)
|
||||
this.name = 'SubagentControlError'
|
||||
}
|
||||
}
|
||||
|
||||
/** What a caller asks for when starting a continuable background child. */
|
||||
export interface ContinuableStartSpec {
|
||||
/** The `ctx.subagents` provider to establish the child on. */
|
||||
@@ -285,13 +278,13 @@ export class SubagentContinuationManager {
|
||||
if (live === undefined) return
|
||||
const activation = this.activations.get(childId)
|
||||
if (activation === undefined) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" has a live agent outside continuation ownership; the message was not delivered`,
|
||||
'OWNERSHIP_CONFLICT',
|
||||
)
|
||||
}
|
||||
if (activation.run !== undefined && activation.run.localAgent !== live) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" registry agent is not the associated activation's agent; the message was not delivered`,
|
||||
'OWNERSHIP_CONFLICT',
|
||||
)
|
||||
@@ -309,12 +302,12 @@ export class SubagentContinuationManager {
|
||||
const taskId = activation.taskId
|
||||
/* v8 ignore next 3 -- the install and Task registration share one synchronous frame, so an observed activation carries its Task id. */
|
||||
if (taskId === undefined) {
|
||||
throw new SubagentControlError(`subagent "${childId}" activation is starting; the message was not delivered`, 'NOT_DELIVERED')
|
||||
throw new SubagentError(`subagent "${childId}" activation is starting; the message was not delivered`, 'NOT_DELIVERED')
|
||||
}
|
||||
// Owner-session authorization plus the live status for admission.
|
||||
const snapshot = this.ctx.tasks.get(taskId, parent)
|
||||
if (snapshot.status !== 'running') {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" task ${taskId} is ${snapshot.status}; the message was not delivered `
|
||||
+ '— retry after it settles to start the next activation',
|
||||
'NOT_DELIVERED',
|
||||
@@ -322,10 +315,10 @@ export class SubagentContinuationManager {
|
||||
}
|
||||
const run = activation.run
|
||||
if (run === undefined) {
|
||||
throw new SubagentControlError(`subagent "${childId}" activation is starting; the message was not delivered`, 'NOT_DELIVERED')
|
||||
throw new SubagentError(`subagent "${childId}" activation is starting; the message was not delivered`, 'NOT_DELIVERED')
|
||||
}
|
||||
if (run.steer === undefined) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" provider does not accept live delivery; the message was not delivered`,
|
||||
'NOT_DELIVERED',
|
||||
)
|
||||
@@ -336,7 +329,7 @@ export class SubagentContinuationManager {
|
||||
// Confirmed steering lost the race with request admission. Deliberately no
|
||||
// cold-resume fallback here: that would attach the message to a turn the
|
||||
// caller did not observe.
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" stopped before delivery; the message was not delivered`,
|
||||
'NOT_DELIVERED',
|
||||
{ cause: error },
|
||||
@@ -364,18 +357,18 @@ export class SubagentContinuationManager {
|
||||
try {
|
||||
loaded = await persistence.load(childId)
|
||||
} catch (error: unknown) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" is unavailable`,
|
||||
'NOT_RESUMABLE',
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
// The persistence seam takes no signal; recheck before any child work.
|
||||
if (signal.aborted) throw new SubagentControlError('subagent resume was cancelled during lookup', 'CANCELLED')
|
||||
if (signal.aborted) throw new SubagentError('subagent resume was cancelled during lookup', 'CANCELLED')
|
||||
// Authorize the persisted header before folding: only the direct parent
|
||||
// recorded at creation may continue this child.
|
||||
if (loaded.meta.parentSession !== parent.id) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" belongs to another parent session`,
|
||||
'UNAUTHORIZED',
|
||||
)
|
||||
@@ -385,7 +378,7 @@ export class SubagentContinuationManager {
|
||||
// itself a continuable child.
|
||||
const descriptor = foldSubagentDescriptor(loaded.events.slice(loaded.meta.seedLength ?? 0))
|
||||
if (descriptor === undefined) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
`subagent "${childId}" has no supported continuation state and cannot be resumed; `
|
||||
+ 'do not retry send_message with this id',
|
||||
'NOT_RESUMABLE',
|
||||
@@ -477,7 +470,7 @@ export class SubagentContinuationManager {
|
||||
private requirePersistence(): SessionPersistence {
|
||||
const persistence = this.ctx.get('sessionPersistence')
|
||||
if (persistence === undefined) {
|
||||
throw new SubagentControlError(
|
||||
throw new SubagentError(
|
||||
'continuable subagents require session persistence (load a dsh-session-persistence backend)',
|
||||
'PERSISTENCE_UNAVAILABLE',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Typed failures shared by subagent service and provider operations.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent
|
||||
*/
|
||||
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/** Typed failure for the subagent seam. */
|
||||
export class SubagentError extends HarnessError {
|
||||
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||
super(message, code, options)
|
||||
this.name = 'SubagentError'
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import { Context, Service } from 'cordis'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -45,6 +44,7 @@ import type {
|
||||
SubagentStartRequest,
|
||||
} from './types.ts'
|
||||
import { SubagentRunId } from './types.ts'
|
||||
import { SubagentError } from './error.ts'
|
||||
import SubagentContinuationManager from './continuation.ts'
|
||||
import type {
|
||||
ContinuableStart,
|
||||
@@ -71,10 +71,10 @@ export {
|
||||
SUBAGENT_DESCRIPTOR_VERSION,
|
||||
} from './descriptor.ts'
|
||||
export type { SubagentDescriptorData, SubagentDescriptorInput } from './descriptor.ts'
|
||||
export { SubagentError } from './error.ts'
|
||||
export {
|
||||
runOutcome,
|
||||
settleRun,
|
||||
SubagentControlError,
|
||||
} from './continuation.ts'
|
||||
export type {
|
||||
ContinuableStart,
|
||||
@@ -194,14 +194,6 @@ export interface SubagentRunEndInfo {
|
||||
readonly lastAssistantMessage?: ContentBlock[]
|
||||
}
|
||||
|
||||
/** Typed error for provider lookup, registration, and capability failures. */
|
||||
export class SubagentError extends HarnessError {
|
||||
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||
super(message, code, options)
|
||||
this.name = 'SubagentError'
|
||||
}
|
||||
}
|
||||
|
||||
/** Named provider registry with raw and Task-backed continuation operations. */
|
||||
export class SubagentService extends Service {
|
||||
private providers = new Map<string, SubagentProvider>()
|
||||
|
||||
@@ -20,7 +20,7 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
|
||||
import SubagentService, {
|
||||
runOutcome,
|
||||
settleRun,
|
||||
SubagentControlError,
|
||||
SubagentError,
|
||||
SUBAGENT_DESCRIPTOR_VERSION,
|
||||
} from '../src/index.ts'
|
||||
|
||||
@@ -625,7 +625,7 @@ describe('SubagentService.sendMessage', () => {
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
await expect(sendMessage(ctx, parent, SessionId('rogue-child'), message('hello')))
|
||||
.rejects.toThrow(SubagentControlError)
|
||||
.rejects.toThrow(SubagentError)
|
||||
await expect(sendMessage(ctx, parent, SessionId('rogue-child'), message('hello')))
|
||||
.rejects.toThrow(/outside continuation ownership.*not delivered/)
|
||||
await handle.dispose()
|
||||
|
||||
Reference in New Issue
Block a user