@qlzt/dsh-subagent 0.0.0-stage → 0.2.1-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/LICENSE +21 -0
- package/README.i18n.yaml +94 -0
- package/README.md +215 -2
- package/README.zh.md +216 -0
- package/lib/index.js +3333 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +991 -0
- package/lib/typert.remote-client.d.ts +25 -0
- package/lib/typert.remote-client.js +114 -0
- package/lib/types/activation.d.ts +114 -0
- package/lib/types/activation.js +112 -0
- package/lib/types/archive-admission.d.ts +16 -0
- package/lib/types/archive-admission.js +151 -0
- package/lib/types/assistant-output.d.ts +48 -0
- package/lib/types/assistant-output.js +73 -0
- package/lib/types/catalog.d.ts +92 -0
- package/lib/types/catalog.js +113 -0
- package/lib/types/child-agent.d.ts +154 -0
- package/lib/types/child-agent.js +211 -0
- package/lib/types/client.d.ts +8 -0
- package/lib/types/client.js +7 -0
- package/lib/types/continuation-messages.d.ts +63 -0
- package/lib/types/continuation-messages.js +114 -0
- package/lib/types/control-types.d.ts +142 -0
- package/lib/types/control-types.js +8 -0
- package/lib/types/control.d.ts +45 -0
- package/lib/types/control.js +79 -0
- package/lib/types/depth.d.ts +31 -0
- package/lib/types/depth.js +39 -0
- package/lib/types/descriptor.d.ts +105 -0
- package/lib/types/descriptor.js +192 -0
- package/lib/types/error.d.ts +11 -0
- package/lib/types/error.js +14 -0
- package/lib/types/index.d.ts +319 -0
- package/lib/types/index.js +455 -0
- package/lib/types/internal.d.ts +58 -0
- package/lib/types/internal.js +58 -0
- package/lib/types/lifecycle.d.ts +83 -0
- package/lib/types/lifecycle.js +115 -0
- package/lib/types/list-children.d.ts +36 -0
- package/lib/types/list-children.js +166 -0
- package/lib/types/manager.d.ts +247 -0
- package/lib/types/manager.js +1294 -0
- package/lib/types/out-of-process.d.ts +92 -0
- package/lib/types/out-of-process.js +178 -0
- package/lib/types/projection-types.d.ts +85 -0
- package/lib/types/projection-types.js +7 -0
- package/lib/types/projection.d.ts +100 -0
- package/lib/types/projection.js +157 -0
- package/lib/types/structured.d.ts +49 -0
- package/lib/types/structured.js +130 -0
- package/lib/types/types.d.ts +353 -0
- package/lib/types/types.js +19 -0
- package/package.json +136 -4
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns subagent startup, message admission, cold resume, and execution lifetimes.
|
|
3
|
+
* One child lock orders input acceptance and closure; one activation map owns
|
|
4
|
+
* capacity, parent relationships, and resource release across all providers.
|
|
5
|
+
*
|
|
6
|
+
* @module @qlzt/dsh-subagent/manager
|
|
7
|
+
*/
|
|
8
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
9
|
+
if (value !== null && value !== void 0) {
|
|
10
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
11
|
+
var dispose, inner;
|
|
12
|
+
if (async) {
|
|
13
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
14
|
+
dispose = value[Symbol.asyncDispose];
|
|
15
|
+
}
|
|
16
|
+
if (dispose === void 0) {
|
|
17
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
18
|
+
dispose = value[Symbol.dispose];
|
|
19
|
+
if (async) inner = dispose;
|
|
20
|
+
}
|
|
21
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
22
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
23
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
24
|
+
}
|
|
25
|
+
else if (async) {
|
|
26
|
+
env.stack.push({ async: true });
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
31
|
+
return function (env) {
|
|
32
|
+
function fail(e) {
|
|
33
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
34
|
+
env.hasError = true;
|
|
35
|
+
}
|
|
36
|
+
var r, s = 0;
|
|
37
|
+
function next() {
|
|
38
|
+
while (r = env.stack.pop()) {
|
|
39
|
+
try {
|
|
40
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
41
|
+
if (r.dispose) {
|
|
42
|
+
var result = r.dispose.call(r.value);
|
|
43
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
44
|
+
}
|
|
45
|
+
else s |= 1;
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
fail(e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
52
|
+
if (env.hasError) throw env.error;
|
|
53
|
+
}
|
|
54
|
+
return next();
|
|
55
|
+
};
|
|
56
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
57
|
+
var e = new Error(message);
|
|
58
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
59
|
+
});
|
|
60
|
+
import { randomUUID } from 'node:crypto';
|
|
61
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
62
|
+
import { brandString } from '@qlzt/dsh-brand';
|
|
63
|
+
import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@qlzt/dsh-llm';
|
|
64
|
+
import { SessionLogOffset } from '@qlzt/dsh-session';
|
|
65
|
+
import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
|
|
66
|
+
import { createAgentMessage, withContinuableReturnGuidance, createSettlementMessage } from "./continuation-messages.js";
|
|
67
|
+
import { assertSubagentMaxDepth } from "./depth.js";
|
|
68
|
+
import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
|
|
69
|
+
import { establishCatalogChild, establishExternalCatalogChild } from "./catalog.js";
|
|
70
|
+
import { SubagentError } from "./error.js";
|
|
71
|
+
import { isAdjacentAgentSendMessageTool } from "./internal.js";
|
|
72
|
+
import { attachStructuredRuntime } from "./structured.js";
|
|
73
|
+
import { ActivationPool, ChildLock, activationAgent, captureLocalResult, observeLocalFailure, requireLocalActivation } from "./activation.js";
|
|
74
|
+
/** Render only the safe outer message from execution or teardown failures. */
|
|
75
|
+
function failureMessage(error) {
|
|
76
|
+
return error instanceof Error ? error.message : 'unknown teardown failure';
|
|
77
|
+
}
|
|
78
|
+
/** Own subagent requests and the resources of every live activation. */
|
|
79
|
+
export class SubagentManager {
|
|
80
|
+
ctx;
|
|
81
|
+
host;
|
|
82
|
+
maxActiveSubagents;
|
|
83
|
+
/** Child session id → its live Activation. Process-local, never durable. */
|
|
84
|
+
resident = new Map();
|
|
85
|
+
/** Root identities retain their pool across child settlement without retaining dead roots. */
|
|
86
|
+
rootPools = new WeakMap();
|
|
87
|
+
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
88
|
+
materializations = new Set();
|
|
89
|
+
/** Per-child serializer shared by delivery, release, and disposal. */
|
|
90
|
+
locks = new ChildLock();
|
|
91
|
+
/** Structural Cordis owner of every Activation handle. */
|
|
92
|
+
ownerCtx;
|
|
93
|
+
/**
|
|
94
|
+
* Exact roots whose host teardown has begun, with the live lineage members
|
|
95
|
+
* observed under each root. Entries remain until that exact root leaves the
|
|
96
|
+
* Agent registry, closing admission throughout its host's teardown without
|
|
97
|
+
* poisoning a later same-id replacement.
|
|
98
|
+
*/
|
|
99
|
+
closingScopes = new Map();
|
|
100
|
+
draining = false;
|
|
101
|
+
/**
|
|
102
|
+
* Build one manager inside the service's Agent-injected context.
|
|
103
|
+
* @param ctx - context providing Agents, Sessions, and teardown ownership.
|
|
104
|
+
* @param host - provider dispatch and lifecycle observation owned by the service.
|
|
105
|
+
* @param maxActiveSubagents - the current configured child capacity.
|
|
106
|
+
*/
|
|
107
|
+
constructor(ctx, host, maxActiveSubagents) {
|
|
108
|
+
this.ctx = ctx;
|
|
109
|
+
this.host = host;
|
|
110
|
+
this.maxActiveSubagents = maxActiveSubagents;
|
|
111
|
+
// Ordinary Cordis owner effects unwind in reverse registration order, which
|
|
112
|
+
// cannot express the dynamic child graph. Register the private scope's
|
|
113
|
+
// structural disposer FIRST and the drain SECOND, so reverse unwind invokes
|
|
114
|
+
// the drain before releasing the scope; a cleanup effect on the same scope
|
|
115
|
+
// as the Agent handles would let structural handle disposal bypass
|
|
116
|
+
// child-first ordering.
|
|
117
|
+
const scope = ctx.plugin(function activationOwner() { });
|
|
118
|
+
this.ownerCtx = scope.ctx;
|
|
119
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
120
|
+
this.closingScopes.delete(agent);
|
|
121
|
+
});
|
|
122
|
+
ctx.effect(function* () {
|
|
123
|
+
yield scope.dispose;
|
|
124
|
+
yield () => this.drain();
|
|
125
|
+
}.bind(this), 'subagents.manager()');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Interrupt one live continuable child's current turn under the supplied authority.
|
|
129
|
+
* @param targetSessionId - the durable child session id to interrupt.
|
|
130
|
+
* @param authority - the human parent address or exact live ancestor Agent.
|
|
131
|
+
*/
|
|
132
|
+
interrupt(targetSessionId, authority) {
|
|
133
|
+
if (authority.kind === 'ancestor') {
|
|
134
|
+
const caller = authority.agent;
|
|
135
|
+
if (this.ctx.agents.get(caller.id) !== caller) {
|
|
136
|
+
throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, 'UNAUTHORIZED');
|
|
137
|
+
}
|
|
138
|
+
if (caller.id === targetSessionId) {
|
|
139
|
+
throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, 'UNAUTHORIZED');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const activation = this.resident.get(targetSessionId);
|
|
143
|
+
if (activation === undefined)
|
|
144
|
+
return;
|
|
145
|
+
if (authority.kind === 'user') {
|
|
146
|
+
if (activation.parent.id !== authority.parentSessionId) {
|
|
147
|
+
throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, 'UNAUTHORIZED');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (!activation.ancestry.has(authority.agent)) {
|
|
151
|
+
throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, 'UNAUTHORIZED');
|
|
152
|
+
}
|
|
153
|
+
// Disposal already stopped the target with a whole-Activation teardown;
|
|
154
|
+
// a second cancel would be a redundant signal on a closing handle.
|
|
155
|
+
if (activation.closing !== undefined)
|
|
156
|
+
return;
|
|
157
|
+
const kind = authority.kind === 'user' ? 'user' : 'parent';
|
|
158
|
+
if (activation.kind === 'local') {
|
|
159
|
+
activation.handle.agent.cancel({ kind }, { keepInbox: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
activation.controller.abort({ kind });
|
|
163
|
+
// The settlement observer reports teardown failures.
|
|
164
|
+
void this.dispose(activation).catch(() => undefined);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Close admission, await every already-admitted materialization through
|
|
169
|
+
* publication or rollback, then dispose the stable live Activation graph
|
|
170
|
+
* child-first.
|
|
171
|
+
*/
|
|
172
|
+
async drain() {
|
|
173
|
+
this.draining = true;
|
|
174
|
+
for (const pending of this.materializations)
|
|
175
|
+
pending.controller.abort();
|
|
176
|
+
await Promise.all([...this.materializations].map(materialization => materialization.settled));
|
|
177
|
+
const owned = new Set();
|
|
178
|
+
for (const activation of this.resident.values()) {
|
|
179
|
+
for (const child of activation.ownedChildren)
|
|
180
|
+
owned.add(child);
|
|
181
|
+
}
|
|
182
|
+
const roots = [...this.resident.values()].filter(activation => !owned.has(activation.childId));
|
|
183
|
+
await this.disposeRoots(roots, 'activation(s)');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Join progressing child work, leaving idle parked inboxes resident.
|
|
187
|
+
* @param parent - exact live parent whose descendants are observed.
|
|
188
|
+
* @returns whether work was joined and the host must recheck parent completion.
|
|
189
|
+
*/
|
|
190
|
+
async waitForChildren(parent) {
|
|
191
|
+
let observed = false;
|
|
192
|
+
while (true) {
|
|
193
|
+
const pending = [...this.materializations].filter(item => item.lineage.includes(parent));
|
|
194
|
+
const children = [...this.resident.values()].filter(item => activationAgent(item) !== parent && item.ancestry.has(parent));
|
|
195
|
+
observed ||= children.some(item => item.kind === 'local' && item.handle.agent.status !== 'idle');
|
|
196
|
+
await Promise.all(children.flatMap(item => item.kind === 'local' ? [item.handle.agent.whenIdle()] : []));
|
|
197
|
+
const waits = pending.map(item => item.settled);
|
|
198
|
+
for (const item of children) {
|
|
199
|
+
if (item.kind === 'external' || item.closing !== undefined) {
|
|
200
|
+
waits.push(item.result.promise.catch(() => undefined));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (item.handle.agent.status !== 'idle') {
|
|
204
|
+
waits.push(item.handle.agent.whenIdle());
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (item.ownedChildren.size > 0) {
|
|
208
|
+
// Published descendants are observed separately; preparation holds
|
|
209
|
+
// keep the owner pending until publication or rollback wakes it.
|
|
210
|
+
if ([...item.ownedChildren].some(id => !this.resident.has(id)))
|
|
211
|
+
waits.push(item.poke.promise);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const inbox = item.handle.agent.inbox;
|
|
215
|
+
if (item.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
|
|
216
|
+
continue;
|
|
217
|
+
waits.push(Promise.race([item.result.promise.catch(() => undefined), item.poke.promise]));
|
|
218
|
+
}
|
|
219
|
+
if (waits.length > 0) {
|
|
220
|
+
observed = true;
|
|
221
|
+
await Promise.all(waits);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// A descendant can be published while whenIdle follows another child.
|
|
225
|
+
if ([...this.materializations].some(item => item.lineage.includes(parent) && !pending.includes(item))
|
|
226
|
+
|| [...this.resident.values()].some(item => activationAgent(item) !== parent
|
|
227
|
+
&& item.ancestry.has(parent) && !children.includes(item)))
|
|
228
|
+
continue;
|
|
229
|
+
return observed;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Stop the managed descendants of exact live host-owned parents.
|
|
234
|
+
* @param parents - exact live roots whose managed descendants must stop.
|
|
235
|
+
*/
|
|
236
|
+
async drainDescendants(parents) {
|
|
237
|
+
const roots = new Set(parents.filter(parent => this.ctx.agents.get(parent.id) === parent));
|
|
238
|
+
if (roots.size === 0)
|
|
239
|
+
return;
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
this.closingMembers(root).add(root);
|
|
242
|
+
}
|
|
243
|
+
const targets = [];
|
|
244
|
+
for (const activation of this.resident.values()) {
|
|
245
|
+
const child = activationAgent(activation);
|
|
246
|
+
const lineage = this.liveLineage(child ?? activation.parent);
|
|
247
|
+
const owners = [...roots].filter(root => child !== root
|
|
248
|
+
&& activation.ancestry.has(root));
|
|
249
|
+
if (owners.length === 0)
|
|
250
|
+
continue;
|
|
251
|
+
targets.push(activation);
|
|
252
|
+
for (const owner of owners) {
|
|
253
|
+
const members = this.closingMembers(owner);
|
|
254
|
+
if (child !== undefined)
|
|
255
|
+
members.add(child);
|
|
256
|
+
for (const agent of lineage)
|
|
257
|
+
members.add(agent);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const materializations = [...this.materializations].filter((materialization) => {
|
|
261
|
+
const owners = [...roots].filter(root => materialization.lineage.includes(root));
|
|
262
|
+
for (const owner of owners) {
|
|
263
|
+
const members = this.closingMembers(owner);
|
|
264
|
+
for (const agent of materialization.lineage)
|
|
265
|
+
members.add(agent);
|
|
266
|
+
}
|
|
267
|
+
if (owners.length > 0)
|
|
268
|
+
materialization.controller.abort();
|
|
269
|
+
return owners.length > 0;
|
|
270
|
+
});
|
|
271
|
+
const ownedTargets = new Set();
|
|
272
|
+
for (const activation of targets) {
|
|
273
|
+
for (const child of activation.ownedChildren)
|
|
274
|
+
ownedTargets.add(child);
|
|
275
|
+
}
|
|
276
|
+
const targetRoots = targets.filter(activation => !ownedTargets.has(activation.childId));
|
|
277
|
+
for (const activation of targets) {
|
|
278
|
+
const disposal = this.dispose(activation);
|
|
279
|
+
void disposal.catch(() => undefined);
|
|
280
|
+
}
|
|
281
|
+
await Promise.all(materializations.map(materialization => materialization.settled));
|
|
282
|
+
await this.disposeRoots(targetRoots, 'scoped activation(s)');
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Release selected resident direct children of one exact live parent.
|
|
286
|
+
* @param parent - exact live direct parent authorizing the selected release.
|
|
287
|
+
* @param childIds - durable direct-child ids to release when resident.
|
|
288
|
+
*/
|
|
289
|
+
async drainChildren(parent, childIds) {
|
|
290
|
+
if (this.ctx.agents.get(parent.id) !== parent) {
|
|
291
|
+
throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED');
|
|
292
|
+
}
|
|
293
|
+
const targets = [];
|
|
294
|
+
for (const childId of new Set(childIds)) {
|
|
295
|
+
const activation = this.resident.get(childId);
|
|
296
|
+
if (activation === undefined)
|
|
297
|
+
continue;
|
|
298
|
+
if (activation.parent.id !== parent.id || !activation.ancestry.has(parent)) {
|
|
299
|
+
throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, 'UNAUTHORIZED');
|
|
300
|
+
}
|
|
301
|
+
targets.push(activation);
|
|
302
|
+
}
|
|
303
|
+
for (const activation of targets) {
|
|
304
|
+
const disposal = this.dispose(activation);
|
|
305
|
+
void disposal.catch(() => undefined);
|
|
306
|
+
}
|
|
307
|
+
await this.disposeRoots(targets, 'selected activation(s)');
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Start one continuable background child and resolve at initial inbox acceptance.
|
|
311
|
+
* Every earlier failure disposes any created handle and rolls back Activation
|
|
312
|
+
* and parent ownership without returning either id.
|
|
313
|
+
* @param spec - provider, delegation request, and caller cancellation.
|
|
314
|
+
* @returns the durable child id and accepted initial prompt message id.
|
|
315
|
+
*/
|
|
316
|
+
async startLocal(spec) {
|
|
317
|
+
const request = spec.request;
|
|
318
|
+
const parent = request.parent;
|
|
319
|
+
this.assertAdmitting(parent);
|
|
320
|
+
const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence();
|
|
321
|
+
assertSubagentMaxDepth(request.maxDepth);
|
|
322
|
+
const childId = spec.childId ?? brandString(randomUUID());
|
|
323
|
+
this.assertChildIdAvailable(childId);
|
|
324
|
+
const childDepth = resolveChildDepth(parent, request.maxDepth);
|
|
325
|
+
// Snapshot before any await: invalid descriptor JSON rejects the call
|
|
326
|
+
// before a child exists, and the detached value is what reaches the log.
|
|
327
|
+
const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
|
|
328
|
+
const agentProvider = agentOptions.provider;
|
|
329
|
+
const agentModel = agentOptions.model;
|
|
330
|
+
const agentReasoningEffort = agentOptions.reasoningEffort;
|
|
331
|
+
const descriptor = snapshotSubagentDescriptor({
|
|
332
|
+
mode: 'continuable',
|
|
333
|
+
provider: spec.provider,
|
|
334
|
+
label: spec.label,
|
|
335
|
+
...agentProvider !== undefined ? { agentProvider } : {},
|
|
336
|
+
...agentModel !== undefined ? { agentModel } : {},
|
|
337
|
+
...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
|
|
338
|
+
...request.persona !== undefined ? { persona: request.persona } : {},
|
|
339
|
+
...request.toolFilter !== undefined ? { toolFilter: request.toolFilter } : {},
|
|
340
|
+
});
|
|
341
|
+
// Capture before the first await: a later parent switch belongs to the
|
|
342
|
+
// parent's future, not to this child.
|
|
343
|
+
const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
|
|
344
|
+
// An idle continuation-managed parent must not settle while a caller is
|
|
345
|
+
// still creating its child. A turn-scoped delegation does not need this,
|
|
346
|
+
// but the service is also callable outside a turn.
|
|
347
|
+
const releaseHold = this.holdOwnership(parent, childId);
|
|
348
|
+
try {
|
|
349
|
+
const cwd = await this.resolveDirectory(request, spec.signal);
|
|
350
|
+
const prepared = await this.host.prepareContinuable(spec.provider, {
|
|
351
|
+
sessionId: childId,
|
|
352
|
+
cwd,
|
|
353
|
+
parent,
|
|
354
|
+
signal: spec.signal,
|
|
355
|
+
});
|
|
356
|
+
spec.signal.throwIfAborted();
|
|
357
|
+
this.assertAdmitting(parent);
|
|
358
|
+
const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
|
|
359
|
+
const seed = prepared.seed;
|
|
360
|
+
let established;
|
|
361
|
+
const messageId = await this.locks.run(childId, async () => {
|
|
362
|
+
spec.signal.throwIfAborted();
|
|
363
|
+
this.assertAdmitting(parent);
|
|
364
|
+
this.assertChildIdAvailable(childId);
|
|
365
|
+
if (spec.childId !== undefined) {
|
|
366
|
+
const persisted = await persistence?.stat(childId, { signal: spec.signal });
|
|
367
|
+
spec.signal.throwIfAborted();
|
|
368
|
+
this.assertAdmitting(parent);
|
|
369
|
+
this.assertChildIdAvailable(childId);
|
|
370
|
+
if (persisted !== undefined) {
|
|
371
|
+
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const activation = await this.materialize({
|
|
375
|
+
kind: 'local',
|
|
376
|
+
childId,
|
|
377
|
+
provider: spec.provider,
|
|
378
|
+
parent,
|
|
379
|
+
create: {
|
|
380
|
+
cwd,
|
|
381
|
+
seed,
|
|
382
|
+
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined),
|
|
383
|
+
inheritedEventCount,
|
|
384
|
+
delegatedPolicies,
|
|
385
|
+
descriptor,
|
|
386
|
+
},
|
|
387
|
+
agentOptions,
|
|
388
|
+
composition: { persona: request.persona, toolFilter: request.toolFilter },
|
|
389
|
+
signal: spec.signal,
|
|
390
|
+
delivery: spec.delivery,
|
|
391
|
+
outputSchema: spec.request.outputSchema,
|
|
392
|
+
});
|
|
393
|
+
established = activation;
|
|
394
|
+
const child = requireLocalActivation(activation).handle.agent;
|
|
395
|
+
const childHeader = child.session.header;
|
|
396
|
+
return await this.submitMaterialized(activation, spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child))
|
|
397
|
+
? withContinuableReturnGuidance(parent.id, request.prompt)
|
|
398
|
+
: request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent, () => {
|
|
399
|
+
establishCatalogChild(parent.session, childHeader, descriptor);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
return { ...this.receipt(established), childId, messageId };
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
releaseHold();
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Start an external backend under activation ownership.
|
|
411
|
+
* @param spec - task, provider, cancellation, and delivery policy.
|
|
412
|
+
* @returns the accepted activation and its result/disposal capabilities.
|
|
413
|
+
*/
|
|
414
|
+
async startExternal(spec) {
|
|
415
|
+
const parent = spec.request.parent;
|
|
416
|
+
this.assertAdmitting(parent);
|
|
417
|
+
const pendingId = brandString(randomUUID());
|
|
418
|
+
const releaseHold = this.holdOwnership(parent, pendingId);
|
|
419
|
+
let activation;
|
|
420
|
+
try {
|
|
421
|
+
const cwd = await this.resolveDirectory(spec.request, spec.signal);
|
|
422
|
+
activation = await this.materialize({
|
|
423
|
+
kind: 'external',
|
|
424
|
+
childId: pendingId,
|
|
425
|
+
provider: spec.provider,
|
|
426
|
+
parent,
|
|
427
|
+
signal: spec.signal,
|
|
428
|
+
delivery: spec.delivery,
|
|
429
|
+
start: signal => this.host.startExternal(spec.provider, {
|
|
430
|
+
...spec.request, cwd, label: spec.label, signal,
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
this.assertAdmitting(parent);
|
|
434
|
+
this.assertLiveParent(parent, activation.childId);
|
|
435
|
+
spec.signal.throwIfAborted();
|
|
436
|
+
if (spec.delivery === 'parent') {
|
|
437
|
+
establishExternalCatalogChild(parent.session, activation.childId, spec.label);
|
|
438
|
+
}
|
|
439
|
+
this.announce(activation);
|
|
440
|
+
return this.receipt(activation);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (activation !== undefined) {
|
|
444
|
+
try {
|
|
445
|
+
await this.dispose(activation);
|
|
446
|
+
}
|
|
447
|
+
catch (cleanupError) {
|
|
448
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
releaseHold();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Capture the explicit or inherited directory while the parent still owns startup. */
|
|
458
|
+
async resolveDirectory(request, signal) {
|
|
459
|
+
const cwd = request.cwd !== undefined && isAbsolute(request.cwd)
|
|
460
|
+
? request.cwd
|
|
461
|
+
: resolve(await this.ctx.workingDirectory.ensure(request.parent, signal), request.cwd ?? '.');
|
|
462
|
+
signal.throwIfAborted();
|
|
463
|
+
return cwd;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Deliver one model-authored message to a direct continuable child or to the
|
|
467
|
+
* sender's direct parent. A missing direct child cold-resumes through the
|
|
468
|
+
* ordinary continuation lifecycle.
|
|
469
|
+
* @param sender - exact live Agent authorizing and originating the message.
|
|
470
|
+
* @param targetId - durable direct-parent or direct-child session id.
|
|
471
|
+
* @param content - model-authored content to deliver.
|
|
472
|
+
* @param options - caller cancellation before acceptance.
|
|
473
|
+
* @returns the accepted message's inbox id.
|
|
474
|
+
*/
|
|
475
|
+
async sendMessage(sender, targetId, content, options) {
|
|
476
|
+
if (this.ctx.agents.get(sender.id) !== sender) {
|
|
477
|
+
throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
|
|
478
|
+
}
|
|
479
|
+
this.assertAdmitting(sender);
|
|
480
|
+
const senderActivation = this.resident.get(sender.id);
|
|
481
|
+
if (senderActivation !== undefined
|
|
482
|
+
&& senderActivation.kind === 'local'
|
|
483
|
+
&& senderActivation.handle.agent === sender
|
|
484
|
+
&& senderActivation.parent.id === targetId) {
|
|
485
|
+
options.signal.throwIfAborted();
|
|
486
|
+
return this.sendToParent(senderActivation, sender, content);
|
|
487
|
+
}
|
|
488
|
+
if (sender.session.header.parentSession === targetId) {
|
|
489
|
+
throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, 'UNAUTHORIZED');
|
|
490
|
+
}
|
|
491
|
+
return this.deliverToChild(sender, targetId, content, {
|
|
492
|
+
signal: options.signal,
|
|
493
|
+
delivery: 'steer',
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Queue one human-authored prompt as a distinct direct-child turn.
|
|
498
|
+
* @param parent - exact live direct parent authorizing delivery.
|
|
499
|
+
* @param childId - durable direct-child session id.
|
|
500
|
+
* @param content - model-visible prompt blocks.
|
|
501
|
+
* @param source - durable attribution for the human prompt.
|
|
502
|
+
* @param signal - caller cancellation before inbox acceptance.
|
|
503
|
+
* @returns the accepted durable message id.
|
|
504
|
+
*/
|
|
505
|
+
async queuePrompt(parent, childId, content, source, signal) {
|
|
506
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Steer one host-authored prompt to a direct continuable child.
|
|
510
|
+
* @param parent - exact live direct parent authorizing delivery.
|
|
511
|
+
* @param childId - durable direct-child session id.
|
|
512
|
+
* @param content - model-visible prompt blocks.
|
|
513
|
+
* @param source - durable attribution for the host prompt.
|
|
514
|
+
* @param signal - caller cancellation before inbox acceptance.
|
|
515
|
+
* @returns the accepted durable message id.
|
|
516
|
+
*/
|
|
517
|
+
async steerPrompt(parent, childId, content, source, signal) {
|
|
518
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' });
|
|
519
|
+
}
|
|
520
|
+
/** Admit local input synchronously before any teardown can begin. */
|
|
521
|
+
deliver(activation, message, delivery) {
|
|
522
|
+
if (activation.closing !== undefined) {
|
|
523
|
+
throw new SubagentError('subagent activation is being disposed; the message was not accepted', 'ACTIVATION_CLOSING');
|
|
524
|
+
}
|
|
525
|
+
const local = requireLocalActivation(activation);
|
|
526
|
+
if (local.structured?.acceptsInput() === false) {
|
|
527
|
+
throw new SubagentError('subagent is submitting or has submitted its structured result; the message was not accepted', 'INPUT_CLOSED');
|
|
528
|
+
}
|
|
529
|
+
if (delivery === 'steer')
|
|
530
|
+
local.handle.agent.steer(message);
|
|
531
|
+
else
|
|
532
|
+
local.handle.agent.followup(message);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Reject one child identity already owned by a live Agent or Session.
|
|
536
|
+
* @param childId - proposed durable child session id.
|
|
537
|
+
*/
|
|
538
|
+
assertChildIdAvailable(childId) {
|
|
539
|
+
if (this.resident.has(childId) || this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
|
|
540
|
+
throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Pre-register `childId` in a continuation-managed parent's owned set so the
|
|
545
|
+
* parent cannot settle while a caller is still establishing or resuming that
|
|
546
|
+
* child. Returns a releaser for the failure path; it removes only a hold
|
|
547
|
+
* this call added, and leaves ownership in place once a live Activation for
|
|
548
|
+
* the child exists.
|
|
549
|
+
* @param parent - the live direct parent the operation is admitted under.
|
|
550
|
+
* @param childId - the durable child the operation addresses.
|
|
551
|
+
* @returns the failure-path releaser; a no-op when nothing was added.
|
|
552
|
+
*/
|
|
553
|
+
holdOwnership(parent, childId) {
|
|
554
|
+
const parentActivation = this.resident.get(parent.id);
|
|
555
|
+
if (parentActivation === undefined || activationAgent(parentActivation) !== parent)
|
|
556
|
+
return () => { };
|
|
557
|
+
if (parentActivation.closing !== undefined) {
|
|
558
|
+
throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
|
|
559
|
+
}
|
|
560
|
+
if (parentActivation.ownedChildren.has(childId))
|
|
561
|
+
return () => { };
|
|
562
|
+
parentActivation.ownedChildren.add(childId);
|
|
563
|
+
return () => {
|
|
564
|
+
const live = this.resident.get(childId);
|
|
565
|
+
/* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
|
|
566
|
+
* between this operation's failure and its releaser running, which no test can schedule
|
|
567
|
+
* deterministically: the ownership edge then belongs to that live Activation, so the
|
|
568
|
+
* conservative keep leaves it for finishDisposal's releaseOwnership. */
|
|
569
|
+
if (live !== undefined && live.closing === undefined)
|
|
570
|
+
return;
|
|
571
|
+
if (parentActivation.ownedChildren.delete(childId))
|
|
572
|
+
this.wake(parentActivation);
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Apply activation admission to a resident parent before delivering input.
|
|
577
|
+
* @param parent - exact live Agent receiving the message.
|
|
578
|
+
* @param message - durable user message to deliver.
|
|
579
|
+
* @param delivery - receiving inbox destination.
|
|
580
|
+
*/
|
|
581
|
+
sendWaking(parent, message, delivery) {
|
|
582
|
+
const parentActivation = this.resident.get(parent.id);
|
|
583
|
+
if (parentActivation !== undefined && activationAgent(parentActivation) === parent) {
|
|
584
|
+
try {
|
|
585
|
+
this.deliver(parentActivation, message, delivery);
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
this.wake(parentActivation);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (delivery === 'steer')
|
|
593
|
+
parent.steer(message);
|
|
594
|
+
else
|
|
595
|
+
parent.followup(message);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Reject new admission once the manager or this exact parent tree began draining.
|
|
599
|
+
* @param agent - exact live Agent whose lineage determines admission.
|
|
600
|
+
*/
|
|
601
|
+
assertAdmitting(agent) {
|
|
602
|
+
const closing = this.closingTeardownFor(agent);
|
|
603
|
+
if (closing === undefined)
|
|
604
|
+
return;
|
|
605
|
+
throw new SubagentError(closing === 'manager'
|
|
606
|
+
? 'subagents are draining; the operation was not admitted'
|
|
607
|
+
: `subagents below parent "${closing.id}" are draining; the operation was not admitted`, 'DRAINING');
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Authorize one operation against the durable direct-parent lineage.
|
|
611
|
+
* @param parent - exact live Agent claiming direct-parent authority.
|
|
612
|
+
* @param childId - durable child session id addressed by the operation.
|
|
613
|
+
* @param parentSession - durable direct-parent id recorded by the child.
|
|
614
|
+
*/
|
|
615
|
+
authorizeLineage(parent, childId, parentSession) {
|
|
616
|
+
this.assertLiveParent(parent, childId);
|
|
617
|
+
if (parentSession !== parent.id) {
|
|
618
|
+
throw new SubagentError(`subagent "${childId}" belongs to another parent session`, 'UNAUTHORIZED');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** Reject parent authority after the live registry entry changes. */
|
|
622
|
+
assertLiveParent(parent, childId) {
|
|
623
|
+
if (this.ctx.agents.get(parent.id) !== parent) {
|
|
624
|
+
throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, 'UNAUTHORIZED');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create or resume one child Agent and publish its Activation.
|
|
629
|
+
* @param inputs - reconstruction and admission inputs for the residency epoch.
|
|
630
|
+
* @returns the published process-local Activation.
|
|
631
|
+
*/
|
|
632
|
+
materialize(inputs) {
|
|
633
|
+
this.assertAdmitting(inputs.parent);
|
|
634
|
+
inputs.signal.throwIfAborted();
|
|
635
|
+
const lineage = this.liveLineage(inputs.parent);
|
|
636
|
+
const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent);
|
|
637
|
+
const releaseSlot = pool.reserve(this.maxActiveSubagents());
|
|
638
|
+
const settled = Promise.withResolvers();
|
|
639
|
+
const controller = new AbortController();
|
|
640
|
+
const materialization = {
|
|
641
|
+
lineage,
|
|
642
|
+
settled: settled.promise,
|
|
643
|
+
controller,
|
|
644
|
+
};
|
|
645
|
+
this.materializations.add(materialization);
|
|
646
|
+
return this.materializeTracked({ ...inputs, signal: AbortSignal.any([inputs.signal, controller.signal]) }, lineage, pool, releaseSlot).catch((error) => {
|
|
647
|
+
releaseSlot();
|
|
648
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
649
|
+
this.assertAdmitting(inputs.parent);
|
|
650
|
+
throw error;
|
|
651
|
+
}).finally(() => {
|
|
652
|
+
this.materializations.delete(materialization);
|
|
653
|
+
settled.resolve();
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Stop and release one Activation through its memoized close transaction.
|
|
658
|
+
* @param activation - exact residency epoch to close.
|
|
659
|
+
* @returns the shared close transaction.
|
|
660
|
+
*/
|
|
661
|
+
dispose(activation) {
|
|
662
|
+
return this.close(activation, () => this.finishDisposal(activation, true));
|
|
663
|
+
}
|
|
664
|
+
/** Close admission synchronously and share one release, including startup rollback. */
|
|
665
|
+
close(activation, release) {
|
|
666
|
+
if (activation.closing !== undefined)
|
|
667
|
+
return activation.closing;
|
|
668
|
+
const completion = Promise.withResolvers();
|
|
669
|
+
activation.closing = completion.promise;
|
|
670
|
+
void release().then(completion.resolve, completion.reject);
|
|
671
|
+
return completion.promise;
|
|
672
|
+
}
|
|
673
|
+
/** Dispose independent roots and report every branch failure after all settle. */
|
|
674
|
+
async disposeRoots(roots, failureSubject) {
|
|
675
|
+
const failures = await Promise.all(roots.map(async (activation) => {
|
|
676
|
+
try {
|
|
677
|
+
await this.dispose(activation);
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
return error;
|
|
682
|
+
}
|
|
683
|
+
}));
|
|
684
|
+
const reasons = failures.filter(failure => failure !== undefined);
|
|
685
|
+
if (reasons.length > 0) {
|
|
686
|
+
throw new SubagentError(`subagent teardown failed for ${reasons.length} ${failureSubject}: `
|
|
687
|
+
+ reasons.map(reason => failureMessage(reason)).join('; '), 'ACTIVATION_TEARDOWN_FAILED');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** Return the retained member set for one exact scoped-teardown root. */
|
|
691
|
+
closingMembers(root) {
|
|
692
|
+
const existing = this.closingScopes.get(root);
|
|
693
|
+
if (existing !== undefined)
|
|
694
|
+
return existing;
|
|
695
|
+
const members = new Set();
|
|
696
|
+
this.closingScopes.set(root, members);
|
|
697
|
+
return members;
|
|
698
|
+
}
|
|
699
|
+
/** Return the exact currently resolvable ancestry from `agent` upward. */
|
|
700
|
+
liveLineage(agent) {
|
|
701
|
+
const lineage = [agent];
|
|
702
|
+
const seen = new Set([agent.id]);
|
|
703
|
+
let parentSession = agent.session.header.parentSession;
|
|
704
|
+
while (parentSession !== undefined) {
|
|
705
|
+
const parent = this.ctx.agents.get(parentSession);
|
|
706
|
+
if (parent === undefined || seen.has(parent.id))
|
|
707
|
+
break;
|
|
708
|
+
lineage.push(parent);
|
|
709
|
+
seen.add(parent.id);
|
|
710
|
+
parentSession = parent.session.header.parentSession;
|
|
711
|
+
}
|
|
712
|
+
return lineage;
|
|
713
|
+
}
|
|
714
|
+
/** Return the teardown that closed continuable admission for this agent's lineage. */
|
|
715
|
+
closingTeardownFor(agent) {
|
|
716
|
+
if (this.draining)
|
|
717
|
+
return 'manager';
|
|
718
|
+
const lineage = this.liveLineage(agent);
|
|
719
|
+
for (const [root, members] of this.closingScopes) {
|
|
720
|
+
if (members.has(agent) || lineage.includes(root))
|
|
721
|
+
return root;
|
|
722
|
+
}
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
/** Resolve a root's pool once; descendants inherit their resident parent's pool directly. */
|
|
726
|
+
rootPool(parent) {
|
|
727
|
+
let pool = this.rootPools.get(parent);
|
|
728
|
+
if (pool === undefined) {
|
|
729
|
+
pool = new ActivationPool();
|
|
730
|
+
this.rootPools.set(parent, pool);
|
|
731
|
+
}
|
|
732
|
+
return pool;
|
|
733
|
+
}
|
|
734
|
+
/** Perform one tracked materialization through publication or rollback. */
|
|
735
|
+
async materializeTracked(inputs, parentLineage, pool, releaseSlot) {
|
|
736
|
+
const { provider, parent } = inputs;
|
|
737
|
+
let childId = inputs.childId;
|
|
738
|
+
inputs.signal.throwIfAborted();
|
|
739
|
+
let resource;
|
|
740
|
+
if (inputs.kind === 'external') {
|
|
741
|
+
const controller = new AbortController();
|
|
742
|
+
const cancelStartup = () => { controller.abort(inputs.signal.reason); };
|
|
743
|
+
inputs.signal.addEventListener('abort', cancelStartup, { once: true });
|
|
744
|
+
try {
|
|
745
|
+
const run = await inputs.start(controller.signal);
|
|
746
|
+
resource = { kind: 'external', run, controller };
|
|
747
|
+
void run.result.catch(() => undefined);
|
|
748
|
+
childId = run.id;
|
|
749
|
+
try {
|
|
750
|
+
this.assertChildIdAvailable(childId);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
await run.dispose();
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
inputs.signal.removeEventListener('abort', cancelStartup);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
const { create } = inputs;
|
|
763
|
+
let structured;
|
|
764
|
+
const setup = async (childCtx, child) => {
|
|
765
|
+
// Only fresh creation appends the descriptor and delegated policy after
|
|
766
|
+
// the inherited marker; a cold resume replays those persisted events.
|
|
767
|
+
if (create !== undefined) {
|
|
768
|
+
child.session.append('subagent/descriptor', create.descriptor);
|
|
769
|
+
appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
|
|
770
|
+
const workingDirectory = childCtx.get('workingDirectory');
|
|
771
|
+
if (workingDirectory === undefined)
|
|
772
|
+
throw new Error('local subagents require the working-directory service');
|
|
773
|
+
await workingDirectory.set(child, create.cwd, inputs.signal);
|
|
774
|
+
}
|
|
775
|
+
applyChildComposition(childCtx, parent, inputs.composition);
|
|
776
|
+
if (inputs.outputSchema !== undefined) {
|
|
777
|
+
structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () => this.resident.get(childId)?.ownedChildren.size === 0
|
|
778
|
+
&& child.inbox.nextStep.length === 0
|
|
779
|
+
&& child.inbox.nextTurn.length === 0);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const handle = create === undefined
|
|
783
|
+
? await this.ownerCtx.agents.resume({
|
|
784
|
+
resumeSessionId: childId,
|
|
785
|
+
parentAgent: parent,
|
|
786
|
+
agentOptions: inputs.agentOptions,
|
|
787
|
+
signal: inputs.signal,
|
|
788
|
+
setup,
|
|
789
|
+
})
|
|
790
|
+
: await this.ownerCtx.agents.create({
|
|
791
|
+
sessionId: childId,
|
|
792
|
+
parentAgent: parent,
|
|
793
|
+
meta: create.meta,
|
|
794
|
+
...(create.seed === undefined ? {} : { seed: create.seed }),
|
|
795
|
+
inheritedEventCount: create.inheritedEventCount,
|
|
796
|
+
agentOptions: inputs.agentOptions,
|
|
797
|
+
signal: inputs.signal,
|
|
798
|
+
setup,
|
|
799
|
+
});
|
|
800
|
+
resource = { kind: 'local', handle, outputStart: handle.agent.session.seq, structured, failureAt: undefined, poke: Promise.withResolvers() };
|
|
801
|
+
}
|
|
802
|
+
const observer = this.host.observeActivation(provider, childId, parent);
|
|
803
|
+
const activation = {
|
|
804
|
+
pool,
|
|
805
|
+
releaseSlot,
|
|
806
|
+
childId,
|
|
807
|
+
...resource,
|
|
808
|
+
parent,
|
|
809
|
+
delivery: inputs.delivery ?? 'parent',
|
|
810
|
+
result: Promise.withResolvers(),
|
|
811
|
+
closing: undefined,
|
|
812
|
+
ancestry: new WeakSet(resource.kind === 'external' ? parentLineage : [resource.handle.agent, ...parentLineage]),
|
|
813
|
+
ownedChildren: new Set(),
|
|
814
|
+
observer,
|
|
815
|
+
announced: false,
|
|
816
|
+
};
|
|
817
|
+
void activation.result.promise.catch(() => undefined);
|
|
818
|
+
this.resident.set(childId, activation);
|
|
819
|
+
try {
|
|
820
|
+
this.assertAdmitting(parent);
|
|
821
|
+
inputs.signal.throwIfAborted();
|
|
822
|
+
if (this.ctx.agents.get(parent.id) !== parent)
|
|
823
|
+
throw new SubagentError('subagent parent is no longer live', 'UNAUTHORIZED');
|
|
824
|
+
this.acquireOwnership(parent, childId);
|
|
825
|
+
const wakeSettlement = () => { this.wake(activation); };
|
|
826
|
+
if (activation.kind === 'local') {
|
|
827
|
+
observeLocalFailure(activation, wakeSettlement);
|
|
828
|
+
activation.handle.agent.ctx.on('agent/inbox/inserted', wakeSettlement);
|
|
829
|
+
activation.handle.agent.ctx.on('agent/inbox/claimed', wakeSettlement);
|
|
830
|
+
activation.handle.agent.ctx.on('agent/inbox/discarded', wakeSettlement);
|
|
831
|
+
}
|
|
832
|
+
observer.start(activationAgent(activation));
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
/* v8 ignore next -- rollback failure must not mask the admission failure
|
|
836
|
+
* that prevented this operation from returning an accepted message id. */
|
|
837
|
+
await this.rollbackUnpublished(activation, error).catch(() => undefined);
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
return activation;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Begin settlement observation after initial acceptance is recorded.
|
|
844
|
+
* @param activation - accepted activation to observe.
|
|
845
|
+
*/
|
|
846
|
+
announce(activation) {
|
|
847
|
+
if (activation.announced)
|
|
848
|
+
return;
|
|
849
|
+
activation.announced = true;
|
|
850
|
+
if (activation.kind === 'local') {
|
|
851
|
+
this.watchLocalSettlement(activation);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
const finish = () => this.close(activation, () => this.finishDisposal(activation, false));
|
|
855
|
+
void activation.run.result.then(finish, finish).catch((error) => { this.reportTeardownFailure(activation, error); });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/** Release an Activation whose start edge was not published. */
|
|
859
|
+
rollbackUnpublished(activation, error) {
|
|
860
|
+
return this.close(activation, async () => {
|
|
861
|
+
try {
|
|
862
|
+
await (activation.kind === 'local' ? activation.handle.dispose() : activation.run.dispose());
|
|
863
|
+
}
|
|
864
|
+
finally {
|
|
865
|
+
this.resident.delete(activation.childId);
|
|
866
|
+
activation.releaseSlot();
|
|
867
|
+
this.releaseOwnership(activation.childId);
|
|
868
|
+
activation.result.reject(error);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/** Register the child in a continuation-managed parent's owned set. */
|
|
873
|
+
acquireOwnership(parent, childId) {
|
|
874
|
+
const parentActivation = this.resident.get(parent.id);
|
|
875
|
+
if (parentActivation === undefined)
|
|
876
|
+
return;
|
|
877
|
+
if (parentActivation.closing !== undefined) {
|
|
878
|
+
throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
|
|
879
|
+
}
|
|
880
|
+
parentActivation.ownedChildren.add(childId);
|
|
881
|
+
this.wake(parentActivation);
|
|
882
|
+
}
|
|
883
|
+
/** Remove one child from its live owner's set and let that owner re-check settlement. */
|
|
884
|
+
releaseOwnership(childId) {
|
|
885
|
+
for (const candidate of this.resident.values()) {
|
|
886
|
+
if (candidate.ownedChildren.delete(childId))
|
|
887
|
+
this.wake(candidate);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/** Let a settlement watcher re-check residency after relevant state changes. */
|
|
891
|
+
wake(activation) {
|
|
892
|
+
if (activation.kind === 'external')
|
|
893
|
+
return;
|
|
894
|
+
activation.poke.resolve();
|
|
895
|
+
activation.poke = Promise.withResolvers();
|
|
896
|
+
}
|
|
897
|
+
/** Follow one Activation to natural settlement. */
|
|
898
|
+
watchLocalSettlement(activation) {
|
|
899
|
+
void (async () => {
|
|
900
|
+
while (true) {
|
|
901
|
+
const idleObservation = activation.poke;
|
|
902
|
+
await activation.handle.agent.whenIdle();
|
|
903
|
+
if (activation.closing !== undefined)
|
|
904
|
+
return;
|
|
905
|
+
const readiness = await this.locks.run(activation.childId, () => Promise.resolve(this.settlementState(activation, idleObservation)));
|
|
906
|
+
if (readiness === 'closed')
|
|
907
|
+
return;
|
|
908
|
+
if (readiness === 'retry')
|
|
909
|
+
continue;
|
|
910
|
+
if (readiness === 'wait') {
|
|
911
|
+
await idleObservation.promise;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const finalSeq = activation.handle.agent.session.seq;
|
|
915
|
+
await this.flushFinalState(activation);
|
|
916
|
+
const attempt = await this.locks.run(activation.childId, () => {
|
|
917
|
+
const state = this.settlementState(activation, idleObservation);
|
|
918
|
+
if (state !== 'ready')
|
|
919
|
+
return Promise.resolve(state);
|
|
920
|
+
if (activation.handle.agent.session.seq !== finalSeq) {
|
|
921
|
+
return Promise.resolve('retry');
|
|
922
|
+
}
|
|
923
|
+
let done;
|
|
924
|
+
try {
|
|
925
|
+
void activation.handle.agent.runMaintenance(() => {
|
|
926
|
+
done = this.close(activation, () => this.finishDisposal(activation, false));
|
|
927
|
+
return Promise.resolve();
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
catch (_error) {
|
|
931
|
+
// Another activity already owns the Agent's idle phase.
|
|
932
|
+
return Promise.resolve('retry');
|
|
933
|
+
}
|
|
934
|
+
return Promise.resolve({ done });
|
|
935
|
+
});
|
|
936
|
+
if (typeof attempt === 'string') {
|
|
937
|
+
if (attempt === 'closed')
|
|
938
|
+
return;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
await attempt.done;
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
this.reportTeardownFailure(activation, error);
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
})();
|
|
950
|
+
}
|
|
951
|
+
/** Report disposal failure once after the natural settlement observer finishes. */
|
|
952
|
+
reportTeardownFailure(activation, error) {
|
|
953
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`);
|
|
954
|
+
}
|
|
955
|
+
/** Check pending input and owned children before closing admission. */
|
|
956
|
+
settlementState(activation, observation) {
|
|
957
|
+
if (activation.closing !== undefined)
|
|
958
|
+
return 'closed';
|
|
959
|
+
if (activation.poke !== observation)
|
|
960
|
+
return 'retry';
|
|
961
|
+
const inbox = activation.handle.agent.inbox;
|
|
962
|
+
if (activation.ownedChildren.size > 0)
|
|
963
|
+
return 'wait';
|
|
964
|
+
if (activation.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
|
|
965
|
+
return 'wait';
|
|
966
|
+
return 'ready';
|
|
967
|
+
}
|
|
968
|
+
/** Propagate stop synchronously and await descendant startup rollback before release. */
|
|
969
|
+
async finishDisposal(activation, stop) {
|
|
970
|
+
this.wake(activation);
|
|
971
|
+
const { childId } = activation;
|
|
972
|
+
const failures = [];
|
|
973
|
+
let result = { output: [], stopReason: 'error' };
|
|
974
|
+
let resultFailure;
|
|
975
|
+
let externalDisposal;
|
|
976
|
+
const child = activationAgent(activation);
|
|
977
|
+
const detail = (error) => child === undefined
|
|
978
|
+
? failureMessage(error)
|
|
979
|
+
: errorChain(error);
|
|
980
|
+
try {
|
|
981
|
+
if (activation.kind === 'external') {
|
|
982
|
+
if (stop) {
|
|
983
|
+
activation.controller.abort({ kind: 'parent' });
|
|
984
|
+
// Some backends settle their result only when disposal begins.
|
|
985
|
+
externalDisposal = Promise.resolve().then(() => activation.run.dispose());
|
|
986
|
+
void externalDisposal.catch(() => undefined);
|
|
987
|
+
}
|
|
988
|
+
result = await activation.run.result;
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
if (stop) {
|
|
992
|
+
activation.handle.agent.cancel({ kind: 'parent' });
|
|
993
|
+
const idle = activation.handle.agent.whenIdle();
|
|
994
|
+
const children = [...activation.ownedChildren]
|
|
995
|
+
.map(child => this.resident.get(child))
|
|
996
|
+
.filter((child) => child !== undefined);
|
|
997
|
+
const pending = [...this.materializations].filter(item => item.lineage.includes(activation.handle.agent));
|
|
998
|
+
const childDisposals = children.map(child => this.dispose(child));
|
|
999
|
+
for (const item of pending)
|
|
1000
|
+
item.controller.abort();
|
|
1001
|
+
childDisposals.push(...pending.map(item => item.settled));
|
|
1002
|
+
const childFailures = await Promise.all(childDisposals.map(async (disposal) => {
|
|
1003
|
+
try {
|
|
1004
|
+
await disposal;
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
return error;
|
|
1009
|
+
}
|
|
1010
|
+
}));
|
|
1011
|
+
const reasons = childFailures.filter(reason => reason !== undefined);
|
|
1012
|
+
if (reasons.length > 0) {
|
|
1013
|
+
failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map(reason => failureMessage(reason)).join('; ')}`, 'ACTIVATION_TEARDOWN_FAILED'));
|
|
1014
|
+
}
|
|
1015
|
+
await idle;
|
|
1016
|
+
await this.flushFinalState(activation);
|
|
1017
|
+
}
|
|
1018
|
+
result = captureLocalResult(activation);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
catch (error) {
|
|
1022
|
+
resultFailure = { error };
|
|
1023
|
+
failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${detail(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
if (activation.kind === 'local')
|
|
1027
|
+
await activation.handle.dispose();
|
|
1028
|
+
else
|
|
1029
|
+
await (externalDisposal ?? activation.run.dispose());
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${detail(error)}`, 'ACTIVATION_TEARDOWN_FAILED', { cause: error }));
|
|
1033
|
+
}
|
|
1034
|
+
let failure;
|
|
1035
|
+
if (failures.length === 1) {
|
|
1036
|
+
failure = failures[0];
|
|
1037
|
+
}
|
|
1038
|
+
else if (failures.length > 1) {
|
|
1039
|
+
failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: `
|
|
1040
|
+
+ failures.map(item => detail(item)).join('; '), 'ACTIVATION_TEARDOWN_FAILED', { cause: new AggregateError(failures) });
|
|
1041
|
+
}
|
|
1042
|
+
this.resident.delete(childId);
|
|
1043
|
+
activation.releaseSlot();
|
|
1044
|
+
const terminal = failure === undefined ? result : { output: [], stopReason: 'error' };
|
|
1045
|
+
this.notifySettlement(activation, result);
|
|
1046
|
+
this.releaseOwnership(childId);
|
|
1047
|
+
activation.observer.settle(terminal);
|
|
1048
|
+
if (resultFailure === undefined)
|
|
1049
|
+
activation.result.resolve(result);
|
|
1050
|
+
else
|
|
1051
|
+
activation.result.reject(resultFailure.error);
|
|
1052
|
+
if (failure !== undefined)
|
|
1053
|
+
throw failure;
|
|
1054
|
+
}
|
|
1055
|
+
/** Deliver the captured execution result to the durable direct parent after cleanup. */
|
|
1056
|
+
notifySettlement(activation, terminal) {
|
|
1057
|
+
if (!activation.announced || activation.delivery === 'caller')
|
|
1058
|
+
return;
|
|
1059
|
+
try {
|
|
1060
|
+
const parent = this.ctx.agents.get(activation.parent.id);
|
|
1061
|
+
if (parent !== activation.parent)
|
|
1062
|
+
return;
|
|
1063
|
+
const message = createSettlementMessage(activation.childId, terminal, activationAgent(activation) !== undefined);
|
|
1064
|
+
if (this.closingTeardownFor(parent) !== undefined) {
|
|
1065
|
+
parent.inject(message);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
this.sendWaking(parent, message, parent.status === 'idle' ? 'queue' : 'steer');
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
|
|
1072
|
+
+ errorChain(error));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/** Request a best-effort final session flush before closing natural-settlement admission. */
|
|
1076
|
+
async flushFinalState(activation) {
|
|
1077
|
+
try {
|
|
1078
|
+
await activation.handle.agent.ctx.sessions.flush(activation.handle.agent.session);
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" best-effort final session flush failed; `
|
|
1082
|
+
+ `the persisted state may be unavailable or stale on resume: ${errorChain(error)}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/** Return capabilities bound to an exact activation instead of a future same-id instance. */
|
|
1086
|
+
receipt(activation) {
|
|
1087
|
+
return {
|
|
1088
|
+
childId: activation.childId,
|
|
1089
|
+
result: activation.result.promise,
|
|
1090
|
+
dispose: () => this.dispose(activation),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/** Route one parent-originated delivery through residency and cold resume. */
|
|
1094
|
+
async deliverToChild(parent, childId, content, options) {
|
|
1095
|
+
this.assertAdmitting(parent);
|
|
1096
|
+
const releaseHold = this.holdOwnership(parent, childId);
|
|
1097
|
+
try {
|
|
1098
|
+
return await this.deliverFollowup(parent, childId, content, options);
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
releaseHold();
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
|
|
1106
|
+
async deliverFollowup(parent, childId, content, options) {
|
|
1107
|
+
while (true) {
|
|
1108
|
+
const live = await this.locks.run(childId, async () => {
|
|
1109
|
+
const activation = this.resident.get(childId);
|
|
1110
|
+
if (activation === undefined)
|
|
1111
|
+
return this.coldResume(parent, childId, content, options);
|
|
1112
|
+
requireLocalActivation(activation);
|
|
1113
|
+
const disposal = activation.closing;
|
|
1114
|
+
/* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
|
|
1115
|
+
* observe the transaction inside the same critical section that opened it. */
|
|
1116
|
+
if (disposal !== undefined) {
|
|
1117
|
+
return disposal.then(() => undefined, () => undefined);
|
|
1118
|
+
}
|
|
1119
|
+
if (contentHasImage(content)) {
|
|
1120
|
+
await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
|
|
1121
|
+
if (activation.closing !== undefined) {
|
|
1122
|
+
await Promise.allSettled([activation.closing]);
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const messageId = this.submitAdmitted(activation, content, options, parent);
|
|
1127
|
+
this.announce(activation);
|
|
1128
|
+
return messageId;
|
|
1129
|
+
});
|
|
1130
|
+
/* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
|
|
1131
|
+
if (live !== undefined)
|
|
1132
|
+
return live;
|
|
1133
|
+
this.assertAdmitting(parent);
|
|
1134
|
+
options.signal.throwIfAborted();
|
|
1135
|
+
/* v8 ignore stop */
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/** Deliver one resident continuable child's message to its live direct parent. */
|
|
1139
|
+
sendToParent(activation, sender, content) {
|
|
1140
|
+
/* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
|
|
1141
|
+
* transaction between exact-agent authorization and this no-await span. */
|
|
1142
|
+
if (activation.closing !== undefined) {
|
|
1143
|
+
throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
|
|
1144
|
+
}
|
|
1145
|
+
const parent = this.ctx.agents.get(activation.parent.id);
|
|
1146
|
+
if (parent !== activation.parent) {
|
|
1147
|
+
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
|
|
1148
|
+
}
|
|
1149
|
+
const message = createAgentMessage(sender, content);
|
|
1150
|
+
this.sendAgentMessage(parent, message);
|
|
1151
|
+
return message.id;
|
|
1152
|
+
}
|
|
1153
|
+
/** Send one Agent message while translating only the target's own rejection. */
|
|
1154
|
+
sendAgentMessage(parent, message) {
|
|
1155
|
+
try {
|
|
1156
|
+
this.sendWaking(parent, message, 'steer');
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Cold-resume a persisted child and submit the waiting turn. The descriptor
|
|
1164
|
+
* supplies every reconstruction input; no subagent provider is dispatched.
|
|
1165
|
+
*/
|
|
1166
|
+
async coldResume(parent, childId, content, options) {
|
|
1167
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1168
|
+
try {
|
|
1169
|
+
const query = this.requireSessionQuery();
|
|
1170
|
+
let observation;
|
|
1171
|
+
try {
|
|
1172
|
+
observation = await query.observeSession(childId, {
|
|
1173
|
+
signal: options.signal,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
options.signal.throwIfAborted();
|
|
1178
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
|
|
1179
|
+
}
|
|
1180
|
+
const source = __addDisposableResource(env_1, observation, false);
|
|
1181
|
+
this.assertAdmitting(parent);
|
|
1182
|
+
this.authorizeLineage(parent, childId, source.header.parentSession);
|
|
1183
|
+
const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
|
|
1184
|
+
if (descriptor === undefined || descriptor.mode !== 'continuable') {
|
|
1185
|
+
throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
|
|
1186
|
+
}
|
|
1187
|
+
let activation;
|
|
1188
|
+
try {
|
|
1189
|
+
activation = await this.materialize({
|
|
1190
|
+
kind: 'local',
|
|
1191
|
+
childId,
|
|
1192
|
+
provider: descriptor.provider,
|
|
1193
|
+
parent,
|
|
1194
|
+
agentOptions: {
|
|
1195
|
+
...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
|
|
1196
|
+
...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
|
|
1197
|
+
...descriptor.agentReasoningEffort !== undefined
|
|
1198
|
+
? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
|
|
1199
|
+
: {},
|
|
1200
|
+
},
|
|
1201
|
+
composition: { persona: descriptor.persona, toolFilter: descriptor.toolFilter },
|
|
1202
|
+
signal: options.signal,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
options.signal.throwIfAborted();
|
|
1207
|
+
if (error instanceof SubagentError)
|
|
1208
|
+
throw error;
|
|
1209
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
|
|
1210
|
+
}
|
|
1211
|
+
return await this.submitMaterialized(activation, content, options, parent);
|
|
1212
|
+
}
|
|
1213
|
+
catch (e_1) {
|
|
1214
|
+
env_1.error = e_1;
|
|
1215
|
+
env_1.hasError = true;
|
|
1216
|
+
}
|
|
1217
|
+
finally {
|
|
1218
|
+
__disposeResources(env_1);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/** Admit a materialized child, commit its creation fact, and release it on failure. */
|
|
1222
|
+
async submitMaterialized(activation, content, options, parent, commit) {
|
|
1223
|
+
try {
|
|
1224
|
+
if (contentHasImage(content)) {
|
|
1225
|
+
await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
|
|
1226
|
+
if (activation.closing !== undefined) {
|
|
1227
|
+
throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const messageId = this.submitAdmitted(activation, content, options, parent);
|
|
1231
|
+
commit?.();
|
|
1232
|
+
this.announce(activation);
|
|
1233
|
+
return messageId;
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
try {
|
|
1237
|
+
await this.dispose(activation);
|
|
1238
|
+
}
|
|
1239
|
+
catch (cleanupError) {
|
|
1240
|
+
this.ctx.logger.warn(`subagent continuation: disposal after admission or catalog append failure also failed: ${String(cleanupError)}`);
|
|
1241
|
+
}
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/** Build and submit one message across the final synchronous admission cutoff. */
|
|
1246
|
+
submitAdmitted(activation, content, options, parent) {
|
|
1247
|
+
const message = options.source === undefined
|
|
1248
|
+
? createAgentMessage(parent, content)
|
|
1249
|
+
: createUserMessage({ content, source: options.source });
|
|
1250
|
+
options.signal.throwIfAborted();
|
|
1251
|
+
this.assertAdmitting(parent);
|
|
1252
|
+
this.authorizeLineage(parent, activation.childId, activation.parent.id);
|
|
1253
|
+
this.acquireOwnership(parent, activation.childId);
|
|
1254
|
+
try {
|
|
1255
|
+
this.deliver(activation, message, options.delivery);
|
|
1256
|
+
}
|
|
1257
|
+
finally {
|
|
1258
|
+
this.wake(activation);
|
|
1259
|
+
}
|
|
1260
|
+
return message.id;
|
|
1261
|
+
}
|
|
1262
|
+
/** Refuse image content for a child whose fixed model accepts text only. */
|
|
1263
|
+
async assertImageCapable(agent, signal) {
|
|
1264
|
+
const { provider, model } = agent.options;
|
|
1265
|
+
if (provider === undefined || model === undefined)
|
|
1266
|
+
return;
|
|
1267
|
+
const llm = this.ctx.get('llm');
|
|
1268
|
+
/* v8 ignore next -- without an LLM registry, delivery defers to projection. */
|
|
1269
|
+
if (llm === undefined)
|
|
1270
|
+
return;
|
|
1271
|
+
const info = await llm.resolveModelInfo(provider, model, signal);
|
|
1272
|
+
if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
|
|
1273
|
+
throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/** Resolve the persistence service continuable children require, or fail loud. */
|
|
1277
|
+
requirePersistence() {
|
|
1278
|
+
const persistence = this.ctx.get('sessionPersistence');
|
|
1279
|
+
if (persistence === undefined) {
|
|
1280
|
+
throw new SubagentError('continuable subagents require session persistence (load a dsh-session-persistence backend)', 'PERSISTENCE_UNAVAILABLE');
|
|
1281
|
+
}
|
|
1282
|
+
return persistence;
|
|
1283
|
+
}
|
|
1284
|
+
/** Resolve the Session query service used for cold child observations. */
|
|
1285
|
+
requireSessionQuery() {
|
|
1286
|
+
const query = this.ctx.get('sessionQuery');
|
|
1287
|
+
if (query === undefined) {
|
|
1288
|
+
throw new SubagentError('continuable subagents require session query (load @qlzt/dsh-session-query)', 'CONTINUATION_UNAVAILABLE');
|
|
1289
|
+
}
|
|
1290
|
+
return query;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
export default SubagentManager;
|
|
1294
|
+
//# sourceMappingURL=manager.js.map
|