refactor(subagent): unify service errors
This commit is contained in:
@@ -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',
|
||||
)
|
||||
|
||||
15
packages/subagent/subagent/src/error.ts
Normal file
15
packages/subagent/subagent/src/error.ts
Normal file
@@ -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