@deepseek-ai/dsh-subagent 0.1.2-alpha.5 → 0.1.3-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +11 -7
- package/README.zh.md +11 -7
- package/lib/index.js +958 -914
- package/lib/typert.host.js +44 -11
- package/lib/typert.remote-client.js +3 -2
- package/lib/types/assistant-output.d.ts +3 -3
- package/lib/types/assistant-output.js +5 -4
- package/lib/types/child-agent.js +2 -2
- package/lib/types/continuation-activation.d.ts +251 -0
- package/lib/types/continuation-activation.js +663 -0
- package/lib/types/continuation-messages.d.ts +62 -0
- package/lib/types/continuation-messages.js +102 -0
- package/lib/types/continuation.d.ts +44 -358
- package/lib/types/continuation.js +135 -987
- package/lib/types/control-types.d.ts +2 -0
- package/lib/types/control.d.ts +4 -0
- package/lib/types/control.js +1 -0
- package/lib/types/inbox.d.ts +43 -0
- package/lib/types/inbox.js +61 -0
- package/lib/types/index.d.ts +11 -11
- package/lib/types/index.js +14 -12
- package/lib/types/internal.d.ts +17 -5
- package/lib/types/internal.js +16 -3
- package/lib/types/out-of-process.d.ts +1 -1
- package/lib/types/out-of-process.js +1 -1
- package/lib/types/types.d.ts +46 -2
- package/package.json +44 -44
- package/lib/types/descriptor-seed.d.ts +0 -21
- package/lib/types/descriptor-seed.js +0 -24
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* persistence,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Continuable-subagent orchestration behind `ctx.subagents`: stable child ids,
|
|
3
|
+
* descriptor persistence, provider preparation, cold resume, authorization,
|
|
4
|
+
* and message routing. {@link ContinuableActivationRegistry} owns the mutable
|
|
5
|
+
* process-local Activation graph and its settlement and disposal lifecycle.
|
|
6
6
|
*
|
|
7
7
|
* A continuable child has one durable Session and at most one process-local
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* residency while the Agent loop owns all turn ordering and execution. No
|
|
13
|
-
* continuable path creates a Task or an intermediate result-bearing wrapper.
|
|
14
|
-
*
|
|
15
|
-
* Because residency is this manager's alone to end, telling the parent that a
|
|
16
|
-
* child settled is its job too. An external `subagent/end` listener cannot do
|
|
17
|
-
* it correctly: that payload names no parent, the child handle is already
|
|
18
|
-
* disposed by then, and the release that wakes the parent's own settlement
|
|
19
|
-
* watcher has already run. See {@link SubagentContinuationManager.notifySettlement}.
|
|
8
|
+
* Activation. The Agent inbox is the only turn queue, so this manager owns
|
|
9
|
+
* durable orchestration while the Agent loop owns all turn ordering and
|
|
10
|
+
* execution. No continuable path creates a Task or an intermediate
|
|
11
|
+
* result-bearing wrapper.
|
|
20
12
|
*
|
|
21
13
|
* @module @deepseek-ai/dsh-subagent
|
|
22
14
|
*/
|
|
@@ -74,109 +66,15 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
74
66
|
});
|
|
75
67
|
import { randomUUID } from 'node:crypto';
|
|
76
68
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
77
|
-
import { ReasoningEffortId,
|
|
69
|
+
import { ReasoningEffortId, contentHasImage, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
78
70
|
import { SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
79
|
-
import {
|
|
80
|
-
import {
|
|
71
|
+
import { childSessionMeta, captureDelegatedPolicyOverrides, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
|
|
72
|
+
import { ContinuableActivationRegistry, } from "./continuation-activation.js";
|
|
73
|
+
import { createAgentMessage, withContinuableReturnGuidance, } from "./continuation-messages.js";
|
|
81
74
|
import { assertSubagentMaxDepth } from "./depth.js";
|
|
82
|
-
import {
|
|
75
|
+
import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
|
|
83
76
|
import { SubagentError } from "./error.js";
|
|
84
77
|
import { isAdjacentAgentSendMessageTool } from "./internal.js";
|
|
85
|
-
/**
|
|
86
|
-
* Read one Activation's current disposal transaction. This indirection exists
|
|
87
|
-
* because TypeScript would otherwise narrow repeated reads of the mutable field
|
|
88
|
-
* inside a long-lived closure to constants instead of re-reading runtime state.
|
|
89
|
-
* @param activation - the Activation to inspect.
|
|
90
|
-
* @returns the in-flight or settled disposal, or `undefined` while resident.
|
|
91
|
-
*/
|
|
92
|
-
function disposalOf(activation) {
|
|
93
|
-
return activation.disposal;
|
|
94
|
-
}
|
|
95
|
-
/** Build durable attribution for one adjacent-Agent message. */
|
|
96
|
-
function agentMessageSource(sender) {
|
|
97
|
-
return {
|
|
98
|
-
kind: 'agent-message',
|
|
99
|
-
form: 'relay',
|
|
100
|
-
senderSessionId: sender.id,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/** Build the model-visible and durable representation of one adjacent-Agent message. */
|
|
104
|
-
function agentMessage(sender, content) {
|
|
105
|
-
return createUserMessage({
|
|
106
|
-
content: [
|
|
107
|
-
{ type: 'text', text: `Agent ${sender.id} sent a message:` },
|
|
108
|
-
...content,
|
|
109
|
-
],
|
|
110
|
-
source: agentMessageSource(sender),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
/** Append adjacent-Agent return guidance to a continuable child's initial task. */
|
|
114
|
-
function continuableInitialPrompt(parentId, prompt) {
|
|
115
|
-
const encodedParentId = JSON.stringify(parentId);
|
|
116
|
-
return [
|
|
117
|
-
...prompt,
|
|
118
|
-
{
|
|
119
|
-
type: 'text',
|
|
120
|
-
text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with `
|
|
121
|
-
+ `send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares `
|
|
122
|
-
+ 'your workspace but does not automatically receive your transcript, tool output, or reasoning. Send '
|
|
123
|
-
+ 'earlier messages as well when a finding changes what the parent should do next; sending a message '
|
|
124
|
-
+ 'does not end your turn.',
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* One line telling a parent that a background child is finished and why, in
|
|
130
|
-
* the parent's own task vocabulary.
|
|
131
|
-
* @param childId - the durable child the parent knows by id.
|
|
132
|
-
* @param stopReason - how the child's last ordinary turn ended.
|
|
133
|
-
* @returns the model-facing opening line of the settlement notice.
|
|
134
|
-
*/
|
|
135
|
-
function settlementSummary(childId, stopReason) {
|
|
136
|
-
const subject = `Background subagent ${childId}`;
|
|
137
|
-
switch (stopReason) {
|
|
138
|
-
case 'completed':
|
|
139
|
-
return `${subject} finished and will do no further work unless you send it more.`;
|
|
140
|
-
case 'aborted':
|
|
141
|
-
return `${subject} was stopped before it finished.`;
|
|
142
|
-
case 'max-tokens':
|
|
143
|
-
return `${subject} ran out of room before it finished.`;
|
|
144
|
-
// A pre-step rejection — a hook deny, a policy plugin — discarded input
|
|
145
|
-
// the child had claimed, so the parent must not treat the task as done.
|
|
146
|
-
case 'refusal':
|
|
147
|
-
return `${subject} declined the task.`;
|
|
148
|
-
case 'error':
|
|
149
|
-
return `${subject} failed before it finished.`;
|
|
150
|
-
/* v8 ignore next 4 -- `SubagentResult['stopReason']` is merge-extensible, so this arm
|
|
151
|
-
* needs a backend that adds a variant; an unnameable ending is reported as unfinished
|
|
152
|
-
* rather than silently as success. */
|
|
153
|
-
default:
|
|
154
|
-
return `${subject} ended abnormally (${String(stopReason)}) before it finished.`;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/** Serialize each durable child's delivery, release, and disposal. */
|
|
158
|
-
class ChildLock {
|
|
159
|
-
tails = new Map();
|
|
160
|
-
/**
|
|
161
|
-
* Run `operation` after every previously queued operation for `childId`.
|
|
162
|
-
* @param childId - the durable child whose operations are linearized.
|
|
163
|
-
* @param operation - the critical section to run in order.
|
|
164
|
-
* @returns the operation's own settlement.
|
|
165
|
-
*/
|
|
166
|
-
run(childId, operation) {
|
|
167
|
-
const previous = this.tails.get(childId) ?? Promise.resolve();
|
|
168
|
-
const result = previous.then(operation, operation);
|
|
169
|
-
// Absorb rejections in the chaining tail so one failed critical section
|
|
170
|
-
// cannot reject an unrelated later caller.
|
|
171
|
-
const tail = result.then(() => undefined, () => undefined);
|
|
172
|
-
this.tails.set(childId, tail);
|
|
173
|
-
void tail.then(() => {
|
|
174
|
-
if (this.tails.get(childId) === tail)
|
|
175
|
-
this.tails.delete(childId);
|
|
176
|
-
});
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
78
|
/**
|
|
181
79
|
* The continuable-subagent orchestration service behind `ctx.subagents`. Tool
|
|
182
80
|
* schema and host adapters are consumers of this one contract; foreground
|
|
@@ -186,63 +84,27 @@ class ChildLock {
|
|
|
186
84
|
export class SubagentContinuationManager {
|
|
187
85
|
ctx;
|
|
188
86
|
host;
|
|
189
|
-
|
|
190
|
-
activations = new Map();
|
|
191
|
-
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
192
|
-
materializations = new Set();
|
|
193
|
-
locks = new ChildLock();
|
|
194
|
-
/** Structural Cordis owner of every Activation handle. */
|
|
195
|
-
ownerCtx;
|
|
196
|
-
/**
|
|
197
|
-
* Exact roots whose host teardown has begun, with the live lineage members
|
|
198
|
-
* observed under each root. Entries remain until that exact root leaves the
|
|
199
|
-
* Agent registry, closing admission throughout its host's teardown without
|
|
200
|
-
* poisoning a later same-id replacement.
|
|
201
|
-
*/
|
|
202
|
-
closingScopes = new Map();
|
|
203
|
-
draining = false;
|
|
87
|
+
activations;
|
|
204
88
|
constructor(ctx, host) {
|
|
205
89
|
this.ctx = ctx;
|
|
206
90
|
this.host = host;
|
|
207
|
-
|
|
208
|
-
// cannot express the dynamic child graph. Register the private scope's
|
|
209
|
-
// structural disposer FIRST and the drain SECOND, so reverse unwind invokes
|
|
210
|
-
// the drain before releasing the scope; a cleanup effect on the same scope
|
|
211
|
-
// as the Agent handles would let structural handle disposal bypass
|
|
212
|
-
// child-first ordering.
|
|
213
|
-
const scope = ctx.plugin(function activationOwner() { });
|
|
214
|
-
this.ownerCtx = scope.ctx;
|
|
215
|
-
ctx.on('agent/disposed', ({ agent }) => {
|
|
216
|
-
this.closingScopes.delete(agent);
|
|
217
|
-
});
|
|
218
|
-
ctx.effect(function* () {
|
|
219
|
-
yield scope.dispose;
|
|
220
|
-
yield () => this.drain();
|
|
221
|
-
}.bind(this), 'subagents.continuations()');
|
|
91
|
+
this.activations = new ContinuableActivationRegistry(ctx, (provider, childId, parent) => host.observeActivation(provider, childId, parent));
|
|
222
92
|
}
|
|
223
93
|
/**
|
|
224
|
-
* Start one continuable background child
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* ownership, and submit the initial prompt. Resolves when inbox acceptance
|
|
228
|
-
* yields the message id — without waiting for the turn to start or for the
|
|
229
|
-
* message to reach the Session log.
|
|
230
|
-
*
|
|
231
|
-
* Every failure before that acceptance rejects without either id, disposing
|
|
232
|
-
* any created handle and rolling back the Activation and parent ownership.
|
|
233
|
-
* The caller signal owns lookup, materialization, and admission only until
|
|
234
|
-
* acceptance; afterwards the manager owns the Activation independently.
|
|
94
|
+
* Start one continuable background child and resolve at initial inbox acceptance.
|
|
95
|
+
* Every earlier failure disposes any created handle and rolls back Activation
|
|
96
|
+
* and parent ownership without returning either id.
|
|
235
97
|
* @param spec - provider, delegation request, and caller cancellation.
|
|
236
|
-
* @returns the durable child id and
|
|
98
|
+
* @returns the durable child id and accepted initial prompt message id.
|
|
237
99
|
*/
|
|
238
100
|
async startContinuable(spec) {
|
|
239
101
|
const request = spec.request;
|
|
240
102
|
const parent = request.parent;
|
|
241
|
-
this.assertAdmitting(parent);
|
|
103
|
+
this.activations.assertAdmitting(parent);
|
|
242
104
|
const persistence = this.requirePersistence();
|
|
243
105
|
assertSubagentMaxDepth(request.maxDepth);
|
|
244
106
|
const childId = spec.childId ?? brandString(randomUUID());
|
|
245
|
-
this.assertChildIdAvailable(childId);
|
|
107
|
+
this.activations.assertChildIdAvailable(childId);
|
|
246
108
|
const childDepth = resolveChildDepth(parent, request.maxDepth);
|
|
247
109
|
// Snapshot before any await: invalid descriptor JSON rejects the call
|
|
248
110
|
// before a child exists, and the detached value is what reaches the log.
|
|
@@ -263,72 +125,74 @@ export class SubagentContinuationManager {
|
|
|
263
125
|
// Capture before the first await: a later parent switch belongs to the
|
|
264
126
|
// parent's future, not to this child.
|
|
265
127
|
const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
128
|
+
// An idle continuation-managed parent must not settle while a caller is
|
|
129
|
+
// still creating its child. A turn-scoped delegation does not need this,
|
|
130
|
+
// but the service is also callable outside a turn.
|
|
131
|
+
const releaseHold = this.activations.holdOwnership(parent, childId);
|
|
132
|
+
try {
|
|
133
|
+
const prepared = await this.host.prepareContinuable(spec.provider, {
|
|
134
|
+
sessionId: childId,
|
|
135
|
+
parent,
|
|
136
|
+
signal: spec.signal,
|
|
137
|
+
});
|
|
276
138
|
spec.signal.throwIfAborted();
|
|
277
|
-
this.assertAdmitting(parent);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
139
|
+
this.activations.assertAdmitting(parent);
|
|
140
|
+
const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
|
|
141
|
+
const seed = prepared.seed;
|
|
142
|
+
const messageId = await this.activations.locks.run(childId, async () => {
|
|
281
143
|
spec.signal.throwIfAborted();
|
|
282
|
-
this.assertAdmitting(parent);
|
|
283
|
-
this.assertChildIdAvailable(childId);
|
|
284
|
-
if (
|
|
285
|
-
|
|
144
|
+
this.activations.assertAdmitting(parent);
|
|
145
|
+
this.activations.assertChildIdAvailable(childId);
|
|
146
|
+
if (spec.childId !== undefined) {
|
|
147
|
+
const persisted = await persistence.stat(childId, { signal: spec.signal });
|
|
148
|
+
spec.signal.throwIfAborted();
|
|
149
|
+
this.activations.assertAdmitting(parent);
|
|
150
|
+
this.activations.assertChildIdAvailable(childId);
|
|
151
|
+
if (persisted !== undefined) {
|
|
152
|
+
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
|
|
153
|
+
}
|
|
286
154
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
155
|
+
const activation = await this.activations.materialize({
|
|
156
|
+
childId,
|
|
157
|
+
provider: spec.provider,
|
|
158
|
+
parent,
|
|
159
|
+
create: {
|
|
160
|
+
seed,
|
|
161
|
+
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined),
|
|
162
|
+
inheritedEventCount,
|
|
163
|
+
delegatedPolicies,
|
|
164
|
+
descriptor,
|
|
165
|
+
},
|
|
166
|
+
agentOptions,
|
|
167
|
+
composition: { persona: request.persona, toolFilter: request.toolFilter },
|
|
168
|
+
signal: spec.signal,
|
|
169
|
+
});
|
|
170
|
+
return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent))
|
|
171
|
+
? withContinuableReturnGuidance(parent.id, request.prompt)
|
|
172
|
+
: request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent);
|
|
301
173
|
});
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
/** Reject one child identity already owned by a live Agent or Session. */
|
|
309
|
-
assertChildIdAvailable(childId) {
|
|
310
|
-
if (this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
|
|
311
|
-
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
|
|
174
|
+
return { childId, messageId };
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
releaseHold();
|
|
178
|
+
throw error;
|
|
312
179
|
}
|
|
313
180
|
}
|
|
314
181
|
/**
|
|
315
182
|
* Deliver one model-authored message to a direct continuable child or to the
|
|
316
|
-
* sender's direct parent.
|
|
317
|
-
*
|
|
318
|
-
* turn. A missing direct child cold-resumes through the ordinary continuation
|
|
319
|
-
* lifecycle. The caller signal owns the operation only until inbox acceptance.
|
|
183
|
+
* sender's direct parent. A missing direct child cold-resumes through the
|
|
184
|
+
* ordinary continuation lifecycle.
|
|
320
185
|
* @param sender - exact live Agent authorizing and originating the message.
|
|
321
186
|
* @param targetId - durable direct-parent or direct-child session id.
|
|
322
187
|
* @param content - model-authored content to deliver.
|
|
323
188
|
* @param options - caller cancellation before acceptance.
|
|
324
189
|
* @returns the accepted message's inbox id.
|
|
325
|
-
* @throws when adjacency, availability, or admission rejects delivery.
|
|
326
190
|
*/
|
|
327
191
|
async sendMessage(sender, targetId, content, options) {
|
|
328
192
|
if (this.ctx.agents.get(sender.id) !== sender) {
|
|
329
193
|
throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
|
|
330
194
|
}
|
|
331
|
-
this.assertAdmitting(sender);
|
|
195
|
+
this.activations.assertAdmitting(sender);
|
|
332
196
|
const senderActivation = this.activations.get(sender.id);
|
|
333
197
|
if (senderActivation !== undefined
|
|
334
198
|
&& senderActivation.handle.agent === sender
|
|
@@ -348,358 +212,124 @@ export class SubagentContinuationManager {
|
|
|
348
212
|
* Queue one human-authored prompt as a distinct direct-child turn.
|
|
349
213
|
* @param parent - exact live direct parent authorizing delivery.
|
|
350
214
|
* @param childId - durable direct-child session id.
|
|
351
|
-
* @param content -
|
|
352
|
-
* @param source - durable
|
|
215
|
+
* @param content - model-visible prompt blocks.
|
|
216
|
+
* @param source - durable attribution for the human prompt.
|
|
353
217
|
* @param signal - caller cancellation before inbox acceptance.
|
|
354
|
-
* @returns the accepted message
|
|
218
|
+
* @returns the accepted durable message id.
|
|
355
219
|
*/
|
|
356
220
|
async queuePrompt(parent, childId, content, source, signal) {
|
|
357
221
|
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
|
|
358
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Steer one host-authored prompt to a direct continuable child.
|
|
225
|
+
* @param parent - exact live direct parent authorizing delivery.
|
|
226
|
+
* @param childId - durable direct-child session id.
|
|
227
|
+
* @param content - model-visible prompt blocks.
|
|
228
|
+
* @param source - durable attribution for the host prompt.
|
|
229
|
+
* @param signal - caller cancellation before inbox acceptance.
|
|
230
|
+
* @returns the accepted durable message id.
|
|
231
|
+
*/
|
|
232
|
+
async steerPrompt(parent, childId, content, source, signal) {
|
|
233
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' });
|
|
234
|
+
}
|
|
359
235
|
/** Route one parent-originated delivery through residency and cold resume. */
|
|
360
236
|
async deliverToChild(parent, childId, content, options) {
|
|
361
|
-
this.assertAdmitting(parent);
|
|
237
|
+
this.activations.assertAdmitting(parent);
|
|
238
|
+
const releaseHold = this.activations.holdOwnership(parent, childId);
|
|
239
|
+
try {
|
|
240
|
+
return await this.deliverFollowup(parent, childId, content, options);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
releaseHold();
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
|
|
248
|
+
async deliverFollowup(parent, childId, content, options) {
|
|
362
249
|
while (true) {
|
|
363
|
-
const live = await this.locks.run(childId, async () => {
|
|
250
|
+
const live = await this.activations.locks.run(childId, async () => {
|
|
364
251
|
const activation = this.activations.get(childId);
|
|
365
252
|
if (activation === undefined)
|
|
366
253
|
return this.coldResume(parent, childId, content, options);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
/* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
|
|
371
|
-
* delivery to observe the transaction inside the same critical section that opened it,
|
|
372
|
-
* which no test can schedule deterministically. The behavior is covered end-to-end by
|
|
373
|
-
* "cold-resumes a delivery that lost the race with final disposal". */
|
|
254
|
+
const disposal = activation.inbox.closing;
|
|
255
|
+
/* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
|
|
256
|
+
* observe the transaction inside the same critical section that opened it. */
|
|
374
257
|
if (disposal !== undefined) {
|
|
375
258
|
return disposal.then(() => undefined, () => undefined);
|
|
376
259
|
}
|
|
377
|
-
// Text-only delivery stays await-free, so the disposal-cutoff check
|
|
378
|
-
// above and the submit share one critical window. The image path
|
|
379
|
-
// awaits a capability read, so it re-checks the cutoff afterwards; a
|
|
380
|
-
// disposal that began during the read is waited out and retried like
|
|
381
|
-
// one observed on entry.
|
|
382
260
|
if (contentHasImage(content)) {
|
|
383
261
|
await this.assertImageCapable(activation.handle.agent, options.signal);
|
|
384
|
-
if (activation.
|
|
385
|
-
await Promise.allSettled([activation.
|
|
262
|
+
if (activation.inbox.closing !== undefined) {
|
|
263
|
+
await Promise.allSettled([activation.inbox.closing]);
|
|
386
264
|
return undefined;
|
|
387
265
|
}
|
|
388
266
|
}
|
|
389
267
|
return this.submitAdmitted(activation, content, options, parent);
|
|
390
268
|
});
|
|
391
|
-
/* v8 ignore start -- only
|
|
392
|
-
* race reaches the retry below, which then cold-resumes a new Activation. */
|
|
269
|
+
/* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
|
|
393
270
|
if (live !== undefined)
|
|
394
271
|
return live;
|
|
395
|
-
this.assertAdmitting(parent);
|
|
272
|
+
this.activations.assertAdmitting(parent);
|
|
396
273
|
options.signal.throwIfAborted();
|
|
397
274
|
/* v8 ignore stop */
|
|
398
275
|
}
|
|
399
276
|
}
|
|
400
277
|
/**
|
|
401
278
|
* Interrupt one live continuable child's current turn. Admission is
|
|
402
|
-
* synchronous and the effect is asynchronous
|
|
403
|
-
*
|
|
404
|
-
* returns without waiting for the target to observe the signal or reach
|
|
405
|
-
* quiescence. The Activation, its handle, accepted unclaimed inbox work, and
|
|
406
|
-
* already-published descendants are untouched; work already claimed into the
|
|
407
|
-
* interrupted turn is not requeued. Once the interrupted driver is idle, a
|
|
408
|
-
* waking send resumes the parked queue.
|
|
409
|
-
*
|
|
410
|
-
* An absent target is an accepted no-op, which uniformly covers natural
|
|
411
|
-
* completion races, repeated requests, one-shot ids, and unknown ids without
|
|
412
|
-
* consulting the durable catalog. A target whose disposal transaction is
|
|
413
|
-
* already open is likewise an accepted no-op after authorization.
|
|
279
|
+
* synchronous and the cancellation effect is asynchronous. An absent or
|
|
280
|
+
* already-closing target is an accepted no-op after authority checks.
|
|
414
281
|
* @param targetSessionId - the durable child session id to interrupt.
|
|
415
282
|
* @param authority - the human parent address or exact live ancestor Agent.
|
|
416
|
-
* @throws {SubagentError} `UNAUTHORIZED` when the presented authority does
|
|
417
|
-
* not own the live target: a stale or self-targeting ancestor caller, a
|
|
418
|
-
* parent address that is not the live target's durable direct parent, or
|
|
419
|
-
* an ancestor outside the target's recorded live lineage.
|
|
420
283
|
*/
|
|
421
284
|
interrupt(targetSessionId, authority) {
|
|
422
|
-
|
|
423
|
-
const caller = authority.agent;
|
|
424
|
-
// A stale caller is rejected even when the target is absent, so a
|
|
425
|
-
// replaced same-id Agent can never probe this manager's state.
|
|
426
|
-
if (this.ctx.agents.get(caller.id) !== caller) {
|
|
427
|
-
throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, 'UNAUTHORIZED');
|
|
428
|
-
}
|
|
429
|
-
if (caller.id === targetSessionId) {
|
|
430
|
-
throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, 'UNAUTHORIZED');
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const activation = this.activations.get(targetSessionId);
|
|
434
|
-
if (activation === undefined)
|
|
435
|
-
return;
|
|
436
|
-
if (authority.kind === 'user') {
|
|
437
|
-
if (activation.handle.agent.session.header.parentSession !== authority.parentSessionId) {
|
|
438
|
-
throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, 'UNAUTHORIZED');
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
else if (!activation.ancestry.has(authority.agent)) {
|
|
442
|
-
throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, 'UNAUTHORIZED');
|
|
443
|
-
}
|
|
444
|
-
// Disposal already stopped the target with a whole-Activation teardown;
|
|
445
|
-
// a second cancel would be a redundant signal on a closing handle.
|
|
446
|
-
if (activation.disposal !== undefined)
|
|
447
|
-
return;
|
|
448
|
-
activation.handle.agent.cancel(authority.kind === 'user' ? { kind: 'user' } : { kind: 'parent' }, { keepInbox: true });
|
|
285
|
+
this.activations.interrupt(targetSessionId, authority);
|
|
449
286
|
}
|
|
450
287
|
/** Deliver one resident continuable child's message to its live direct parent. */
|
|
451
288
|
sendToParent(activation, sender, content) {
|
|
452
289
|
/* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
|
|
453
290
|
* transaction between exact-agent authorization and this no-await span. */
|
|
454
|
-
if (activation.
|
|
291
|
+
if (activation.inbox.closing !== undefined) {
|
|
455
292
|
throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
|
|
456
293
|
}
|
|
457
294
|
const parent = this.ctx.agents.get(activation.parentSession);
|
|
458
295
|
if (parent === undefined) {
|
|
459
296
|
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
|
|
460
297
|
}
|
|
461
|
-
const message =
|
|
462
|
-
this.
|
|
298
|
+
const message = createAgentMessage(sender, content);
|
|
299
|
+
this.sendAgentMessage(parent, message);
|
|
463
300
|
return message.id;
|
|
464
301
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Perform one waking send to a parent, accounted against that parent's own
|
|
467
|
-
* Activation when it has one. Registering the id before the send is what
|
|
468
|
-
* keeps a continuation-managed parent from being judged quiescent in the
|
|
469
|
-
* window between a waking send and the microtask that admits it.
|
|
470
|
-
* @param parent - the exact live parent receiving the waking message.
|
|
471
|
-
* @param message - the message whose id is accounted.
|
|
472
|
-
* @param send - the synchronous waking send to perform.
|
|
473
|
-
*/
|
|
474
|
-
sendWaking(parent, message, send) {
|
|
475
|
-
const parentActivation = this.activations.get(parent.id);
|
|
476
|
-
if (parentActivation !== undefined && parentActivation.handle.agent === parent) {
|
|
477
|
-
this.admitWaking(parentActivation, message.id, send);
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
send();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
302
|
/** Send one Agent message while translating only the target's own rejection. */
|
|
484
303
|
sendAgentMessage(parent, message) {
|
|
485
304
|
try {
|
|
486
|
-
|
|
305
|
+
this.activations.sendWaking(parent, message, 'steer');
|
|
487
306
|
}
|
|
488
307
|
catch (error) {
|
|
489
308
|
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
|
|
490
309
|
}
|
|
491
310
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Close admission, await every already-admitted materialization through
|
|
494
|
-
* publication or rollback, then dispose the stable live Activation forest
|
|
495
|
-
* child-first. Sibling branches drain independently: one failure is recorded
|
|
496
|
-
* but never prevents the remaining handles from being attempted, and the
|
|
497
|
-
* aggregate rejects only after every branch settles.
|
|
498
|
-
* @returns once materialization is quiescent and every live Activation released its handle.
|
|
499
|
-
* @throws an aggregate error when any branch failed to release.
|
|
500
|
-
*/
|
|
311
|
+
/** Close manager-wide admission and release every live Activation. */
|
|
501
312
|
async drain() {
|
|
502
|
-
|
|
503
|
-
// already past that cutoff remain tracked until their handle is installed
|
|
504
|
-
// or rollback completes, producing a stable forest for the later snapshot.
|
|
505
|
-
this.draining = true;
|
|
506
|
-
await Promise.all([...this.materializations].map(materialization => materialization.settled));
|
|
507
|
-
// Snapshot roots after closing admission: a root is an Activation no live
|
|
508
|
-
// Activation owns, so disposing roots recurses child-first into the forest.
|
|
509
|
-
const owned = new Set();
|
|
510
|
-
for (const activation of this.activations.values()) {
|
|
511
|
-
for (const child of activation.ownedChildren)
|
|
512
|
-
owned.add(child);
|
|
513
|
-
}
|
|
514
|
-
const roots = [...this.activations.values()].filter(activation => !owned.has(activation.childId));
|
|
515
|
-
await this.disposeRoots(roots, 'activation(s)');
|
|
313
|
+
await this.activations.drain();
|
|
516
314
|
}
|
|
517
315
|
/**
|
|
518
316
|
* Stop only the continuable descendants of exact live host-owned parents.
|
|
519
|
-
* Admission stays closed for those parent trees until each exact parent
|
|
520
|
-
* leaves the Agent registry; unrelated trees and manager-wide admission stay
|
|
521
|
-
* live.
|
|
522
317
|
* @param parents - exact live roots whose continuable descendants must stop.
|
|
523
|
-
* @returns once every retained descendant Activation released its handle.
|
|
524
|
-
* @throws an aggregate error after all scoped branches settle when any failed.
|
|
525
318
|
*/
|
|
526
319
|
async drainDescendants(parents) {
|
|
527
|
-
|
|
528
|
-
if (roots.size === 0)
|
|
529
|
-
return;
|
|
530
|
-
// Publish the scoped admission cutoff before the first await. Merge with an
|
|
531
|
-
// earlier call for the same exact root so a converging drain cannot forget
|
|
532
|
-
// descendants whose release is already in flight.
|
|
533
|
-
for (const root of roots) {
|
|
534
|
-
this.closingMembers(root).add(root);
|
|
535
|
-
}
|
|
536
|
-
const targets = [];
|
|
537
|
-
for (const activation of this.activations.values()) {
|
|
538
|
-
const lineage = this.liveLineage(activation.handle.agent);
|
|
539
|
-
// Strict descendants only: a continuable Agent may itself be a
|
|
540
|
-
// host-owned root, and its host remains responsible for that root handle.
|
|
541
|
-
const owners = [...roots].filter(root => activation.handle.agent !== root
|
|
542
|
-
&& activation.ancestry.has(root));
|
|
543
|
-
if (owners.length === 0)
|
|
544
|
-
continue;
|
|
545
|
-
targets.push(activation);
|
|
546
|
-
for (const owner of owners) {
|
|
547
|
-
const members = this.closingMembers(owner);
|
|
548
|
-
members.add(activation.handle.agent);
|
|
549
|
-
for (const agent of lineage)
|
|
550
|
-
members.add(agent);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
const materializations = [...this.materializations].filter((materialization) => {
|
|
554
|
-
const owners = [...roots].filter(root => materialization.lineage.includes(root));
|
|
555
|
-
for (const owner of owners) {
|
|
556
|
-
const members = this.closingMembers(owner);
|
|
557
|
-
for (const agent of materialization.lineage)
|
|
558
|
-
members.add(agent);
|
|
559
|
-
}
|
|
560
|
-
return owners.length > 0;
|
|
561
|
-
});
|
|
562
|
-
const ownedTargets = new Set();
|
|
563
|
-
for (const activation of targets) {
|
|
564
|
-
for (const child of activation.ownedChildren)
|
|
565
|
-
ownedTargets.add(child);
|
|
566
|
-
}
|
|
567
|
-
const targetRoots = targets.filter(activation => !ownedTargets.has(activation.childId));
|
|
568
|
-
// Open every selected transaction before the materialization barrier.
|
|
569
|
-
// Disposal propagates cancellation top-down in the same synchronous span;
|
|
570
|
-
// handle release remains child-first.
|
|
571
|
-
for (const activation of targets) {
|
|
572
|
-
const disposal = this.dispose(activation);
|
|
573
|
-
void disposal.catch(() => undefined);
|
|
574
|
-
}
|
|
575
|
-
await Promise.all(materializations.map(materialization => materialization.settled));
|
|
576
|
-
await this.disposeRoots(targetRoots, 'scoped activation(s)');
|
|
320
|
+
await this.activations.drainDescendants(parents);
|
|
577
321
|
}
|
|
578
322
|
/**
|
|
579
|
-
* Release selected resident direct children of one exact live parent
|
|
580
|
-
* closing admission for the parent's other continuable children. Owned
|
|
581
|
-
* descendants are released recursively through the same lifecycle.
|
|
323
|
+
* Release selected resident direct children of one exact live parent.
|
|
582
324
|
* @param parent - exact live direct parent authorizing the selected release.
|
|
583
325
|
* @param childIds - durable direct-child ids to release when resident.
|
|
584
|
-
* @returns once every selected Activation released its handle.
|
|
585
|
-
* @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
|
|
586
|
-
* parent's direct continuable child or the parent identity is stale.
|
|
587
326
|
*/
|
|
588
327
|
async drainChildren(parent, childIds) {
|
|
589
|
-
|
|
590
|
-
throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED');
|
|
591
|
-
}
|
|
592
|
-
const targets = [];
|
|
593
|
-
for (const childId of new Set(childIds)) {
|
|
594
|
-
const activation = this.activations.get(childId);
|
|
595
|
-
if (activation === undefined)
|
|
596
|
-
continue;
|
|
597
|
-
if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) {
|
|
598
|
-
throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, 'UNAUTHORIZED');
|
|
599
|
-
}
|
|
600
|
-
targets.push(activation);
|
|
601
|
-
}
|
|
602
|
-
// Open every transaction before the first await so cancellation propagates
|
|
603
|
-
// across the selected roots in one synchronous span.
|
|
604
|
-
for (const activation of targets) {
|
|
605
|
-
const disposal = this.dispose(activation);
|
|
606
|
-
void disposal.catch(() => undefined);
|
|
607
|
-
}
|
|
608
|
-
await this.disposeRoots(targets, 'selected activation(s)');
|
|
609
|
-
}
|
|
610
|
-
/** Dispose independent roots and report every branch failure after all settle. */
|
|
611
|
-
async disposeRoots(roots, failureSubject) {
|
|
612
|
-
const failures = await Promise.all(roots.map(async (activation) => {
|
|
613
|
-
try {
|
|
614
|
-
await this.dispose(activation);
|
|
615
|
-
return undefined;
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
return error;
|
|
619
|
-
}
|
|
620
|
-
}));
|
|
621
|
-
const reasons = failures.filter(failure => failure !== undefined);
|
|
622
|
-
if (reasons.length > 0) {
|
|
623
|
-
throw new SubagentError(`continuable subagent teardown failed for ${reasons.length} ${failureSubject}: `
|
|
624
|
-
+ reasons.map(reason => errorChain(reason)).join('; '), 'ACTIVATION_TEARDOWN_FAILED');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
/** Return the retained member set for one exact scoped-teardown root. */
|
|
628
|
-
closingMembers(root) {
|
|
629
|
-
const existing = this.closingScopes.get(root);
|
|
630
|
-
if (existing !== undefined)
|
|
631
|
-
return existing;
|
|
632
|
-
const members = new Set();
|
|
633
|
-
this.closingScopes.set(root, members);
|
|
634
|
-
return members;
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Return the exact currently resolvable ancestry from `agent` upward. The
|
|
638
|
-
* first element is always the supplied identity, even when it is already
|
|
639
|
-
* stale; each ancestor after it must be the registry's current exact entry.
|
|
640
|
-
*/
|
|
641
|
-
liveLineage(agent) {
|
|
642
|
-
const lineage = [agent];
|
|
643
|
-
const seen = new Set([agent.id]);
|
|
644
|
-
let parentSession = agent.session.header.parentSession;
|
|
645
|
-
while (parentSession !== undefined) {
|
|
646
|
-
const parent = this.ctx.agents.get(parentSession);
|
|
647
|
-
if (parent === undefined || seen.has(parent.id))
|
|
648
|
-
break;
|
|
649
|
-
lineage.push(parent);
|
|
650
|
-
seen.add(parent.id);
|
|
651
|
-
parentSession = parent.session.header.parentSession;
|
|
652
|
-
}
|
|
653
|
-
return lineage;
|
|
328
|
+
await this.activations.drainChildren(parent, childIds);
|
|
654
329
|
}
|
|
655
330
|
/**
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* whose forest is closing.
|
|
659
|
-
* @param agent - the agent whose lineage is tested.
|
|
660
|
-
* @returns the closing teardown, or `undefined` while admission is open.
|
|
661
|
-
*/
|
|
662
|
-
closingTeardownFor(agent) {
|
|
663
|
-
if (this.draining)
|
|
664
|
-
return 'manager';
|
|
665
|
-
const lineage = this.liveLineage(agent);
|
|
666
|
-
for (const [root, members] of this.closingScopes) {
|
|
667
|
-
if (members.has(agent) || lineage.includes(root))
|
|
668
|
-
return root;
|
|
669
|
-
}
|
|
670
|
-
return undefined;
|
|
671
|
-
}
|
|
672
|
-
/** Reject new admission once the manager or this exact parent tree began draining. */
|
|
673
|
-
assertAdmitting(agent) {
|
|
674
|
-
const closing = this.closingTeardownFor(agent);
|
|
675
|
-
if (closing === undefined)
|
|
676
|
-
return;
|
|
677
|
-
throw new SubagentError(closing === 'manager'
|
|
678
|
-
? 'continuable subagents are draining; the operation was not admitted'
|
|
679
|
-
: `continuable subagents below parent "${closing.id}" are draining; the operation was not admitted`, 'DRAINING');
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Derive residency from Agent quiescence and the owned-child set. `running`
|
|
683
|
-
* covers an active admission, an open turn, or accepted waking inbox work.
|
|
684
|
-
*
|
|
685
|
-
* `Agent.status` alone is insufficient: it stays `idle` between an accepted
|
|
686
|
-
* waking send and the microtask that admits it, so a synchronous inbox
|
|
687
|
-
* observer would see `settled` while a turn is already queued. `accepted`
|
|
688
|
-
* holds the ids this manager admitted but has not yet seen drained.
|
|
689
|
-
*/
|
|
690
|
-
stateOf(activation) {
|
|
691
|
-
if (activation.handle.agent.status === 'running' || activation.accepted.size > 0)
|
|
692
|
-
return 'running';
|
|
693
|
-
if (activation.ownedChildren.size > 0)
|
|
694
|
-
return 'waiting';
|
|
695
|
-
return 'settled';
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Cold-resume a persisted child: retain and authorize its prepared Session, fold the
|
|
699
|
-
* generic descriptor, create the Activation through `ctx.agents.resume()`,
|
|
700
|
-
* and submit the waiting turn. This never dispatches through a subagent
|
|
701
|
-
* provider — the persisted Session already holds the initial prefix and the
|
|
702
|
-
* descriptor is the whole reconstruction input.
|
|
331
|
+
* Cold-resume a persisted child and submit the waiting turn. The descriptor
|
|
332
|
+
* supplies every reconstruction input; no subagent provider is dispatched.
|
|
703
333
|
*/
|
|
704
334
|
async coldResume(parent, childId, content, options) {
|
|
705
335
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
@@ -716,20 +346,15 @@ export class SubagentContinuationManager {
|
|
|
716
346
|
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
|
|
717
347
|
}
|
|
718
348
|
const source = __addDisposableResource(env_1, observation, false);
|
|
719
|
-
this.assertAdmitting(parent);
|
|
720
|
-
|
|
721
|
-
// exact live direct parent may continue it.
|
|
722
|
-
this.authorizeLineage(parent, childId, source.header.parentSession);
|
|
723
|
-
// Fold only the child's own suffix: a fork seed replays the parent's log,
|
|
724
|
-
// which may carry an ANCESTOR's descriptor when the parent is itself a
|
|
725
|
-
// continuable child.
|
|
349
|
+
this.activations.assertAdmitting(parent);
|
|
350
|
+
this.activations.authorizeLineage(parent, childId, source.header.parentSession);
|
|
726
351
|
const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
|
|
727
352
|
if (descriptor === undefined || descriptor.mode !== 'continuable') {
|
|
728
353
|
throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
|
|
729
354
|
}
|
|
730
355
|
let activation;
|
|
731
356
|
try {
|
|
732
|
-
activation = await this.materialize({
|
|
357
|
+
activation = await this.activations.materialize({
|
|
733
358
|
childId,
|
|
734
359
|
provider: descriptor.provider,
|
|
735
360
|
parent,
|
|
@@ -760,22 +385,12 @@ export class SubagentContinuationManager {
|
|
|
760
385
|
__disposeResources(env_1);
|
|
761
386
|
}
|
|
762
387
|
}
|
|
763
|
-
/**
|
|
764
|
-
* Submit to a freshly materialized Activation or roll it back completely.
|
|
765
|
-
* @param activation - the just-published Activation to admit or release.
|
|
766
|
-
* @param content - the initial or resumed message content.
|
|
767
|
-
* @param options - durable source, scheduling, and pre-acceptance cancellation.
|
|
768
|
-
* @param parent - the live direct parent authorizing admission.
|
|
769
|
-
* @returns the accepted inbox message id.
|
|
770
|
-
*/
|
|
388
|
+
/** Submit to a freshly materialized Activation or roll it back completely. */
|
|
771
389
|
async submitMaterialized(activation, content, options, parent) {
|
|
772
390
|
try {
|
|
773
391
|
if (contentHasImage(content)) {
|
|
774
|
-
// The capability read awaits with the activation already published, so
|
|
775
|
-
// the disposal cutoff is re-checked before the submit; a drain that
|
|
776
|
-
// began during the read turns into a clean closing rejection.
|
|
777
392
|
await this.assertImageCapable(activation.handle.agent, options.signal);
|
|
778
|
-
if (activation.
|
|
393
|
+
if (activation.inbox.closing !== undefined) {
|
|
779
394
|
throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
|
|
780
395
|
}
|
|
781
396
|
}
|
|
@@ -784,29 +399,24 @@ export class SubagentContinuationManager {
|
|
|
784
399
|
catch (error) {
|
|
785
400
|
/* v8 ignore next -- rollback disposal failures must not mask the
|
|
786
401
|
* pre-acceptance signal, drain, or lifecycle failure. */
|
|
787
|
-
await this.dispose(activation).catch(() => undefined);
|
|
402
|
+
await this.activations.dispose(activation).catch(() => undefined);
|
|
788
403
|
throw error;
|
|
789
404
|
}
|
|
790
405
|
}
|
|
791
|
-
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
* @param agent - the live or freshly materialized child agent.
|
|
800
|
-
* @param signal - caller cancellation bounding the model-info read.
|
|
801
|
-
* @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
|
|
802
|
-
*/
|
|
406
|
+
/** Build and submit one message across the final synchronous admission cutoff. */
|
|
407
|
+
submitAdmitted(activation, content, options, parent) {
|
|
408
|
+
const message = options.source === undefined
|
|
409
|
+
? createAgentMessage(parent, content)
|
|
410
|
+
: createUserMessage({ content, source: options.source });
|
|
411
|
+
return this.activations.submitAdmitted(activation, message, options.delivery, parent, options.signal);
|
|
412
|
+
}
|
|
413
|
+
/** Refuse image content for a child whose fixed model accepts text only. */
|
|
803
414
|
async assertImageCapable(agent, signal) {
|
|
804
415
|
const { provider, model } = agent.options;
|
|
805
416
|
if (provider === undefined || model === undefined)
|
|
806
417
|
return;
|
|
807
418
|
const llm = this.ctx.get('llm');
|
|
808
|
-
/* v8 ignore next --
|
|
809
|
-
* to refuse against; delivery then defers to the text-only projection. */
|
|
419
|
+
/* v8 ignore next -- without an LLM registry, delivery defers to projection. */
|
|
810
420
|
if (llm === undefined)
|
|
811
421
|
return;
|
|
812
422
|
const info = await llm.resolveModelInfo(provider, model, signal);
|
|
@@ -814,468 +424,6 @@ export class SubagentContinuationManager {
|
|
|
814
424
|
throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
|
|
815
425
|
}
|
|
816
426
|
}
|
|
817
|
-
/**
|
|
818
|
-
* Create or resume the child Agent through the private activation-owner
|
|
819
|
-
* scope, install the handle in a fresh Activation, and register ownership on
|
|
820
|
-
* a continuation-managed parent. Rejection leaves no Activation, no handle,
|
|
821
|
-
* and no ownership membership.
|
|
822
|
-
*/
|
|
823
|
-
materialize(inputs) {
|
|
824
|
-
this.assertAdmitting(inputs.parent);
|
|
825
|
-
const settled = Promise.withResolvers();
|
|
826
|
-
const lineage = this.liveLineage(inputs.parent);
|
|
827
|
-
const materialization = {
|
|
828
|
-
lineage,
|
|
829
|
-
settled: settled.promise,
|
|
830
|
-
};
|
|
831
|
-
this.materializations.add(materialization);
|
|
832
|
-
return this.materializeTracked(inputs, lineage).finally(() => {
|
|
833
|
-
this.materializations.delete(materialization);
|
|
834
|
-
settled.resolve();
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Perform one tracked materialization. The caller keeps the drain barrier
|
|
839
|
-
* registered until this either returns a resident Activation or finishes
|
|
840
|
-
* rollback.
|
|
841
|
-
*/
|
|
842
|
-
async materializeTracked(inputs, parentLineage) {
|
|
843
|
-
const { childId, provider, parent, create } = inputs;
|
|
844
|
-
// No id pre-check here: the child lock serializes each durable child, both
|
|
845
|
-
// callers reach this only after confirming no Activation exists, and
|
|
846
|
-
// `AgentRegistry.enter()` is the authoritative collision boundary for an id
|
|
847
|
-
// some other owner holds — a duplicate would reject there with rollback.
|
|
848
|
-
inputs.signal.throwIfAborted();
|
|
849
|
-
const setup = (childCtx) => {
|
|
850
|
-
// Only fresh creation seeds the delegation policy onto the child's own
|
|
851
|
-
// log (after any fork seed, so fresh policy wins stale seed state); a
|
|
852
|
-
// cold resume replays those persisted events instead.
|
|
853
|
-
if (create !== undefined) {
|
|
854
|
-
appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
|
|
855
|
-
}
|
|
856
|
-
applyChildComposition(childCtx, parent, inputs.composition);
|
|
857
|
-
};
|
|
858
|
-
const observer = this.host.observeActivation(provider, childId, parent);
|
|
859
|
-
// Agent creation owns rollback before handle transfer. A rejection leaves
|
|
860
|
-
// no resident Activation and therefore publishes no lifecycle edge.
|
|
861
|
-
const handle = create === undefined
|
|
862
|
-
? await this.ownerCtx.agents.resume({
|
|
863
|
-
resumeSessionId: childId,
|
|
864
|
-
agentOptions: inputs.agentOptions,
|
|
865
|
-
signal: inputs.signal,
|
|
866
|
-
setup,
|
|
867
|
-
})
|
|
868
|
-
: await this.ownerCtx.agents.create({
|
|
869
|
-
sessionId: childId,
|
|
870
|
-
meta: create.meta,
|
|
871
|
-
seed: create.seed,
|
|
872
|
-
inheritedEventCount: create.inheritedEventCount,
|
|
873
|
-
agentOptions: inputs.agentOptions,
|
|
874
|
-
signal: inputs.signal,
|
|
875
|
-
setup,
|
|
876
|
-
});
|
|
877
|
-
const activation = {
|
|
878
|
-
childId,
|
|
879
|
-
// The durable lineage, not merely the caller: creation stamps this same
|
|
880
|
-
// agent into the child's header, and cold resume authorized it against
|
|
881
|
-
// the persisted header before materializing.
|
|
882
|
-
parentSession: parent.id,
|
|
883
|
-
provider,
|
|
884
|
-
handle,
|
|
885
|
-
ancestry: new WeakSet([handle.agent, ...parentLineage]),
|
|
886
|
-
ownedChildren: new Set(),
|
|
887
|
-
observer,
|
|
888
|
-
disposal: undefined,
|
|
889
|
-
accepted: new Set(),
|
|
890
|
-
announced: false,
|
|
891
|
-
poke: Promise.withResolvers(),
|
|
892
|
-
};
|
|
893
|
-
// After transfer, any failure must dispose the created handle, remove the
|
|
894
|
-
// Activation, and roll back parent ownership before rejecting.
|
|
895
|
-
this.activations.set(childId, activation);
|
|
896
|
-
try {
|
|
897
|
-
inputs.signal.throwIfAborted();
|
|
898
|
-
this.assertAdmitting(parent);
|
|
899
|
-
this.acquireOwnership(parent, childId);
|
|
900
|
-
// Every accepted id leaves the inbox exactly once, through dequeue or
|
|
901
|
-
// discard. Clearing it there is what lets `stateOf()` distinguish a truly
|
|
902
|
-
// quiet Agent from one whose accepted turn has not been admitted yet.
|
|
903
|
-
// Registered through the child's own scoped context, so scope filtering
|
|
904
|
-
// already restricts both listeners to this exact agent.
|
|
905
|
-
handle.agent.ctx.on('agent/inbox/claimed', ({ message }) => {
|
|
906
|
-
/* v8 ignore next -- a claim of an id this manager never admitted needs
|
|
907
|
-
* another sender on the same child, which no current path allows. */
|
|
908
|
-
if (activation.accepted.delete(message.id))
|
|
909
|
-
this.wake(activation);
|
|
910
|
-
});
|
|
911
|
-
handle.agent.ctx.on('agent/inbox/discarded', ({ message }) => {
|
|
912
|
-
if (activation.accepted.delete(message.id))
|
|
913
|
-
this.wake(activation);
|
|
914
|
-
});
|
|
915
|
-
// Agent creation committed setup at its publication boundary;
|
|
916
|
-
// revocations from here on are immediate live revocation.
|
|
917
|
-
// Publish the start edge before any turn can run, so observers see this
|
|
918
|
-
// epoch before its first request.
|
|
919
|
-
observer.start(handle.agent);
|
|
920
|
-
}
|
|
921
|
-
catch (error) {
|
|
922
|
-
// Listener exceptions are contained by the lifecycle emitter; a start
|
|
923
|
-
// publication throw therefore leaves no residency edge to pair.
|
|
924
|
-
/* v8 ignore next -- rollback failure must not mask the admission failure
|
|
925
|
-
* that prevented this operation from returning an accepted message id. */
|
|
926
|
-
await this.rollbackUnpublished(activation).catch(() => undefined);
|
|
927
|
-
throw error;
|
|
928
|
-
}
|
|
929
|
-
this.watchSettlement(activation);
|
|
930
|
-
return activation;
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Release an Activation whose start edge was not published. The memoized
|
|
934
|
-
* transaction remains in the live map until handle disposal settles, so a
|
|
935
|
-
* concurrent drain or delivery observes the same closing boundary.
|
|
936
|
-
*/
|
|
937
|
-
rollbackUnpublished(activation) {
|
|
938
|
-
return (activation.disposal ??= (async () => {
|
|
939
|
-
try {
|
|
940
|
-
await activation.handle.dispose();
|
|
941
|
-
}
|
|
942
|
-
finally {
|
|
943
|
-
this.activations.delete(activation.childId);
|
|
944
|
-
this.releaseOwnership(activation.childId);
|
|
945
|
-
}
|
|
946
|
-
})());
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Register the child in a continuation-managed parent's owned set before the
|
|
950
|
-
* child can run, so that parent cannot settle while the child is live. A
|
|
951
|
-
* top-level or other non-continuation Agent has no Activation and stays
|
|
952
|
-
* outside the waiting graph.
|
|
953
|
-
*/
|
|
954
|
-
acquireOwnership(parent, childId) {
|
|
955
|
-
const parentActivation = this.activations.get(parent.id);
|
|
956
|
-
if (parentActivation === undefined)
|
|
957
|
-
return;
|
|
958
|
-
if (parentActivation.disposal !== undefined) {
|
|
959
|
-
throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
|
|
960
|
-
}
|
|
961
|
-
parentActivation.ownedChildren.add(childId);
|
|
962
|
-
}
|
|
963
|
-
/** Remove one child from its live owner's set and let that owner re-check settlement. */
|
|
964
|
-
releaseOwnership(childId) {
|
|
965
|
-
for (const candidate of this.activations.values()) {
|
|
966
|
-
if (candidate.ownedChildren.delete(childId))
|
|
967
|
-
this.wake(candidate);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
/** Let a settlement watcher re-observe quiescence after ownership or inbox changes. */
|
|
971
|
-
wake(activation) {
|
|
972
|
-
activation.poke.resolve();
|
|
973
|
-
activation.poke = Promise.withResolvers();
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Submit one message as the child's next FIFO turn and return its accepted
|
|
977
|
-
* inbox id. Acceptance is the operation's success boundary; the manager owns
|
|
978
|
-
* the Activation independently afterwards.
|
|
979
|
-
*/
|
|
980
|
-
submit(activation, content, options, parent) {
|
|
981
|
-
// Parent-originated delivery keeps the parent live through ownership, so
|
|
982
|
-
// establish it before the message can enter the child's inbox.
|
|
983
|
-
this.acquireOwnership(parent, activation.childId);
|
|
984
|
-
const message = options.delivery === 'steer'
|
|
985
|
-
? agentMessage(parent, content)
|
|
986
|
-
: createUserMessage({ content, source: options.source });
|
|
987
|
-
const accepted = this.admitWaking(activation, message.id, () => {
|
|
988
|
-
if (options.delivery === 'steer')
|
|
989
|
-
activation.handle.agent.steer(message);
|
|
990
|
-
else
|
|
991
|
-
activation.handle.agent.followup(message);
|
|
992
|
-
});
|
|
993
|
-
// Past this point the caller has an id for this child, so its eventual
|
|
994
|
-
// settlement is something the parent is owed an account of.
|
|
995
|
-
activation.announced = true;
|
|
996
|
-
return accepted;
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Account one waking send across a resident Activation's settlement window.
|
|
1000
|
-
* @param activation - Activation receiving waking inbox work.
|
|
1001
|
-
* @param messageId - stable identity of the message about to be sent.
|
|
1002
|
-
* @param send - synchronous send that publishes one enqueue occurrence.
|
|
1003
|
-
* @returns the accepted message id.
|
|
1004
|
-
*/
|
|
1005
|
-
admitWaking(activation, messageId, send) {
|
|
1006
|
-
// Waking Agent sends publish inbox events synchronously, so observers must
|
|
1007
|
-
// see this Activation as busy before the call begins.
|
|
1008
|
-
activation.accepted.add(messageId);
|
|
1009
|
-
try {
|
|
1010
|
-
send();
|
|
1011
|
-
}
|
|
1012
|
-
catch (error) {
|
|
1013
|
-
activation.accepted.delete(messageId);
|
|
1014
|
-
throw error;
|
|
1015
|
-
}
|
|
1016
|
-
// Accepted waking work keeps this Activation live until whenIdle() observes
|
|
1017
|
-
// the complete waking suffix.
|
|
1018
|
-
this.wake(activation);
|
|
1019
|
-
return messageId;
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Cross the final admission cutoff and submit without yielding. Signal abort,
|
|
1023
|
-
* manager drain, or Activation disposal that wins before this synchronous
|
|
1024
|
-
* span rejects without inbox acceptance.
|
|
1025
|
-
*/
|
|
1026
|
-
submitAdmitted(activation, content, options, parent) {
|
|
1027
|
-
options.signal.throwIfAborted();
|
|
1028
|
-
this.assertAdmitting(parent);
|
|
1029
|
-
/* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
|
|
1030
|
-
* this field between the caller's live check and this no-await boundary. */
|
|
1031
|
-
if (disposalOf(activation) !== undefined) {
|
|
1032
|
-
throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, 'ACTIVATION_CLOSING');
|
|
1033
|
-
}
|
|
1034
|
-
this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
|
|
1035
|
-
return this.submit(activation, content, options, parent);
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Authorize one operation against the durable direct-parent lineage. Other
|
|
1039
|
-
* agents, ancestors, teams, workflows, and hosts remain rejected until an
|
|
1040
|
-
* explicit authority protocol has a production consumer.
|
|
1041
|
-
*/
|
|
1042
|
-
authorizeLineage(parent, childId, parentSession) {
|
|
1043
|
-
if (this.ctx.agents.get(parent.id) !== parent) {
|
|
1044
|
-
throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, 'UNAUTHORIZED');
|
|
1045
|
-
}
|
|
1046
|
-
if (parentSession !== parent.id) {
|
|
1047
|
-
throw new SubagentError(`subagent "${childId}" belongs to another parent session`, 'UNAUTHORIZED');
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Follow one Activation to settlement: wait for Agent quiescence, then for
|
|
1052
|
-
* every owned child to complete disposal, and dispose the handle once both
|
|
1053
|
-
* hold. A `next-turn` delivered while `waiting` wakes the same Agent and
|
|
1054
|
-
* returns it to `running`, so this re-observes rather than settling early.
|
|
1055
|
-
*/
|
|
1056
|
-
watchSettlement(activation) {
|
|
1057
|
-
void (async () => {
|
|
1058
|
-
while (disposalOf(activation) === undefined) {
|
|
1059
|
-
const poked = activation.poke.promise;
|
|
1060
|
-
await Promise.race([activation.handle.agent.whenIdle(), poked]);
|
|
1061
|
-
if (disposalOf(activation) !== undefined)
|
|
1062
|
-
return;
|
|
1063
|
-
// Re-check settlement INSIDE the child lock and begin disposal in the
|
|
1064
|
-
// same critical section, so a concurrent delivery either wins admission
|
|
1065
|
-
// before the transaction opens or waits for release and cold-resumes.
|
|
1066
|
-
// Deciding outside the lock would let a delivery observe a not-yet
|
|
1067
|
-
// resident handle that this watcher is already about to tear down.
|
|
1068
|
-
const settling = await this.locks.run(activation.childId, () => {
|
|
1069
|
-
if (disposalOf(activation) !== undefined || this.stateOf(activation) !== 'settled') {
|
|
1070
|
-
return Promise.resolve({ settling: false });
|
|
1071
|
-
}
|
|
1072
|
-
// `dispose()` assigns its memoized transaction synchronously, so
|
|
1073
|
-
// admission is closed before this critical section releases.
|
|
1074
|
-
return Promise.resolve({ settling: true, done: this.dispose(activation) });
|
|
1075
|
-
});
|
|
1076
|
-
if (!settling.settling) {
|
|
1077
|
-
// Still running, or waiting on descendants: re-observe after the next
|
|
1078
|
-
// accepted message or ownership release.
|
|
1079
|
-
if (activation.handle.agent.status !== 'running')
|
|
1080
|
-
await poked;
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
try {
|
|
1084
|
-
await settling.done;
|
|
1085
|
-
}
|
|
1086
|
-
catch (error) {
|
|
1087
|
-
this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${errorChain(error)}`);
|
|
1088
|
-
}
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
})();
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Stop one Activation immediately, then release it child-first. The memoized
|
|
1095
|
-
* transaction is installed before cancellation or recursive callbacks, so
|
|
1096
|
-
* admission and reentrant teardown converge on the same owner.
|
|
1097
|
-
*
|
|
1098
|
-
* The final session flush is best effort and never prevents handle disposal
|
|
1099
|
-
* or ownership release, because retaining a child would permanently pin its
|
|
1100
|
-
* ancestors in `waiting`.
|
|
1101
|
-
* @param activation - the residency epoch to stop and release.
|
|
1102
|
-
* @returns the one disposal transaction owned by this Activation.
|
|
1103
|
-
*/
|
|
1104
|
-
dispose(activation) {
|
|
1105
|
-
const existing = activation.disposal;
|
|
1106
|
-
if (existing !== undefined)
|
|
1107
|
-
return existing;
|
|
1108
|
-
const completion = Promise.withResolvers();
|
|
1109
|
-
// Presence is the admission cutoff. Assign it before the async helper starts
|
|
1110
|
-
// because that helper cancels Agents and may synchronously re-enter callers.
|
|
1111
|
-
activation.disposal = completion.promise;
|
|
1112
|
-
void this.finishDisposal(activation).then(completion.resolve, completion.reject);
|
|
1113
|
-
return completion.promise;
|
|
1114
|
-
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Propagate stop synchronously, then finish the child-first release.
|
|
1117
|
-
* @param activation - the Activation whose disposal transaction is installed.
|
|
1118
|
-
* @returns once the handle and ownership edge are released.
|
|
1119
|
-
*/
|
|
1120
|
-
async finishDisposal(activation) {
|
|
1121
|
-
this.wake(activation);
|
|
1122
|
-
const { childId } = activation;
|
|
1123
|
-
// Stop top-down before the first await. Slow descendant cleanup may delay
|
|
1124
|
-
// release, but it cannot let this ancestor continue model or tool work.
|
|
1125
|
-
activation.handle.agent.cancel({ kind: 'parent' });
|
|
1126
|
-
const idle = activation.handle.agent.whenIdle();
|
|
1127
|
-
const children = [...activation.ownedChildren]
|
|
1128
|
-
.map(child => this.activations.get(child))
|
|
1129
|
-
.filter((child) => child !== undefined);
|
|
1130
|
-
const childDisposals = children.map(child => this.dispose(child));
|
|
1131
|
-
const failures = [];
|
|
1132
|
-
try {
|
|
1133
|
-
// Release remains child-first even though cancellation propagated
|
|
1134
|
-
// top-down: every owned child completes before this handle is removed.
|
|
1135
|
-
const childFailures = await Promise.all(childDisposals.map(async (disposal) => {
|
|
1136
|
-
try {
|
|
1137
|
-
await disposal;
|
|
1138
|
-
return undefined;
|
|
1139
|
-
}
|
|
1140
|
-
catch (error) {
|
|
1141
|
-
return error;
|
|
1142
|
-
}
|
|
1143
|
-
}));
|
|
1144
|
-
const reasons = childFailures.filter(reason => reason !== undefined);
|
|
1145
|
-
if (reasons.length > 0) {
|
|
1146
|
-
failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map(reason => errorChain(reason)).join('; ')}`, 'ACTIVATION_TEARDOWN_FAILED'));
|
|
1147
|
-
}
|
|
1148
|
-
// Quiesce before the flush: a turn still running would keep
|
|
1149
|
-
// appending events the flush cannot cover.
|
|
1150
|
-
await idle;
|
|
1151
|
-
await this.flushFinalState(activation);
|
|
1152
|
-
// Capture the child-dependent edge data while the child is still live:
|
|
1153
|
-
// handle disposal unregisters it, and consumers read its log and scope.
|
|
1154
|
-
activation.observer.capture(activation.handle.agent);
|
|
1155
|
-
}
|
|
1156
|
-
catch (error) {
|
|
1157
|
-
failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
|
|
1158
|
-
}
|
|
1159
|
-
try {
|
|
1160
|
-
await activation.handle.dispose();
|
|
1161
|
-
}
|
|
1162
|
-
catch (error) {
|
|
1163
|
-
failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
|
|
1164
|
-
}
|
|
1165
|
-
let failure;
|
|
1166
|
-
if (failures.length === 1) {
|
|
1167
|
-
failure = failures[0];
|
|
1168
|
-
}
|
|
1169
|
-
else if (failures.length > 1) {
|
|
1170
|
-
failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
|
|
1171
|
-
+ failures.map(item => errorChain(item)).join('; '), 'ACTIVATION_TEARDOWN_FAILED', { cause: new AggregateError(failures) });
|
|
1172
|
-
}
|
|
1173
|
-
// Only now is the Activation gone: keeping the entry until disposal settles
|
|
1174
|
-
// makes a racing delivery wait for release rather than cold-resume into the
|
|
1175
|
-
// still-registered agent.
|
|
1176
|
-
this.activations.delete(childId);
|
|
1177
|
-
// BEFORE releasing ownership, while the parent still counts this child and
|
|
1178
|
-
// therefore cannot be judged settled. Delivering after the release would
|
|
1179
|
-
// race a parent watcher that resumes one microtask later, finds itself
|
|
1180
|
-
// childless and quiet, and disposes an Agent whose `cancel()` clears the
|
|
1181
|
-
// inbox this notice is sitting in.
|
|
1182
|
-
this.notifySettlement(activation, activation.observer.terminal(failure));
|
|
1183
|
-
// Release ownership even on failure: a retained failed child would pin its
|
|
1184
|
-
// ancestors in `waiting` forever.
|
|
1185
|
-
this.releaseOwnership(childId);
|
|
1186
|
-
// Emit once the disposal outcome is known, so a rejecting scoped cleanup
|
|
1187
|
-
// cannot be reported as a successful epoch.
|
|
1188
|
-
activation.observer.settle(failure);
|
|
1189
|
-
if (failure !== undefined)
|
|
1190
|
-
throw failure;
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Tell the durable direct parent that this child produced everything it is
|
|
1194
|
-
* going to. Unconditional for every child the caller received an id for: it
|
|
1195
|
-
* does not consider whether the child reported, because the cases that most
|
|
1196
|
-
* need it — a token ceiling, a model failure, cancellation, teardown — are
|
|
1197
|
-
* exactly the ones where the child never got to choose. A materialization
|
|
1198
|
-
* rolled back before its first acceptance stays silent, since the caller was
|
|
1199
|
-
* told that child was not established. A parent that is no longer live is not
|
|
1200
|
-
* an error; the child's own Session remains the durable record either way.
|
|
1201
|
-
* A parent whose own lineage is already closing receives the notice without a
|
|
1202
|
-
* wake, because teardown is not a reason to start a turn.
|
|
1203
|
-
*
|
|
1204
|
-
* Never blocks disposal. A delivery failure is logged and dropped, because
|
|
1205
|
-
* retaining a child to retry a notice would pin its whole ancestry in
|
|
1206
|
-
* `waiting` forever.
|
|
1207
|
-
* @param activation - the settling Activation, still owned by its parent.
|
|
1208
|
-
* @param terminal - how this epoch ended, as the terminal edge will report it.
|
|
1209
|
-
*/
|
|
1210
|
-
notifySettlement(activation, terminal) {
|
|
1211
|
-
if (!activation.announced)
|
|
1212
|
-
return;
|
|
1213
|
-
try {
|
|
1214
|
-
const parent = this.ctx.agents.get(activation.parentSession);
|
|
1215
|
-
if (parent === undefined)
|
|
1216
|
-
return;
|
|
1217
|
-
const summary = settlementSummary(activation.childId, terminal.stopReason);
|
|
1218
|
-
const message = createUserMessage({
|
|
1219
|
-
content: [
|
|
1220
|
-
{ type: 'text', text: summary },
|
|
1221
|
-
...terminal.output === undefined
|
|
1222
|
-
? [{ type: 'text', text: 'It left no closing message.' }]
|
|
1223
|
-
: [{ type: 'text', text: 'Its closing message:' }, ...terminal.output],
|
|
1224
|
-
],
|
|
1225
|
-
source: {
|
|
1226
|
-
kind: 'subagent-settled',
|
|
1227
|
-
form: 'notice',
|
|
1228
|
-
summary: boundContextSummary(summary),
|
|
1229
|
-
senderSessionId: activation.childId,
|
|
1230
|
-
},
|
|
1231
|
-
});
|
|
1232
|
-
// A parent whose own teardown already began must not be woken. Waking is
|
|
1233
|
-
// not a queue operation: `followup()` on a quiescent Agent starts a turn,
|
|
1234
|
-
// and `cancel()` does not arm against a later one, so a notice arriving
|
|
1235
|
-
// during teardown would spend a model request on an Agent its host is
|
|
1236
|
-
// about to dispose — once per tree layer, since each layer's own notice
|
|
1237
|
-
// then wakes the layer above it. Injecting delivers to a parent still
|
|
1238
|
-
// reading its inbox and records the account in the log either way; it
|
|
1239
|
-
// does NOT survive that parent's own disposal, whose `keepInbox: false`
|
|
1240
|
-
// cancel durably clears whatever it never claimed.
|
|
1241
|
-
if (this.closingTeardownFor(parent) !== undefined) {
|
|
1242
|
-
parent.inject(message);
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
// An idle parent has nothing else to look at, so it gets one ordinary
|
|
1246
|
-
// turn. A busy parent is steered instead of woken: `Inbox.claim()` takes
|
|
1247
|
-
// the whole next-step batch at one boundary, so several children settling
|
|
1248
|
-
// together cost one step rather than one turn each. Steering rather than
|
|
1249
|
-
// injecting closes the window where a driver retires between this status
|
|
1250
|
-
// read and the send, which would strand the notice unclaimed.
|
|
1251
|
-
this.sendWaking(parent, message, () => {
|
|
1252
|
-
if (parent.status === 'idle')
|
|
1253
|
-
parent.followup(message);
|
|
1254
|
-
else
|
|
1255
|
-
parent.steer(message);
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
catch (error) {
|
|
1259
|
-
this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
|
|
1260
|
-
+ errorChain(error));
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Request a best-effort final session flush after the child is quiescent.
|
|
1265
|
-
* Listener failure is logged because flush participation cannot identify a
|
|
1266
|
-
* particular persistence backend, and teardown must still release ownership.
|
|
1267
|
-
* @param activation - the Activation whose final events should be flushed.
|
|
1268
|
-
*/
|
|
1269
|
-
async flushFinalState(activation) {
|
|
1270
|
-
const child = activation.handle.agent;
|
|
1271
|
-
try {
|
|
1272
|
-
await child.ctx.sessions.flush(child.session);
|
|
1273
|
-
}
|
|
1274
|
-
catch (error) {
|
|
1275
|
-
this.ctx.logger.warn(`subagent "${activation.childId}" best-effort final session flush failed; `
|
|
1276
|
-
+ `the persisted state may be unavailable or stale on resume: ${errorChain(error)}`);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
427
|
/** Resolve the persistence service continuable children require, or fail loud. */
|
|
1280
428
|
requirePersistence() {
|
|
1281
429
|
const persistence = this.ctx.get('sessionPersistence');
|