@morlay/dsh-subagent 0.0.9 → 0.0.11-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +86 -129
- package/dist/index.mjs +43 -49
- package/dist/rows.d.mts +1 -3
- package/dist/rows.mjs +1 -2
- package/package.json +28 -28
- package/src/continuation-messages.ts +73 -59
- package/src/index.ts +263 -189
- package/src/manager.ts +900 -716
- package/src/vendor/dsh-subagent/activation.ts +81 -89
- package/src/vendor/dsh-subagent/archive-admission.ts +38 -45
- package/src/vendor/dsh-subagent/assistant-output.ts +17 -19
- package/src/vendor/dsh-subagent/catalog.ts +79 -111
- package/src/vendor/dsh-subagent/child-agent.ts +83 -92
- package/src/vendor/dsh-subagent/continuation-messages.ts +64 -68
- package/src/vendor/dsh-subagent/control-types.ts +71 -68
- package/src/vendor/dsh-subagent/control.ts +39 -55
- package/src/vendor/dsh-subagent/depth.ts +14 -18
- package/src/vendor/dsh-subagent/descriptor.ts +107 -113
- package/src/vendor/dsh-subagent/error.ts +3 -3
- package/src/vendor/dsh-subagent/index.ts +193 -254
- package/src/vendor/dsh-subagent/internal.ts +17 -21
- package/src/vendor/dsh-subagent/lifecycle.ts +48 -51
- package/src/vendor/dsh-subagent/list-children.ts +57 -65
- package/src/vendor/dsh-subagent/manager.ts +726 -862
- package/src/vendor/dsh-subagent/out-of-process.ts +59 -61
- package/src/vendor/dsh-subagent/projection-types.ts +40 -38
- package/src/vendor/dsh-subagent/projection.ts +85 -107
- package/src/vendor/dsh-subagent/structured.ts +48 -57
- package/src/vendor/dsh-subagent/types.ts +68 -68
|
@@ -6,132 +6,101 @@
|
|
|
6
6
|
* @module @deepseek-ai/dsh-subagent/manager
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { randomUUID } from
|
|
10
|
-
import { isAbsolute, resolve } from
|
|
11
|
-
import type {} from
|
|
12
|
-
import type { Context } from
|
|
13
|
-
import type { Agent, AgentOptions, CreateAgentOptions } from
|
|
14
|
-
import { brandString } from
|
|
9
|
+
import { randomUUID } from 'node:crypto'
|
|
10
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-working-directory'
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import type { Agent, AgentOptions, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
|
|
14
|
+
import { brandString } from '@deepseek-ai/dsh-brand'
|
|
15
|
+
import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
|
|
16
|
+
import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'
|
|
17
|
+
import { SessionLogOffset } from '@deepseek-ai/dsh-session'
|
|
18
|
+
import type { SessionId, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session'
|
|
19
|
+
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
|
|
20
|
+
import type { SessionObservation, SessionQueryEngine } from '@deepseek-ai/dsh-session-query'
|
|
21
|
+
import type { ObjectJsonSchema } from '@deepseek-ai/dsh-tools'
|
|
15
22
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import type {
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
28
|
-
appendDelegatedPolicyOverrides,
|
|
29
|
-
applyChildComposition,
|
|
30
|
-
captureDelegatedPolicyOverrides,
|
|
31
|
-
childSessionMeta,
|
|
32
|
-
resolveChildAgentOptions,
|
|
33
|
-
resolveChildDepth,
|
|
34
|
-
} from "./child-agent.ts";
|
|
35
|
-
import type { ChildComposition, DelegatedPolicyOverrides } from "./child-agent.ts";
|
|
36
|
-
import {
|
|
37
|
-
createAgentMessage,
|
|
38
|
-
withContinuableReturnGuidance,
|
|
39
|
-
createSettlementMessage,
|
|
40
|
-
} from "./continuation-messages.ts";
|
|
41
|
-
import { assertSubagentMaxDepth } from "./depth.ts";
|
|
42
|
-
import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.ts";
|
|
43
|
-
import type { SubagentDescriptorData } from "./descriptor.ts";
|
|
44
|
-
import { establishCatalogChild, establishExternalCatalogChild } from "./catalog.ts";
|
|
45
|
-
import { SubagentError } from "./error.ts";
|
|
46
|
-
import { isAdjacentAgentSendMessageTool } from "./internal.ts";
|
|
47
|
-
import type { ActivationObserver } from "./lifecycle.ts";
|
|
23
|
+
appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides,
|
|
24
|
+
childSessionMeta, resolveChildAgentOptions, resolveChildDepth,
|
|
25
|
+
} from './child-agent.ts'
|
|
26
|
+
import type { ChildComposition, DelegatedPolicyOverrides } from './child-agent.ts'
|
|
27
|
+
import { createAgentMessage, withContinuableReturnGuidance, createSettlementMessage } from './continuation-messages.ts'
|
|
28
|
+
import { assertSubagentMaxDepth } from './depth.ts'
|
|
29
|
+
import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor.ts'
|
|
30
|
+
import type { SubagentDescriptorData } from './descriptor.ts'
|
|
31
|
+
import { establishCatalogChild, establishExternalCatalogChild } from './catalog.ts'
|
|
32
|
+
import { SubagentError } from './error.ts'
|
|
33
|
+
import { isAdjacentAgentSendMessageTool } from './internal.ts'
|
|
34
|
+
import type { ActivationObserver } from './lifecycle.ts'
|
|
48
35
|
import type {
|
|
49
|
-
ContinuableCreateRequest,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
ResolvedSubagentStartRequest,
|
|
59
|
-
} from "./types.ts";
|
|
60
|
-
import { attachStructuredRuntime } from "./structured.ts";
|
|
61
|
-
import type { StructuredAttachment } from "./structured.ts";
|
|
62
|
-
import type { SubagentDelivery } from "./control-types.ts";
|
|
63
|
-
import {
|
|
64
|
-
ActivationPool,
|
|
65
|
-
ChildLock,
|
|
66
|
-
activationAgent,
|
|
67
|
-
captureLocalResult,
|
|
68
|
-
observeLocalFailure,
|
|
69
|
-
requireLocalActivation,
|
|
70
|
-
} from "./activation.ts";
|
|
71
|
-
import type { Activation, ActivationResource, LocalActivation } from "./activation.ts";
|
|
36
|
+
ContinuableCreateRequest, ContinuableCreateSpec, SubagentInterruptAuthority,
|
|
37
|
+
SubagentActivationSpec, SubagentActivation, SubagentStartRequest, SubagentRun,
|
|
38
|
+
SubagentSendMessageOptions, SubagentResult, ResolvedSubagentStartRequest,
|
|
39
|
+
} from './types.ts'
|
|
40
|
+
import { attachStructuredRuntime } from './structured.ts'
|
|
41
|
+
import type { StructuredAttachment } from './structured.ts'
|
|
42
|
+
import type { SubagentDelivery } from './control-types.ts'
|
|
43
|
+
import { ActivationPool, ChildLock, activationAgent, captureLocalResult, observeLocalFailure, requireLocalActivation } from './activation.ts'
|
|
44
|
+
import type { Activation, ActivationResource, LocalActivation } from './activation.ts'
|
|
72
45
|
|
|
73
46
|
/** Inputs shared by model steering and human prompt delivery. */
|
|
74
47
|
type ChildDeliveryOptions =
|
|
75
48
|
| {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| { readonly delivery:
|
|
49
|
+
readonly delivery: 'steer'
|
|
50
|
+
/**
|
|
51
|
+
* A provided host source is preserved on the user message; omission attributes
|
|
52
|
+
* an adjacent-Agent message to the parent.
|
|
53
|
+
*/
|
|
54
|
+
readonly source?: MessageSource
|
|
55
|
+
readonly signal: AbortSignal
|
|
56
|
+
}
|
|
57
|
+
| { readonly delivery: 'queue'; readonly source: MessageSource; readonly signal: AbortSignal }
|
|
85
58
|
|
|
86
59
|
/** Package-private hooks supplied by the owning service. */
|
|
87
60
|
interface SubagentHost {
|
|
88
|
-
startExternal(name: string, request: ResolvedSubagentStartRequest): Promise<SubagentRun
|
|
61
|
+
startExternal(name: string, request: ResolvedSubagentStartRequest): Promise<SubagentRun>
|
|
89
62
|
/** Resolve one provider's detached continuable-creation contribution. */
|
|
90
|
-
prepareContinuable(
|
|
91
|
-
name: string,
|
|
92
|
-
request: ContinuableCreateRequest,
|
|
93
|
-
): Promise<ContinuableCreateSpec>;
|
|
63
|
+
prepareContinuable(name: string, request: ContinuableCreateRequest): Promise<ContinuableCreateSpec>
|
|
94
64
|
/** Build the lifecycle observer for one Activation residency epoch. */
|
|
95
|
-
observeActivation(provider: string, childId: SessionId, parent: Agent): ActivationObserver
|
|
65
|
+
observeActivation(provider: string, childId: SessionId, parent: Agent): ActivationObserver
|
|
96
66
|
}
|
|
97
67
|
|
|
98
68
|
/** Inputs shared by fresh and resumed Activation materialization. */
|
|
99
69
|
interface MaterializeBase {
|
|
100
|
-
childId: SessionId
|
|
101
|
-
provider: string
|
|
102
|
-
parent: Agent
|
|
103
|
-
signal: AbortSignal
|
|
104
|
-
delivery?:
|
|
70
|
+
childId: SessionId
|
|
71
|
+
provider: string
|
|
72
|
+
parent: Agent
|
|
73
|
+
signal: AbortSignal
|
|
74
|
+
delivery?: 'parent' | 'caller' | undefined
|
|
105
75
|
}
|
|
106
76
|
|
|
107
77
|
/** Creation inputs for a local residency or one external backend execution. */
|
|
108
|
-
type MaterializeInputs = MaterializeBase &
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
78
|
+
type MaterializeInputs = MaterializeBase & (
|
|
79
|
+
| {
|
|
80
|
+
kind: 'local'
|
|
81
|
+
/**
|
|
82
|
+
* Creation inputs; absent for a cold resume, which loads the persisted
|
|
83
|
+
* session — including the delegation policy events a fresh creation seeded,
|
|
84
|
+
* so a resume never re-captures the parent's policy.
|
|
85
|
+
*/
|
|
86
|
+
create?: {
|
|
87
|
+
/** Absolute initial directory captured before provider preparation. */
|
|
88
|
+
cwd: string
|
|
89
|
+
seed: readonly SessionEvent[] | undefined
|
|
90
|
+
meta: NonNullable<CreateAgentOptions['meta']>
|
|
91
|
+
/** Exact parent-log prefix length inside {@link seed}. */
|
|
92
|
+
inheritedEventCount: SessionLogOffset
|
|
93
|
+
/** Policy captured at delegation: the parent's sandbox override plus the approval pin. */
|
|
94
|
+
delegatedPolicies: DelegatedPolicyOverrides
|
|
95
|
+
/** Child-owned composition record appended after the inherited marker. */
|
|
96
|
+
descriptor: SubagentDescriptorData
|
|
97
|
+
}
|
|
98
|
+
agentOptions: AgentOptions
|
|
99
|
+
composition: ChildComposition
|
|
100
|
+
outputSchema?: ObjectJsonSchema | undefined
|
|
101
|
+
}
|
|
102
|
+
| { kind: 'external'; start: (signal: AbortSignal) => Promise<SubagentRun> }
|
|
103
|
+
)
|
|
135
104
|
|
|
136
105
|
/**
|
|
137
106
|
* One admitted materialization and the exact live ancestry observed at its
|
|
@@ -139,42 +108,44 @@ type MaterializeInputs = MaterializeBase &
|
|
|
139
108
|
* keep waiting even if an intermediate Agent leaves the registry meanwhile.
|
|
140
109
|
*/
|
|
141
110
|
interface Materialization {
|
|
142
|
-
readonly lineage: readonly Agent[]
|
|
143
|
-
readonly settled: Promise<void
|
|
144
|
-
readonly controller: AbortController
|
|
111
|
+
readonly lineage: readonly Agent[]
|
|
112
|
+
readonly settled: Promise<void>
|
|
113
|
+
readonly controller: AbortController
|
|
145
114
|
}
|
|
146
115
|
|
|
147
116
|
/** Residency state observed by the natural-settlement watcher. */
|
|
148
|
-
type SettlementState =
|
|
117
|
+
type SettlementState = 'closed' | 'retry' | 'wait' | 'ready'
|
|
149
118
|
|
|
150
119
|
/** Result of the final child-lock settlement decision. */
|
|
151
|
-
type SettlementAttempt =
|
|
120
|
+
type SettlementAttempt =
|
|
121
|
+
| Exclude<SettlementState, 'ready'>
|
|
122
|
+
| { readonly done: Promise<void> }
|
|
152
123
|
|
|
153
124
|
/** Render only the safe outer message from execution or teardown failures. */
|
|
154
125
|
function failureMessage(error: unknown): string {
|
|
155
|
-
return error instanceof Error ? error.message :
|
|
126
|
+
return error instanceof Error ? error.message : 'unknown teardown failure'
|
|
156
127
|
}
|
|
157
128
|
|
|
158
129
|
/** Own subagent requests and the resources of every live activation. */
|
|
159
130
|
export class SubagentManager {
|
|
160
131
|
/** Child session id → its live Activation. Process-local, never durable. */
|
|
161
|
-
private readonly resident = new Map<SessionId, Activation>()
|
|
132
|
+
private readonly resident = new Map<SessionId, Activation>()
|
|
162
133
|
/** Root identities retain their pool across child settlement without retaining dead roots. */
|
|
163
|
-
private readonly rootPools = new WeakMap<Agent, ActivationPool>()
|
|
134
|
+
private readonly rootPools = new WeakMap<Agent, ActivationPool>()
|
|
164
135
|
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
165
|
-
private readonly materializations = new Set<Materialization>()
|
|
136
|
+
private readonly materializations = new Set<Materialization>()
|
|
166
137
|
/** Per-child serializer shared by delivery, release, and disposal. */
|
|
167
|
-
private readonly locks = new ChildLock()
|
|
138
|
+
private readonly locks = new ChildLock()
|
|
168
139
|
/** Structural Cordis owner of every Activation handle. */
|
|
169
|
-
private readonly ownerCtx: Context
|
|
140
|
+
private readonly ownerCtx: Context
|
|
170
141
|
/**
|
|
171
142
|
* Exact roots whose host teardown has begun, with the live lineage members
|
|
172
143
|
* observed under each root. Entries remain until that exact root leaves the
|
|
173
144
|
* Agent registry, closing admission throughout its host's teardown without
|
|
174
145
|
* poisoning a later same-id replacement.
|
|
175
146
|
*/
|
|
176
|
-
private readonly closingScopes = new Map<Agent, Set<Agent>>()
|
|
177
|
-
private draining = false
|
|
147
|
+
private readonly closingScopes = new Map<Agent, Set<Agent>>()
|
|
148
|
+
private draining = false
|
|
178
149
|
|
|
179
150
|
/**
|
|
180
151
|
* Build one manager inside the service's Agent-injected context.
|
|
@@ -193,18 +164,15 @@ export class SubagentManager {
|
|
|
193
164
|
// the drain before releasing the scope; a cleanup effect on the same scope
|
|
194
165
|
// as the Agent handles would let structural handle disposal bypass
|
|
195
166
|
// child-first ordering.
|
|
196
|
-
const scope = ctx.plugin(function activationOwner() {})
|
|
197
|
-
this.ownerCtx = scope.ctx
|
|
198
|
-
ctx.on(
|
|
199
|
-
this.closingScopes.delete(agent)
|
|
200
|
-
})
|
|
201
|
-
ctx.effect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}.bind(this),
|
|
206
|
-
"subagents.manager()",
|
|
207
|
-
);
|
|
167
|
+
const scope = ctx.plugin(function activationOwner() {})
|
|
168
|
+
this.ownerCtx = scope.ctx
|
|
169
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
170
|
+
this.closingScopes.delete(agent)
|
|
171
|
+
})
|
|
172
|
+
ctx.effect(function* (this: SubagentManager) {
|
|
173
|
+
yield scope.dispose
|
|
174
|
+
yield () => this.drain()
|
|
175
|
+
}.bind(this), 'subagents.manager()')
|
|
208
176
|
}
|
|
209
177
|
|
|
210
178
|
/**
|
|
@@ -212,44 +180,50 @@ export class SubagentManager {
|
|
|
212
180
|
* @param targetSessionId - the durable child session id to interrupt.
|
|
213
181
|
* @param authority - the human parent address or exact live ancestor Agent.
|
|
214
182
|
*/
|
|
215
|
-
interrupt(
|
|
216
|
-
|
|
217
|
-
|
|
183
|
+
interrupt(
|
|
184
|
+
targetSessionId: SessionId,
|
|
185
|
+
authority: SubagentInterruptAuthority,
|
|
186
|
+
): void {
|
|
187
|
+
if (authority.kind === 'ancestor') {
|
|
188
|
+
const caller = authority.agent
|
|
218
189
|
if (this.ctx.agents.get(caller.id) !== caller) {
|
|
219
190
|
throw new SubagentError(
|
|
220
191
|
`interrupting "${targetSessionId}" requires the exact live ancestor agent`,
|
|
221
|
-
|
|
222
|
-
)
|
|
192
|
+
'UNAUTHORIZED',
|
|
193
|
+
)
|
|
223
194
|
}
|
|
224
195
|
if (caller.id === targetSessionId) {
|
|
225
|
-
throw new SubagentError(
|
|
196
|
+
throw new SubagentError(
|
|
197
|
+
`agent "${caller.id}" cannot interrupt itself`,
|
|
198
|
+
'UNAUTHORIZED',
|
|
199
|
+
)
|
|
226
200
|
}
|
|
227
201
|
}
|
|
228
|
-
const activation = this.resident.get(targetSessionId)
|
|
229
|
-
if (activation === undefined) return
|
|
230
|
-
if (authority.kind ===
|
|
202
|
+
const activation = this.resident.get(targetSessionId)
|
|
203
|
+
if (activation === undefined) return
|
|
204
|
+
if (authority.kind === 'user') {
|
|
231
205
|
if (activation.parent.id !== authority.parentSessionId) {
|
|
232
206
|
throw new SubagentError(
|
|
233
207
|
`subagent "${targetSessionId}" belongs to another parent session`,
|
|
234
|
-
|
|
235
|
-
)
|
|
208
|
+
'UNAUTHORIZED',
|
|
209
|
+
)
|
|
236
210
|
}
|
|
237
211
|
} else if (!activation.ancestry.has(authority.agent)) {
|
|
238
212
|
throw new SubagentError(
|
|
239
213
|
`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`,
|
|
240
|
-
|
|
241
|
-
)
|
|
214
|
+
'UNAUTHORIZED',
|
|
215
|
+
)
|
|
242
216
|
}
|
|
243
217
|
// Disposal already stopped the target with a whole-Activation teardown;
|
|
244
218
|
// a second cancel would be a redundant signal on a closing handle.
|
|
245
|
-
if (activation.closing !== undefined) return
|
|
246
|
-
const kind = authority.kind ===
|
|
247
|
-
if (activation.kind ===
|
|
248
|
-
activation.handle.agent.cancel({ kind }, { keepInbox: true })
|
|
219
|
+
if (activation.closing !== undefined) return
|
|
220
|
+
const kind = authority.kind === 'user' ? 'user' : 'parent'
|
|
221
|
+
if (activation.kind === 'local') {
|
|
222
|
+
activation.handle.agent.cancel({ kind }, { keepInbox: true })
|
|
249
223
|
} else {
|
|
250
|
-
activation.controller.abort({ kind })
|
|
224
|
+
activation.controller.abort({ kind })
|
|
251
225
|
// The settlement observer reports teardown failures.
|
|
252
|
-
void this.dispose(activation).catch(() => undefined)
|
|
226
|
+
void this.dispose(activation).catch(() => undefined)
|
|
253
227
|
}
|
|
254
228
|
}
|
|
255
229
|
|
|
@@ -259,17 +233,15 @@ export class SubagentManager {
|
|
|
259
233
|
* child-first.
|
|
260
234
|
*/
|
|
261
235
|
async drain(): Promise<void> {
|
|
262
|
-
this.draining = true
|
|
263
|
-
for (const pending of this.materializations) pending.controller.abort()
|
|
264
|
-
await Promise.all([...this.materializations].map(
|
|
265
|
-
const owned = new Set<SessionId>()
|
|
236
|
+
this.draining = true
|
|
237
|
+
for (const pending of this.materializations) pending.controller.abort()
|
|
238
|
+
await Promise.all([...this.materializations].map(materialization => materialization.settled))
|
|
239
|
+
const owned = new Set<SessionId>()
|
|
266
240
|
for (const activation of this.resident.values()) {
|
|
267
|
-
for (const child of activation.ownedChildren) owned.add(child)
|
|
241
|
+
for (const child of activation.ownedChildren) owned.add(child)
|
|
268
242
|
}
|
|
269
|
-
const roots = [...this.resident.values()].filter(
|
|
270
|
-
|
|
271
|
-
);
|
|
272
|
-
await this.disposeRoots(roots, "activation(s)");
|
|
243
|
+
const roots = [...this.resident.values()].filter(activation => !owned.has(activation.childId))
|
|
244
|
+
await this.disposeRoots(roots, 'activation(s)')
|
|
273
245
|
}
|
|
274
246
|
|
|
275
247
|
/**
|
|
@@ -278,62 +250,42 @@ export class SubagentManager {
|
|
|
278
250
|
* @returns whether work was joined and the host must recheck parent completion.
|
|
279
251
|
*/
|
|
280
252
|
async waitForChildren(parent: Agent): Promise<boolean> {
|
|
281
|
-
let observed = false
|
|
253
|
+
let observed = false
|
|
282
254
|
while (true) {
|
|
283
|
-
const pending = [...this.materializations].filter(
|
|
284
|
-
const children = [...this.resident.values()].filter(
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
(item) => item.kind === "local" && item.handle.agent.status !== "idle",
|
|
289
|
-
);
|
|
290
|
-
await Promise.all(
|
|
291
|
-
children.flatMap((item) => (item.kind === "local" ? [item.handle.agent.whenIdle()] : [])),
|
|
292
|
-
);
|
|
293
|
-
const waits: Promise<unknown>[] = pending.map((item) => item.settled);
|
|
255
|
+
const pending = [...this.materializations].filter(item => item.lineage.includes(parent))
|
|
256
|
+
const children = [...this.resident.values()].filter(item => activationAgent(item) !== parent && item.ancestry.has(parent))
|
|
257
|
+
observed ||= children.some(item => item.kind === 'local' && item.handle.agent.status !== 'idle')
|
|
258
|
+
await Promise.all(children.flatMap(item => item.kind === 'local' ? [item.handle.agent.whenIdle()] : []))
|
|
259
|
+
const waits: Promise<unknown>[] = pending.map(item => item.settled)
|
|
294
260
|
for (const item of children) {
|
|
295
|
-
if (item.kind ===
|
|
296
|
-
waits.push(item.result.promise.catch(() => undefined))
|
|
297
|
-
continue
|
|
261
|
+
if (item.kind === 'external' || item.closing !== undefined) {
|
|
262
|
+
waits.push(item.result.promise.catch(() => undefined))
|
|
263
|
+
continue
|
|
298
264
|
}
|
|
299
|
-
if (item.handle.agent.status !==
|
|
300
|
-
waits.push(item.handle.agent.whenIdle())
|
|
301
|
-
continue
|
|
265
|
+
if (item.handle.agent.status !== 'idle') {
|
|
266
|
+
waits.push(item.handle.agent.whenIdle())
|
|
267
|
+
continue
|
|
302
268
|
}
|
|
303
269
|
if (item.ownedChildren.size > 0) {
|
|
304
270
|
// Published descendants are observed separately; preparation holds
|
|
305
271
|
// keep the owner pending until publication or rollback wakes it.
|
|
306
|
-
if ([...item.ownedChildren].some(
|
|
307
|
-
|
|
308
|
-
continue;
|
|
272
|
+
if ([...item.ownedChildren].some(id => !this.resident.has(id))) waits.push(item.poke.promise)
|
|
273
|
+
continue
|
|
309
274
|
}
|
|
310
|
-
const inbox = item.handle.agent.inbox
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
(inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)
|
|
314
|
-
)
|
|
315
|
-
continue;
|
|
316
|
-
waits.push(Promise.race([item.result.promise.catch(() => undefined), item.poke.promise]));
|
|
275
|
+
const inbox = item.handle.agent.inbox
|
|
276
|
+
if (item.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)) continue
|
|
277
|
+
waits.push(Promise.race([item.result.promise.catch(() => undefined), item.poke.promise]))
|
|
317
278
|
}
|
|
318
279
|
if (waits.length > 0) {
|
|
319
|
-
observed = true
|
|
320
|
-
await Promise.all(waits)
|
|
321
|
-
continue
|
|
280
|
+
observed = true
|
|
281
|
+
await Promise.all(waits)
|
|
282
|
+
continue
|
|
322
283
|
}
|
|
323
284
|
// A descendant can be published while whenIdle follows another child.
|
|
324
|
-
if (
|
|
325
|
-
[...this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
[...this.resident.values()].some(
|
|
329
|
-
(item) =>
|
|
330
|
-
activationAgent(item) !== parent &&
|
|
331
|
-
item.ancestry.has(parent) &&
|
|
332
|
-
!children.includes(item),
|
|
333
|
-
)
|
|
334
|
-
)
|
|
335
|
-
continue;
|
|
336
|
-
return observed;
|
|
285
|
+
if ([...this.materializations].some(item => item.lineage.includes(parent) && !pending.includes(item))
|
|
286
|
+
|| [...this.resident.values()].some(item => activationAgent(item) !== parent
|
|
287
|
+
&& item.ancestry.has(parent) && !children.includes(item))) continue
|
|
288
|
+
return observed
|
|
337
289
|
}
|
|
338
290
|
}
|
|
339
291
|
|
|
@@ -342,49 +294,50 @@ export class SubagentManager {
|
|
|
342
294
|
* @param parents - exact live roots whose managed descendants must stop.
|
|
343
295
|
*/
|
|
344
296
|
async drainDescendants(parents: readonly Agent[]): Promise<void> {
|
|
345
|
-
const roots = new Set(parents.filter(
|
|
346
|
-
if (roots.size === 0) return
|
|
297
|
+
const roots = new Set(parents.filter(parent => this.ctx.agents.get(parent.id) === parent))
|
|
298
|
+
if (roots.size === 0) return
|
|
347
299
|
|
|
348
300
|
for (const root of roots) {
|
|
349
|
-
this.closingMembers(root).add(root)
|
|
301
|
+
this.closingMembers(root).add(root)
|
|
350
302
|
}
|
|
351
303
|
|
|
352
|
-
const targets: Activation[] = []
|
|
304
|
+
const targets: Activation[] = []
|
|
353
305
|
for (const activation of this.resident.values()) {
|
|
354
|
-
const child = activationAgent(activation)
|
|
355
|
-
const lineage = this.liveLineage(child ?? activation.parent)
|
|
356
|
-
const owners = [...roots].filter(
|
|
357
|
-
|
|
358
|
-
|
|
306
|
+
const child = activationAgent(activation)
|
|
307
|
+
const lineage = this.liveLineage(child ?? activation.parent)
|
|
308
|
+
const owners = [...roots].filter(root => child !== root
|
|
309
|
+
&& activation.ancestry.has(root))
|
|
310
|
+
if (owners.length === 0) continue
|
|
311
|
+
targets.push(activation)
|
|
359
312
|
for (const owner of owners) {
|
|
360
|
-
const members = this.closingMembers(owner)
|
|
361
|
-
if (child !== undefined) members.add(child)
|
|
362
|
-
for (const agent of lineage) members.add(agent)
|
|
313
|
+
const members = this.closingMembers(owner)
|
|
314
|
+
if (child !== undefined) members.add(child)
|
|
315
|
+
for (const agent of lineage) members.add(agent)
|
|
363
316
|
}
|
|
364
317
|
}
|
|
365
318
|
const materializations = [...this.materializations].filter((materialization) => {
|
|
366
|
-
const owners = [...roots].filter(
|
|
319
|
+
const owners = [...roots].filter(root => materialization.lineage.includes(root))
|
|
367
320
|
for (const owner of owners) {
|
|
368
|
-
const members = this.closingMembers(owner)
|
|
369
|
-
for (const agent of materialization.lineage) members.add(agent)
|
|
321
|
+
const members = this.closingMembers(owner)
|
|
322
|
+
for (const agent of materialization.lineage) members.add(agent)
|
|
370
323
|
}
|
|
371
|
-
if (owners.length > 0) materialization.controller.abort()
|
|
372
|
-
return owners.length > 0
|
|
373
|
-
})
|
|
324
|
+
if (owners.length > 0) materialization.controller.abort()
|
|
325
|
+
return owners.length > 0
|
|
326
|
+
})
|
|
374
327
|
|
|
375
|
-
const ownedTargets = new Set<SessionId>()
|
|
328
|
+
const ownedTargets = new Set<SessionId>()
|
|
376
329
|
for (const activation of targets) {
|
|
377
|
-
for (const child of activation.ownedChildren) ownedTargets.add(child)
|
|
330
|
+
for (const child of activation.ownedChildren) ownedTargets.add(child)
|
|
378
331
|
}
|
|
379
|
-
const targetRoots = targets.filter(
|
|
332
|
+
const targetRoots = targets.filter(activation => !ownedTargets.has(activation.childId))
|
|
380
333
|
|
|
381
334
|
for (const activation of targets) {
|
|
382
|
-
const disposal = this.dispose(activation)
|
|
383
|
-
void disposal.catch(() => undefined)
|
|
335
|
+
const disposal = this.dispose(activation)
|
|
336
|
+
void disposal.catch(() => undefined)
|
|
384
337
|
}
|
|
385
338
|
|
|
386
|
-
await Promise.all(materializations.map(
|
|
387
|
-
await this.disposeRoots(targetRoots,
|
|
339
|
+
await Promise.all(materializations.map(materialization => materialization.settled))
|
|
340
|
+
await this.disposeRoots(targetRoots, 'scoped activation(s)')
|
|
388
341
|
}
|
|
389
342
|
|
|
390
343
|
/**
|
|
@@ -394,29 +347,26 @@ export class SubagentManager {
|
|
|
394
347
|
*/
|
|
395
348
|
async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void> {
|
|
396
349
|
if (this.ctx.agents.get(parent.id) !== parent) {
|
|
397
|
-
throw new SubagentError(
|
|
398
|
-
"selected child teardown requires the exact live parent agent",
|
|
399
|
-
"UNAUTHORIZED",
|
|
400
|
-
);
|
|
350
|
+
throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED')
|
|
401
351
|
}
|
|
402
|
-
const targets: Activation[] = []
|
|
352
|
+
const targets: Activation[] = []
|
|
403
353
|
for (const childId of new Set(childIds)) {
|
|
404
|
-
const activation = this.resident.get(childId)
|
|
405
|
-
if (activation === undefined) continue
|
|
354
|
+
const activation = this.resident.get(childId)
|
|
355
|
+
if (activation === undefined) continue
|
|
406
356
|
if (activation.parent.id !== parent.id || !activation.ancestry.has(parent)) {
|
|
407
357
|
throw new SubagentError(
|
|
408
358
|
`subagent "${childId}" is not a direct child of agent "${parent.id}"`,
|
|
409
|
-
|
|
410
|
-
)
|
|
359
|
+
'UNAUTHORIZED',
|
|
360
|
+
)
|
|
411
361
|
}
|
|
412
|
-
targets.push(activation)
|
|
362
|
+
targets.push(activation)
|
|
413
363
|
}
|
|
414
364
|
|
|
415
365
|
for (const activation of targets) {
|
|
416
|
-
const disposal = this.dispose(activation)
|
|
417
|
-
void disposal.catch(() => undefined)
|
|
366
|
+
const disposal = this.dispose(activation)
|
|
367
|
+
void disposal.catch(() => undefined)
|
|
418
368
|
}
|
|
419
|
-
await this.disposeRoots(targets,
|
|
369
|
+
await this.disposeRoots(targets, 'selected activation(s)')
|
|
420
370
|
}
|
|
421
371
|
|
|
422
372
|
/**
|
|
@@ -426,81 +376,73 @@ export class SubagentManager {
|
|
|
426
376
|
* @param spec - provider, delegation request, and caller cancellation.
|
|
427
377
|
* @returns the durable child id and accepted initial prompt message id.
|
|
428
378
|
*/
|
|
429
|
-
async startLocal(
|
|
430
|
-
spec
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const childId = spec.childId ?? brandString<SessionId>(randomUUID());
|
|
439
|
-
this.assertChildIdAvailable(childId);
|
|
440
|
-
const childDepth = resolveChildDepth(parent, request.maxDepth);
|
|
379
|
+
async startLocal(spec: SubagentActivationSpec): Promise<SubagentActivation & { messageId: MessageId }> {
|
|
380
|
+
const request = spec.request
|
|
381
|
+
const parent = request.parent
|
|
382
|
+
this.assertAdmitting(parent)
|
|
383
|
+
const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence()
|
|
384
|
+
assertSubagentMaxDepth(request.maxDepth)
|
|
385
|
+
const childId = spec.childId ?? brandString<SessionId>(randomUUID())
|
|
386
|
+
this.assertChildIdAvailable(childId)
|
|
387
|
+
const childDepth = resolveChildDepth(parent, request.maxDepth)
|
|
441
388
|
// Snapshot before any await: invalid descriptor JSON rejects the call
|
|
442
389
|
// before a child exists, and the detached value is what reaches the log.
|
|
443
|
-
const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth)
|
|
444
|
-
const agentProvider = agentOptions.provider
|
|
445
|
-
const agentModel = agentOptions.model
|
|
446
|
-
const agentReasoningEffort = agentOptions.reasoningEffort
|
|
390
|
+
const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth)
|
|
391
|
+
const agentProvider = agentOptions.provider
|
|
392
|
+
const agentModel = agentOptions.model
|
|
393
|
+
const agentReasoningEffort = agentOptions.reasoningEffort
|
|
447
394
|
const descriptor = snapshotSubagentDescriptor({
|
|
448
|
-
mode:
|
|
395
|
+
mode: 'continuable',
|
|
449
396
|
provider: spec.provider,
|
|
450
397
|
label: spec.label,
|
|
451
|
-
...
|
|
452
|
-
...
|
|
453
|
-
...
|
|
454
|
-
})
|
|
398
|
+
...agentProvider !== undefined ? { agentProvider } : {},
|
|
399
|
+
...agentModel !== undefined ? { agentModel } : {},
|
|
400
|
+
...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
|
|
401
|
+
})
|
|
455
402
|
// Capture before the first await: a later parent switch belongs to the
|
|
456
403
|
// parent's future, not to this child.
|
|
457
|
-
const delegatedPolicies = captureDelegatedPolicyOverrides(parent)
|
|
404
|
+
const delegatedPolicies = captureDelegatedPolicyOverrides(parent)
|
|
458
405
|
|
|
459
406
|
// An idle continuation-managed parent must not settle while a caller is
|
|
460
407
|
// still creating its child. A turn-scoped delegation does not need this,
|
|
461
408
|
// but the service is also callable outside a turn.
|
|
462
|
-
const releaseHold = this.holdOwnership(parent, childId)
|
|
409
|
+
const releaseHold = this.holdOwnership(parent, childId)
|
|
463
410
|
try {
|
|
464
|
-
const cwd = await this.resolveDirectory(request, spec.signal)
|
|
411
|
+
const cwd = await this.resolveDirectory(request, spec.signal)
|
|
465
412
|
const prepared = await this.host.prepareContinuable(spec.provider, {
|
|
466
413
|
sessionId: childId,
|
|
467
414
|
cwd,
|
|
468
415
|
parent,
|
|
469
416
|
signal: spec.signal,
|
|
470
|
-
})
|
|
471
|
-
spec.signal.throwIfAborted()
|
|
472
|
-
this.assertAdmitting(parent)
|
|
417
|
+
})
|
|
418
|
+
spec.signal.throwIfAborted()
|
|
419
|
+
this.assertAdmitting(parent)
|
|
473
420
|
|
|
474
|
-
const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0)
|
|
475
|
-
const seed = prepared.seed
|
|
476
|
-
let established!: Activation
|
|
421
|
+
const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0)
|
|
422
|
+
const seed = prepared.seed
|
|
423
|
+
let established!: Activation
|
|
477
424
|
const messageId = await this.locks.run(childId, async () => {
|
|
478
|
-
spec.signal.throwIfAborted()
|
|
479
|
-
this.assertAdmitting(parent)
|
|
480
|
-
this.assertChildIdAvailable(childId)
|
|
425
|
+
spec.signal.throwIfAborted()
|
|
426
|
+
this.assertAdmitting(parent)
|
|
427
|
+
this.assertChildIdAvailable(childId)
|
|
481
428
|
if (spec.childId !== undefined) {
|
|
482
|
-
const persisted = await persistence?.stat(childId, { signal: spec.signal })
|
|
483
|
-
spec.signal.throwIfAborted()
|
|
484
|
-
this.assertAdmitting(parent)
|
|
485
|
-
this.assertChildIdAvailable(childId)
|
|
429
|
+
const persisted = await persistence?.stat(childId, { signal: spec.signal })
|
|
430
|
+
spec.signal.throwIfAborted()
|
|
431
|
+
this.assertAdmitting(parent)
|
|
432
|
+
this.assertChildIdAvailable(childId)
|
|
486
433
|
if (persisted !== undefined) {
|
|
487
|
-
throw new SubagentError(`subagent "${childId}" already exists`,
|
|
434
|
+
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD')
|
|
488
435
|
}
|
|
489
436
|
}
|
|
490
437
|
const activation = await this.materialize({
|
|
491
|
-
kind:
|
|
438
|
+
kind: 'local',
|
|
492
439
|
childId,
|
|
493
440
|
provider: spec.provider,
|
|
494
441
|
parent,
|
|
495
442
|
create: {
|
|
496
443
|
cwd,
|
|
497
444
|
seed,
|
|
498
|
-
meta: childSessionMeta(
|
|
499
|
-
parent,
|
|
500
|
-
childDepth,
|
|
501
|
-
prepared.seed !== undefined,
|
|
502
|
-
request.agentPreset,
|
|
503
|
-
),
|
|
445
|
+
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, request.agentPreset),
|
|
504
446
|
inheritedEventCount,
|
|
505
447
|
delegatedPolicies,
|
|
506
448
|
descriptor,
|
|
@@ -510,27 +452,26 @@ export class SubagentManager {
|
|
|
510
452
|
signal: spec.signal,
|
|
511
453
|
delivery: spec.delivery,
|
|
512
454
|
outputSchema: spec.request.outputSchema,
|
|
513
|
-
})
|
|
514
|
-
established = activation
|
|
515
|
-
const child = requireLocalActivation(activation).handle.agent
|
|
516
|
-
const childHeader = child.session.header
|
|
455
|
+
})
|
|
456
|
+
established = activation
|
|
457
|
+
const child = requireLocalActivation(activation).handle.agent
|
|
458
|
+
const childHeader = child.session.header
|
|
517
459
|
return await this.submitMaterialized(
|
|
518
460
|
activation,
|
|
519
|
-
spec.delivery !==
|
|
520
|
-
isAdjacentAgentSendMessageTool(this.ctx.get("tools")?.get("send_message", child))
|
|
461
|
+
spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child))
|
|
521
462
|
? withContinuableReturnGuidance(parent.id, request.prompt)
|
|
522
463
|
: request.prompt,
|
|
523
|
-
{ source: { kind:
|
|
464
|
+
{ source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' },
|
|
524
465
|
parent,
|
|
525
466
|
() => {
|
|
526
|
-
establishCatalogChild(parent.session, childHeader, descriptor)
|
|
467
|
+
establishCatalogChild(parent.session, childHeader, descriptor)
|
|
527
468
|
},
|
|
528
|
-
)
|
|
529
|
-
})
|
|
530
|
-
return { ...this.receipt(established), childId, messageId }
|
|
469
|
+
)
|
|
470
|
+
})
|
|
471
|
+
return { ...this.receipt(established), childId, messageId }
|
|
531
472
|
} catch (error: unknown) {
|
|
532
|
-
releaseHold()
|
|
533
|
-
throw error
|
|
473
|
+
releaseHold()
|
|
474
|
+
throw error
|
|
534
475
|
}
|
|
535
476
|
}
|
|
536
477
|
|
|
@@ -540,66 +481,53 @@ export class SubagentManager {
|
|
|
540
481
|
* @returns the accepted activation and its result/disposal capabilities.
|
|
541
482
|
*/
|
|
542
483
|
async startExternal(spec: SubagentActivationSpec): Promise<SubagentActivation> {
|
|
543
|
-
const parent = spec.request.parent
|
|
544
|
-
this.assertAdmitting(parent)
|
|
545
|
-
const pendingId = brandString<SessionId>(randomUUID())
|
|
546
|
-
const releaseHold = this.holdOwnership(parent, pendingId)
|
|
547
|
-
let activation: Activation | undefined
|
|
484
|
+
const parent = spec.request.parent
|
|
485
|
+
this.assertAdmitting(parent)
|
|
486
|
+
const pendingId = brandString<SessionId>(randomUUID())
|
|
487
|
+
const releaseHold = this.holdOwnership(parent, pendingId)
|
|
488
|
+
let activation: Activation | undefined
|
|
548
489
|
try {
|
|
549
|
-
const cwd = await this.resolveDirectory(spec.request, spec.signal)
|
|
490
|
+
const cwd = await this.resolveDirectory(spec.request, spec.signal)
|
|
550
491
|
activation = await this.materialize({
|
|
551
|
-
kind:
|
|
492
|
+
kind: 'external',
|
|
552
493
|
childId: pendingId,
|
|
553
494
|
provider: spec.provider,
|
|
554
495
|
parent,
|
|
555
496
|
signal: spec.signal,
|
|
556
497
|
delivery: spec.delivery,
|
|
557
|
-
start:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this.assertLiveParent(parent, activation.childId);
|
|
567
|
-
spec.signal.throwIfAborted();
|
|
568
|
-
if (spec.delivery === "parent") {
|
|
569
|
-
establishExternalCatalogChild(parent.session, activation.childId, spec.label);
|
|
498
|
+
start: signal => this.host.startExternal(spec.provider, {
|
|
499
|
+
...spec.request, cwd, label: spec.label, signal,
|
|
500
|
+
}),
|
|
501
|
+
})
|
|
502
|
+
this.assertAdmitting(parent)
|
|
503
|
+
this.assertLiveParent(parent, activation.childId)
|
|
504
|
+
spec.signal.throwIfAborted()
|
|
505
|
+
if (spec.delivery === 'parent') {
|
|
506
|
+
establishExternalCatalogChild(parent.session, activation.childId, spec.label)
|
|
570
507
|
}
|
|
571
|
-
this.announce(activation)
|
|
572
|
-
return this.receipt(activation)
|
|
508
|
+
this.announce(activation)
|
|
509
|
+
return this.receipt(activation)
|
|
573
510
|
} catch (error: unknown) {
|
|
574
511
|
if (activation !== undefined) {
|
|
575
512
|
try {
|
|
576
|
-
await this.dispose(activation)
|
|
513
|
+
await this.dispose(activation)
|
|
577
514
|
} catch (cleanupError: unknown) {
|
|
578
|
-
this.ctx.logger.warn(
|
|
579
|
-
`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`,
|
|
580
|
-
);
|
|
515
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`)
|
|
581
516
|
}
|
|
582
517
|
}
|
|
583
|
-
throw error
|
|
518
|
+
throw error
|
|
584
519
|
} finally {
|
|
585
|
-
releaseHold()
|
|
520
|
+
releaseHold()
|
|
586
521
|
}
|
|
587
522
|
}
|
|
588
523
|
|
|
589
524
|
/** Capture the explicit or inherited directory while the parent still owns startup. */
|
|
590
|
-
private async resolveDirectory(
|
|
591
|
-
request
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
? request.cwd
|
|
597
|
-
: resolve(
|
|
598
|
-
await this.ctx.workingDirectory.ensure(request.parent, signal),
|
|
599
|
-
request.cwd ?? ".",
|
|
600
|
-
);
|
|
601
|
-
signal.throwIfAborted();
|
|
602
|
-
return cwd;
|
|
525
|
+
private async resolveDirectory(request: Pick<SubagentStartRequest, 'parent' | 'cwd'>, signal: AbortSignal): Promise<string> {
|
|
526
|
+
const cwd = request.cwd !== undefined && isAbsolute(request.cwd)
|
|
527
|
+
? request.cwd
|
|
528
|
+
: resolve(await this.ctx.workingDirectory.ensure(request.parent, signal), request.cwd ?? '.')
|
|
529
|
+
signal.throwIfAborted()
|
|
530
|
+
return cwd
|
|
603
531
|
}
|
|
604
532
|
|
|
605
533
|
/**
|
|
@@ -620,31 +548,29 @@ export class SubagentManager {
|
|
|
620
548
|
): Promise<MessageId> {
|
|
621
549
|
if (this.ctx.agents.get(sender.id) !== sender) {
|
|
622
550
|
throw new SubagentError(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
)
|
|
551
|
+
'message delivery requires the exact live sender agent',
|
|
552
|
+
'UNAUTHORIZED',
|
|
553
|
+
)
|
|
626
554
|
}
|
|
627
|
-
this.assertAdmitting(sender)
|
|
628
|
-
const senderActivation = this.resident.get(sender.id)
|
|
629
|
-
if (
|
|
630
|
-
senderActivation
|
|
631
|
-
senderActivation.
|
|
632
|
-
senderActivation.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
options.signal.throwIfAborted();
|
|
636
|
-
return this.sendToParent(senderActivation, sender, content);
|
|
555
|
+
this.assertAdmitting(sender)
|
|
556
|
+
const senderActivation = this.resident.get(sender.id)
|
|
557
|
+
if (senderActivation !== undefined
|
|
558
|
+
&& senderActivation.kind === 'local'
|
|
559
|
+
&& senderActivation.handle.agent === sender
|
|
560
|
+
&& senderActivation.parent.id === targetId) {
|
|
561
|
+
options.signal.throwIfAborted()
|
|
562
|
+
return this.sendToParent(senderActivation, sender, content)
|
|
637
563
|
}
|
|
638
564
|
if (sender.session.header.parentSession === targetId) {
|
|
639
565
|
throw new SubagentError(
|
|
640
566
|
`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`,
|
|
641
|
-
|
|
642
|
-
)
|
|
567
|
+
'UNAUTHORIZED',
|
|
568
|
+
)
|
|
643
569
|
}
|
|
644
570
|
return this.deliverToChild(sender, targetId, content, {
|
|
645
571
|
signal: options.signal,
|
|
646
|
-
delivery:
|
|
647
|
-
})
|
|
572
|
+
delivery: 'steer',
|
|
573
|
+
})
|
|
648
574
|
}
|
|
649
575
|
|
|
650
576
|
/**
|
|
@@ -663,7 +589,7 @@ export class SubagentManager {
|
|
|
663
589
|
source: MessageSource,
|
|
664
590
|
signal: AbortSignal,
|
|
665
591
|
): Promise<MessageId> {
|
|
666
|
-
return this.deliverToChild(parent, childId, content, { source, signal, delivery:
|
|
592
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' })
|
|
667
593
|
}
|
|
668
594
|
|
|
669
595
|
/**
|
|
@@ -682,26 +608,23 @@ export class SubagentManager {
|
|
|
682
608
|
source: MessageSource,
|
|
683
609
|
signal: AbortSignal,
|
|
684
610
|
): Promise<MessageId> {
|
|
685
|
-
return this.deliverToChild(parent, childId, content, { source, signal, delivery:
|
|
611
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' })
|
|
686
612
|
}
|
|
687
613
|
|
|
688
614
|
/** Admit local input synchronously before any teardown can begin. */
|
|
689
615
|
private deliver(activation: Activation, message: UserMessage, delivery: SubagentDelivery): void {
|
|
690
616
|
if (activation.closing !== undefined) {
|
|
691
617
|
throw new SubagentError(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
)
|
|
618
|
+
'subagent activation is being disposed; the message was not accepted',
|
|
619
|
+
'ACTIVATION_CLOSING',
|
|
620
|
+
)
|
|
695
621
|
}
|
|
696
|
-
const local = requireLocalActivation(activation)
|
|
622
|
+
const local = requireLocalActivation(activation)
|
|
697
623
|
if (local.structured?.acceptsInput() === false) {
|
|
698
|
-
throw new SubagentError(
|
|
699
|
-
"subagent is submitting or has submitted its structured result; the message was not accepted",
|
|
700
|
-
"INPUT_CLOSED",
|
|
701
|
-
);
|
|
624
|
+
throw new SubagentError('subagent is submitting or has submitted its structured result; the message was not accepted', 'INPUT_CLOSED')
|
|
702
625
|
}
|
|
703
|
-
if (delivery ===
|
|
704
|
-
else local.handle.agent.followup(message)
|
|
626
|
+
if (delivery === 'steer') local.handle.agent.steer(message)
|
|
627
|
+
else local.handle.agent.followup(message)
|
|
705
628
|
}
|
|
706
629
|
|
|
707
630
|
/**
|
|
@@ -709,12 +632,8 @@ export class SubagentManager {
|
|
|
709
632
|
* @param childId - proposed durable child session id.
|
|
710
633
|
*/
|
|
711
634
|
private assertChildIdAvailable(childId: SessionId): void {
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
this.ctx.agents.get(childId) !== undefined ||
|
|
715
|
-
this.ctx.get("sessions")?.get(childId) !== undefined
|
|
716
|
-
) {
|
|
717
|
-
throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
|
|
635
|
+
if (this.resident.has(childId) || this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
|
|
636
|
+
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD')
|
|
718
637
|
}
|
|
719
638
|
}
|
|
720
639
|
|
|
@@ -729,26 +648,25 @@ export class SubagentManager {
|
|
|
729
648
|
* @returns the failure-path releaser; a no-op when nothing was added.
|
|
730
649
|
*/
|
|
731
650
|
private holdOwnership(parent: Agent, childId: SessionId): () => void {
|
|
732
|
-
const parentActivation = this.resident.get(parent.id)
|
|
733
|
-
if (parentActivation === undefined || activationAgent(parentActivation) !== parent)
|
|
734
|
-
return () => {};
|
|
651
|
+
const parentActivation = this.resident.get(parent.id)
|
|
652
|
+
if (parentActivation === undefined || activationAgent(parentActivation) !== parent) return () => {}
|
|
735
653
|
if (parentActivation.closing !== undefined) {
|
|
736
654
|
throw new SubagentError(
|
|
737
655
|
`subagent parent "${parent.id}" is being disposed; the child was not established`,
|
|
738
|
-
|
|
739
|
-
)
|
|
656
|
+
'ACTIVATION_CLOSING',
|
|
657
|
+
)
|
|
740
658
|
}
|
|
741
|
-
if (parentActivation.ownedChildren.has(childId)) return () => {}
|
|
742
|
-
parentActivation.ownedChildren.add(childId)
|
|
659
|
+
if (parentActivation.ownedChildren.has(childId)) return () => {}
|
|
660
|
+
parentActivation.ownedChildren.add(childId)
|
|
743
661
|
return () => {
|
|
744
|
-
const live = this.resident.get(childId)
|
|
662
|
+
const live = this.resident.get(childId)
|
|
745
663
|
/* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
|
|
746
664
|
* between this operation's failure and its releaser running, which no test can schedule
|
|
747
665
|
* deterministically: the ownership edge then belongs to that live Activation, so the
|
|
748
666
|
* conservative keep leaves it for finishDisposal's releaseOwnership. */
|
|
749
|
-
if (live !== undefined && live.closing === undefined) return
|
|
750
|
-
if (parentActivation.ownedChildren.delete(childId)) this.wake(parentActivation)
|
|
751
|
-
}
|
|
667
|
+
if (live !== undefined && live.closing === undefined) return
|
|
668
|
+
if (parentActivation.ownedChildren.delete(childId)) this.wake(parentActivation)
|
|
669
|
+
}
|
|
752
670
|
}
|
|
753
671
|
|
|
754
672
|
/**
|
|
@@ -758,17 +676,17 @@ export class SubagentManager {
|
|
|
758
676
|
* @param delivery - receiving inbox destination.
|
|
759
677
|
*/
|
|
760
678
|
private sendWaking(parent: Agent, message: UserMessage, delivery: SubagentDelivery): void {
|
|
761
|
-
const parentActivation = this.resident.get(parent.id)
|
|
679
|
+
const parentActivation = this.resident.get(parent.id)
|
|
762
680
|
if (parentActivation !== undefined && activationAgent(parentActivation) === parent) {
|
|
763
681
|
try {
|
|
764
|
-
this.deliver(parentActivation, message, delivery)
|
|
682
|
+
this.deliver(parentActivation, message, delivery)
|
|
765
683
|
} finally {
|
|
766
|
-
this.wake(parentActivation)
|
|
684
|
+
this.wake(parentActivation)
|
|
767
685
|
}
|
|
768
|
-
return
|
|
686
|
+
return
|
|
769
687
|
}
|
|
770
|
-
if (delivery ===
|
|
771
|
-
else parent.followup(message)
|
|
688
|
+
if (delivery === 'steer') parent.steer(message)
|
|
689
|
+
else parent.followup(message)
|
|
772
690
|
}
|
|
773
691
|
|
|
774
692
|
/**
|
|
@@ -776,14 +694,14 @@ export class SubagentManager {
|
|
|
776
694
|
* @param agent - exact live Agent whose lineage determines admission.
|
|
777
695
|
*/
|
|
778
696
|
private assertAdmitting(agent: Agent): void {
|
|
779
|
-
const closing = this.closingTeardownFor(agent)
|
|
780
|
-
if (closing === undefined) return
|
|
697
|
+
const closing = this.closingTeardownFor(agent)
|
|
698
|
+
if (closing === undefined) return
|
|
781
699
|
throw new SubagentError(
|
|
782
|
-
closing ===
|
|
783
|
-
?
|
|
700
|
+
closing === 'manager'
|
|
701
|
+
? 'subagents are draining; the operation was not admitted'
|
|
784
702
|
: `subagents below parent "${closing.id}" are draining; the operation was not admitted`,
|
|
785
|
-
|
|
786
|
-
)
|
|
703
|
+
'DRAINING',
|
|
704
|
+
)
|
|
787
705
|
}
|
|
788
706
|
|
|
789
707
|
/**
|
|
@@ -797,12 +715,9 @@ export class SubagentManager {
|
|
|
797
715
|
childId: SessionId,
|
|
798
716
|
parentSession: SessionId | undefined,
|
|
799
717
|
): void {
|
|
800
|
-
this.assertLiveParent(parent, childId)
|
|
718
|
+
this.assertLiveParent(parent, childId)
|
|
801
719
|
if (parentSession !== parent.id) {
|
|
802
|
-
throw new SubagentError(
|
|
803
|
-
`subagent "${childId}" belongs to another parent session`,
|
|
804
|
-
"UNAUTHORIZED",
|
|
805
|
-
);
|
|
720
|
+
throw new SubagentError(`subagent "${childId}" belongs to another parent session`, 'UNAUTHORIZED')
|
|
806
721
|
}
|
|
807
722
|
}
|
|
808
723
|
|
|
@@ -811,8 +726,8 @@ export class SubagentManager {
|
|
|
811
726
|
if (this.ctx.agents.get(parent.id) !== parent) {
|
|
812
727
|
throw new SubagentError(
|
|
813
728
|
`subagent "${childId}" delivery requires the exact live parent agent`,
|
|
814
|
-
|
|
815
|
-
)
|
|
729
|
+
'UNAUTHORIZED',
|
|
730
|
+
)
|
|
816
731
|
}
|
|
817
732
|
}
|
|
818
733
|
|
|
@@ -822,35 +737,29 @@ export class SubagentManager {
|
|
|
822
737
|
* @returns the published process-local Activation.
|
|
823
738
|
*/
|
|
824
739
|
private materialize(inputs: MaterializeInputs): Promise<Activation> {
|
|
825
|
-
this.assertAdmitting(inputs.parent)
|
|
826
|
-
inputs.signal.throwIfAborted()
|
|
827
|
-
const lineage = this.liveLineage(inputs.parent)
|
|
828
|
-
const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent)
|
|
829
|
-
const releaseSlot = pool.reserve(this.maxActiveSubagents())
|
|
830
|
-
const settled = Promise.withResolvers<void>()
|
|
831
|
-
const controller = new AbortController()
|
|
740
|
+
this.assertAdmitting(inputs.parent)
|
|
741
|
+
inputs.signal.throwIfAborted()
|
|
742
|
+
const lineage = this.liveLineage(inputs.parent)
|
|
743
|
+
const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent)
|
|
744
|
+
const releaseSlot = pool.reserve(this.maxActiveSubagents())
|
|
745
|
+
const settled = Promise.withResolvers<void>()
|
|
746
|
+
const controller = new AbortController()
|
|
832
747
|
const materialization: Materialization = {
|
|
833
748
|
lineage,
|
|
834
749
|
settled: settled.promise,
|
|
835
750
|
controller,
|
|
836
|
-
}
|
|
837
|
-
this.materializations.add(materialization)
|
|
751
|
+
}
|
|
752
|
+
this.materializations.add(materialization)
|
|
838
753
|
return this.materializeTracked(
|
|
839
|
-
{ ...inputs, signal: AbortSignal.any([inputs.signal, controller.signal]) },
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
throw error;
|
|
849
|
-
})
|
|
850
|
-
.finally(() => {
|
|
851
|
-
this.materializations.delete(materialization);
|
|
852
|
-
settled.resolve();
|
|
853
|
-
});
|
|
754
|
+
{ ...inputs, signal: AbortSignal.any([inputs.signal, controller.signal]) }, lineage, pool, releaseSlot,
|
|
755
|
+
).catch((error: unknown) => {
|
|
756
|
+
releaseSlot()
|
|
757
|
+
if (error instanceof Error && error.name === 'AbortError') this.assertAdmitting(inputs.parent)
|
|
758
|
+
throw error
|
|
759
|
+
}).finally(() => {
|
|
760
|
+
this.materializations.delete(materialization)
|
|
761
|
+
settled.resolve()
|
|
762
|
+
})
|
|
854
763
|
}
|
|
855
764
|
|
|
856
765
|
/**
|
|
@@ -859,85 +768,83 @@ export class SubagentManager {
|
|
|
859
768
|
* @returns the shared close transaction.
|
|
860
769
|
*/
|
|
861
770
|
private dispose(activation: Activation): Promise<void> {
|
|
862
|
-
return this.close(activation, () => this.finishDisposal(activation, true))
|
|
771
|
+
return this.close(activation, () => this.finishDisposal(activation, true))
|
|
863
772
|
}
|
|
864
773
|
|
|
865
774
|
/** Close admission synchronously and share one release, including startup rollback. */
|
|
866
775
|
private close(activation: Activation, release: () => Promise<void>): Promise<void> {
|
|
867
|
-
if (activation.closing !== undefined) return activation.closing
|
|
868
|
-
const completion = Promise.withResolvers<void>()
|
|
869
|
-
activation.closing = completion.promise
|
|
870
|
-
void release().then(completion.resolve, completion.reject)
|
|
871
|
-
return completion.promise
|
|
776
|
+
if (activation.closing !== undefined) return activation.closing
|
|
777
|
+
const completion = Promise.withResolvers<void>()
|
|
778
|
+
activation.closing = completion.promise
|
|
779
|
+
void release().then(completion.resolve, completion.reject)
|
|
780
|
+
return completion.promise
|
|
872
781
|
}
|
|
873
782
|
|
|
874
783
|
/** Dispose independent roots and report every branch failure after all settle. */
|
|
875
784
|
private async disposeRoots(
|
|
876
785
|
roots: readonly Activation[],
|
|
877
|
-
failureSubject:
|
|
786
|
+
failureSubject: 'activation(s)' | 'scoped activation(s)' | 'selected activation(s)',
|
|
878
787
|
): Promise<void> {
|
|
879
|
-
const failures = await Promise.all(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
);
|
|
889
|
-
const reasons = failures.filter((failure) => failure !== undefined);
|
|
788
|
+
const failures = await Promise.all(roots.map(async (activation) => {
|
|
789
|
+
try {
|
|
790
|
+
await this.dispose(activation)
|
|
791
|
+
return undefined
|
|
792
|
+
} catch (error: unknown) {
|
|
793
|
+
return error
|
|
794
|
+
}
|
|
795
|
+
}))
|
|
796
|
+
const reasons = failures.filter(failure => failure !== undefined)
|
|
890
797
|
if (reasons.length > 0) {
|
|
891
798
|
throw new SubagentError(
|
|
892
|
-
`subagent teardown failed for ${reasons.length} ${failureSubject}: `
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
)
|
|
799
|
+
`subagent teardown failed for ${reasons.length} ${failureSubject}: `
|
|
800
|
+
+ reasons.map(reason => failureMessage(reason)).join('; '),
|
|
801
|
+
'ACTIVATION_TEARDOWN_FAILED',
|
|
802
|
+
)
|
|
896
803
|
}
|
|
897
804
|
}
|
|
898
805
|
|
|
899
806
|
/** Return the retained member set for one exact scoped-teardown root. */
|
|
900
807
|
private closingMembers(root: Agent): Set<Agent> {
|
|
901
|
-
const existing = this.closingScopes.get(root)
|
|
902
|
-
if (existing !== undefined) return existing
|
|
903
|
-
const members = new Set<Agent>()
|
|
904
|
-
this.closingScopes.set(root, members)
|
|
905
|
-
return members
|
|
808
|
+
const existing = this.closingScopes.get(root)
|
|
809
|
+
if (existing !== undefined) return existing
|
|
810
|
+
const members = new Set<Agent>()
|
|
811
|
+
this.closingScopes.set(root, members)
|
|
812
|
+
return members
|
|
906
813
|
}
|
|
907
814
|
|
|
908
815
|
/** Return the exact currently resolvable ancestry from `agent` upward. */
|
|
909
816
|
private liveLineage(agent: Agent): Agent[] {
|
|
910
|
-
const lineage = [agent]
|
|
911
|
-
const seen = new Set<SessionId>([agent.id])
|
|
912
|
-
let parentSession = agent.session.header.parentSession
|
|
817
|
+
const lineage = [agent]
|
|
818
|
+
const seen = new Set<SessionId>([agent.id])
|
|
819
|
+
let parentSession = agent.session.header.parentSession
|
|
913
820
|
while (parentSession !== undefined) {
|
|
914
|
-
const parent = this.ctx.agents.get(parentSession)
|
|
915
|
-
if (parent === undefined || seen.has(parent.id)) break
|
|
916
|
-
lineage.push(parent)
|
|
917
|
-
seen.add(parent.id)
|
|
918
|
-
parentSession = parent.session.header.parentSession
|
|
821
|
+
const parent = this.ctx.agents.get(parentSession)
|
|
822
|
+
if (parent === undefined || seen.has(parent.id)) break
|
|
823
|
+
lineage.push(parent)
|
|
824
|
+
seen.add(parent.id)
|
|
825
|
+
parentSession = parent.session.header.parentSession
|
|
919
826
|
}
|
|
920
|
-
return lineage
|
|
827
|
+
return lineage
|
|
921
828
|
}
|
|
922
829
|
|
|
923
830
|
/** Return the teardown that closed continuable admission for this agent's lineage. */
|
|
924
|
-
private closingTeardownFor(agent: Agent): Agent |
|
|
925
|
-
if (this.draining) return
|
|
926
|
-
const lineage = this.liveLineage(agent)
|
|
831
|
+
private closingTeardownFor(agent: Agent): Agent | 'manager' | undefined {
|
|
832
|
+
if (this.draining) return 'manager'
|
|
833
|
+
const lineage = this.liveLineage(agent)
|
|
927
834
|
for (const [root, members] of this.closingScopes) {
|
|
928
|
-
if (members.has(agent) || lineage.includes(root)) return root
|
|
835
|
+
if (members.has(agent) || lineage.includes(root)) return root
|
|
929
836
|
}
|
|
930
|
-
return undefined
|
|
837
|
+
return undefined
|
|
931
838
|
}
|
|
932
839
|
|
|
933
840
|
/** Resolve a root's pool once; descendants inherit their resident parent's pool directly. */
|
|
934
841
|
private rootPool(parent: Agent): ActivationPool {
|
|
935
|
-
let pool = this.rootPools.get(parent)
|
|
842
|
+
let pool = this.rootPools.get(parent)
|
|
936
843
|
if (pool === undefined) {
|
|
937
|
-
pool = new ActivationPool()
|
|
938
|
-
this.rootPools.set(parent, pool)
|
|
844
|
+
pool = new ActivationPool()
|
|
845
|
+
this.rootPools.set(parent, pool)
|
|
939
846
|
}
|
|
940
|
-
return pool
|
|
847
|
+
return pool
|
|
941
848
|
}
|
|
942
849
|
|
|
943
850
|
/** Perform one tracked materialization through publication or rollback. */
|
|
@@ -947,95 +854,84 @@ export class SubagentManager {
|
|
|
947
854
|
pool: ActivationPool,
|
|
948
855
|
releaseSlot: () => void,
|
|
949
856
|
): Promise<Activation> {
|
|
950
|
-
const { provider, parent } = inputs
|
|
951
|
-
let childId = inputs.childId
|
|
952
|
-
inputs.signal.throwIfAborted()
|
|
953
|
-
let resource: ActivationResource
|
|
954
|
-
if (inputs.kind ===
|
|
955
|
-
const controller = new AbortController()
|
|
956
|
-
const cancelStartup = (): void => {
|
|
957
|
-
|
|
958
|
-
};
|
|
959
|
-
inputs.signal.addEventListener("abort", cancelStartup, { once: true });
|
|
857
|
+
const { provider, parent } = inputs
|
|
858
|
+
let childId = inputs.childId
|
|
859
|
+
inputs.signal.throwIfAborted()
|
|
860
|
+
let resource: ActivationResource
|
|
861
|
+
if (inputs.kind === 'external') {
|
|
862
|
+
const controller = new AbortController()
|
|
863
|
+
const cancelStartup = (): void => { controller.abort(inputs.signal.reason) }
|
|
864
|
+
inputs.signal.addEventListener('abort', cancelStartup, { once: true })
|
|
960
865
|
try {
|
|
961
|
-
const run = await inputs.start(controller.signal)
|
|
962
|
-
resource = { kind:
|
|
963
|
-
void run.result.catch(() => undefined)
|
|
964
|
-
childId = run.id
|
|
965
|
-
try {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
await run.dispose();
|
|
969
|
-
throw error;
|
|
866
|
+
const run = await inputs.start(controller.signal)
|
|
867
|
+
resource = { kind: 'external', run, controller }
|
|
868
|
+
void run.result.catch(() => undefined)
|
|
869
|
+
childId = run.id
|
|
870
|
+
try { this.assertChildIdAvailable(childId) } catch (error: unknown) {
|
|
871
|
+
await run.dispose()
|
|
872
|
+
throw error
|
|
970
873
|
}
|
|
971
874
|
} finally {
|
|
972
|
-
inputs.signal.removeEventListener(
|
|
875
|
+
inputs.signal.removeEventListener('abort', cancelStartup)
|
|
973
876
|
}
|
|
974
877
|
} else {
|
|
975
|
-
const { create } = inputs
|
|
976
|
-
let structured: StructuredAttachment | undefined
|
|
878
|
+
const { create } = inputs
|
|
879
|
+
let structured: StructuredAttachment | undefined
|
|
977
880
|
const setup = async (childCtx: Context, child: Agent): Promise<void> => {
|
|
978
881
|
// Only fresh creation appends the descriptor and delegated policy after
|
|
979
882
|
// the inherited marker; a cold resume replays those persisted events.
|
|
980
883
|
if (inputs.composition.agentPreset !== undefined) {
|
|
981
|
-
await applyChildComposition(childCtx, parent, inputs.composition)
|
|
884
|
+
await applyChildComposition(childCtx, parent, inputs.composition)
|
|
982
885
|
}
|
|
983
886
|
if (create !== undefined) {
|
|
984
887
|
if (inputs.composition.agentPreset !== undefined) {
|
|
985
|
-
child.session.append(
|
|
986
|
-
agentPreset: inputs.composition.agentPreset,
|
|
987
|
-
});
|
|
888
|
+
child.session.append('agent-preset/selected', { agentPreset: inputs.composition.agentPreset })
|
|
988
889
|
}
|
|
989
|
-
child.session.append(
|
|
990
|
-
appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies)
|
|
991
|
-
const workingDirectory = childCtx.get(
|
|
992
|
-
if (workingDirectory === undefined)
|
|
993
|
-
|
|
994
|
-
await workingDirectory.set(child, create.cwd, inputs.signal);
|
|
890
|
+
child.session.append('subagent/descriptor', create.descriptor)
|
|
891
|
+
appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies)
|
|
892
|
+
const workingDirectory = childCtx.get('workingDirectory')
|
|
893
|
+
if (workingDirectory === undefined) throw new Error('local subagents require the working-directory service')
|
|
894
|
+
await workingDirectory.set(child, create.cwd, inputs.signal)
|
|
995
895
|
}
|
|
996
|
-
if (inputs.composition.agentPreset === undefined)
|
|
997
|
-
await applyChildComposition(childCtx, parent, inputs.composition);
|
|
896
|
+
if (inputs.composition.agentPreset === undefined) await applyChildComposition(childCtx, parent, inputs.composition)
|
|
998
897
|
if (inputs.outputSchema !== undefined) {
|
|
999
|
-
structured = attachStructuredRuntime(
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
child.inbox.nextStep.length === 0 &&
|
|
1005
|
-
child.inbox.nextTurn.length === 0,
|
|
1006
|
-
);
|
|
898
|
+
structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () =>
|
|
899
|
+
this.resident.get(childId)?.ownedChildren.size === 0
|
|
900
|
+
&& child.inbox.nextStep.length === 0
|
|
901
|
+
&& child.inbox.nextTurn.length === 0,
|
|
902
|
+
)
|
|
1007
903
|
}
|
|
1008
|
-
}
|
|
1009
|
-
const handle =
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
:
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
});
|
|
904
|
+
}
|
|
905
|
+
const handle = create === undefined
|
|
906
|
+
? await this.ownerCtx.agents.resume({
|
|
907
|
+
resumeSessionId: childId,
|
|
908
|
+
parentAgent: parent,
|
|
909
|
+
agentOptions: inputs.agentOptions,
|
|
910
|
+
signal: inputs.signal,
|
|
911
|
+
setup,
|
|
912
|
+
})
|
|
913
|
+
: await this.ownerCtx.agents.create({
|
|
914
|
+
sessionId: childId,
|
|
915
|
+
parentAgent: parent,
|
|
916
|
+
meta: create.meta,
|
|
917
|
+
...(create.seed === undefined ? {} : { seed: create.seed }),
|
|
918
|
+
inheritedEventCount: create.inheritedEventCount,
|
|
919
|
+
agentOptions: inputs.agentOptions,
|
|
920
|
+
signal: inputs.signal,
|
|
921
|
+
setup,
|
|
922
|
+
})
|
|
1028
923
|
|
|
1029
924
|
resource = {
|
|
1030
|
-
kind:
|
|
925
|
+
kind: 'local',
|
|
1031
926
|
handle,
|
|
1032
927
|
outputStart: handle.agent.session.seq,
|
|
1033
928
|
structured,
|
|
1034
929
|
failureAt: undefined,
|
|
1035
930
|
poke: Promise.withResolvers<void>(),
|
|
1036
|
-
|
|
931
|
+
omitClosingText: false,
|
|
932
|
+
}
|
|
1037
933
|
}
|
|
1038
|
-
const observer = this.host.observeActivation(provider, childId, parent)
|
|
934
|
+
const observer = this.host.observeActivation(provider, childId, parent)
|
|
1039
935
|
|
|
1040
936
|
const activation: Activation = {
|
|
1041
937
|
pool,
|
|
@@ -1043,41 +939,47 @@ export class SubagentManager {
|
|
|
1043
939
|
childId,
|
|
1044
940
|
...resource,
|
|
1045
941
|
parent,
|
|
1046
|
-
delivery: inputs.delivery ??
|
|
942
|
+
delivery: inputs.delivery ?? 'parent',
|
|
1047
943
|
result: Promise.withResolvers<SubagentResult>(),
|
|
1048
944
|
closing: undefined,
|
|
1049
|
-
ancestry: new WeakSet(
|
|
1050
|
-
resource.kind === "external" ? parentLineage : [resource.handle.agent, ...parentLineage],
|
|
1051
|
-
),
|
|
945
|
+
ancestry: new WeakSet(resource.kind === 'external' ? parentLineage : [resource.handle.agent, ...parentLineage]),
|
|
1052
946
|
ownedChildren: new Set(),
|
|
1053
947
|
observer,
|
|
1054
948
|
announced: false,
|
|
1055
|
-
}
|
|
1056
|
-
void activation.result.promise.catch(() => undefined)
|
|
1057
|
-
this.resident.set(childId, activation)
|
|
949
|
+
}
|
|
950
|
+
void activation.result.promise.catch(() => undefined)
|
|
951
|
+
this.resident.set(childId, activation)
|
|
1058
952
|
try {
|
|
1059
|
-
this.assertAdmitting(parent)
|
|
1060
|
-
inputs.signal.throwIfAborted()
|
|
1061
|
-
if (this.ctx.agents.get(parent.id) !== parent)
|
|
1062
|
-
|
|
1063
|
-
this.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
953
|
+
this.assertAdmitting(parent)
|
|
954
|
+
inputs.signal.throwIfAborted()
|
|
955
|
+
if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError('subagent parent is no longer live', 'UNAUTHORIZED')
|
|
956
|
+
this.acquireOwnership(parent, childId)
|
|
957
|
+
const wakeSettlement = (): void => { this.wake(activation) }
|
|
958
|
+
if (activation.kind === 'local') {
|
|
959
|
+
observeLocalFailure(activation, wakeSettlement)
|
|
960
|
+
activation.handle.agent.ctx.on('agent/inbox/inserted', wakeSettlement)
|
|
961
|
+
activation.handle.agent.ctx.on('agent/inbox/claimed', () => {
|
|
962
|
+
// Claimed input means the closing text may report work the child's
|
|
963
|
+
// last message to its parent did not.
|
|
964
|
+
activation.omitClosingText = false
|
|
965
|
+
wakeSettlement()
|
|
966
|
+
})
|
|
967
|
+
activation.handle.agent.ctx.on('agent/inbox/discarded', wakeSettlement)
|
|
968
|
+
// A tool call logged after the message also voids it; logged calls
|
|
969
|
+
// cannot be skipped by tool policy. `send_message` is not concurrency
|
|
970
|
+
// safe, so a later call in its step is logged after it executes.
|
|
971
|
+
activation.handle.agent.ctx.on('session/event', (_session, event) => {
|
|
972
|
+
if (event.type === 'tool/call') activation.omitClosingText = false
|
|
973
|
+
})
|
|
1072
974
|
}
|
|
1073
|
-
observer.start(activationAgent(activation))
|
|
975
|
+
observer.start(activationAgent(activation))
|
|
1074
976
|
} catch (error: unknown) {
|
|
1075
977
|
/* v8 ignore next -- rollback failure must not mask the admission failure
|
|
1076
978
|
* that prevented this operation from returning an accepted message id. */
|
|
1077
|
-
await this.rollbackUnpublished(activation, error).catch(() => undefined)
|
|
1078
|
-
throw error
|
|
979
|
+
await this.rollbackUnpublished(activation, error).catch(() => undefined)
|
|
980
|
+
throw error
|
|
1079
981
|
}
|
|
1080
|
-
return activation
|
|
982
|
+
return activation
|
|
1081
983
|
}
|
|
1082
984
|
|
|
1083
985
|
/**
|
|
@@ -1085,16 +987,13 @@ export class SubagentManager {
|
|
|
1085
987
|
* @param activation - accepted activation to observe.
|
|
1086
988
|
*/
|
|
1087
989
|
private announce(activation: Activation): void {
|
|
1088
|
-
if (activation.announced) return
|
|
1089
|
-
activation.announced = true
|
|
1090
|
-
if (activation.kind ===
|
|
1091
|
-
this.watchLocalSettlement(activation)
|
|
990
|
+
if (activation.announced) return
|
|
991
|
+
activation.announced = true
|
|
992
|
+
if (activation.kind === 'local') {
|
|
993
|
+
this.watchLocalSettlement(activation)
|
|
1092
994
|
} else {
|
|
1093
|
-
const finish = (): Promise<void> =>
|
|
1094
|
-
|
|
1095
|
-
void activation.run.result.then(finish, finish).catch((error: unknown) => {
|
|
1096
|
-
this.reportTeardownFailure(activation, error);
|
|
1097
|
-
});
|
|
995
|
+
const finish = (): Promise<void> => this.close(activation, () => this.finishDisposal(activation, false))
|
|
996
|
+
void activation.run.result.then(finish, finish).catch((error: unknown) => { this.reportTeardownFailure(activation, error) })
|
|
1098
997
|
}
|
|
1099
998
|
}
|
|
1100
999
|
|
|
@@ -1102,103 +1001,99 @@ export class SubagentManager {
|
|
|
1102
1001
|
private rollbackUnpublished(activation: Activation, error: unknown): Promise<void> {
|
|
1103
1002
|
return this.close(activation, async () => {
|
|
1104
1003
|
try {
|
|
1105
|
-
await (activation.kind ===
|
|
1106
|
-
? activation.handle.dispose()
|
|
1107
|
-
: activation.run.dispose());
|
|
1004
|
+
await (activation.kind === 'local' ? activation.handle.dispose() : activation.run.dispose())
|
|
1108
1005
|
} finally {
|
|
1109
|
-
this.resident.delete(activation.childId)
|
|
1110
|
-
activation.releaseSlot()
|
|
1111
|
-
this.releaseOwnership(activation.childId)
|
|
1112
|
-
activation.result.reject(error)
|
|
1006
|
+
this.resident.delete(activation.childId)
|
|
1007
|
+
activation.releaseSlot()
|
|
1008
|
+
this.releaseOwnership(activation.childId)
|
|
1009
|
+
activation.result.reject(error)
|
|
1113
1010
|
}
|
|
1114
|
-
})
|
|
1011
|
+
})
|
|
1115
1012
|
}
|
|
1116
1013
|
|
|
1117
1014
|
/** Register the child in a continuation-managed parent's owned set. */
|
|
1118
1015
|
private acquireOwnership(parent: Agent, childId: SessionId): void {
|
|
1119
|
-
const parentActivation = this.resident.get(parent.id)
|
|
1120
|
-
if (parentActivation === undefined) return
|
|
1016
|
+
const parentActivation = this.resident.get(parent.id)
|
|
1017
|
+
if (parentActivation === undefined) return
|
|
1121
1018
|
if (parentActivation.closing !== undefined) {
|
|
1122
1019
|
throw new SubagentError(
|
|
1123
1020
|
`subagent parent "${parent.id}" is being disposed; the child was not established`,
|
|
1124
|
-
|
|
1125
|
-
)
|
|
1021
|
+
'ACTIVATION_CLOSING',
|
|
1022
|
+
)
|
|
1126
1023
|
}
|
|
1127
|
-
parentActivation.ownedChildren.add(childId)
|
|
1128
|
-
this.wake(parentActivation)
|
|
1024
|
+
parentActivation.ownedChildren.add(childId)
|
|
1025
|
+
this.wake(parentActivation)
|
|
1129
1026
|
}
|
|
1130
1027
|
|
|
1131
1028
|
/** Remove one child from its live owner's set and let that owner re-check settlement. */
|
|
1132
1029
|
private releaseOwnership(childId: SessionId): void {
|
|
1133
1030
|
for (const candidate of this.resident.values()) {
|
|
1134
|
-
if (candidate.ownedChildren.delete(childId)) this.wake(candidate)
|
|
1031
|
+
if (candidate.ownedChildren.delete(childId)) this.wake(candidate)
|
|
1135
1032
|
}
|
|
1136
1033
|
}
|
|
1137
1034
|
|
|
1138
1035
|
/** Let a settlement watcher re-check residency after relevant state changes. */
|
|
1139
1036
|
private wake(activation: Activation): void {
|
|
1140
|
-
if (activation.kind ===
|
|
1141
|
-
activation.poke.resolve()
|
|
1142
|
-
activation.poke = Promise.withResolvers<void>()
|
|
1037
|
+
if (activation.kind === 'external') return
|
|
1038
|
+
activation.poke.resolve()
|
|
1039
|
+
activation.poke = Promise.withResolvers<void>()
|
|
1143
1040
|
}
|
|
1144
1041
|
|
|
1145
1042
|
/** Follow one Activation to natural settlement. */
|
|
1146
1043
|
private watchLocalSettlement(activation: LocalActivation): void {
|
|
1147
1044
|
void (async () => {
|
|
1148
1045
|
while (true) {
|
|
1149
|
-
const idleObservation = activation.poke
|
|
1150
|
-
await activation.handle.agent.whenIdle()
|
|
1151
|
-
if (activation.closing !== undefined) return
|
|
1152
|
-
const readiness = await this.locks.run(activation.childId, () =>
|
|
1153
|
-
|
|
1154
|
-
)
|
|
1155
|
-
if (readiness ===
|
|
1156
|
-
if (readiness ===
|
|
1157
|
-
if (readiness ===
|
|
1158
|
-
await idleObservation.promise
|
|
1159
|
-
continue
|
|
1046
|
+
const idleObservation = activation.poke
|
|
1047
|
+
await activation.handle.agent.whenIdle()
|
|
1048
|
+
if (activation.closing !== undefined) return
|
|
1049
|
+
const readiness = await this.locks.run(activation.childId, () => Promise.resolve(
|
|
1050
|
+
this.settlementState(activation, idleObservation),
|
|
1051
|
+
))
|
|
1052
|
+
if (readiness === 'closed') return
|
|
1053
|
+
if (readiness === 'retry') continue
|
|
1054
|
+
if (readiness === 'wait') {
|
|
1055
|
+
await idleObservation.promise
|
|
1056
|
+
continue
|
|
1160
1057
|
}
|
|
1161
1058
|
|
|
1162
|
-
const finalSeq = activation.handle.agent.session.seq
|
|
1163
|
-
await this.flushFinalState(activation)
|
|
1059
|
+
const finalSeq = activation.handle.agent.session.seq
|
|
1060
|
+
await this.flushFinalState(activation)
|
|
1164
1061
|
const attempt = await this.locks.run<SettlementAttempt>(activation.childId, () => {
|
|
1165
|
-
const state = this.settlementState(activation, idleObservation)
|
|
1166
|
-
if (state !==
|
|
1062
|
+
const state = this.settlementState(activation, idleObservation)
|
|
1063
|
+
if (state !== 'ready') return Promise.resolve(state)
|
|
1167
1064
|
if (activation.handle.agent.session.seq !== finalSeq) {
|
|
1168
|
-
return Promise.resolve(
|
|
1065
|
+
return Promise.resolve('retry')
|
|
1169
1066
|
}
|
|
1170
|
-
let done!: Promise<void
|
|
1067
|
+
let done!: Promise<void>
|
|
1171
1068
|
try {
|
|
1172
1069
|
void activation.handle.agent.runMaintenance(() => {
|
|
1173
|
-
done = this.close(activation, () => this.finishDisposal(activation, false))
|
|
1174
|
-
return Promise.resolve()
|
|
1175
|
-
})
|
|
1070
|
+
done = this.close(activation, () => this.finishDisposal(activation, false))
|
|
1071
|
+
return Promise.resolve()
|
|
1072
|
+
})
|
|
1176
1073
|
} catch (_error: unknown) {
|
|
1177
1074
|
// Another activity already owns the Agent's idle phase.
|
|
1178
|
-
return Promise.resolve(
|
|
1075
|
+
return Promise.resolve('retry')
|
|
1179
1076
|
}
|
|
1180
|
-
return Promise.resolve({ done })
|
|
1181
|
-
})
|
|
1077
|
+
return Promise.resolve({ done })
|
|
1078
|
+
})
|
|
1182
1079
|
|
|
1183
|
-
if (typeof attempt ===
|
|
1184
|
-
if (attempt ===
|
|
1185
|
-
continue
|
|
1080
|
+
if (typeof attempt === 'string') {
|
|
1081
|
+
if (attempt === 'closed') return
|
|
1082
|
+
continue
|
|
1186
1083
|
}
|
|
1187
1084
|
try {
|
|
1188
|
-
await attempt.done
|
|
1085
|
+
await attempt.done
|
|
1189
1086
|
} catch (error: unknown) {
|
|
1190
|
-
this.reportTeardownFailure(activation, error)
|
|
1087
|
+
this.reportTeardownFailure(activation, error)
|
|
1191
1088
|
}
|
|
1192
|
-
return
|
|
1089
|
+
return
|
|
1193
1090
|
}
|
|
1194
|
-
})()
|
|
1091
|
+
})()
|
|
1195
1092
|
}
|
|
1196
1093
|
|
|
1197
1094
|
/** Report disposal failure once after the natural settlement observer finishes. */
|
|
1198
1095
|
private reportTeardownFailure(activation: Activation, error: unknown): void {
|
|
1199
|
-
this.ctx.logger.warn(
|
|
1200
|
-
`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`,
|
|
1201
|
-
);
|
|
1096
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`)
|
|
1202
1097
|
}
|
|
1203
1098
|
|
|
1204
1099
|
/** Check pending input and owned children before closing admission. */
|
|
@@ -1206,154 +1101,138 @@ export class SubagentManager {
|
|
|
1206
1101
|
activation: LocalActivation,
|
|
1207
1102
|
observation: PromiseWithResolvers<void>,
|
|
1208
1103
|
): SettlementState {
|
|
1209
|
-
if (activation.closing !== undefined) return
|
|
1210
|
-
if (activation.poke !== observation) return
|
|
1211
|
-
const inbox = activation.handle.agent.inbox
|
|
1212
|
-
if (activation.ownedChildren.size > 0) return
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
(inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)
|
|
1216
|
-
)
|
|
1217
|
-
return "wait";
|
|
1218
|
-
return "ready";
|
|
1104
|
+
if (activation.closing !== undefined) return 'closed'
|
|
1105
|
+
if (activation.poke !== observation) return 'retry'
|
|
1106
|
+
const inbox = activation.handle.agent.inbox
|
|
1107
|
+
if (activation.ownedChildren.size > 0) return 'wait'
|
|
1108
|
+
if (activation.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)) return 'wait'
|
|
1109
|
+
return 'ready'
|
|
1219
1110
|
}
|
|
1220
1111
|
|
|
1221
1112
|
/** Propagate stop synchronously and await descendant startup rollback before release. */
|
|
1222
1113
|
private async finishDisposal(activation: Activation, stop: boolean): Promise<void> {
|
|
1223
|
-
this.wake(activation)
|
|
1224
|
-
const { childId } = activation
|
|
1225
|
-
const failures: SubagentError[] = []
|
|
1226
|
-
let result: SubagentResult = { output: [], stopReason:
|
|
1227
|
-
let resultFailure: { error: unknown } | undefined
|
|
1228
|
-
let externalDisposal: Promise<void> | undefined
|
|
1229
|
-
const child = activationAgent(activation)
|
|
1230
|
-
const detail = (error: unknown): string =>
|
|
1231
|
-
|
|
1114
|
+
this.wake(activation)
|
|
1115
|
+
const { childId } = activation
|
|
1116
|
+
const failures: SubagentError[] = []
|
|
1117
|
+
let result: SubagentResult = { output: [], stopReason: 'error' }
|
|
1118
|
+
let resultFailure: { error: unknown } | undefined
|
|
1119
|
+
let externalDisposal: Promise<void> | undefined
|
|
1120
|
+
const child = activationAgent(activation)
|
|
1121
|
+
const detail = (error: unknown): string => child === undefined
|
|
1122
|
+
? failureMessage(error)
|
|
1123
|
+
: errorChain(error)
|
|
1232
1124
|
try {
|
|
1233
|
-
if (activation.kind ===
|
|
1125
|
+
if (activation.kind === 'external') {
|
|
1234
1126
|
if (stop) {
|
|
1235
|
-
activation.controller.abort({ kind:
|
|
1127
|
+
activation.controller.abort({ kind: 'parent' })
|
|
1236
1128
|
// Some backends settle their result only when disposal begins.
|
|
1237
|
-
externalDisposal = Promise.resolve().then(() => activation.run.dispose())
|
|
1238
|
-
void externalDisposal.catch(() => undefined)
|
|
1129
|
+
externalDisposal = Promise.resolve().then(() => activation.run.dispose())
|
|
1130
|
+
void externalDisposal.catch(() => undefined)
|
|
1239
1131
|
}
|
|
1240
|
-
result = await activation.run.result
|
|
1132
|
+
result = await activation.run.result
|
|
1241
1133
|
} else {
|
|
1242
1134
|
if (stop) {
|
|
1243
|
-
activation.handle.agent.cancel({ kind:
|
|
1244
|
-
const idle = activation.handle.agent.whenIdle()
|
|
1135
|
+
activation.handle.agent.cancel({ kind: 'parent' })
|
|
1136
|
+
const idle = activation.handle.agent.whenIdle()
|
|
1245
1137
|
const children = [...activation.ownedChildren]
|
|
1246
|
-
.map(
|
|
1247
|
-
.filter((child): child is Activation => child !== undefined)
|
|
1248
|
-
const pending = [...this.materializations].filter(
|
|
1249
|
-
|
|
1250
|
-
)
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
}
|
|
1262
|
-
}),
|
|
1263
|
-
);
|
|
1264
|
-
const reasons = childFailures.filter((reason) => reason !== undefined);
|
|
1138
|
+
.map(child => this.resident.get(child))
|
|
1139
|
+
.filter((child): child is Activation => child !== undefined)
|
|
1140
|
+
const pending = [...this.materializations].filter(item => item.lineage.includes(activation.handle.agent))
|
|
1141
|
+
const childDisposals = children.map(child => this.dispose(child))
|
|
1142
|
+
for (const item of pending) item.controller.abort()
|
|
1143
|
+
childDisposals.push(...pending.map(item => item.settled))
|
|
1144
|
+
const childFailures = await Promise.all(childDisposals.map(async (disposal) => {
|
|
1145
|
+
try {
|
|
1146
|
+
await disposal
|
|
1147
|
+
return undefined
|
|
1148
|
+
} catch (error: unknown) {
|
|
1149
|
+
return error
|
|
1150
|
+
}
|
|
1151
|
+
}))
|
|
1152
|
+
const reasons = childFailures.filter(reason => reason !== undefined)
|
|
1265
1153
|
if (reasons.length > 0) {
|
|
1266
|
-
failures.push(
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
),
|
|
1271
|
-
);
|
|
1154
|
+
failures.push(new SubagentError(
|
|
1155
|
+
`subagent "${childId}" child teardown failed: ${reasons.map(reason => failureMessage(reason)).join('; ')}`,
|
|
1156
|
+
'ACTIVATION_TEARDOWN_FAILED',
|
|
1157
|
+
))
|
|
1272
1158
|
}
|
|
1273
|
-
await idle
|
|
1274
|
-
await this.flushFinalState(activation)
|
|
1159
|
+
await idle
|
|
1160
|
+
await this.flushFinalState(activation)
|
|
1275
1161
|
}
|
|
1276
|
-
result = captureLocalResult(activation)
|
|
1162
|
+
result = captureLocalResult(activation)
|
|
1277
1163
|
}
|
|
1278
1164
|
} catch (error: unknown) {
|
|
1279
|
-
resultFailure = { error }
|
|
1280
|
-
failures.push(
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
),
|
|
1286
|
-
);
|
|
1165
|
+
resultFailure = { error }
|
|
1166
|
+
failures.push(new SubagentError(
|
|
1167
|
+
`subagent "${childId}" activation teardown failed: ${detail(error)}`,
|
|
1168
|
+
'ACTIVATION_TEARDOWN_FAILED',
|
|
1169
|
+
{ cause: error },
|
|
1170
|
+
))
|
|
1287
1171
|
}
|
|
1288
1172
|
try {
|
|
1289
|
-
if (activation.kind ===
|
|
1290
|
-
else await (externalDisposal ?? activation.run.dispose())
|
|
1173
|
+
if (activation.kind === 'local') await activation.handle.dispose()
|
|
1174
|
+
else await (externalDisposal ?? activation.run.dispose())
|
|
1291
1175
|
} catch (error: unknown) {
|
|
1292
|
-
failures.push(
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
),
|
|
1298
|
-
);
|
|
1176
|
+
failures.push(new SubagentError(
|
|
1177
|
+
`subagent "${childId}" activation handle disposal failed: ${detail(error)}`,
|
|
1178
|
+
'ACTIVATION_TEARDOWN_FAILED',
|
|
1179
|
+
{ cause: error },
|
|
1180
|
+
))
|
|
1299
1181
|
}
|
|
1300
1182
|
|
|
1301
|
-
let failure: SubagentError | undefined
|
|
1183
|
+
let failure: SubagentError | undefined
|
|
1302
1184
|
if (failures.length === 1) {
|
|
1303
|
-
failure = failures[0]
|
|
1185
|
+
failure = failures[0]
|
|
1304
1186
|
} else if (failures.length > 1) {
|
|
1305
1187
|
failure = new SubagentError(
|
|
1306
|
-
`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
|
|
1307
|
-
|
|
1308
|
-
|
|
1188
|
+
`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
|
|
1189
|
+
+ failures.map(item => detail(item)).join('; '),
|
|
1190
|
+
'ACTIVATION_TEARDOWN_FAILED',
|
|
1309
1191
|
{ cause: new AggregateError(failures) },
|
|
1310
|
-
)
|
|
1192
|
+
)
|
|
1311
1193
|
}
|
|
1312
|
-
this.resident.delete(childId)
|
|
1313
|
-
activation.releaseSlot()
|
|
1314
|
-
const terminal: SubagentResult =
|
|
1315
|
-
|
|
1316
|
-
this.
|
|
1317
|
-
|
|
1318
|
-
activation.
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (failure !== undefined) throw failure;
|
|
1194
|
+
this.resident.delete(childId)
|
|
1195
|
+
activation.releaseSlot()
|
|
1196
|
+
const terminal: SubagentResult = failure === undefined ? result : { output: [], stopReason: 'error' }
|
|
1197
|
+
this.notifySettlement(activation, result)
|
|
1198
|
+
this.releaseOwnership(childId)
|
|
1199
|
+
activation.observer.settle(terminal)
|
|
1200
|
+
if (resultFailure === undefined) activation.result.resolve(result)
|
|
1201
|
+
else activation.result.reject(resultFailure.error)
|
|
1202
|
+
if (failure !== undefined) throw failure
|
|
1322
1203
|
}
|
|
1323
1204
|
|
|
1324
1205
|
/** Deliver the captured execution result to the durable direct parent after cleanup. */
|
|
1325
1206
|
private notifySettlement(activation: Activation, terminal: SubagentResult): void {
|
|
1326
|
-
if (!activation.announced || activation.delivery ===
|
|
1207
|
+
if (!activation.announced || activation.delivery === 'caller') return
|
|
1327
1208
|
try {
|
|
1328
|
-
const parent = this.ctx.agents.get(activation.parent.id)
|
|
1329
|
-
if (parent !== activation.parent) return
|
|
1209
|
+
const parent = this.ctx.agents.get(activation.parent.id)
|
|
1210
|
+
if (parent !== activation.parent) return
|
|
1330
1211
|
const message = createSettlementMessage(
|
|
1331
|
-
activation.childId,
|
|
1332
|
-
|
|
1333
|
-
activationAgent(activation) !== undefined,
|
|
1334
|
-
);
|
|
1212
|
+
activation.childId, terminal, activation.kind === 'local', activation.kind === 'local' && activation.omitClosingText,
|
|
1213
|
+
)
|
|
1335
1214
|
if (this.closingTeardownFor(parent) !== undefined) {
|
|
1336
|
-
parent.inject(message)
|
|
1337
|
-
return
|
|
1215
|
+
parent.inject(message)
|
|
1216
|
+
return
|
|
1338
1217
|
}
|
|
1339
|
-
this.sendWaking(parent, message, parent.status ===
|
|
1218
|
+
this.sendWaking(parent, message, parent.status === 'idle' ? 'queue' : 'steer')
|
|
1340
1219
|
} catch (error: unknown) {
|
|
1341
1220
|
this.ctx.logger.warn(
|
|
1342
|
-
`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
|
|
1343
|
-
|
|
1344
|
-
)
|
|
1221
|
+
`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
|
|
1222
|
+
+ errorChain(error),
|
|
1223
|
+
)
|
|
1345
1224
|
}
|
|
1346
1225
|
}
|
|
1347
1226
|
|
|
1348
1227
|
/** Request a best-effort final session flush before closing natural-settlement admission. */
|
|
1349
1228
|
private async flushFinalState(activation: LocalActivation): Promise<void> {
|
|
1350
1229
|
try {
|
|
1351
|
-
await activation.handle.agent.ctx.sessions.flush(activation.handle.agent.session)
|
|
1230
|
+
await activation.handle.agent.ctx.sessions.flush(activation.handle.agent.session)
|
|
1352
1231
|
} catch (error: unknown) {
|
|
1353
1232
|
this.ctx.logger.warn(
|
|
1354
|
-
`subagent "${activation.childId}" best-effort final session flush failed; `
|
|
1355
|
-
|
|
1356
|
-
)
|
|
1233
|
+
`subagent "${activation.childId}" best-effort final session flush failed; `
|
|
1234
|
+
+ `the persisted state may be unavailable or stale on resume: ${errorChain(error)}`,
|
|
1235
|
+
)
|
|
1357
1236
|
}
|
|
1358
1237
|
}
|
|
1359
1238
|
|
|
@@ -1363,7 +1242,7 @@ export class SubagentManager {
|
|
|
1363
1242
|
childId: activation.childId,
|
|
1364
1243
|
result: activation.result.promise,
|
|
1365
1244
|
dispose: () => this.dispose(activation),
|
|
1366
|
-
}
|
|
1245
|
+
}
|
|
1367
1246
|
}
|
|
1368
1247
|
|
|
1369
1248
|
/** Route one parent-originated delivery through residency and cold resume. */
|
|
@@ -1373,13 +1252,13 @@ export class SubagentManager {
|
|
|
1373
1252
|
content: ContentBlock[],
|
|
1374
1253
|
options: ChildDeliveryOptions,
|
|
1375
1254
|
): Promise<MessageId> {
|
|
1376
|
-
this.assertAdmitting(parent)
|
|
1377
|
-
const releaseHold = this.holdOwnership(parent, childId)
|
|
1255
|
+
this.assertAdmitting(parent)
|
|
1256
|
+
const releaseHold = this.holdOwnership(parent, childId)
|
|
1378
1257
|
try {
|
|
1379
|
-
return await this.deliverFollowup(parent, childId, content, options)
|
|
1258
|
+
return await this.deliverFollowup(parent, childId, content, options)
|
|
1380
1259
|
} catch (error: unknown) {
|
|
1381
|
-
releaseHold()
|
|
1382
|
-
throw error
|
|
1260
|
+
releaseHold()
|
|
1261
|
+
throw error
|
|
1383
1262
|
}
|
|
1384
1263
|
}
|
|
1385
1264
|
|
|
@@ -1392,72 +1271,74 @@ export class SubagentManager {
|
|
|
1392
1271
|
): Promise<MessageId> {
|
|
1393
1272
|
while (true) {
|
|
1394
1273
|
const live = await this.locks.run(childId, async () => {
|
|
1395
|
-
const activation = this.resident.get(childId)
|
|
1396
|
-
if (activation === undefined) return this.coldResume(parent, childId, content, options)
|
|
1397
|
-
requireLocalActivation(activation)
|
|
1398
|
-
const disposal = activation.closing
|
|
1274
|
+
const activation = this.resident.get(childId)
|
|
1275
|
+
if (activation === undefined) return this.coldResume(parent, childId, content, options)
|
|
1276
|
+
requireLocalActivation(activation)
|
|
1277
|
+
const disposal = activation.closing
|
|
1399
1278
|
/* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
|
|
1400
1279
|
* observe the transaction inside the same critical section that opened it. */
|
|
1401
1280
|
if (disposal !== undefined) {
|
|
1402
|
-
return disposal.then(
|
|
1403
|
-
() => undefined,
|
|
1404
|
-
() => undefined,
|
|
1405
|
-
);
|
|
1281
|
+
return disposal.then(() => undefined, () => undefined)
|
|
1406
1282
|
}
|
|
1407
1283
|
if (contentHasImage(content)) {
|
|
1408
|
-
await this.assertImageCapable(
|
|
1409
|
-
requireLocalActivation(activation).handle.agent,
|
|
1410
|
-
options.signal,
|
|
1411
|
-
);
|
|
1284
|
+
await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal)
|
|
1412
1285
|
if (activation.closing !== undefined) {
|
|
1413
|
-
await Promise.allSettled([activation.closing])
|
|
1414
|
-
return undefined
|
|
1286
|
+
await Promise.allSettled([activation.closing])
|
|
1287
|
+
return undefined
|
|
1415
1288
|
}
|
|
1416
1289
|
}
|
|
1417
|
-
const messageId = this.submitAdmitted(activation, content, options, parent)
|
|
1418
|
-
this.announce(activation)
|
|
1419
|
-
return messageId
|
|
1420
|
-
})
|
|
1290
|
+
const messageId = this.submitAdmitted(activation, content, options, parent)
|
|
1291
|
+
this.announce(activation)
|
|
1292
|
+
return messageId
|
|
1293
|
+
})
|
|
1421
1294
|
/* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
|
|
1422
|
-
if (live !== undefined) return live
|
|
1423
|
-
this.assertAdmitting(parent)
|
|
1424
|
-
options.signal.throwIfAborted()
|
|
1295
|
+
if (live !== undefined) return live
|
|
1296
|
+
this.assertAdmitting(parent)
|
|
1297
|
+
options.signal.throwIfAborted()
|
|
1425
1298
|
/* v8 ignore stop */
|
|
1426
1299
|
}
|
|
1427
1300
|
}
|
|
1428
1301
|
|
|
1429
1302
|
/** Deliver one resident continuable child's message to its live direct parent. */
|
|
1430
|
-
private sendToParent(
|
|
1303
|
+
private sendToParent(
|
|
1304
|
+
activation: LocalActivation,
|
|
1305
|
+
sender: Agent,
|
|
1306
|
+
content: ContentBlock[],
|
|
1307
|
+
): MessageId {
|
|
1431
1308
|
/* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
|
|
1432
1309
|
* transaction between exact-agent authorization and this no-await span. */
|
|
1433
1310
|
if (activation.closing !== undefined) {
|
|
1434
1311
|
throw new SubagentError(
|
|
1435
1312
|
`subagent "${sender.id}" activation is being disposed; the message was not delivered`,
|
|
1436
|
-
|
|
1437
|
-
)
|
|
1313
|
+
'ACTIVATION_CLOSING',
|
|
1314
|
+
)
|
|
1438
1315
|
}
|
|
1439
|
-
const parent = this.ctx.agents.get(activation.parent.id)
|
|
1316
|
+
const parent = this.ctx.agents.get(activation.parent.id)
|
|
1440
1317
|
if (parent !== activation.parent) {
|
|
1441
1318
|
throw new SubagentError(
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
)
|
|
1319
|
+
'direct parent is not live; the message was not delivered',
|
|
1320
|
+
'PARENT_UNAVAILABLE',
|
|
1321
|
+
)
|
|
1445
1322
|
}
|
|
1446
|
-
const message = createAgentMessage(sender, content)
|
|
1447
|
-
this.sendAgentMessage(parent, message)
|
|
1448
|
-
|
|
1323
|
+
const message = createAgentMessage(sender, content)
|
|
1324
|
+
this.sendAgentMessage(parent, message)
|
|
1325
|
+
activation.omitClosingText = true
|
|
1326
|
+
return message.id
|
|
1449
1327
|
}
|
|
1450
1328
|
|
|
1451
1329
|
/** Send one Agent message while translating only the target's own rejection. */
|
|
1452
|
-
private sendAgentMessage(
|
|
1330
|
+
private sendAgentMessage(
|
|
1331
|
+
parent: Agent,
|
|
1332
|
+
message: ReturnType<typeof createUserMessage>,
|
|
1333
|
+
): void {
|
|
1453
1334
|
try {
|
|
1454
|
-
this.sendWaking(parent, message,
|
|
1335
|
+
this.sendWaking(parent, message, 'steer')
|
|
1455
1336
|
} catch (error: unknown) {
|
|
1456
1337
|
throw new SubagentError(
|
|
1457
|
-
|
|
1458
|
-
|
|
1338
|
+
'direct parent is not live; the message was not delivered',
|
|
1339
|
+
'PARENT_UNAVAILABLE',
|
|
1459
1340
|
{ cause: error },
|
|
1460
|
-
)
|
|
1341
|
+
)
|
|
1461
1342
|
}
|
|
1462
1343
|
}
|
|
1463
1344
|
|
|
@@ -1471,68 +1352,55 @@ export class SubagentManager {
|
|
|
1471
1352
|
content: ContentBlock[],
|
|
1472
1353
|
options: ChildDeliveryOptions,
|
|
1473
1354
|
): Promise<MessageId> {
|
|
1474
|
-
const query = this.requireSessionQuery()
|
|
1475
|
-
let observation: SessionObservation
|
|
1355
|
+
const query = this.requireSessionQuery()
|
|
1356
|
+
let observation: SessionObservation
|
|
1476
1357
|
try {
|
|
1477
1358
|
observation = await query.observeSession(childId, {
|
|
1478
1359
|
signal: options.signal,
|
|
1479
|
-
})
|
|
1360
|
+
})
|
|
1480
1361
|
} catch (error: unknown) {
|
|
1481
|
-
options.signal.throwIfAborted()
|
|
1482
|
-
throw new SubagentError(`subagent "${childId}" is unavailable`,
|
|
1483
|
-
cause: error,
|
|
1484
|
-
});
|
|
1362
|
+
options.signal.throwIfAborted()
|
|
1363
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error })
|
|
1485
1364
|
}
|
|
1486
|
-
using source = observation
|
|
1487
|
-
this.assertAdmitting(parent)
|
|
1488
|
-
this.authorizeLineage(parent, childId, source.header.parentSession)
|
|
1489
|
-
const ownEvents = source.events.slice(source.inheritedEventCount)
|
|
1490
|
-
const descriptor = foldSubagentDescriptor(ownEvents)
|
|
1491
|
-
if (descriptor === undefined || descriptor.mode !==
|
|
1365
|
+
using source = observation
|
|
1366
|
+
this.assertAdmitting(parent)
|
|
1367
|
+
this.authorizeLineage(parent, childId, source.header.parentSession)
|
|
1368
|
+
const ownEvents = source.events.slice(source.inheritedEventCount)
|
|
1369
|
+
const descriptor = foldSubagentDescriptor(ownEvents)
|
|
1370
|
+
if (descriptor === undefined || descriptor.mode !== 'continuable') {
|
|
1492
1371
|
throw new SubagentError(
|
|
1493
1372
|
`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`,
|
|
1494
|
-
|
|
1495
|
-
)
|
|
1373
|
+
'NOT_RESUMABLE',
|
|
1374
|
+
)
|
|
1496
1375
|
}
|
|
1497
|
-
let activation: Activation
|
|
1376
|
+
let activation: Activation
|
|
1498
1377
|
try {
|
|
1499
|
-
const independentlySelected = hasIndependentPresetSelection(ownEvents)
|
|
1500
|
-
const agentPreset = independentlySelected
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (independentlySelected && typeof agentPreset !== "string") {
|
|
1504
|
-
throw new SubagentError(
|
|
1505
|
-
`subagent "${childId}" has no agent preset projection for restoration`,
|
|
1506
|
-
"NOT_RESUMABLE",
|
|
1507
|
-
);
|
|
1378
|
+
const independentlySelected = hasIndependentPresetSelection(ownEvents)
|
|
1379
|
+
const agentPreset = independentlySelected ? source.projections?.values.agentPreset : undefined
|
|
1380
|
+
if (independentlySelected && typeof agentPreset !== 'string') {
|
|
1381
|
+
throw new SubagentError(`subagent "${childId}" has no agent preset projection for restoration`, 'NOT_RESUMABLE')
|
|
1508
1382
|
}
|
|
1509
1383
|
activation = await this.materialize({
|
|
1510
|
-
kind:
|
|
1384
|
+
kind: 'local',
|
|
1511
1385
|
childId,
|
|
1512
1386
|
provider: descriptor.provider,
|
|
1513
1387
|
parent,
|
|
1514
1388
|
agentOptions: {
|
|
1515
|
-
...
|
|
1516
|
-
...
|
|
1517
|
-
...
|
|
1389
|
+
...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
|
|
1390
|
+
...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
|
|
1391
|
+
...descriptor.agentReasoningEffort !== undefined
|
|
1518
1392
|
? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
|
|
1519
|
-
: {}
|
|
1520
|
-
},
|
|
1521
|
-
composition: {
|
|
1522
|
-
agentPreset: agentPreset ?? undefined,
|
|
1523
|
-
persona: descriptor.persona,
|
|
1524
|
-
toolFilter: descriptor.toolFilter,
|
|
1393
|
+
: {},
|
|
1525
1394
|
},
|
|
1395
|
+
composition: { agentPreset: agentPreset ?? undefined, persona: descriptor.persona, toolFilter: descriptor.toolFilter },
|
|
1526
1396
|
signal: options.signal,
|
|
1527
|
-
})
|
|
1397
|
+
})
|
|
1528
1398
|
} catch (error: unknown) {
|
|
1529
|
-
options.signal.throwIfAborted()
|
|
1530
|
-
if (error instanceof SubagentError) throw error
|
|
1531
|
-
throw new SubagentError(`subagent "${childId}" is unavailable`,
|
|
1532
|
-
cause: error,
|
|
1533
|
-
});
|
|
1399
|
+
options.signal.throwIfAborted()
|
|
1400
|
+
if (error instanceof SubagentError) throw error
|
|
1401
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error })
|
|
1534
1402
|
}
|
|
1535
|
-
return await this.submitMaterialized(activation, content, options, parent)
|
|
1403
|
+
return await this.submitMaterialized(activation, content, options, parent)
|
|
1536
1404
|
}
|
|
1537
1405
|
|
|
1538
1406
|
/** Admit a materialized child, commit its creation fact, and release it on failure. */
|
|
@@ -1545,30 +1413,24 @@ export class SubagentManager {
|
|
|
1545
1413
|
): Promise<MessageId> {
|
|
1546
1414
|
try {
|
|
1547
1415
|
if (contentHasImage(content)) {
|
|
1548
|
-
await this.assertImageCapable(
|
|
1549
|
-
requireLocalActivation(activation).handle.agent,
|
|
1550
|
-
options.signal,
|
|
1551
|
-
);
|
|
1416
|
+
await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal)
|
|
1552
1417
|
if (activation.closing !== undefined) {
|
|
1553
|
-
throw new SubagentError(
|
|
1554
|
-
`subagent "${activation.childId}" is closing`,
|
|
1555
|
-
"ACTIVATION_CLOSING",
|
|
1556
|
-
);
|
|
1418
|
+
throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING')
|
|
1557
1419
|
}
|
|
1558
1420
|
}
|
|
1559
|
-
const messageId = this.submitAdmitted(activation, content, options, parent)
|
|
1560
|
-
commit?.()
|
|
1561
|
-
this.announce(activation)
|
|
1562
|
-
return messageId
|
|
1421
|
+
const messageId = this.submitAdmitted(activation, content, options, parent)
|
|
1422
|
+
commit?.()
|
|
1423
|
+
this.announce(activation)
|
|
1424
|
+
return messageId
|
|
1563
1425
|
} catch (error: unknown) {
|
|
1564
1426
|
try {
|
|
1565
|
-
await this.dispose(activation)
|
|
1427
|
+
await this.dispose(activation)
|
|
1566
1428
|
} catch (cleanupError: unknown) {
|
|
1567
1429
|
this.ctx.logger.warn(
|
|
1568
1430
|
`subagent continuation: disposal after admission or catalog append failure also failed: ${String(cleanupError)}`,
|
|
1569
|
-
)
|
|
1431
|
+
)
|
|
1570
1432
|
}
|
|
1571
|
-
throw error
|
|
1433
|
+
throw error
|
|
1572
1434
|
}
|
|
1573
1435
|
}
|
|
1574
1436
|
|
|
@@ -1579,71 +1441,73 @@ export class SubagentManager {
|
|
|
1579
1441
|
options: ChildDeliveryOptions,
|
|
1580
1442
|
parent: Agent,
|
|
1581
1443
|
): MessageId {
|
|
1582
|
-
const message =
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
this.
|
|
1588
|
-
this.
|
|
1589
|
-
this.acquireOwnership(parent, activation.childId);
|
|
1444
|
+
const message = options.source === undefined
|
|
1445
|
+
? createAgentMessage(parent, content)
|
|
1446
|
+
: createUserMessage({ content, source: options.source })
|
|
1447
|
+
options.signal.throwIfAborted()
|
|
1448
|
+
this.assertAdmitting(parent)
|
|
1449
|
+
this.authorizeLineage(parent, activation.childId, activation.parent.id)
|
|
1450
|
+
this.acquireOwnership(parent, activation.childId)
|
|
1590
1451
|
try {
|
|
1591
|
-
this.deliver(activation, message, options.delivery)
|
|
1452
|
+
this.deliver(activation, message, options.delivery)
|
|
1592
1453
|
} finally {
|
|
1593
|
-
this.wake(activation)
|
|
1454
|
+
this.wake(activation)
|
|
1594
1455
|
}
|
|
1595
|
-
return message.id
|
|
1456
|
+
return message.id
|
|
1596
1457
|
}
|
|
1597
1458
|
|
|
1598
1459
|
/** Refuse image content for a child whose fixed model accepts text only. */
|
|
1599
|
-
private async assertImageCapable(
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1460
|
+
private async assertImageCapable(
|
|
1461
|
+
agent: Agent,
|
|
1462
|
+
signal: AbortSignal,
|
|
1463
|
+
): Promise<void> {
|
|
1464
|
+
const { provider, model } = agent.options
|
|
1465
|
+
if (provider === undefined || model === undefined) return
|
|
1466
|
+
const llm = this.ctx.get('llm')
|
|
1603
1467
|
/* v8 ignore next -- without an LLM registry, delivery defers to projection. */
|
|
1604
|
-
if (llm === undefined) return
|
|
1605
|
-
const info = await llm.resolveModelInfo(provider, model, signal)
|
|
1606
|
-
if (info.inputModalities !== undefined && !info.inputModalities.includes(
|
|
1468
|
+
if (llm === undefined) return
|
|
1469
|
+
const info = await llm.resolveModelInfo(provider, model, signal)
|
|
1470
|
+
if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
|
|
1607
1471
|
throw new SubagentError(
|
|
1608
1472
|
`Model "${model}" does not support image input.`,
|
|
1609
|
-
|
|
1610
|
-
)
|
|
1473
|
+
'MODEL_DOES_NOT_SUPPORT_IMAGES',
|
|
1474
|
+
)
|
|
1611
1475
|
}
|
|
1612
1476
|
}
|
|
1613
1477
|
|
|
1614
1478
|
/** Resolve the persistence service continuable children require, or fail loud. */
|
|
1615
1479
|
private requirePersistence(): SessionPersistence {
|
|
1616
|
-
const persistence = this.ctx.get(
|
|
1480
|
+
const persistence = this.ctx.get('sessionPersistence')
|
|
1617
1481
|
if (persistence === undefined) {
|
|
1618
1482
|
throw new SubagentError(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
)
|
|
1483
|
+
'continuable subagents require session persistence (load a dsh-session-persistence backend)',
|
|
1484
|
+
'PERSISTENCE_UNAVAILABLE',
|
|
1485
|
+
)
|
|
1622
1486
|
}
|
|
1623
|
-
return persistence
|
|
1487
|
+
return persistence
|
|
1624
1488
|
}
|
|
1625
1489
|
|
|
1626
1490
|
/** Resolve the Session query service used for cold child observations. */
|
|
1627
1491
|
private requireSessionQuery(): SessionQueryEngine {
|
|
1628
|
-
const query = this.ctx.get(
|
|
1492
|
+
const query = this.ctx.get('sessionQuery')
|
|
1629
1493
|
if (query === undefined) {
|
|
1630
1494
|
throw new SubagentError(
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
)
|
|
1495
|
+
'continuable subagents require session query (load @deepseek-ai/dsh-session-query)',
|
|
1496
|
+
'CONTINUATION_UNAVAILABLE',
|
|
1497
|
+
)
|
|
1634
1498
|
}
|
|
1635
|
-
return query
|
|
1499
|
+
return query
|
|
1636
1500
|
}
|
|
1637
1501
|
}
|
|
1638
1502
|
|
|
1639
|
-
export default SubagentManager
|
|
1503
|
+
export default SubagentManager
|
|
1640
1504
|
|
|
1641
1505
|
/** Creation-time selection precedes the own descriptor; historical forks can select after it. */
|
|
1642
1506
|
function hasIndependentPresetSelection(events: readonly SessionEvent[]): boolean {
|
|
1643
1507
|
for (const event of events) {
|
|
1644
|
-
if (event.type ===
|
|
1645
|
-
if (event.type ===
|
|
1508
|
+
if (event.type === 'subagent/descriptor') return false
|
|
1509
|
+
if (event.type === 'agent-preset/selected') return true
|
|
1646
1510
|
}
|
|
1647
1511
|
/* v8 ignore next -- coldResume rejects logs without a supported own descriptor before this lookup. */
|
|
1648
|
-
return false
|
|
1512
|
+
return false
|
|
1649
1513
|
}
|