@deepseek-ai/dsh-subagent 0.0.1-rc.1
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 +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +132 -0
- package/README.zh.md +132 -0
- package/lib/index.js +2392 -0
- package/lib/invariant.js +76 -0
- package/lib/types/activation-setup-registry.d.ts +57 -0
- package/lib/types/activation-setup-registry.js +148 -0
- package/lib/types/child-agent.d.ts +139 -0
- package/lib/types/child-agent.js +169 -0
- package/lib/types/client.d.ts +7 -0
- package/lib/types/client.js +7 -0
- package/lib/types/continuation.d.ts +375 -0
- package/lib/types/continuation.js +951 -0
- package/lib/types/depth.d.ts +31 -0
- package/lib/types/depth.js +39 -0
- package/lib/types/descriptor-seed.d.ts +21 -0
- package/lib/types/descriptor-seed.js +24 -0
- package/lib/types/descriptor.d.ts +139 -0
- package/lib/types/descriptor.js +189 -0
- package/lib/types/error.d.ts +11 -0
- package/lib/types/error.js +14 -0
- package/lib/types/index.d.ts +278 -0
- package/lib/types/index.js +338 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +91 -0
- package/lib/types/lifecycle.d.ts +93 -0
- package/lib/types/lifecycle.js +169 -0
- package/lib/types/list-children.d.ts +112 -0
- package/lib/types/list-children.js +316 -0
- package/lib/types/out-of-process.d.ts +115 -0
- package/lib/types/out-of-process.js +181 -0
- package/lib/types/projection-types.d.ts +60 -0
- package/lib/types/projection-types.js +7 -0
- package/lib/types/projection.d.ts +48 -0
- package/lib/types/projection.js +135 -0
- package/lib/types/run-settlement.d.ts +17 -0
- package/lib/types/run-settlement.js +59 -0
- package/lib/types/types.d.ts +293 -0
- package/lib/types/types.js +19 -0
- package/package.json +106 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2392 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
3
|
+
import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { HarnessError, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { Session, SessionId, findLastMessageTurnEnd, snapshotJsonValue } from "@deepseek-ai/dsh-session";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
9
|
+
import { isAbsolute, resolve } from "node:path";
|
|
10
|
+
//#region lib/types/error.js
|
|
11
|
+
/**
|
|
12
|
+
* Typed failures shared by subagent service and provider operations.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepseek-ai/dsh-subagent
|
|
15
|
+
*/
|
|
16
|
+
/** Typed failure for the subagent seam. */
|
|
17
|
+
var SubagentError = class extends HarnessError {
|
|
18
|
+
constructor(message, code, options) {
|
|
19
|
+
super(message, code, options);
|
|
20
|
+
this.name = "SubagentError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region lib/types/depth.js
|
|
25
|
+
/**
|
|
26
|
+
* Delegation-depth accounting: the recursion budget a parent passes to its
|
|
27
|
+
* children. Kept apart from the service so composition helpers can read it
|
|
28
|
+
* without importing the registry.
|
|
29
|
+
*
|
|
30
|
+
* @module @deepseek-ai/dsh-subagent/depth
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Read an agent's delegation depth, treating absence as top-level depth zero.
|
|
34
|
+
* The persisted session header is authoritative and monotone: runtime
|
|
35
|
+
* `AgentOptions.subagentDepth` may DEEPEN the count but can never lower it —
|
|
36
|
+
* a resumed child arrives with fresh options, and counting it from zero would
|
|
37
|
+
* let it delegate as if it were top-level.
|
|
38
|
+
* @param agent - the agent whose header and options carry the depth.
|
|
39
|
+
* @returns its non-negative safe-integer depth.
|
|
40
|
+
* @throws if the runtime `AgentOptions.subagentDepth` is not a non-negative safe integer.
|
|
41
|
+
*/
|
|
42
|
+
function delegationDepthOf(agent) {
|
|
43
|
+
const runtime = agent.options.subagentDepth;
|
|
44
|
+
if (runtime !== void 0 && (!Number.isSafeInteger(runtime) || runtime < 0 || Object.is(runtime, -0))) throw new TypeError("agent subagentDepth must be a non-negative safe integer");
|
|
45
|
+
return Math.max(agent.session.header.delegationDepth ?? 0, runtime ?? 0);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reject a recursion cap that cannot represent an exact delegation depth.
|
|
49
|
+
* @param maxDepth - the optional runtime value to validate.
|
|
50
|
+
*/
|
|
51
|
+
function assertSubagentMaxDepth(maxDepth) {
|
|
52
|
+
if (maxDepth !== void 0 && (typeof maxDepth !== "number" || !Number.isSafeInteger(maxDepth) || maxDepth < 0 || Object.is(maxDepth, -0))) throw new TypeError("subagent maxDepth must be a non-negative safe integer");
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region lib/types/types.js
|
|
56
|
+
/**
|
|
57
|
+
* The seam's consumer-facing contracts: request, result, and capability types
|
|
58
|
+
* for {@link SubagentProvider}, plus the `subagent/start` and `subagent/end`
|
|
59
|
+
* payloads that plugins and hosts observe. Internal control interfaces belong
|
|
60
|
+
* with their implementation — the lifecycle observer in `./lifecycle.ts`, the
|
|
61
|
+
* continuation host in `./continuation.ts` — so this module stays the published
|
|
62
|
+
* surface rather than a bag of everything type-shaped.
|
|
63
|
+
*
|
|
64
|
+
* @module @deepseek-ai/dsh-subagent/types
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* Brand a string as a {@link SubagentRunId}.
|
|
68
|
+
* @param id - the raw run id.
|
|
69
|
+
* @returns the same string, branded.
|
|
70
|
+
*/
|
|
71
|
+
function SubagentRunId(id) {
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region lib/types/lifecycle.js
|
|
76
|
+
/**
|
|
77
|
+
* Lifecycle-edge publication for both subagent shapes: the contained emitter,
|
|
78
|
+
* the one-shot run observer, and the continuable Activation observer.
|
|
79
|
+
*
|
|
80
|
+
* The public payload contracts ({@link SubagentRunInfo},
|
|
81
|
+
* {@link SubagentRunEndInfo}) live in `./types.ts` with the rest of the seam's
|
|
82
|
+
* consumer-facing types; this module owns only the implementation and the
|
|
83
|
+
* package-private {@link ActivationObserver} the continuation manager consumes.
|
|
84
|
+
* Keeping the internal control interface out of the published surface is
|
|
85
|
+
* deliberate: the observer's `start`/`capture`/`settle` ordering is a contract
|
|
86
|
+
* between this module and one in-package caller, not something a plugin may
|
|
87
|
+
* depend on.
|
|
88
|
+
*
|
|
89
|
+
* @module @deepseek-ai/dsh-subagent/lifecycle
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Build the contained lifecycle emitter this seam publishes every edge through.
|
|
93
|
+
* Every listener is independently contained: a synchronous throw or a rejected
|
|
94
|
+
* returned promise is logged without starving peer listeners, changing the run,
|
|
95
|
+
* or — for provider removal, which fires from a disposer — breaking teardown.
|
|
96
|
+
* @param ctx - the service's own context, owning dispatch and the logger.
|
|
97
|
+
* @param carrier - resolve the scoped dispatch carrier for one delegating parent.
|
|
98
|
+
* @returns the emitter both observers and the provider registry publish through.
|
|
99
|
+
*/
|
|
100
|
+
function createLifecycleEmitter(ctx, carrier) {
|
|
101
|
+
return (name, info, parent) => {
|
|
102
|
+
const dispatchArgs = parent === void 0 ? [name, info] : [
|
|
103
|
+
carrier(parent),
|
|
104
|
+
name,
|
|
105
|
+
info
|
|
106
|
+
];
|
|
107
|
+
for (const callback of ctx.events.dispatch("emit", dispatchArgs)) try {
|
|
108
|
+
const returned = callback(info);
|
|
109
|
+
Promise.resolve(returned).catch((error) => {
|
|
110
|
+
ctx.logger.warn(`subagent: ${name} listener rejected: ${renderThrown(error)}`);
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
ctx.logger.warn(`subagent: ${name} listener threw: ${renderThrown(error)}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Emit the start/end lifecycle pair for one accepted one-shot run.
|
|
119
|
+
* @param emit - the contained lifecycle emitter.
|
|
120
|
+
* @param provider - the provider that established the run.
|
|
121
|
+
* @param parent - the delegating parent keying scoped dispatch.
|
|
122
|
+
* @param run - the published run whose settlement closes the pair.
|
|
123
|
+
* @returns the same run, unchanged.
|
|
124
|
+
*/
|
|
125
|
+
function observeRun(emit, provider, parent, run) {
|
|
126
|
+
const identity = {
|
|
127
|
+
runId: SubagentRunId(randomUUID()),
|
|
128
|
+
provider,
|
|
129
|
+
id: run.id,
|
|
130
|
+
local: run.localAgent !== void 0
|
|
131
|
+
};
|
|
132
|
+
run.result.then((result) => {
|
|
133
|
+
emit("subagent/end", {
|
|
134
|
+
...identity,
|
|
135
|
+
stopReason: result.stopReason,
|
|
136
|
+
lastAssistantMessage: result.output
|
|
137
|
+
}, parent);
|
|
138
|
+
}, () => {
|
|
139
|
+
emit("subagent/end", {
|
|
140
|
+
...identity,
|
|
141
|
+
stopReason: "error"
|
|
142
|
+
}, parent);
|
|
143
|
+
});
|
|
144
|
+
emit("subagent/start", identity, parent);
|
|
145
|
+
return run;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build the observer for one continuable Activation's residency epoch. Observers
|
|
149
|
+
* see the same vocabulary as a one-shot run, so a child's start and settlement
|
|
150
|
+
* remain observable without exposing whether the manager materialized, woke, or
|
|
151
|
+
* cold-resumed it. Creation failure before residency emits no lifecycle edge.
|
|
152
|
+
* @param emit - the contained lifecycle emitter.
|
|
153
|
+
* @param provider - the provider name recorded in the durable descriptor.
|
|
154
|
+
* @param childId - the durable child session id.
|
|
155
|
+
* @param parent - the exact live direct parent keying scoped dispatch.
|
|
156
|
+
* @returns the observer whose edges this epoch publishes.
|
|
157
|
+
*/
|
|
158
|
+
function createActivationObserver(emit, provider, childId, parent) {
|
|
159
|
+
const identity = {
|
|
160
|
+
runId: SubagentRunId(randomUUID()),
|
|
161
|
+
provider,
|
|
162
|
+
id: childId,
|
|
163
|
+
local: true
|
|
164
|
+
};
|
|
165
|
+
let boundary = 0;
|
|
166
|
+
let captured = { stopReason: "completed" };
|
|
167
|
+
return {
|
|
168
|
+
start: (child) => {
|
|
169
|
+
boundary = child.session.events.length;
|
|
170
|
+
emit("subagent/start", identity, parent);
|
|
171
|
+
},
|
|
172
|
+
capture: (child) => {
|
|
173
|
+
const own = child.session.events.slice(boundary);
|
|
174
|
+
const output = lastAssistantOutput(own);
|
|
175
|
+
captured = {
|
|
176
|
+
stopReason: epochStopReason(own),
|
|
177
|
+
...output === void 0 ? {} : { output }
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
settle: (failure) => {
|
|
181
|
+
const output = failure === void 0 ? captured.output : void 0;
|
|
182
|
+
emit("subagent/end", {
|
|
183
|
+
...identity,
|
|
184
|
+
stopReason: failure === void 0 ? captured.stopReason : "error",
|
|
185
|
+
...output === void 0 ? {} : { lastAssistantMessage: output }
|
|
186
|
+
}, parent);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Why this child's last ordinary turn ended, for the terminal lifecycle edge.
|
|
192
|
+
* The child's own `turn/end` is authoritative: teardown succeeding says nothing
|
|
193
|
+
* about whether the model errored, hit its token ceiling, or was cancelled, so
|
|
194
|
+
* deriving the reason from disposal would report failed work as completed.
|
|
195
|
+
* @param events - this epoch's own event suffix.
|
|
196
|
+
* @returns its terminal stop reason; `completed` when no ordinary turn closed.
|
|
197
|
+
*/
|
|
198
|
+
function epochStopReason(events) {
|
|
199
|
+
const reason = findLastMessageTurnEnd(events)?.data.reason;
|
|
200
|
+
if (reason === void 0) return "completed";
|
|
201
|
+
switch (reason.kind) {
|
|
202
|
+
case "max-tokens": return "max-tokens";
|
|
203
|
+
case "aborted":
|
|
204
|
+
case "interrupted": return "aborted";
|
|
205
|
+
case "error": return "error";
|
|
206
|
+
case "completed": return "completed";
|
|
207
|
+
/* v8 ignore next 3 -- `TurnEndReason` is merge-extensible, so this arm needs a
|
|
208
|
+
* backend that adds a variant; treating an unnameable reason as success would
|
|
209
|
+
* report failed work as completed. */
|
|
210
|
+
default: return "error";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The child's last assistant message content, for one Activation's terminal
|
|
215
|
+
* lifecycle edge. Absent when no assistant message reached the log.
|
|
216
|
+
* @param events - this epoch's own event suffix.
|
|
217
|
+
* @returns its final assistant content, or `undefined` when it produced none.
|
|
218
|
+
*/
|
|
219
|
+
function lastAssistantOutput(events) {
|
|
220
|
+
return events.findLast((event) => event.type === "assistant/message")?.data.message.content;
|
|
221
|
+
}
|
|
222
|
+
/** Render any listener-thrown value without letting coercion escape containment. */
|
|
223
|
+
function renderThrown(value) {
|
|
224
|
+
try {
|
|
225
|
+
return value instanceof Error ? `${value.name}: ${value.message}` : String(value);
|
|
226
|
+
} catch {
|
|
227
|
+
return "<unrenderable thrown value>";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region lib/types/descriptor.js
|
|
232
|
+
/**
|
|
233
|
+
* The durable subagent-child descriptor: the versioned, model-hidden
|
|
234
|
+
* `subagent/descriptor` session event that identifies every session-backed
|
|
235
|
+
* subagent and records whether it is one-shot or continuable. Continuable
|
|
236
|
+
* descriptors additionally preserve the declared composition required for
|
|
237
|
+
* cold resume. Providers append it turn-enclosed in the child's initial turn.
|
|
238
|
+
*
|
|
239
|
+
* The descriptor deliberately snapshots explicit fields rather than the
|
|
240
|
+
* merge-extensible `AgentOptions` object: an unrelated extension value cannot
|
|
241
|
+
* make continuation fail merely because it is not JSON, and later composition
|
|
242
|
+
* inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
|
|
243
|
+
* omits `subagentDepth` — cold resume trusts the persisted header's
|
|
244
|
+
* `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
|
|
245
|
+
* to one activation's result contract rather than durable child composition.
|
|
246
|
+
* Per-activation knobs such as `maxTokens` are omitted for the same reason as
|
|
247
|
+
* `outputSchema`: they budget one activation. Cold resume requires the exact
|
|
248
|
+
* live parent for authorization but reconstructs child options only from the
|
|
249
|
+
* durable descriptor, so it neither restores the prior budget nor inherits
|
|
250
|
+
* the parent's current one; the resumed route's defaults apply instead.
|
|
251
|
+
*
|
|
252
|
+
* @module @deepseek-ai/dsh-subagent/descriptor
|
|
253
|
+
*/
|
|
254
|
+
/**
|
|
255
|
+
* The current descriptor format version, stamped into every appended
|
|
256
|
+
* `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
|
|
257
|
+
* Supporting another composition input is a deliberate version change, never
|
|
258
|
+
* an implicit extra field.
|
|
259
|
+
*/
|
|
260
|
+
const SUBAGENT_DESCRIPTOR_VERSION = 2;
|
|
261
|
+
const DESCRIPTOR_BASE_KEYS = [
|
|
262
|
+
"version",
|
|
263
|
+
"mode",
|
|
264
|
+
"provider",
|
|
265
|
+
"label"
|
|
266
|
+
];
|
|
267
|
+
const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
|
|
268
|
+
const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
|
|
269
|
+
...DESCRIPTOR_BASE_KEYS,
|
|
270
|
+
"agentProvider",
|
|
271
|
+
"agentModel",
|
|
272
|
+
"persona",
|
|
273
|
+
"toolFilter"
|
|
274
|
+
]);
|
|
275
|
+
const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
|
|
276
|
+
/** Whether a persisted JSON value is an object record. */
|
|
277
|
+
function isRecord(value) {
|
|
278
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
279
|
+
}
|
|
280
|
+
/** Reject fields outside one versioned record's declared schema. */
|
|
281
|
+
function assertKnownKeys(value, keys, path) {
|
|
282
|
+
const unknown = Object.keys(value).find((key) => !keys.has(key));
|
|
283
|
+
if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
|
|
284
|
+
}
|
|
285
|
+
/** Read one optional string field from a persisted descriptor record. */
|
|
286
|
+
function optionalString(value, key) {
|
|
287
|
+
if (!Object.hasOwn(value, key)) return void 0;
|
|
288
|
+
const field = value[key];
|
|
289
|
+
if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
|
|
290
|
+
return field;
|
|
291
|
+
}
|
|
292
|
+
/** Read one optional string-array field from a persisted tool restriction. */
|
|
293
|
+
function optionalStringArray(value, key) {
|
|
294
|
+
if (!Object.hasOwn(value, key)) return void 0;
|
|
295
|
+
const field = value[key];
|
|
296
|
+
if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
|
|
297
|
+
const items = field;
|
|
298
|
+
if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
301
|
+
/** Validate and reconstruct a persisted tool restriction. */
|
|
302
|
+
function parseToolFilter(value) {
|
|
303
|
+
if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
|
|
304
|
+
assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
|
|
305
|
+
const allow = optionalStringArray(value, "allow");
|
|
306
|
+
const deny = optionalStringArray(value, "deny");
|
|
307
|
+
if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
|
|
308
|
+
return {
|
|
309
|
+
...allow !== void 0 ? { allow } : {},
|
|
310
|
+
...deny !== void 0 ? { deny } : {}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** Validate one persisted descriptor payload for the current runtime. */
|
|
314
|
+
function parseSubagentDescriptor(value) {
|
|
315
|
+
if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
|
|
316
|
+
const version = value["version"];
|
|
317
|
+
if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
|
|
318
|
+
if (version !== 2) return void 0;
|
|
319
|
+
const mode = value["mode"];
|
|
320
|
+
if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
|
|
321
|
+
assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
|
|
322
|
+
const provider = value["provider"];
|
|
323
|
+
if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
|
|
324
|
+
if (mode === "one-shot") {
|
|
325
|
+
const label = optionalString(value, "label");
|
|
326
|
+
return {
|
|
327
|
+
version: 2,
|
|
328
|
+
mode,
|
|
329
|
+
provider,
|
|
330
|
+
...label !== void 0 ? { label } : {}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const label = value["label"];
|
|
334
|
+
if (typeof label !== "string") throw new Error("persisted subagent descriptor label must be a string");
|
|
335
|
+
const agentProvider = optionalString(value, "agentProvider");
|
|
336
|
+
const agentModel = optionalString(value, "agentModel");
|
|
337
|
+
const persona = optionalString(value, "persona");
|
|
338
|
+
const toolFilter = Object.hasOwn(value, "toolFilter") ? parseToolFilter(value["toolFilter"]) : void 0;
|
|
339
|
+
return {
|
|
340
|
+
version: 2,
|
|
341
|
+
mode,
|
|
342
|
+
provider,
|
|
343
|
+
label,
|
|
344
|
+
...agentProvider !== void 0 ? { agentProvider } : {},
|
|
345
|
+
...agentModel !== void 0 ? { agentModel } : {},
|
|
346
|
+
...persona !== void 0 ? { persona } : {},
|
|
347
|
+
...toolFilter !== void 0 ? { toolFilter } : {}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function snapshotSubagentDescriptor(input) {
|
|
351
|
+
const snapshot = snapshotJsonValue(input.mode === "one-shot" ? {
|
|
352
|
+
version: 2,
|
|
353
|
+
mode: input.mode,
|
|
354
|
+
provider: input.provider,
|
|
355
|
+
...input.label !== void 0 ? { label: input.label } : {}
|
|
356
|
+
} : {
|
|
357
|
+
version: 2,
|
|
358
|
+
mode: input.mode,
|
|
359
|
+
provider: input.provider,
|
|
360
|
+
label: input.label,
|
|
361
|
+
...input.agentProvider !== void 0 ? { agentProvider: input.agentProvider } : {},
|
|
362
|
+
...input.agentModel !== void 0 ? { agentModel: input.agentModel } : {},
|
|
363
|
+
...input.persona !== void 0 ? { persona: input.persona } : {},
|
|
364
|
+
...input.toolFilter !== void 0 ? { toolFilter: input.toolFilter } : {}
|
|
365
|
+
});
|
|
366
|
+
if (snapshot === void 0) throw new Error("subagent descriptor is not losslessly JSON-serializable");
|
|
367
|
+
return snapshot;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Fold a persisted child log to its supported descriptor. The first
|
|
371
|
+
* `subagent/descriptor` event is authoritative — the establishing provider
|
|
372
|
+
* appends exactly one, so a later same-type event cannot rewrite the declared
|
|
373
|
+
* composition.
|
|
374
|
+
* @param events - the loaded child session events.
|
|
375
|
+
* @returns the descriptor, or `undefined` when the log has none or its
|
|
376
|
+
* version is not {@link SUBAGENT_DESCRIPTOR_VERSION} (the child cannot be
|
|
377
|
+
* classified by this runtime).
|
|
378
|
+
* @throws when a current-version persisted payload does not match its complete
|
|
379
|
+
* declared schema.
|
|
380
|
+
*/
|
|
381
|
+
function foldSubagentDescriptor(events) {
|
|
382
|
+
const event = events.find((candidate) => candidate.type === "subagent/descriptor");
|
|
383
|
+
if (event === void 0) return void 0;
|
|
384
|
+
return parseSubagentDescriptor(event.data);
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region lib/types/child-agent.js
|
|
388
|
+
/**
|
|
389
|
+
* Shared in-process child composition: the delegation-depth budget, the
|
|
390
|
+
* durable session metadata, the resolved child `AgentOptions`, the delegated
|
|
391
|
+
* policy seed, and the scoped setup a child agent needs. Both the one-shot
|
|
392
|
+
* provider driver and the continuation manager compose children this way, so
|
|
393
|
+
* depth accounting, lineage stamping, and delegation policy have one home.
|
|
394
|
+
*
|
|
395
|
+
* @module @deepseek-ai/dsh-subagent/child-agent
|
|
396
|
+
*/
|
|
397
|
+
/** Thrown when starting a child would exceed the requested depth cap. */
|
|
398
|
+
var SubagentDepthError = class extends Error {
|
|
399
|
+
attemptedDepth;
|
|
400
|
+
maxDepth;
|
|
401
|
+
constructor(attemptedDepth, maxDepth) {
|
|
402
|
+
super(`subagent depth ${attemptedDepth} exceeds maxDepth ${maxDepth}`);
|
|
403
|
+
this.attemptedDepth = attemptedDepth;
|
|
404
|
+
this.maxDepth = maxDepth;
|
|
405
|
+
this.name = "SubagentDepthError";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Resolve the child's delegation depth from its parent and enforce an optional
|
|
410
|
+
* cap. The persisted parent header is the monotone floor, so a resumed parent
|
|
411
|
+
* cannot delegate as if it were top-level.
|
|
412
|
+
* @param parent - the delegating parent agent.
|
|
413
|
+
* @param maxDepth - optional absolute cap the resolved depth must not exceed.
|
|
414
|
+
* @returns the child's non-negative safe-integer depth.
|
|
415
|
+
* @throws {SubagentDepthError} when the resolved depth exceeds `maxDepth`.
|
|
416
|
+
* @throws {RangeError} when the resolved depth leaves the safe-integer range.
|
|
417
|
+
*/
|
|
418
|
+
function resolveChildDepth(parent, maxDepth) {
|
|
419
|
+
const childDepth = delegationDepthOf(parent) + 1;
|
|
420
|
+
if (!Number.isSafeInteger(childDepth)) throw new RangeError("subagent child depth exceeds the safe-integer range");
|
|
421
|
+
if (maxDepth !== void 0 && childDepth > maxDepth) throw new SubagentDepthError(childDepth, maxDepth);
|
|
422
|
+
return childDepth;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve the child's `AgentOptions`: the parent's provider/model/maxTokens
|
|
426
|
+
* route unless the request overrides it, stamped with the child's own
|
|
427
|
+
* delegation depth.
|
|
428
|
+
* @param parent - the delegating parent whose route the child inherits.
|
|
429
|
+
* @param requested - per-child overrides, if any.
|
|
430
|
+
* @param childDepth - the resolved delegation depth to stamp.
|
|
431
|
+
* @returns the resolved options for `ctx.agents.create()`.
|
|
432
|
+
*/
|
|
433
|
+
function resolveChildAgentOptions(parent, requested, childDepth) {
|
|
434
|
+
const parentProvider = parent.options.provider;
|
|
435
|
+
const parentModel = parent.options.model;
|
|
436
|
+
const parentMaxTokens = parent.options.maxTokens;
|
|
437
|
+
return {
|
|
438
|
+
...parentProvider !== void 0 ? { provider: parentProvider } : {},
|
|
439
|
+
...parentModel !== void 0 ? { model: parentModel } : {},
|
|
440
|
+
...parentMaxTokens !== void 0 ? { maxTokens: parentMaxTokens } : {},
|
|
441
|
+
...requested,
|
|
442
|
+
subagentDepth: childDepth
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Build the child session's durable creation metadata: the parent's workspace,
|
|
447
|
+
* its direct lineage, coarse product origin, the recursion budget that must
|
|
448
|
+
* survive persistence, the seed boundary that separates inherited parent
|
|
449
|
+
* history from child work, and the composition the child runs under.
|
|
450
|
+
*
|
|
451
|
+
* The preset is read from the parent's LIVE scope chain rather than from its
|
|
452
|
+
* header, because a parent that switched preset while blank runs on the newer
|
|
453
|
+
* composition and its header still names the older one. Recording it is what
|
|
454
|
+
* makes a child's history reconstructable: without it a cold read of the child
|
|
455
|
+
* resolves the deployment default and rebuilds turns under a tool set the
|
|
456
|
+
* child never had.
|
|
457
|
+
* @param parent - the delegating parent agent.
|
|
458
|
+
* @param childDepth - the resolved delegation depth to persist.
|
|
459
|
+
* @param lineageSeedLength - how many leading events came from the parent's log.
|
|
460
|
+
* @returns the `meta` for `ctx.agents.create()`.
|
|
461
|
+
*/
|
|
462
|
+
function childSessionMeta(parent, childDepth, lineageSeedLength) {
|
|
463
|
+
const parentHeader = parent.session.header;
|
|
464
|
+
const agentPreset = parent.ctx.get("agentPresets")?.composedPreset(parent.ctx);
|
|
465
|
+
return {
|
|
466
|
+
...parentHeader.cwd !== void 0 ? { cwd: parentHeader.cwd } : {},
|
|
467
|
+
...agentPreset === void 0 ? {} : { agentPreset },
|
|
468
|
+
parentSession: parentHeader.id,
|
|
469
|
+
origin: "subagent",
|
|
470
|
+
delegationDepth: childDepth,
|
|
471
|
+
...lineageSeedLength > 0 ? { seedLength: lineageSeedLength } : {}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Model-facing delegation-scope statement for every in-process child. A
|
|
476
|
+
* runtime-context contribution rather than a system-prompt section, so the
|
|
477
|
+
* deployment's system prompt stays uniform across parents and children.
|
|
478
|
+
*/
|
|
479
|
+
const SUBAGENT_DELEGATION_CONTEXT = "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.";
|
|
480
|
+
/**
|
|
481
|
+
* Compose one child inside its creation window: join its parent's preset,
|
|
482
|
+
* register the fixed delegation-scope statement, then apply the child's own
|
|
483
|
+
* shadowing persona section and tool restriction, all owned by the child's
|
|
484
|
+
* scope and therefore invisible to its parent and siblings. Creation and cold
|
|
485
|
+
* resume both pass through here.
|
|
486
|
+
*
|
|
487
|
+
* The join comes first and the child's own registrations second, which is the
|
|
488
|
+
* order the layering already implies — the nearest scope wins a name, and a
|
|
489
|
+
* per-child restriction intersects with everything its chain admits — but
|
|
490
|
+
* stating it here keeps the two steps from being read as independent.
|
|
491
|
+
*
|
|
492
|
+
* The join and the per-child registrations live in ONE call because a child
|
|
493
|
+
* composed without the join is exactly the defect this function exists to
|
|
494
|
+
* prevent: with every model-facing row on the agent plane, a child that joins
|
|
495
|
+
* no preset sees an empty tool registry and none of its parent's prompt
|
|
496
|
+
* sections. Taking the parent as a parameter is what makes that omission
|
|
497
|
+
* unrepresentable at the call sites.
|
|
498
|
+
* @param childCtx - the child agent's scoped creation context.
|
|
499
|
+
* @param parent - the delegating parent whose composition the child joins.
|
|
500
|
+
* @param composition - the per-child persona and tool filter to install.
|
|
501
|
+
*/
|
|
502
|
+
function applyChildComposition(childCtx, parent, composition) {
|
|
503
|
+
childCtx.get("agentPresets")?.composeFrom(childCtx, parent.ctx);
|
|
504
|
+
childCtx.systemPrompt.context({
|
|
505
|
+
name: "subagent:delegation",
|
|
506
|
+
order: 120,
|
|
507
|
+
text: SUBAGENT_DELEGATION_CONTEXT
|
|
508
|
+
});
|
|
509
|
+
if (composition.persona !== void 0) childCtx.systemPrompt.section({
|
|
510
|
+
name: "deployment:persona",
|
|
511
|
+
order: 0,
|
|
512
|
+
text: composition.persona
|
|
513
|
+
});
|
|
514
|
+
if (composition.toolFilter !== void 0) childCtx.tools.restrict(composition.toolFilter);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Capture the policy to seed into one delegation. Call synchronously before
|
|
518
|
+
* the child start's first await: a later parent switch belongs to the
|
|
519
|
+
* parent's future, not to this child. Only the parent session's explicit
|
|
520
|
+
* sandbox override is captured — never deployment defaults or one-shot
|
|
521
|
+
* grants — and the approval policy is pinned to `'never'` regardless of the
|
|
522
|
+
* parent's own policy.
|
|
523
|
+
* @param parent - the delegating parent agent.
|
|
524
|
+
* @returns the sandbox override (or `undefined` without one) and the approval pin.
|
|
525
|
+
*/
|
|
526
|
+
function captureDelegatedPolicyOverrides(parent) {
|
|
527
|
+
return {
|
|
528
|
+
sandboxMode: parent.ctx.get("sandboxPolicy")?.overrideOf(parent.session),
|
|
529
|
+
approvalPolicy: parent.ctx.get("approval") === void 0 ? void 0 : "never"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Append the captured delegation policy onto the child's own log as
|
|
534
|
+
* `source: 'delegation'` events inside the unpublished creation window, so the
|
|
535
|
+
* child's effective policy is reconstructable from its log alone. Appends land
|
|
536
|
+
* after any fork seed, so fresh policy wins stale seed state; later child
|
|
537
|
+
* switches still win over these events.
|
|
538
|
+
* @param childSession - the unpublished child's session.
|
|
539
|
+
* @param overrides - the policy captured at delegation.
|
|
540
|
+
*/
|
|
541
|
+
function appendDelegatedPolicyOverrides(childSession, overrides) {
|
|
542
|
+
if (overrides.sandboxMode !== void 0) childSession.append("sandbox/mode", {
|
|
543
|
+
mode: overrides.sandboxMode,
|
|
544
|
+
source: "delegation"
|
|
545
|
+
});
|
|
546
|
+
if (overrides.approvalPolicy !== void 0) childSession.append("approval/policy", {
|
|
547
|
+
policy: overrides.approvalPolicy,
|
|
548
|
+
source: "delegation"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region lib/types/descriptor-seed.js
|
|
553
|
+
/**
|
|
554
|
+
* Seeding of a continuable child's durable descriptor event: the model-hidden
|
|
555
|
+
* record of the child's declared composition before its first request, so a
|
|
556
|
+
* later cold resume can reconstruct it from its own log.
|
|
557
|
+
*
|
|
558
|
+
* @module @deepseek-ai/dsh-subagent/descriptor-seed
|
|
559
|
+
*/
|
|
560
|
+
/**
|
|
561
|
+
* Build the child's creation seed: any inherited parent-history prefix followed
|
|
562
|
+
* by one model-hidden, between-turn `descriptor` event. Staging through a
|
|
563
|
+
* `Session` assigns the sequence number and enforces the same lossless-JSON
|
|
564
|
+
* rules the durable log does.
|
|
565
|
+
* @param childId - the reserved child session id the staged log belongs to.
|
|
566
|
+
* @param seed - the inherited completed-turn prefix, or `undefined` for a fresh child.
|
|
567
|
+
* @param descriptor - the snapshotted composition record to persist.
|
|
568
|
+
* @returns the complete seed events, contiguous from sequence zero.
|
|
569
|
+
*/
|
|
570
|
+
function seedDescriptorTurn(childId, seed, descriptor) {
|
|
571
|
+
const staged = Session.create(childId, seed);
|
|
572
|
+
staged.append("subagent/descriptor", descriptor);
|
|
573
|
+
return [...staged.events];
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region lib/types/continuation.js
|
|
577
|
+
/**
|
|
578
|
+
* Internal continuable-subagent manager: stable child ids, descriptor
|
|
579
|
+
* persistence, activation admission, the live ownership graph, cold resume,
|
|
580
|
+
* and child-first disposal behind `ctx.subagents`.
|
|
581
|
+
*
|
|
582
|
+
* A continuable child has one durable Session and at most one process-local
|
|
583
|
+
* {@link Activation} — one residency epoch for a reconstructed child Agent. An
|
|
584
|
+
* Activation is not a request, result, cancellation, or Task boundary: it may
|
|
585
|
+
* execute many FIFO turns and stays resident while descendants it created are
|
|
586
|
+
* still running. The Agent inbox is the only turn queue, so this manager owns
|
|
587
|
+
* residency while the Agent loop owns all turn ordering and execution. No
|
|
588
|
+
* continuable path creates a Task or an intermediate result-bearing wrapper.
|
|
589
|
+
*
|
|
590
|
+
* @module @deepseek-ai/dsh-subagent
|
|
591
|
+
*/
|
|
592
|
+
/**
|
|
593
|
+
* Read one Activation's current disposal transaction. This indirection exists
|
|
594
|
+
* because TypeScript would otherwise narrow repeated reads of the mutable field
|
|
595
|
+
* inside a long-lived closure to constants instead of re-reading runtime state.
|
|
596
|
+
* @param activation - the Activation to inspect.
|
|
597
|
+
* @returns the in-flight or settled disposal, or `undefined` while resident.
|
|
598
|
+
*/
|
|
599
|
+
function disposalOf(activation) {
|
|
600
|
+
return activation.disposal;
|
|
601
|
+
}
|
|
602
|
+
/** Serialize each durable child's delivery, release, and disposal. */
|
|
603
|
+
var ChildLock = class {
|
|
604
|
+
tails = /* @__PURE__ */ new Map();
|
|
605
|
+
/**
|
|
606
|
+
* Run `operation` after every previously queued operation for `childId`.
|
|
607
|
+
* @param childId - the durable child whose operations are linearized.
|
|
608
|
+
* @param operation - the critical section to run in order.
|
|
609
|
+
* @returns the operation's own settlement.
|
|
610
|
+
*/
|
|
611
|
+
run(childId, operation) {
|
|
612
|
+
const result = (this.tails.get(childId) ?? Promise.resolve()).then(operation, operation);
|
|
613
|
+
const tail = result.then(() => void 0, () => void 0);
|
|
614
|
+
this.tails.set(childId, tail);
|
|
615
|
+
tail.then(() => {
|
|
616
|
+
if (this.tails.get(childId) === tail) this.tails.delete(childId);
|
|
617
|
+
});
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
/**
|
|
622
|
+
* The continuable-subagent orchestration service behind `ctx.subagents`. Tool
|
|
623
|
+
* schema and host adapters are consumers of this one contract; foreground
|
|
624
|
+
* one-shot delegation keeps calling `ctx.subagents.start()` and never enters
|
|
625
|
+
* this lifecycle.
|
|
626
|
+
*/
|
|
627
|
+
var SubagentContinuationManager = class {
|
|
628
|
+
ctx;
|
|
629
|
+
host;
|
|
630
|
+
setupRegistry;
|
|
631
|
+
/** Child session id → its live Activation. Process-local, never durable. */
|
|
632
|
+
activations = /* @__PURE__ */ new Map();
|
|
633
|
+
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
634
|
+
materializations = /* @__PURE__ */ new Set();
|
|
635
|
+
locks = new ChildLock();
|
|
636
|
+
/** Structural Cordis owner of every Activation handle. */
|
|
637
|
+
ownerCtx;
|
|
638
|
+
/**
|
|
639
|
+
* Exact roots whose host teardown has begun, with the live lineage members
|
|
640
|
+
* observed under each root. Entries remain until that exact root leaves the
|
|
641
|
+
* Agent registry, closing admission throughout its host's teardown without
|
|
642
|
+
* poisoning a later same-id replacement.
|
|
643
|
+
*/
|
|
644
|
+
closingScopes = /* @__PURE__ */ new Map();
|
|
645
|
+
draining = false;
|
|
646
|
+
constructor(ctx, host, setupRegistry) {
|
|
647
|
+
this.ctx = ctx;
|
|
648
|
+
this.host = host;
|
|
649
|
+
this.setupRegistry = setupRegistry;
|
|
650
|
+
const scope = ctx.plugin(function activationOwner() {});
|
|
651
|
+
this.ownerCtx = scope.ctx;
|
|
652
|
+
ctx.on("agent/disposed", ({ agent }) => {
|
|
653
|
+
this.closingScopes.delete(agent);
|
|
654
|
+
});
|
|
655
|
+
ctx.effect(function* () {
|
|
656
|
+
yield scope.dispose;
|
|
657
|
+
yield () => this.drain();
|
|
658
|
+
}.bind(this), "subagents.continuations()");
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Start one continuable background child: reserve its durable identity,
|
|
662
|
+
* resolve the provider's detached creation spec, create the child Agent
|
|
663
|
+
* through the private activation-owner scope, establish any continuable-parent
|
|
664
|
+
* ownership, and submit the initial prompt. Resolves when inbox acceptance
|
|
665
|
+
* yields the message id — without waiting for the turn to start or for the
|
|
666
|
+
* message to reach the Session log.
|
|
667
|
+
*
|
|
668
|
+
* Every failure before that acceptance rejects without either id, disposing
|
|
669
|
+
* any created handle and rolling back the Activation and parent ownership.
|
|
670
|
+
* The caller signal owns lookup, materialization, and admission only until
|
|
671
|
+
* acceptance; afterwards the manager owns the Activation independently.
|
|
672
|
+
* @param spec - provider, delegation request, and caller cancellation.
|
|
673
|
+
* @returns the durable child id and the accepted initial prompt's message id.
|
|
674
|
+
*/
|
|
675
|
+
async startContinuable(spec) {
|
|
676
|
+
const request = spec.request;
|
|
677
|
+
const parent = request.parent;
|
|
678
|
+
this.assertAdmitting(parent);
|
|
679
|
+
this.requirePersistence();
|
|
680
|
+
assertSubagentMaxDepth(request.maxDepth);
|
|
681
|
+
const childId = SessionId(randomUUID());
|
|
682
|
+
const childDepth = resolveChildDepth(parent, request.maxDepth);
|
|
683
|
+
const agentProvider = request.agentOptions?.provider ?? parent.options.provider;
|
|
684
|
+
const agentModel = request.agentOptions?.model ?? parent.options.model;
|
|
685
|
+
const descriptor = snapshotSubagentDescriptor({
|
|
686
|
+
mode: "continuable",
|
|
687
|
+
provider: spec.provider,
|
|
688
|
+
label: spec.label,
|
|
689
|
+
...agentProvider !== void 0 ? { agentProvider } : {},
|
|
690
|
+
...agentModel !== void 0 ? { agentModel } : {},
|
|
691
|
+
...request.persona !== void 0 ? { persona: request.persona } : {},
|
|
692
|
+
...request.toolFilter !== void 0 ? { toolFilter: request.toolFilter } : {}
|
|
693
|
+
});
|
|
694
|
+
const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
|
|
695
|
+
const prepared = await this.host.prepareContinuable(spec.provider, {
|
|
696
|
+
sessionId: childId,
|
|
697
|
+
parent,
|
|
698
|
+
signal: spec.signal
|
|
699
|
+
});
|
|
700
|
+
spec.signal.throwIfAborted();
|
|
701
|
+
this.assertAdmitting(parent);
|
|
702
|
+
const lineageSeedLength = prepared.seed?.length ?? 0;
|
|
703
|
+
const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
|
|
704
|
+
return {
|
|
705
|
+
childId,
|
|
706
|
+
messageId: await this.locks.run(childId, async () => {
|
|
707
|
+
const activation = await this.materialize({
|
|
708
|
+
childId,
|
|
709
|
+
provider: spec.provider,
|
|
710
|
+
parent,
|
|
711
|
+
create: {
|
|
712
|
+
seed,
|
|
713
|
+
meta: childSessionMeta(parent, childDepth, lineageSeedLength),
|
|
714
|
+
delegatedPolicies
|
|
715
|
+
},
|
|
716
|
+
agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
|
|
717
|
+
composition: {
|
|
718
|
+
persona: request.persona,
|
|
719
|
+
toolFilter: request.toolFilter
|
|
720
|
+
},
|
|
721
|
+
signal: spec.signal
|
|
722
|
+
});
|
|
723
|
+
return this.submitMaterialized(activation, request.prompt, { kind: "user" }, parent, spec.signal);
|
|
724
|
+
})
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Deliver one later message to a known continuable child as its next FIFO
|
|
729
|
+
* turn. Routing depends only on Activation residency: a `running` Activation
|
|
730
|
+
* enqueues, a `waiting` one wakes the same Agent, and an absent one
|
|
731
|
+
* cold-resumes a new Activation from the persisted Session. The Agent inbox
|
|
732
|
+
* is the only queue, so every accepted message has one observable order.
|
|
733
|
+
*
|
|
734
|
+
* The caller signal owns lookup, materialization, and admission only until
|
|
735
|
+
* inbox acceptance; afterwards the accepted turn cannot be cancelled through
|
|
736
|
+
* this service.
|
|
737
|
+
* @param parent - the exact live direct parent authorizing this delivery.
|
|
738
|
+
* @param childId - the durable child session id.
|
|
739
|
+
* @param content - the user-role content to deliver.
|
|
740
|
+
* @param options - the message source fields and caller cancellation.
|
|
741
|
+
* @returns the accepted message's inbox id.
|
|
742
|
+
* @throws when parent authority, availability, or admission rejects the delivery.
|
|
743
|
+
*/
|
|
744
|
+
async followup(parent, childId, content, options) {
|
|
745
|
+
this.assertAdmitting(parent);
|
|
746
|
+
while (true) {
|
|
747
|
+
const live = await this.locks.run(childId, async () => {
|
|
748
|
+
const activation = this.activations.get(childId);
|
|
749
|
+
if (activation === void 0) return this.coldResume(parent, childId, content, options);
|
|
750
|
+
/* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
|
|
751
|
+
* delivery to observe the transaction inside the same critical section that opened it,
|
|
752
|
+
* which no test can schedule deterministically. The behavior is covered end-to-end by
|
|
753
|
+
* "cold-resumes a delivery that lost the race with final disposal". */
|
|
754
|
+
if (activation.disposal !== void 0) return activation.disposal.then(() => void 0, () => void 0);
|
|
755
|
+
return this.submitAdmitted(activation, content, options.source, parent, options.signal);
|
|
756
|
+
});
|
|
757
|
+
/* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
|
|
758
|
+
* race reaches the retry below, which then cold-resumes a new Activation. */
|
|
759
|
+
if (live !== void 0) return live;
|
|
760
|
+
this.assertAdmitting(parent);
|
|
761
|
+
options.signal.throwIfAborted();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Interrupt one live continuable child's current turn. Admission is
|
|
766
|
+
* synchronous and the effect is asynchronous: this authorizes the caller,
|
|
767
|
+
* requests `Agent.cancel(cause, { keepInbox: true })` on the target, and
|
|
768
|
+
* returns without waiting for the target to observe the signal or reach
|
|
769
|
+
* quiescence. The Activation, its handle, accepted unclaimed inbox work, and
|
|
770
|
+
* already-published descendants are untouched; work already claimed into the
|
|
771
|
+
* interrupted turn is not requeued. Once the interrupted driver is idle, a
|
|
772
|
+
* waking send resumes the parked queue.
|
|
773
|
+
*
|
|
774
|
+
* An absent target is an accepted no-op, which uniformly covers natural
|
|
775
|
+
* completion races, repeated requests, one-shot ids, and unknown ids without
|
|
776
|
+
* consulting the durable catalog. A target whose disposal transaction is
|
|
777
|
+
* already open is likewise an accepted no-op after authorization.
|
|
778
|
+
* @param targetSessionId - the durable child session id to interrupt.
|
|
779
|
+
* @param authority - the human parent address or exact live ancestor Agent.
|
|
780
|
+
* @throws {SubagentError} `UNAUTHORIZED` when the presented authority does
|
|
781
|
+
* not own the live target: a stale or self-targeting ancestor caller, a
|
|
782
|
+
* parent address that is not the live target's durable direct parent, or
|
|
783
|
+
* an ancestor outside the target's recorded live lineage.
|
|
784
|
+
*/
|
|
785
|
+
interrupt(targetSessionId, authority) {
|
|
786
|
+
if (authority.kind === "ancestor") {
|
|
787
|
+
const caller = authority.agent;
|
|
788
|
+
if (this.ctx.agents.get(caller.id) !== caller) throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, "UNAUTHORIZED");
|
|
789
|
+
if (caller.id === targetSessionId) throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, "UNAUTHORIZED");
|
|
790
|
+
}
|
|
791
|
+
const activation = this.activations.get(targetSessionId);
|
|
792
|
+
if (activation === void 0) return;
|
|
793
|
+
if (authority.kind === "user") {
|
|
794
|
+
if (activation.handle.agent.session.header.parentSession !== authority.parentSessionId) throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, "UNAUTHORIZED");
|
|
795
|
+
} else if (!activation.ancestry.has(authority.agent)) throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, "UNAUTHORIZED");
|
|
796
|
+
if (activation.disposal !== void 0) return;
|
|
797
|
+
activation.handle.agent.cancel(authority.kind === "user" ? { kind: "user" } : { kind: "parent" }, { keepInbox: true });
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Deliver explicitly selected content from one resident continuable child to
|
|
801
|
+
* its durable direct parent. Sender authorization, parent resolution, and
|
|
802
|
+
* send acceptance share one no-await span. Reporting neither concludes the
|
|
803
|
+
* child's turn nor changes its Activation lifetime.
|
|
804
|
+
* @param child - exact live reporting child; this is the authority credential.
|
|
805
|
+
* @param content - selected model-facing content.
|
|
806
|
+
* @param options - scheduling policy and pre-acceptance cancellation.
|
|
807
|
+
* @returns the stable identity of the message accepted by the parent.
|
|
808
|
+
* @throws {SubagentError} when the sender is unauthorized, the parent is not
|
|
809
|
+
* live, or continuation admission is closing.
|
|
810
|
+
*/
|
|
811
|
+
async reportFrom(child, content, options) {
|
|
812
|
+
options.signal.throwIfAborted();
|
|
813
|
+
this.assertAdmitting(child);
|
|
814
|
+
const activation = this.authorizeReporter(child);
|
|
815
|
+
const parent = this.resolveReportParent(child);
|
|
816
|
+
return this.deliverReport(activation, parent, content, options.delivery);
|
|
817
|
+
}
|
|
818
|
+
/** Authorize only the exact Agent of one resident Activation. */
|
|
819
|
+
authorizeReporter(child) {
|
|
820
|
+
const activation = this.activations.get(child.id);
|
|
821
|
+
if (activation === void 0 || activation.handle.agent !== child) throw new SubagentError(`agent "${child.id}" is not a live continuable subagent and cannot report`, "UNAUTHORIZED");
|
|
822
|
+
/* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
|
|
823
|
+
* transaction between exact-agent authorization and this no-await cutoff. */
|
|
824
|
+
if (activation.disposal !== void 0) throw new SubagentError(`subagent "${child.id}" activation is being disposed; the report was not delivered`, "ACTIVATION_CLOSING");
|
|
825
|
+
return activation;
|
|
826
|
+
}
|
|
827
|
+
/** Resolve the reporting child's live direct parent from durable lineage. */
|
|
828
|
+
resolveReportParent(child) {
|
|
829
|
+
const parentId = child.session.header.parentSession;
|
|
830
|
+
/* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
|
|
831
|
+
const parent = parentId === void 0 ? void 0 : this.ctx.agents.get(parentId);
|
|
832
|
+
if (parent === void 0) throw new SubagentError("direct parent is not live; report was not delivered", "PARENT_UNAVAILABLE");
|
|
833
|
+
return parent;
|
|
834
|
+
}
|
|
835
|
+
/** Deliver one framed report through the selected parent scheduling preset. */
|
|
836
|
+
deliverReport(activation, parent, content, delivery) {
|
|
837
|
+
const message = createUserMessage({
|
|
838
|
+
content: [{
|
|
839
|
+
type: "text",
|
|
840
|
+
text: `Background subagent ${activation.childId} reported:`
|
|
841
|
+
}, ...content],
|
|
842
|
+
source: {
|
|
843
|
+
kind: "subagent-report",
|
|
844
|
+
form: "relay",
|
|
845
|
+
senderSessionId: activation.childId
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
const parentActivation = this.activations.get(parent.id);
|
|
849
|
+
if (delivery === "wakeup" && parentActivation !== void 0 && parentActivation.handle.agent === parent) this.admitWaking(parentActivation, message.id, () => {
|
|
850
|
+
this.sendReport(parent, message, delivery);
|
|
851
|
+
});
|
|
852
|
+
else this.sendReport(parent, message, delivery);
|
|
853
|
+
return message.id;
|
|
854
|
+
}
|
|
855
|
+
/** Send one report while translating only the parent's own rejection. */
|
|
856
|
+
sendReport(parent, message, delivery) {
|
|
857
|
+
try {
|
|
858
|
+
if (delivery === "wakeup") parent.followup(message);
|
|
859
|
+
else parent.inject(message);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
throw new SubagentError("direct parent is not live; report was not delivered", "PARENT_UNAVAILABLE", { cause: error });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Close admission, await every already-admitted materialization through
|
|
866
|
+
* publication or rollback, then dispose the stable live Activation forest
|
|
867
|
+
* child-first. Sibling branches drain independently: one failure is recorded
|
|
868
|
+
* but never prevents the remaining handles from being attempted, and the
|
|
869
|
+
* aggregate rejects only after every branch settles.
|
|
870
|
+
* @returns once materialization is quiescent and every live Activation released its handle.
|
|
871
|
+
* @throws an aggregate error when any branch failed to release.
|
|
872
|
+
*/
|
|
873
|
+
async drain() {
|
|
874
|
+
this.draining = true;
|
|
875
|
+
await Promise.all([...this.materializations].map((materialization) => materialization.settled));
|
|
876
|
+
const owned = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const activation of this.activations.values()) for (const child of activation.ownedChildren) owned.add(child);
|
|
878
|
+
const roots = [...this.activations.values()].filter((activation) => !owned.has(activation.childId));
|
|
879
|
+
await this.disposeRoots(roots, "activation(s)");
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Stop only the continuable descendants of exact live host-owned parents.
|
|
883
|
+
* Admission stays closed for those parent trees until each exact parent
|
|
884
|
+
* leaves the Agent registry; unrelated trees and manager-wide admission stay
|
|
885
|
+
* live.
|
|
886
|
+
* @param parents - exact live roots whose continuable descendants must stop.
|
|
887
|
+
* @returns once every retained descendant Activation released its handle.
|
|
888
|
+
* @throws an aggregate error after all scoped branches settle when any failed.
|
|
889
|
+
*/
|
|
890
|
+
async drainDescendants(parents) {
|
|
891
|
+
const roots = new Set(parents.filter((parent) => this.ctx.agents.get(parent.id) === parent));
|
|
892
|
+
if (roots.size === 0) return;
|
|
893
|
+
for (const root of roots) this.closingMembers(root).add(root);
|
|
894
|
+
const targets = [];
|
|
895
|
+
for (const activation of this.activations.values()) {
|
|
896
|
+
const lineage = this.liveLineage(activation.handle.agent);
|
|
897
|
+
const owners = [...roots].filter((root) => activation.handle.agent !== root && activation.ancestry.has(root));
|
|
898
|
+
if (owners.length === 0) continue;
|
|
899
|
+
targets.push(activation);
|
|
900
|
+
for (const owner of owners) {
|
|
901
|
+
const members = this.closingMembers(owner);
|
|
902
|
+
members.add(activation.handle.agent);
|
|
903
|
+
for (const agent of lineage) members.add(agent);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const materializations = [...this.materializations].filter((materialization) => {
|
|
907
|
+
const owners = [...roots].filter((root) => materialization.lineage.includes(root));
|
|
908
|
+
for (const owner of owners) {
|
|
909
|
+
const members = this.closingMembers(owner);
|
|
910
|
+
for (const agent of materialization.lineage) members.add(agent);
|
|
911
|
+
}
|
|
912
|
+
return owners.length > 0;
|
|
913
|
+
});
|
|
914
|
+
const ownedTargets = /* @__PURE__ */ new Set();
|
|
915
|
+
for (const activation of targets) for (const child of activation.ownedChildren) ownedTargets.add(child);
|
|
916
|
+
const targetRoots = targets.filter((activation) => !ownedTargets.has(activation.childId));
|
|
917
|
+
for (const activation of targets) this.dispose(activation).catch(() => void 0);
|
|
918
|
+
await Promise.all(materializations.map((materialization) => materialization.settled));
|
|
919
|
+
await this.disposeRoots(targetRoots, "scoped activation(s)");
|
|
920
|
+
}
|
|
921
|
+
/** Dispose independent roots and report every branch failure after all settle. */
|
|
922
|
+
async disposeRoots(roots, failureSubject) {
|
|
923
|
+
const reasons = (await Promise.all(roots.map(async (activation) => {
|
|
924
|
+
try {
|
|
925
|
+
await this.dispose(activation);
|
|
926
|
+
return;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
return error;
|
|
929
|
+
}
|
|
930
|
+
}))).filter((failure) => failure !== void 0);
|
|
931
|
+
if (reasons.length > 0) throw new SubagentError(`continuable subagent teardown failed for ${reasons.length} ${failureSubject}: ` + reasons.map((reason) => errorChain(reason)).join("; "), "ACTIVATION_TEARDOWN_FAILED");
|
|
932
|
+
}
|
|
933
|
+
/** Return the retained member set for one exact scoped-teardown root. */
|
|
934
|
+
closingMembers(root) {
|
|
935
|
+
const existing = this.closingScopes.get(root);
|
|
936
|
+
if (existing !== void 0) return existing;
|
|
937
|
+
const members = /* @__PURE__ */ new Set();
|
|
938
|
+
this.closingScopes.set(root, members);
|
|
939
|
+
return members;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Return the exact currently resolvable ancestry from `agent` upward. The
|
|
943
|
+
* first element is always the supplied identity, even when it is already
|
|
944
|
+
* stale; each ancestor after it must be the registry's current exact entry.
|
|
945
|
+
*/
|
|
946
|
+
liveLineage(agent) {
|
|
947
|
+
const lineage = [agent];
|
|
948
|
+
const seen = new Set([agent.id]);
|
|
949
|
+
let parentSession = agent.session.header.parentSession;
|
|
950
|
+
while (parentSession !== void 0) {
|
|
951
|
+
const parent = this.ctx.agents.get(parentSession);
|
|
952
|
+
if (parent === void 0 || seen.has(parent.id)) break;
|
|
953
|
+
lineage.push(parent);
|
|
954
|
+
seen.add(parent.id);
|
|
955
|
+
parentSession = parent.session.header.parentSession;
|
|
956
|
+
}
|
|
957
|
+
return lineage;
|
|
958
|
+
}
|
|
959
|
+
/** Reject new admission once the manager or this exact parent tree began draining. */
|
|
960
|
+
assertAdmitting(agent) {
|
|
961
|
+
if (this.draining) throw new SubagentError("continuable subagents are draining; the operation was not admitted", "DRAINING");
|
|
962
|
+
const lineage = this.liveLineage(agent);
|
|
963
|
+
for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) throw new SubagentError(`continuable subagents below parent "${root.id}" are draining; the operation was not admitted`, "DRAINING");
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Derive residency from Agent quiescence and the owned-child set. `running`
|
|
967
|
+
* covers an active admission, an open turn, or accepted waking inbox work.
|
|
968
|
+
*
|
|
969
|
+
* `Agent.status` alone is insufficient: it stays `idle` between an accepted
|
|
970
|
+
* waking send and the microtask that admits it, so a synchronous inbox
|
|
971
|
+
* observer would see `settled` while a turn is already queued. `accepted`
|
|
972
|
+
* holds the ids this manager admitted but has not yet seen drained.
|
|
973
|
+
*/
|
|
974
|
+
stateOf(activation) {
|
|
975
|
+
if (activation.handle.agent.status === "running" || activation.accepted.size > 0) return "running";
|
|
976
|
+
if (activation.ownedChildren.size > 0) return "waiting";
|
|
977
|
+
return "settled";
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Cold-resume a persisted child: inspect and authorize its Session, fold the
|
|
981
|
+
* generic descriptor, create the Activation through `ctx.agents.resume()`,
|
|
982
|
+
* and submit the waiting turn. This never dispatches through a subagent
|
|
983
|
+
* provider — the persisted Session already holds the initial prefix and the
|
|
984
|
+
* descriptor is the whole reconstruction input.
|
|
985
|
+
*/
|
|
986
|
+
async coldResume(parent, childId, content, options) {
|
|
987
|
+
const persistence = this.requirePersistence();
|
|
988
|
+
let loaded;
|
|
989
|
+
try {
|
|
990
|
+
loaded = await persistence.inspect(childId, options.signal);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
options.signal.throwIfAborted();
|
|
993
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
|
|
994
|
+
}
|
|
995
|
+
options.signal.throwIfAborted();
|
|
996
|
+
this.assertAdmitting(parent);
|
|
997
|
+
this.authorizeLineage(parent, childId, loaded.meta.parentSession);
|
|
998
|
+
const descriptor = foldSubagentDescriptor(loaded.events.slice(loaded.meta.seedLength ?? 0));
|
|
999
|
+
if (descriptor === void 0 || descriptor.mode !== "continuable") throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; do not retry send_message with this id`, "NOT_RESUMABLE");
|
|
1000
|
+
let activation;
|
|
1001
|
+
try {
|
|
1002
|
+
activation = await this.materialize({
|
|
1003
|
+
childId,
|
|
1004
|
+
provider: descriptor.provider,
|
|
1005
|
+
parent,
|
|
1006
|
+
agentOptions: {
|
|
1007
|
+
...descriptor.agentProvider !== void 0 ? { provider: descriptor.agentProvider } : {},
|
|
1008
|
+
...descriptor.agentModel !== void 0 ? { model: descriptor.agentModel } : {}
|
|
1009
|
+
},
|
|
1010
|
+
composition: {
|
|
1011
|
+
persona: descriptor.persona,
|
|
1012
|
+
toolFilter: descriptor.toolFilter
|
|
1013
|
+
},
|
|
1014
|
+
signal: options.signal
|
|
1015
|
+
});
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
options.signal.throwIfAborted();
|
|
1018
|
+
if (error instanceof SubagentError) throw error;
|
|
1019
|
+
throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
|
|
1020
|
+
}
|
|
1021
|
+
return this.submitMaterialized(activation, content, options.source, parent, options.signal);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Submit to a freshly materialized Activation or roll it back completely.
|
|
1025
|
+
* @param activation - the just-published Activation to admit or release.
|
|
1026
|
+
* @param content - the initial or resumed message content.
|
|
1027
|
+
* @param source - durable fields naming who supplied the accepted message.
|
|
1028
|
+
* @param parent - the live direct parent authorizing admission.
|
|
1029
|
+
* @param signal - caller cancellation owning admission until acceptance.
|
|
1030
|
+
* @returns the accepted inbox message id.
|
|
1031
|
+
*/
|
|
1032
|
+
async submitMaterialized(activation, content, source, parent, signal) {
|
|
1033
|
+
try {
|
|
1034
|
+
return this.submitAdmitted(activation, content, source, parent, signal);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
/* v8 ignore next -- rollback disposal failures must not mask the
|
|
1037
|
+
* pre-acceptance signal, drain, or lifecycle failure. */
|
|
1038
|
+
await this.dispose(activation).catch(() => void 0);
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Create or resume the child Agent through the private activation-owner
|
|
1044
|
+
* scope, install the handle in a fresh Activation, and register ownership on
|
|
1045
|
+
* a continuation-managed parent. Rejection leaves no Activation, no handle,
|
|
1046
|
+
* and no ownership membership.
|
|
1047
|
+
*/
|
|
1048
|
+
materialize(inputs) {
|
|
1049
|
+
this.assertAdmitting(inputs.parent);
|
|
1050
|
+
const settled = Promise.withResolvers();
|
|
1051
|
+
const lineage = this.liveLineage(inputs.parent);
|
|
1052
|
+
const materialization = {
|
|
1053
|
+
lineage,
|
|
1054
|
+
settled: settled.promise
|
|
1055
|
+
};
|
|
1056
|
+
this.materializations.add(materialization);
|
|
1057
|
+
return this.materializeTracked(inputs, lineage).finally(() => {
|
|
1058
|
+
this.materializations.delete(materialization);
|
|
1059
|
+
settled.resolve();
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Perform one tracked materialization. The caller keeps the drain barrier
|
|
1064
|
+
* registered until this either returns a resident Activation or finishes
|
|
1065
|
+
* rollback.
|
|
1066
|
+
*/
|
|
1067
|
+
async materializeTracked(inputs, parentLineage) {
|
|
1068
|
+
const { childId, provider, parent, create } = inputs;
|
|
1069
|
+
inputs.signal.throwIfAborted();
|
|
1070
|
+
const setup = (childCtx) => {
|
|
1071
|
+
if (create !== void 0) appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
|
|
1072
|
+
applyChildComposition(childCtx, parent, inputs.composition);
|
|
1073
|
+
return this.setupRegistry.apply(childCtx);
|
|
1074
|
+
};
|
|
1075
|
+
const observer = this.host.observeActivation(provider, childId, parent);
|
|
1076
|
+
const handle = create === void 0 ? await this.ownerCtx.agents.resume({
|
|
1077
|
+
resumeSessionId: childId,
|
|
1078
|
+
agentOptions: inputs.agentOptions,
|
|
1079
|
+
signal: inputs.signal,
|
|
1080
|
+
setup
|
|
1081
|
+
}) : await this.ownerCtx.agents.create({
|
|
1082
|
+
sessionId: childId,
|
|
1083
|
+
meta: create.meta,
|
|
1084
|
+
seed: create.seed,
|
|
1085
|
+
agentOptions: inputs.agentOptions,
|
|
1086
|
+
signal: inputs.signal,
|
|
1087
|
+
setup
|
|
1088
|
+
});
|
|
1089
|
+
const activation = {
|
|
1090
|
+
childId,
|
|
1091
|
+
provider,
|
|
1092
|
+
handle,
|
|
1093
|
+
ancestry: new WeakSet([handle.agent, ...parentLineage]),
|
|
1094
|
+
ownedChildren: /* @__PURE__ */ new Set(),
|
|
1095
|
+
observer,
|
|
1096
|
+
disposal: void 0,
|
|
1097
|
+
accepted: /* @__PURE__ */ new Set(),
|
|
1098
|
+
poke: Promise.withResolvers()
|
|
1099
|
+
};
|
|
1100
|
+
this.activations.set(childId, activation);
|
|
1101
|
+
try {
|
|
1102
|
+
inputs.signal.throwIfAborted();
|
|
1103
|
+
this.assertAdmitting(parent);
|
|
1104
|
+
this.acquireOwnership(parent, childId);
|
|
1105
|
+
handle.agent.ctx.on("agent/inbox/claimed", ({ message }) => {
|
|
1106
|
+
/* v8 ignore next -- a claim of an id this manager never admitted needs
|
|
1107
|
+
* another sender on the same child, which no current path allows. */
|
|
1108
|
+
if (activation.accepted.delete(message.id)) this.wake(activation);
|
|
1109
|
+
});
|
|
1110
|
+
handle.agent.ctx.on("agent/inbox/discarded", ({ message }) => {
|
|
1111
|
+
if (activation.accepted.delete(message.id)) this.wake(activation);
|
|
1112
|
+
});
|
|
1113
|
+
observer.start(handle.agent);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
/* v8 ignore next -- rollback failure must not mask the admission failure
|
|
1116
|
+
* that prevented this operation from returning an accepted message id. */
|
|
1117
|
+
await this.rollbackUnpublished(activation).catch(() => void 0);
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
this.watchSettlement(activation);
|
|
1121
|
+
return activation;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Release an Activation whose start edge was not published. The memoized
|
|
1125
|
+
* transaction remains in the live map until handle disposal settles, so a
|
|
1126
|
+
* concurrent drain or delivery observes the same closing boundary.
|
|
1127
|
+
*/
|
|
1128
|
+
rollbackUnpublished(activation) {
|
|
1129
|
+
return activation.disposal ??= (async () => {
|
|
1130
|
+
try {
|
|
1131
|
+
await activation.handle.dispose();
|
|
1132
|
+
} finally {
|
|
1133
|
+
this.activations.delete(activation.childId);
|
|
1134
|
+
this.releaseOwnership(activation.childId);
|
|
1135
|
+
}
|
|
1136
|
+
})();
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Register the child in a continuation-managed parent's owned set before the
|
|
1140
|
+
* child can run, so that parent cannot settle while the child is live. A
|
|
1141
|
+
* top-level or other non-continuation Agent has no Activation and stays
|
|
1142
|
+
* outside the waiting graph.
|
|
1143
|
+
*/
|
|
1144
|
+
acquireOwnership(parent, childId) {
|
|
1145
|
+
const parentActivation = this.activations.get(parent.id);
|
|
1146
|
+
if (parentActivation === void 0) return;
|
|
1147
|
+
if (parentActivation.disposal !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
|
|
1148
|
+
parentActivation.ownedChildren.add(childId);
|
|
1149
|
+
}
|
|
1150
|
+
/** Remove one child from its live owner's set and let that owner re-check settlement. */
|
|
1151
|
+
releaseOwnership(childId) {
|
|
1152
|
+
for (const candidate of this.activations.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
|
|
1153
|
+
}
|
|
1154
|
+
/** Let a settlement watcher re-observe quiescence after ownership or inbox changes. */
|
|
1155
|
+
wake(activation) {
|
|
1156
|
+
activation.poke.resolve();
|
|
1157
|
+
activation.poke = Promise.withResolvers();
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Submit one message as the child's next FIFO turn and return its accepted
|
|
1161
|
+
* inbox id. Acceptance is the operation's success boundary; the manager owns
|
|
1162
|
+
* the Activation independently afterwards.
|
|
1163
|
+
*/
|
|
1164
|
+
submit(activation, content, source, parent) {
|
|
1165
|
+
this.acquireOwnership(parent, activation.childId);
|
|
1166
|
+
const message = createUserMessage({
|
|
1167
|
+
content,
|
|
1168
|
+
source
|
|
1169
|
+
});
|
|
1170
|
+
return this.admitWaking(activation, message.id, () => {
|
|
1171
|
+
activation.handle.agent.followup(message);
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Account one waking send across a resident Activation's settlement window.
|
|
1176
|
+
* @param activation - Activation receiving waking inbox work.
|
|
1177
|
+
* @param messageId - stable identity of the message about to be sent.
|
|
1178
|
+
* @param send - synchronous send that publishes one enqueue occurrence.
|
|
1179
|
+
* @returns the accepted message id.
|
|
1180
|
+
*/
|
|
1181
|
+
admitWaking(activation, messageId, send) {
|
|
1182
|
+
activation.accepted.add(messageId);
|
|
1183
|
+
try {
|
|
1184
|
+
send();
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
activation.accepted.delete(messageId);
|
|
1187
|
+
throw error;
|
|
1188
|
+
}
|
|
1189
|
+
this.wake(activation);
|
|
1190
|
+
return messageId;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Cross the final admission cutoff and submit without yielding. Signal abort,
|
|
1194
|
+
* manager drain, or Activation disposal that wins before this synchronous
|
|
1195
|
+
* span rejects without inbox acceptance.
|
|
1196
|
+
*/
|
|
1197
|
+
submitAdmitted(activation, content, source, parent, signal) {
|
|
1198
|
+
signal.throwIfAborted();
|
|
1199
|
+
this.assertAdmitting(parent);
|
|
1200
|
+
/* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
|
|
1201
|
+
* this field between the caller's live check and this no-await boundary. */
|
|
1202
|
+
if (disposalOf(activation) !== void 0) throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, "ACTIVATION_CLOSING");
|
|
1203
|
+
this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
|
|
1204
|
+
return this.submit(activation, content, source, parent);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Authorize one operation against the durable direct-parent lineage. Other
|
|
1208
|
+
* agents, ancestors, teams, workflows, and hosts remain rejected until an
|
|
1209
|
+
* explicit authority protocol has a production consumer.
|
|
1210
|
+
*/
|
|
1211
|
+
authorizeLineage(parent, childId, parentSession) {
|
|
1212
|
+
if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
|
|
1213
|
+
if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Follow one Activation to settlement: wait for Agent quiescence, then for
|
|
1217
|
+
* every owned child to complete disposal, and dispose the handle once both
|
|
1218
|
+
* hold. A `next-turn` delivered while `waiting` wakes the same Agent and
|
|
1219
|
+
* returns it to `running`, so this re-observes rather than settling early.
|
|
1220
|
+
*/
|
|
1221
|
+
watchSettlement(activation) {
|
|
1222
|
+
(async () => {
|
|
1223
|
+
while (disposalOf(activation) === void 0) {
|
|
1224
|
+
const poked = activation.poke.promise;
|
|
1225
|
+
await Promise.race([activation.handle.agent.whenIdle(), poked]);
|
|
1226
|
+
if (disposalOf(activation) !== void 0) return;
|
|
1227
|
+
const settling = await this.locks.run(activation.childId, () => {
|
|
1228
|
+
if (disposalOf(activation) !== void 0 || this.stateOf(activation) !== "settled") return Promise.resolve({ settling: false });
|
|
1229
|
+
return Promise.resolve({
|
|
1230
|
+
settling: true,
|
|
1231
|
+
done: this.dispose(activation)
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
if (!settling.settling) {
|
|
1235
|
+
if (activation.handle.agent.status !== "running") await poked;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
await settling.done;
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${errorChain(error)}`);
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
})();
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Stop one Activation immediately, then release it child-first. The memoized
|
|
1249
|
+
* transaction is installed before cancellation or recursive callbacks, so
|
|
1250
|
+
* admission and reentrant teardown converge on the same owner.
|
|
1251
|
+
*
|
|
1252
|
+
* The final session flush is best effort and never prevents handle disposal
|
|
1253
|
+
* or ownership release, because retaining a child would permanently pin its
|
|
1254
|
+
* ancestors in `waiting`.
|
|
1255
|
+
* @param activation - the residency epoch to stop and release.
|
|
1256
|
+
* @returns the one disposal transaction owned by this Activation.
|
|
1257
|
+
*/
|
|
1258
|
+
dispose(activation) {
|
|
1259
|
+
const existing = activation.disposal;
|
|
1260
|
+
if (existing !== void 0) return existing;
|
|
1261
|
+
const completion = Promise.withResolvers();
|
|
1262
|
+
activation.disposal = completion.promise;
|
|
1263
|
+
this.finishDisposal(activation).then(completion.resolve, completion.reject);
|
|
1264
|
+
return completion.promise;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Propagate stop synchronously, then finish the child-first release.
|
|
1268
|
+
* @param activation - the Activation whose disposal transaction is installed.
|
|
1269
|
+
* @returns once the handle and ownership edge are released.
|
|
1270
|
+
*/
|
|
1271
|
+
async finishDisposal(activation) {
|
|
1272
|
+
this.wake(activation);
|
|
1273
|
+
const { childId } = activation;
|
|
1274
|
+
activation.handle.agent.cancel({ kind: "parent" });
|
|
1275
|
+
const idle = activation.handle.agent.whenIdle();
|
|
1276
|
+
const childDisposals = [...activation.ownedChildren].map((child) => this.activations.get(child)).filter((child) => child !== void 0).map((child) => this.dispose(child));
|
|
1277
|
+
const failures = [];
|
|
1278
|
+
try {
|
|
1279
|
+
const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
|
|
1280
|
+
try {
|
|
1281
|
+
await disposal;
|
|
1282
|
+
return;
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
return error;
|
|
1285
|
+
}
|
|
1286
|
+
}))).filter((reason) => reason !== void 0);
|
|
1287
|
+
if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => errorChain(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
|
|
1288
|
+
await idle;
|
|
1289
|
+
await this.flushFinalState(activation);
|
|
1290
|
+
activation.observer.capture(activation.handle.agent);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
await activation.handle.dispose();
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
|
|
1298
|
+
}
|
|
1299
|
+
let failure;
|
|
1300
|
+
if (failures.length === 1) failure = failures[0];
|
|
1301
|
+
else if (failures.length > 1) failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: ` + failures.map((item) => errorChain(item)).join("; "), "ACTIVATION_TEARDOWN_FAILED", { cause: new AggregateError(failures) });
|
|
1302
|
+
this.activations.delete(childId);
|
|
1303
|
+
this.releaseOwnership(childId);
|
|
1304
|
+
activation.observer.settle(failure);
|
|
1305
|
+
if (failure !== void 0) throw failure;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Request a best-effort final session flush after the child is quiescent.
|
|
1309
|
+
* Listener failure is logged because flush participation cannot identify a
|
|
1310
|
+
* particular persistence backend, and teardown must still release ownership.
|
|
1311
|
+
* @param activation - the Activation whose final events should be flushed.
|
|
1312
|
+
*/
|
|
1313
|
+
async flushFinalState(activation) {
|
|
1314
|
+
const child = activation.handle.agent;
|
|
1315
|
+
try {
|
|
1316
|
+
await child.ctx.sessions.flush(child.session);
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" best-effort final session flush failed; the persisted state may be unavailable or stale on resume: ${errorChain(error)}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** Resolve the persistence service continuable children require, or fail loud. */
|
|
1322
|
+
requirePersistence() {
|
|
1323
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
1324
|
+
if (persistence === void 0) throw new SubagentError("continuable subagents require session persistence (load a dsh-session-persistence backend)", "PERSISTENCE_UNAVAILABLE");
|
|
1325
|
+
return persistence;
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region lib/types/activation-setup-registry.js
|
|
1330
|
+
/**
|
|
1331
|
+
* Internal registry of deployment capabilities composed into every continuable
|
|
1332
|
+
* child's unpublished creation context.
|
|
1333
|
+
*
|
|
1334
|
+
* A contribution grants a child-scoped capability without teaching the
|
|
1335
|
+
* continuation manager which capabilities exist. The manager owns residency;
|
|
1336
|
+
* this registry owns the join between plugin lifetime, unpublished setup, and
|
|
1337
|
+
* Activation disposal, so no installation outlives either owner and no removed
|
|
1338
|
+
* contribution can be installed after revocation reports completion.
|
|
1339
|
+
*
|
|
1340
|
+
* @module @deepseek-ai/dsh-subagent/activation-setup-registry
|
|
1341
|
+
*/
|
|
1342
|
+
/** Re-read mutable removal state after a contribution may have revoked itself. */
|
|
1343
|
+
function isRemoved(registration) {
|
|
1344
|
+
return registration.removed;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Owns continuable-child setup registrations, installations, rollback, child
|
|
1348
|
+
* cleanup, and immediate live revocation.
|
|
1349
|
+
*/
|
|
1350
|
+
var SubagentActivationSetupRegistry = class {
|
|
1351
|
+
/** Live contributions in installation order. */
|
|
1352
|
+
registrations = /* @__PURE__ */ new Set();
|
|
1353
|
+
/** Child context to its live installations. */
|
|
1354
|
+
byChild = /* @__PURE__ */ new Map();
|
|
1355
|
+
/**
|
|
1356
|
+
* Register one contribution.
|
|
1357
|
+
* @param contribution - synchronous child-scope installer.
|
|
1358
|
+
* @returns an idempotent registration undo.
|
|
1359
|
+
* @throws after attempting every installation when any disposer fails.
|
|
1360
|
+
*/
|
|
1361
|
+
register(contribution) {
|
|
1362
|
+
const registration = {
|
|
1363
|
+
contribution,
|
|
1364
|
+
removed: false,
|
|
1365
|
+
installations: /* @__PURE__ */ new Set()
|
|
1366
|
+
};
|
|
1367
|
+
this.registrations.add(registration);
|
|
1368
|
+
return () => {
|
|
1369
|
+
if (registration.removed) return;
|
|
1370
|
+
registration.removed = true;
|
|
1371
|
+
this.registrations.delete(registration);
|
|
1372
|
+
this.releaseAll([...registration.installations], "contribution removal");
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Install every live contribution into one unpublished child context.
|
|
1377
|
+
* @param childCtx - the child's unpublished scoped context.
|
|
1378
|
+
* @returns the provisioning commit consumed at Agent publication.
|
|
1379
|
+
*/
|
|
1380
|
+
apply(childCtx) {
|
|
1381
|
+
const state = {
|
|
1382
|
+
installations: [],
|
|
1383
|
+
invalidated: false
|
|
1384
|
+
};
|
|
1385
|
+
try {
|
|
1386
|
+
for (const registration of [...this.registrations]) {
|
|
1387
|
+
/* v8 ignore next -- only a synchronous re-entrant revocation of an
|
|
1388
|
+
* already-snapshotted registration reaches this guard. */
|
|
1389
|
+
if (registration.removed) continue;
|
|
1390
|
+
const installation = {
|
|
1391
|
+
registration,
|
|
1392
|
+
childCtx,
|
|
1393
|
+
dispose: registration.contribution(childCtx),
|
|
1394
|
+
released: false,
|
|
1395
|
+
transaction: state
|
|
1396
|
+
};
|
|
1397
|
+
registration.installations.add(installation);
|
|
1398
|
+
state.installations.push(installation);
|
|
1399
|
+
let indexed = this.byChild.get(childCtx);
|
|
1400
|
+
if (indexed === void 0) {
|
|
1401
|
+
indexed = /* @__PURE__ */ new Set();
|
|
1402
|
+
this.byChild.set(childCtx, indexed);
|
|
1403
|
+
}
|
|
1404
|
+
indexed.add(installation);
|
|
1405
|
+
if (isRemoved(registration)) this.release(installation);
|
|
1406
|
+
}
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
try {
|
|
1409
|
+
this.releaseAll([...state.installations], "setup rollback");
|
|
1410
|
+
} catch (releaseFailure) {}
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
childCtx.effect(() => () => {
|
|
1414
|
+
this.releaseChild(childCtx);
|
|
1415
|
+
}, "subagents.activationSetup()");
|
|
1416
|
+
return { commit: () => {
|
|
1417
|
+
if (state.invalidated) throw new SubagentError("a continuable-subagent setup contribution was revoked while this child was being built; the child was not established", "ACTIVATION_SETUP_REVOKED");
|
|
1418
|
+
for (const installation of state.installations) installation.transaction = void 0;
|
|
1419
|
+
} };
|
|
1420
|
+
}
|
|
1421
|
+
/** Release every remaining installation owned by one disposed child scope. */
|
|
1422
|
+
releaseChild(childCtx) {
|
|
1423
|
+
const indexed = this.byChild.get(childCtx) ?? [];
|
|
1424
|
+
this.releaseAll([...indexed], "child scope disposal");
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Release a batch completely before reporting disposer failures.
|
|
1428
|
+
* @param installations - records to release.
|
|
1429
|
+
* @param during - operation name for diagnostics.
|
|
1430
|
+
*/
|
|
1431
|
+
releaseAll(installations, during) {
|
|
1432
|
+
const failures = [];
|
|
1433
|
+
for (const installation of installations) try {
|
|
1434
|
+
this.release(installation);
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
failures.push(error);
|
|
1437
|
+
}
|
|
1438
|
+
if (failures.length === 0) return;
|
|
1439
|
+
throw new SubagentError(`continuable-subagent setup ${during} failed to release ${failures.length} installation(s): ` + failures.map((failure) => errorChain(failure)).join("; "), "ACTIVATION_SETUP_RELEASE_FAILED");
|
|
1440
|
+
}
|
|
1441
|
+
/** Drop one installation from both indices and dispose it exactly once. */
|
|
1442
|
+
release(installation) {
|
|
1443
|
+
if (installation.released) return;
|
|
1444
|
+
installation.released = true;
|
|
1445
|
+
installation.registration.installations.delete(installation);
|
|
1446
|
+
const indexed = this.byChild.get(installation.childCtx);
|
|
1447
|
+
/* v8 ignore next 4 -- every live installation is indexed until this method removes it. */
|
|
1448
|
+
if (indexed !== void 0) {
|
|
1449
|
+
indexed.delete(installation);
|
|
1450
|
+
if (indexed.size === 0) this.byChild.delete(installation.childCtx);
|
|
1451
|
+
}
|
|
1452
|
+
if (installation.transaction !== void 0) installation.transaction.invalidated = true;
|
|
1453
|
+
installation.dispose();
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region lib/types/list-children.js
|
|
1458
|
+
/**
|
|
1459
|
+
* Read-only enumeration of durable subagent children and descendant trees
|
|
1460
|
+
* straight from the live session store and optional session persistence — no
|
|
1461
|
+
* query service. Candidates come from one live-preferred corpus; each child's
|
|
1462
|
+
* mode/label is the registered `subagent` projection unit's value, resolved
|
|
1463
|
+
* down a three-rung ladder: the registry's watermark cache for a live child,
|
|
1464
|
+
* a durable projection-cache row when it serves an own-suffix identity (the
|
|
1465
|
+
* seq gate), and one persistence inspection folded through the registry
|
|
1466
|
+
* otherwise, validated against the enumerated lifecycle. The projection fold
|
|
1467
|
+
* is the single classification authority — this module parses no descriptor
|
|
1468
|
+
* itself. Absent persistence, enumeration is live-only: a cold child is
|
|
1469
|
+
* unreachable for resume anyway, so its absence is capability absence, not an
|
|
1470
|
+
* error. The module owns no catalog state and does not consult Activation,
|
|
1471
|
+
* Agent-registry, continuation-manager, or provider state.
|
|
1472
|
+
*
|
|
1473
|
+
* @module @deepseek-ai/dsh-subagent
|
|
1474
|
+
*/
|
|
1475
|
+
/**
|
|
1476
|
+
* Concurrent cold inspections per listing; a constant because it bounds one
|
|
1477
|
+
* read-only scan of local media, not deployment behavior. Should a networked
|
|
1478
|
+
* persistence backend appear, promote it to a validated `Config` field.
|
|
1479
|
+
*/
|
|
1480
|
+
const COLD_READ_CONCURRENCY = 4;
|
|
1481
|
+
/**
|
|
1482
|
+
* Enumerate one parent's origin-classified direct children from the
|
|
1483
|
+
* live-preferred merge of `ctx.sessions` and optional session persistence,
|
|
1484
|
+
* serving each identity from the `subagent` projection unit: the registry's
|
|
1485
|
+
* watermark snapshot for a live child; for a cold one, a durable
|
|
1486
|
+
* projection-cache row when it serves an own-suffix identity (the seq gate),
|
|
1487
|
+
* else one bounded-concurrency persistence inspection folded through the
|
|
1488
|
+
* registry.
|
|
1489
|
+
* @see SubagentService.listChildren for the public cancellation and failure contract.
|
|
1490
|
+
* @param ctx - context carrying the session store, the projection registry,
|
|
1491
|
+
* optional persistence, and the optional projection cache.
|
|
1492
|
+
* @param parentSessionId - parent session whose direct children are listed.
|
|
1493
|
+
* @param signal - caller-owned cancellation observed around every persistence read.
|
|
1494
|
+
* @returns children and per-child diagnostics ordered by `createdAt`, then id.
|
|
1495
|
+
* @throws {@link SubagentError} when the projection registry or the session
|
|
1496
|
+
* store is not mounted, or the caller cancels the listing.
|
|
1497
|
+
*/
|
|
1498
|
+
async function listChildren(ctx, parentSessionId, signal) {
|
|
1499
|
+
const listing = await prepareListing(ctx, signal);
|
|
1500
|
+
return (await resolveCandidateRows([...listing.corpus.values()].filter((record) => record.header.parentSession === parentSessionId && record.header.origin === "subagent").sort(compareCorpusRecords), listing, signal)).filter((row) => row !== void 0);
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Enumerate every session-backed subagent below one root in stable pre-order.
|
|
1504
|
+
* Ordinary sessions and one-shot children remain traversal nodes, so a
|
|
1505
|
+
* continuable child below either is still discovered. Classification uses the
|
|
1506
|
+
* same projection-backed runtime as {@link listChildren}; no Agent is loaded or
|
|
1507
|
+
* resumed.
|
|
1508
|
+
* @see SubagentService.listDescendants for the public cancellation and failure contract.
|
|
1509
|
+
* @param ctx - context carrying the session store, projection registry, and optional persistence/cache.
|
|
1510
|
+
* @param rootSessionId - session whose complete descendant tree is listed.
|
|
1511
|
+
* @param signal - caller-owned cancellation observed around every persistence read.
|
|
1512
|
+
* @returns interpreted subagents with durable direct-parent and root-relative depth.
|
|
1513
|
+
* @throws {@link SubagentError} under the same conditions as {@link listChildren}.
|
|
1514
|
+
*/
|
|
1515
|
+
async function listDescendants(ctx, rootSessionId, signal) {
|
|
1516
|
+
const listing = await prepareListing(ctx, signal);
|
|
1517
|
+
const positioned = descendantCandidates(listing.corpus, rootSessionId);
|
|
1518
|
+
const rows = await resolveCandidateRows(positioned.map((candidate) => candidate.record), listing, signal);
|
|
1519
|
+
const entries = [];
|
|
1520
|
+
positioned.forEach((position, index) => {
|
|
1521
|
+
const row = rows[index];
|
|
1522
|
+
if (row !== void 0) entries.push({
|
|
1523
|
+
...row,
|
|
1524
|
+
parentId: position.parentId,
|
|
1525
|
+
depth: position.depth
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
return entries;
|
|
1529
|
+
}
|
|
1530
|
+
/** Resolve listing services once and build one live-preferred session corpus. */
|
|
1531
|
+
async function prepareListing(ctx, signal) {
|
|
1532
|
+
const projections = ctx.get("sessionProjections");
|
|
1533
|
+
if (projections === void 0) throw new SubagentError("listing subagents requires the sessionProjections registry (load @deepseek-ai/dsh-session-projection)", "SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE");
|
|
1534
|
+
const sessions = ctx.get("sessions");
|
|
1535
|
+
if (sessions === void 0) throw new SubagentError("listing subagents requires the session store (load @deepseek-ai/dsh-session)", "SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE");
|
|
1536
|
+
assertListingNotCancelled(signal);
|
|
1537
|
+
const persistence = ctx.get("sessionPersistence");
|
|
1538
|
+
const cache = ctx.get("sessionProjectionCache");
|
|
1539
|
+
let persistedHeaders = [];
|
|
1540
|
+
if (persistence !== void 0) {
|
|
1541
|
+
try {
|
|
1542
|
+
persistedHeaders = await persistence.list(signal);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
assertListingNotCancelled(signal);
|
|
1545
|
+
throw error;
|
|
1546
|
+
}
|
|
1547
|
+
assertListingNotCancelled(signal);
|
|
1548
|
+
}
|
|
1549
|
+
const corpus = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const header of persistedHeaders) corpus.set(header.id, {
|
|
1551
|
+
header,
|
|
1552
|
+
live: void 0
|
|
1553
|
+
});
|
|
1554
|
+
for (const session of sessions.list()) corpus.set(session.header.id, {
|
|
1555
|
+
header: session.header,
|
|
1556
|
+
live: session
|
|
1557
|
+
});
|
|
1558
|
+
const subagentParents = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const record of corpus.values()) if (record.header.origin === "subagent" && record.header.parentSession !== void 0) subagentParents.add(record.header.parentSession);
|
|
1560
|
+
return {
|
|
1561
|
+
projections,
|
|
1562
|
+
persistence,
|
|
1563
|
+
cache,
|
|
1564
|
+
corpus,
|
|
1565
|
+
subagentParents
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
/** Resolve projection-backed rows for aligned candidates with bounded cold reads. */
|
|
1569
|
+
async function resolveCandidateRows(candidates, listing, signal) {
|
|
1570
|
+
const { projections, persistence, cache, subagentParents } = listing;
|
|
1571
|
+
const rows = Array.from({ length: candidates.length });
|
|
1572
|
+
const coldReads = [];
|
|
1573
|
+
candidates.forEach((candidate, index) => {
|
|
1574
|
+
const childId = candidate.header.id;
|
|
1575
|
+
if (candidate.live === void 0) {
|
|
1576
|
+
coldReads.push({
|
|
1577
|
+
index,
|
|
1578
|
+
header: candidate.header
|
|
1579
|
+
});
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
let identity;
|
|
1583
|
+
try {
|
|
1584
|
+
identity = projections.snapshot(candidate.live).values.subagent;
|
|
1585
|
+
} catch {
|
|
1586
|
+
rows[index] = {
|
|
1587
|
+
kind: "diagnostic",
|
|
1588
|
+
id: childId,
|
|
1589
|
+
reason: "corrupt"
|
|
1590
|
+
};
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (identity === void 0 || identity === null) return;
|
|
1594
|
+
rows[index] = childRow(childId, identity, "running", subagentParents.has(childId));
|
|
1595
|
+
});
|
|
1596
|
+
if (persistence !== void 0 && coldReads.length > 0) {
|
|
1597
|
+
const queue = [...coldReads];
|
|
1598
|
+
await Promise.all(Array.from({ length: Math.min(COLD_READ_CONCURRENCY, queue.length) }, async () => {
|
|
1599
|
+
for (let job = queue.shift(); job !== void 0; job = queue.shift()) rows[job.index] = await resolveColdIdentity(persistence, projections, cache, job.header, subagentParents.has(job.header.id), signal);
|
|
1600
|
+
}));
|
|
1601
|
+
}
|
|
1602
|
+
assertListingNotCancelled(signal);
|
|
1603
|
+
return rows;
|
|
1604
|
+
}
|
|
1605
|
+
/** Build origin-classified candidates from the complete tree without recursion. */
|
|
1606
|
+
function descendantCandidates(corpus, rootSessionId) {
|
|
1607
|
+
const children = /* @__PURE__ */ new Map();
|
|
1608
|
+
for (const record of corpus.values()) {
|
|
1609
|
+
const parentId = record.header.parentSession;
|
|
1610
|
+
if (parentId === void 0) continue;
|
|
1611
|
+
const siblings = children.get(parentId);
|
|
1612
|
+
if (siblings === void 0) children.set(parentId, [record]);
|
|
1613
|
+
else siblings.push(record);
|
|
1614
|
+
}
|
|
1615
|
+
for (const siblings of children.values()) siblings.sort(compareCorpusRecords);
|
|
1616
|
+
const positioned = [];
|
|
1617
|
+
const stack = (children.get(rootSessionId) ?? []).map((record) => ({
|
|
1618
|
+
record,
|
|
1619
|
+
parentId: rootSessionId,
|
|
1620
|
+
depth: 1
|
|
1621
|
+
})).reverse();
|
|
1622
|
+
const visited = new Set([rootSessionId]);
|
|
1623
|
+
while (stack.length > 0) {
|
|
1624
|
+
const position = stack.pop();
|
|
1625
|
+
const id = position.record.header.id;
|
|
1626
|
+
if (visited.has(id)) continue;
|
|
1627
|
+
visited.add(id);
|
|
1628
|
+
if (position.record.header.origin === "subagent") positioned.push(position);
|
|
1629
|
+
const descendants = children.get(id) ?? [];
|
|
1630
|
+
for (const record of [...descendants].reverse()) stack.push({
|
|
1631
|
+
record,
|
|
1632
|
+
parentId: id,
|
|
1633
|
+
depth: position.depth + 1
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
return positioned;
|
|
1637
|
+
}
|
|
1638
|
+
/** Compare siblings by durable creation time, then id. */
|
|
1639
|
+
function compareCorpusRecords(a, b) {
|
|
1640
|
+
return a.header.createdAt - b.header.createdAt || a.header.id.localeCompare(b.header.id);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Resolve one cold candidate down the remaining ladder: a durable
|
|
1644
|
+
* projection-cache row when it serves an own-suffix identity (the seq gate),
|
|
1645
|
+
* otherwise one persistence inspection folded through the projection
|
|
1646
|
+
* registry (the same detached recipe the API proxy uses for detached session
|
|
1647
|
+
* projections). A failed inspection is one transient `unavailable` row
|
|
1648
|
+
* retried on the next listing; an inspection naming another lifecycle, and a
|
|
1649
|
+
* settled log the fold cannot identify — or that makes any registered unit
|
|
1650
|
+
* throw — are final, so they report `corrupt`.
|
|
1651
|
+
*/
|
|
1652
|
+
async function resolveColdIdentity(persistence, projections, cache, header, hasChildren, signal) {
|
|
1653
|
+
const childId = header.id;
|
|
1654
|
+
if (cache !== void 0) {
|
|
1655
|
+
let cached;
|
|
1656
|
+
try {
|
|
1657
|
+
cached = cache.cachedSnapshot(header)?.values.subagent;
|
|
1658
|
+
} catch {
|
|
1659
|
+
cached = void 0;
|
|
1660
|
+
}
|
|
1661
|
+
if (cached !== void 0 && cached !== null && cached.seq >= (header.seedLength ?? 0)) return childRow(childId, cached, "inactive", hasChildren);
|
|
1662
|
+
}
|
|
1663
|
+
assertListingNotCancelled(signal);
|
|
1664
|
+
let inspected;
|
|
1665
|
+
try {
|
|
1666
|
+
inspected = await persistence.inspect(childId, signal);
|
|
1667
|
+
} catch {
|
|
1668
|
+
assertListingNotCancelled(signal);
|
|
1669
|
+
return {
|
|
1670
|
+
kind: "diagnostic",
|
|
1671
|
+
id: childId,
|
|
1672
|
+
reason: "unavailable"
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
assertListingNotCancelled(signal);
|
|
1676
|
+
if (!sameLifecycle(inspected.meta, header)) return {
|
|
1677
|
+
kind: "diagnostic",
|
|
1678
|
+
id: childId,
|
|
1679
|
+
reason: "corrupt"
|
|
1680
|
+
};
|
|
1681
|
+
let identity;
|
|
1682
|
+
try {
|
|
1683
|
+
identity = projections.restore({}, inspected.events, 0).snapshot.values.subagent;
|
|
1684
|
+
} catch {
|
|
1685
|
+
return {
|
|
1686
|
+
kind: "diagnostic",
|
|
1687
|
+
id: childId,
|
|
1688
|
+
reason: "corrupt"
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
if (identity === void 0 || identity === null) return {
|
|
1692
|
+
kind: "diagnostic",
|
|
1693
|
+
id: childId,
|
|
1694
|
+
reason: "corrupt"
|
|
1695
|
+
};
|
|
1696
|
+
return childRow(childId, identity, "inactive", hasChildren);
|
|
1697
|
+
}
|
|
1698
|
+
/** Materialize one served identity as its child row. */
|
|
1699
|
+
function childRow(id, identity, activity, hasChildren) {
|
|
1700
|
+
return identity.mode === "one-shot" ? {
|
|
1701
|
+
kind: "child",
|
|
1702
|
+
id,
|
|
1703
|
+
mode: "one-shot",
|
|
1704
|
+
...identity.label !== void 0 ? { label: identity.label } : {},
|
|
1705
|
+
activity,
|
|
1706
|
+
hasChildren
|
|
1707
|
+
} : {
|
|
1708
|
+
kind: "child",
|
|
1709
|
+
id,
|
|
1710
|
+
mode: "continuable",
|
|
1711
|
+
label: identity.label,
|
|
1712
|
+
activity,
|
|
1713
|
+
hasChildren
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
/** Immutable header fields that distinguish one session lifecycle from another under the same id. */
|
|
1717
|
+
const LIFECYCLE_WITNESS_KEYS = [
|
|
1718
|
+
"version",
|
|
1719
|
+
"id",
|
|
1720
|
+
"createdAt",
|
|
1721
|
+
"cwd",
|
|
1722
|
+
"parentSession",
|
|
1723
|
+
"seedLength",
|
|
1724
|
+
"delegationDepth"
|
|
1725
|
+
];
|
|
1726
|
+
/** Whether an inspected log still belongs to the enumerated lifecycle. */
|
|
1727
|
+
function sameLifecycle(meta, expected) {
|
|
1728
|
+
return LIFECYCLE_WITNESS_KEYS.every((key) => meta[key] === expected[key]);
|
|
1729
|
+
}
|
|
1730
|
+
/** Stop a listing at its next cancellation checkpoint. */
|
|
1731
|
+
function assertListingNotCancelled(signal) {
|
|
1732
|
+
if (signal?.aborted) throw new SubagentError("subagent listing was cancelled", "CANCELLED");
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Fold turn boundaries around the child's own durable descriptor.
|
|
1736
|
+
*
|
|
1737
|
+
* A fork seed may contain an ancestor descriptor and completed turns. Every
|
|
1738
|
+
* descriptor therefore resets the accumulated state; the healthy catalog
|
|
1739
|
+
* admits only a child with exactly one descriptor in its own suffix, making
|
|
1740
|
+
* the final reset the child's authoritative timing origin.
|
|
1741
|
+
*/
|
|
1742
|
+
const subagentTimingProjectionDefinition = {
|
|
1743
|
+
key: "subagentTiming",
|
|
1744
|
+
schema: z.object({
|
|
1745
|
+
settledMs: z.number().int().nonnegative(),
|
|
1746
|
+
active: z.object({
|
|
1747
|
+
since: z.number().int().nonnegative(),
|
|
1748
|
+
through: z.number().int().nonnegative()
|
|
1749
|
+
}).strict().optional()
|
|
1750
|
+
}).strict(),
|
|
1751
|
+
init: () => ({
|
|
1752
|
+
descriptorSeen: false,
|
|
1753
|
+
settledMs: 0
|
|
1754
|
+
}),
|
|
1755
|
+
apply: (state, event) => {
|
|
1756
|
+
if (event.type === "turn/start") return state.descriptorSeen ? {
|
|
1757
|
+
...state,
|
|
1758
|
+
active: {
|
|
1759
|
+
since: event.time,
|
|
1760
|
+
through: event.time
|
|
1761
|
+
}
|
|
1762
|
+
} : {
|
|
1763
|
+
...state,
|
|
1764
|
+
pendingTurnStart: event.time
|
|
1765
|
+
};
|
|
1766
|
+
if (event.type === "subagent/descriptor") {
|
|
1767
|
+
const activeSince = state.active?.since ?? state.pendingTurnStart;
|
|
1768
|
+
return {
|
|
1769
|
+
descriptorSeen: true,
|
|
1770
|
+
settledMs: 0,
|
|
1771
|
+
...activeSince === void 0 ? {} : { active: {
|
|
1772
|
+
since: activeSince,
|
|
1773
|
+
through: event.time
|
|
1774
|
+
} }
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
if (event.type === "turn/end") {
|
|
1778
|
+
if (!state.descriptorSeen) {
|
|
1779
|
+
if (state.pendingTurnStart === void 0) return state;
|
|
1780
|
+
const { pendingTurnStart: _closed, ...next } = state;
|
|
1781
|
+
return next;
|
|
1782
|
+
}
|
|
1783
|
+
if (state.active === void 0) return state;
|
|
1784
|
+
const { active, ...rest } = state;
|
|
1785
|
+
return {
|
|
1786
|
+
...rest,
|
|
1787
|
+
settledMs: state.settledMs + Math.max(0, event.time - active.since)
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
if (state.active === void 0) return state;
|
|
1791
|
+
return {
|
|
1792
|
+
...state,
|
|
1793
|
+
active: {
|
|
1794
|
+
...state.active,
|
|
1795
|
+
through: event.time
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
},
|
|
1799
|
+
view: (state) => ({
|
|
1800
|
+
settledMs: state.settledMs,
|
|
1801
|
+
...state.active === void 0 ? {} : { active: state.active }
|
|
1802
|
+
}),
|
|
1803
|
+
stateVersion: 2
|
|
1804
|
+
};
|
|
1805
|
+
const identitySchema = z.discriminatedUnion("mode", [z.object({
|
|
1806
|
+
mode: z.literal("one-shot"),
|
|
1807
|
+
label: z.string().optional(),
|
|
1808
|
+
seq: z.number().int().nonnegative()
|
|
1809
|
+
}).strict(), z.object({
|
|
1810
|
+
mode: z.literal("continuable"),
|
|
1811
|
+
label: z.string(),
|
|
1812
|
+
seq: z.number().int().nonnegative()
|
|
1813
|
+
}).strict()]).nullable();
|
|
1814
|
+
/** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
|
|
1815
|
+
function descriptorIdentity(event) {
|
|
1816
|
+
let descriptor;
|
|
1817
|
+
try {
|
|
1818
|
+
descriptor = foldSubagentDescriptor([event]);
|
|
1819
|
+
} catch {
|
|
1820
|
+
descriptor = void 0;
|
|
1821
|
+
}
|
|
1822
|
+
if (descriptor === void 0) return void 0;
|
|
1823
|
+
return descriptor.mode === "one-shot" ? {
|
|
1824
|
+
mode: "one-shot",
|
|
1825
|
+
...descriptor.label !== void 0 ? { label: descriptor.label } : {},
|
|
1826
|
+
seq: event.seq
|
|
1827
|
+
} : {
|
|
1828
|
+
mode: "continuable",
|
|
1829
|
+
label: descriptor.label,
|
|
1830
|
+
seq: event.seq
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Fold the durable mode/label identity from `subagent/descriptor` events,
|
|
1835
|
+
* last-wins: a fork seed may replay an ancestor's descriptor, and the child's
|
|
1836
|
+
* own descriptor must override it — the same reset discipline as
|
|
1837
|
+
* {@link subagentTimingProjectionDefinition}. A malformed or unknown-version
|
|
1838
|
+
* payload resets to the `null` sentinel instead of throwing, so a fork of a
|
|
1839
|
+
* healthy ancestor never inherits an identity its own descriptor failed to
|
|
1840
|
+
* establish — and the reset survives every JSON push frame, so a consumer
|
|
1841
|
+
* holding the earlier identity replaces it instead of keeping it stale;
|
|
1842
|
+
* `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
|
|
1843
|
+
*/
|
|
1844
|
+
const subagentIdentityProjectionDefinition = {
|
|
1845
|
+
key: "subagent",
|
|
1846
|
+
schema: identitySchema,
|
|
1847
|
+
init: () => ({}),
|
|
1848
|
+
apply: (state, event) => {
|
|
1849
|
+
if (event.type !== "subagent/descriptor") return state;
|
|
1850
|
+
const identity = descriptorIdentity(event);
|
|
1851
|
+
return identity === void 0 ? {} : { identity };
|
|
1852
|
+
},
|
|
1853
|
+
view: (state) => state.identity ?? null,
|
|
1854
|
+
stateVersion: 2
|
|
1855
|
+
};
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region lib/types/out-of-process.js
|
|
1858
|
+
/**
|
|
1859
|
+
* Provider-side vocabulary for OUT-OF-PROCESS subagent backends — the pieces
|
|
1860
|
+
* that enforce this seam's own contracts around a child in another process:
|
|
1861
|
+
* the no-capabilities advertisement, timing-bound validation, child
|
|
1862
|
+
* working-directory resolution (config override, else the delegating parent
|
|
1863
|
+
* session's workspace), the never-reject result settlement, and the standard
|
|
1864
|
+
* run-handle publication. Backends compose these with their own wire drivers;
|
|
1865
|
+
* the process machinery itself (spawn, env scrub, tree-scoped teardown)
|
|
1866
|
+
* belongs to the `dsh-subprocess` seam.
|
|
1867
|
+
*
|
|
1868
|
+
* @module @deepseek-ai/dsh-subagent/out-of-process
|
|
1869
|
+
*/
|
|
1870
|
+
/**
|
|
1871
|
+
* The capability advertisement of an out-of-process backend: NONE. A child in
|
|
1872
|
+
* another process cannot honor parent-enforced start features
|
|
1873
|
+
* (`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a
|
|
1874
|
+
* request needing any of them before `start` runs — never accepted-then-ignored.
|
|
1875
|
+
*/
|
|
1876
|
+
const NO_START_CAPABILITIES = Object.freeze({
|
|
1877
|
+
outputSchema: false,
|
|
1878
|
+
depthLimit: false,
|
|
1879
|
+
toolFilter: false,
|
|
1880
|
+
persona: false
|
|
1881
|
+
});
|
|
1882
|
+
/**
|
|
1883
|
+
* Assert a configured timing bound is a positive finite number (it bounds a
|
|
1884
|
+
* teardown or shutdown wait; zero, negative, or NaN would skip or wedge it).
|
|
1885
|
+
* @param prefix - the consuming plugin's diagnostic prefix (e.g. `subagent-acp`).
|
|
1886
|
+
* @param name - the config field name, for the diagnostic.
|
|
1887
|
+
* @param value - the configured value.
|
|
1888
|
+
*/
|
|
1889
|
+
function assertPositiveFinite(prefix, name, value) {
|
|
1890
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${prefix}: ${name} must be a positive finite number`);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Whether `path` names an existing directory the harness can ENTER. The
|
|
1894
|
+
* search-permission probe matters: `statSync().isDirectory()` is true for a
|
|
1895
|
+
* mode-600 directory, but a subprocess cwd needs `X_OK` or spawn fails EACCES.
|
|
1896
|
+
*/
|
|
1897
|
+
function isEnterableDirectory(path) {
|
|
1898
|
+
try {
|
|
1899
|
+
if (!statSync(path).isDirectory()) return false;
|
|
1900
|
+
accessSync(path, constants.X_OK);
|
|
1901
|
+
return true;
|
|
1902
|
+
} catch {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Assert `cwd` can actually host the child: absolute (it doubles as the
|
|
1908
|
+
* child's workspace identity, and a relative path would be re-anchored to the
|
|
1909
|
+
* server process's launch directory) and an existing directory (fail here,
|
|
1910
|
+
* before the process boundary, instead of as an ambiguous spawn ENOENT).
|
|
1911
|
+
* @param prefix - the consuming plugin's diagnostic prefix.
|
|
1912
|
+
* @param label - which source supplied the value, for the diagnostic.
|
|
1913
|
+
* @param cwd - the candidate working directory.
|
|
1914
|
+
* @returns `cwd`, validated.
|
|
1915
|
+
*/
|
|
1916
|
+
function assertUsableCwd(prefix, label, cwd) {
|
|
1917
|
+
if (!isAbsolute(cwd)) throw new Error(`${prefix}: ${label} must be an absolute path: ${cwd}`);
|
|
1918
|
+
if (!isEnterableDirectory(cwd)) throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`);
|
|
1919
|
+
return cwd;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Validate a configured `cwd` override ONCE, at plugin load: reject the empty
|
|
1923
|
+
* string (`path.resolve('')` is the process cwd — it would silently
|
|
1924
|
+
* reintroduce the launch-directory fallback this resolution removes),
|
|
1925
|
+
* interpret a relative path against the harness launch directory, and require
|
|
1926
|
+
* an enterable directory.
|
|
1927
|
+
* @param prefix - the consuming plugin's diagnostic prefix.
|
|
1928
|
+
* @param cwd - the configured override, or `undefined` when the config omits it.
|
|
1929
|
+
* @returns the validated absolute override, or `undefined` when omitted.
|
|
1930
|
+
*/
|
|
1931
|
+
function validateConfiguredCwd(prefix, cwd) {
|
|
1932
|
+
if (cwd === void 0) return void 0;
|
|
1933
|
+
if (cwd === "") throw new Error(`${prefix}: config cwd must not be empty — omit the key to inherit the parent session cwd`);
|
|
1934
|
+
return assertUsableCwd(prefix, "config cwd", resolve(cwd));
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Resolve the child's working directory at start: the deployment override
|
|
1938
|
+
* when configured (already validated at load), else the parent session's
|
|
1939
|
+
* workspace cwd (validated here, its earliest resolvable point). Fails loud
|
|
1940
|
+
* when neither exists — falling back to the harness process cwd would
|
|
1941
|
+
* silently bind the child to the server's launch directory instead of the
|
|
1942
|
+
* delegating session's workspace (one server process serves many sessions,
|
|
1943
|
+
* each with its own cwd).
|
|
1944
|
+
* @param prefix - the consuming plugin's diagnostic prefix.
|
|
1945
|
+
* @param configured - the load-validated override, or `undefined`.
|
|
1946
|
+
* @param parentCwd - the delegating parent session's workspace cwd, if any.
|
|
1947
|
+
* @returns the absolute child working directory.
|
|
1948
|
+
*/
|
|
1949
|
+
function resolveChildCwd(prefix, configured, parentCwd) {
|
|
1950
|
+
if (configured !== void 0) return configured;
|
|
1951
|
+
if (parentCwd === void 0) throw new Error(`${prefix}: no working directory for the child — configure \`cwd\` or delegate from a parent session that has one`);
|
|
1952
|
+
return assertUsableCwd(prefix, "parent session cwd", parentCwd);
|
|
1953
|
+
}
|
|
1954
|
+
/** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
|
|
1955
|
+
function toError(value) {
|
|
1956
|
+
/* v8 ignore next */
|
|
1957
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Settle an out-of-process run result under the seam contract: `result` never
|
|
1961
|
+
* rejects after publication. A normally completed or rejected attempt resolves
|
|
1962
|
+
* as `aborted` when cancellation already settled locally; another rejection is
|
|
1963
|
+
* flattened to `stopReason: 'error'` through the contained diagnostic sink.
|
|
1964
|
+
* The abort listener is removed on every path.
|
|
1965
|
+
* @param parts - the attempt, output snapshot, cancellation state, sink, and signal wiring.
|
|
1966
|
+
* @returns the terminal result (never a rejection).
|
|
1967
|
+
*/
|
|
1968
|
+
async function settleRunResult(parts) {
|
|
1969
|
+
try {
|
|
1970
|
+
const result = await parts.attempt();
|
|
1971
|
+
return parts.cancelled() ? {
|
|
1972
|
+
output: parts.collectOutput(),
|
|
1973
|
+
stopReason: "aborted"
|
|
1974
|
+
} : result;
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
if (parts.cancelled()) return {
|
|
1977
|
+
output: parts.collectOutput(),
|
|
1978
|
+
stopReason: "aborted"
|
|
1979
|
+
};
|
|
1980
|
+
try {
|
|
1981
|
+
parts.onError?.(toError(error), "error");
|
|
1982
|
+
} catch {}
|
|
1983
|
+
return {
|
|
1984
|
+
output: parts.collectOutput(),
|
|
1985
|
+
stopReason: "error"
|
|
1986
|
+
};
|
|
1987
|
+
} finally {
|
|
1988
|
+
parts.signal.removeEventListener("abort", parts.onAbort);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Publish the seam run handle for an out-of-process child. `dispose()` is
|
|
1993
|
+
* idempotent (one memoized teardown): it removes the abort listener, settles
|
|
1994
|
+
* local cancellation — there is no assumption the child cooperates — and then
|
|
1995
|
+
* awaits the backend's teardown to actual exit.
|
|
1996
|
+
* @param parts - the run identity, result, cancellation wiring, and teardown.
|
|
1997
|
+
* @returns the seam run handle (`localAgent` is `undefined` for remote runs).
|
|
1998
|
+
*/
|
|
1999
|
+
function subprocessRunHandle(parts) {
|
|
2000
|
+
let disposal;
|
|
2001
|
+
return {
|
|
2002
|
+
id: parts.id,
|
|
2003
|
+
localAgent: void 0,
|
|
2004
|
+
result: parts.result,
|
|
2005
|
+
dispose() {
|
|
2006
|
+
if (disposal !== void 0) return disposal;
|
|
2007
|
+
parts.signal.removeEventListener("abort", parts.onAbort);
|
|
2008
|
+
parts.requestCancel();
|
|
2009
|
+
disposal = parts.teardown();
|
|
2010
|
+
return disposal;
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
//#endregion
|
|
2015
|
+
//#region lib/types/run-settlement.js
|
|
2016
|
+
/**
|
|
2017
|
+
* Settlement of one ONE-SHOT subagent run into a background-Task outcome. Only
|
|
2018
|
+
* the one-shot background path uses Tasks; continuable children have no Task,
|
|
2019
|
+
* no per-message result, and no Task cancellation.
|
|
2020
|
+
*
|
|
2021
|
+
* @module @deepseek-ai/dsh-subagent/run-settlement
|
|
2022
|
+
*/
|
|
2023
|
+
/** Flatten a child's final output blocks to the task's final text. */
|
|
2024
|
+
function finalText(blocks) {
|
|
2025
|
+
return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Map a child result to the task outcome: completed carries final text,
|
|
2029
|
+
* aborted is killed, and every other reason is failed without partial output.
|
|
2030
|
+
* @param result - child terminal result.
|
|
2031
|
+
* @returns outcome for the `ctx.tasks` registration.
|
|
2032
|
+
*/
|
|
2033
|
+
function runOutcome(result) {
|
|
2034
|
+
switch (result.stopReason) {
|
|
2035
|
+
case "completed": return {
|
|
2036
|
+
status: "completed",
|
|
2037
|
+
output: finalText(result.output)
|
|
2038
|
+
};
|
|
2039
|
+
case "aborted": return { status: "killed" };
|
|
2040
|
+
case "error":
|
|
2041
|
+
case "max-tokens":
|
|
2042
|
+
case "refusal": return {
|
|
2043
|
+
status: "failed",
|
|
2044
|
+
detail: result.stopReason
|
|
2045
|
+
};
|
|
2046
|
+
default: return {
|
|
2047
|
+
status: "failed",
|
|
2048
|
+
detail: String(result.stopReason)
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Await the child result, dispose the run, then return its task outcome. Result
|
|
2054
|
+
* and disposal failures become `failed`; when both fail, both details survive.
|
|
2055
|
+
* @param run - live run to settle and release.
|
|
2056
|
+
* @returns outcome after child resources are released.
|
|
2057
|
+
*/
|
|
2058
|
+
async function settleRun(run) {
|
|
2059
|
+
let outcome;
|
|
2060
|
+
try {
|
|
2061
|
+
outcome = runOutcome(await run.result);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
outcome = {
|
|
2064
|
+
status: "failed",
|
|
2065
|
+
detail: String(error)
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
await run.dispose();
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
return {
|
|
2072
|
+
status: "failed",
|
|
2073
|
+
detail: `${outcome.detail === void 0 ? "" : `${outcome.detail}; `}dispose failed: ${String(error)}`
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
return outcome;
|
|
2077
|
+
}
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region lib/types/index.js
|
|
2080
|
+
/**
|
|
2081
|
+
* Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a
|
|
2082
|
+
* capability-validating asynchronous start surface. Providers establish a
|
|
2083
|
+
* child before returning its run, so fulfillment is the single publication and
|
|
2084
|
+
* ownership-transfer boundary.
|
|
2085
|
+
*
|
|
2086
|
+
* Unlike the bash seam (one executor per context, second load throws), MULTIPLE
|
|
2087
|
+
* providers coexist here: each registers under a unique name and a caller picks
|
|
2088
|
+
* one by name. The shape mirrors the LLM adapter registry
|
|
2089
|
+
* (`LlmService.registerAdapter`), not the single-service bash executor.
|
|
2090
|
+
*
|
|
2091
|
+
* This package owns the Service Definition role of the capability seam. Service providers
|
|
2092
|
+
* (`@deepseek-ai/dsh-subagent-spawn`, `-fork`, `-acp`) and the model-facing
|
|
2093
|
+
* consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages.
|
|
2094
|
+
*
|
|
2095
|
+
* Public operations express caller intent: `start` returns one published owned
|
|
2096
|
+
* one-shot run, `startContinuable` establishes a durable continuable child, and
|
|
2097
|
+
* `followup` delivers later content without exposing whether the child is
|
|
2098
|
+
* resident. Continuable children never become a {@link SubagentRun}: the
|
|
2099
|
+
* continuation manager holds their `AgentHandle` directly and orders every turn
|
|
2100
|
+
* through the child's own inbox, so providers contribute only the detached
|
|
2101
|
+
* creation spec and see no handle, turn, or teardown. Child and descendant
|
|
2102
|
+
* discovery read the live session store and optional session persistence
|
|
2103
|
+
* directly and do not require that continuation runtime.
|
|
2104
|
+
*
|
|
2105
|
+
* Same-process providers are trusted typed collaborators. Requests, provider
|
|
2106
|
+
* descriptors, results, and lifecycle payloads are borrowed immutable values;
|
|
2107
|
+
* serialization and hostile-input validation belong at real process, worker,
|
|
2108
|
+
* persistence, and model boundaries.
|
|
2109
|
+
*
|
|
2110
|
+
* @module @deepseek-ai/dsh-subagent
|
|
2111
|
+
*/
|
|
2112
|
+
/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
|
|
2113
|
+
var SubagentService = class extends Service {
|
|
2114
|
+
providers = /* @__PURE__ */ new Map();
|
|
2115
|
+
continuations;
|
|
2116
|
+
/** Deployment contributions composed into unpublished continuable children. */
|
|
2117
|
+
setupRegistry = new SubagentActivationSetupRegistry();
|
|
2118
|
+
/**
|
|
2119
|
+
* The contained lifecycle-edge publisher. Built here because scoped dispatch
|
|
2120
|
+
* keys its carrier by this exact service instance, whose own context filter
|
|
2121
|
+
* composes into the carrier.
|
|
2122
|
+
*/
|
|
2123
|
+
emitLifecycle;
|
|
2124
|
+
constructor(ctx) {
|
|
2125
|
+
super(ctx, "subagents");
|
|
2126
|
+
this.emitLifecycle = createLifecycleEmitter(this.ctx, (parent) => scopeTarget(this, parent));
|
|
2127
|
+
ctx.inject(["agents"], (childCtx) => {
|
|
2128
|
+
const manager = new SubagentContinuationManager(childCtx, {
|
|
2129
|
+
prepareContinuable: (name, request) => this.prepareContinuable(name, request),
|
|
2130
|
+
observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent)
|
|
2131
|
+
}, this.setupRegistry);
|
|
2132
|
+
this.continuations = manager;
|
|
2133
|
+
childCtx.effect(() => () => {
|
|
2134
|
+
/* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
|
|
2135
|
+
if (this.continuations === manager) this.continuations = void 0;
|
|
2136
|
+
}, "subagents.continuationBinding()");
|
|
2137
|
+
});
|
|
2138
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
2139
|
+
projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition);
|
|
2140
|
+
projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition);
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Establish one durable continuable child and deliver its initial prompt.
|
|
2145
|
+
* Resolves when the child's inbox accepts that prompt, without waiting for the
|
|
2146
|
+
* turn to start or for the message to reach the Session log; any earlier
|
|
2147
|
+
* failure rejects with no ids and rolls back the child entirely.
|
|
2148
|
+
* @param spec - provider, delegation request, and caller cancellation.
|
|
2149
|
+
* @returns the durable child id and the accepted prompt's message id.
|
|
2150
|
+
* @throws when continuation services are unavailable or materialization fails.
|
|
2151
|
+
*/
|
|
2152
|
+
async startContinuable(spec) {
|
|
2153
|
+
return this.requireContinuations().startContinuable(spec);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Deliver one later message to a continuable child as its next FIFO turn. A
|
|
2157
|
+
* resident child's Agent inbox accepts it directly (waking a `waiting`
|
|
2158
|
+
* Activation), while an absent one is cold-resumed from its persisted
|
|
2159
|
+
* Session. The Agent inbox is the only queue, so every accepted message has
|
|
2160
|
+
* one observable order.
|
|
2161
|
+
* @param parent - the exact live direct parent authorizing this delivery.
|
|
2162
|
+
* @param childId - durable child session id.
|
|
2163
|
+
* @param content - user-role content to deliver.
|
|
2164
|
+
* @param options - the message source fields and caller cancellation, which stops the
|
|
2165
|
+
* operation only before inbox acceptance.
|
|
2166
|
+
* @returns the accepted message's inbox id.
|
|
2167
|
+
* @throws when continuation services are unavailable, parent authority is
|
|
2168
|
+
* rejected, or the message was not admitted.
|
|
2169
|
+
*/
|
|
2170
|
+
async followup(parent, childId, content, options) {
|
|
2171
|
+
return this.requireContinuations().followup(parent, childId, content, options);
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Interrupt one live continuable child's current turn under a human parent
|
|
2175
|
+
* address or an exact live ancestor Agent. Fire-and-return: the cancel
|
|
2176
|
+
* signal is issued before this returns, but the target may keep running
|
|
2177
|
+
* until it observes the signal. Unclaimed pending inbox work, the Activation,
|
|
2178
|
+
* and published descendants are preserved; claimed work is not requeued.
|
|
2179
|
+
* Once the interrupted driver is idle, a waking send resumes the parked FIFO
|
|
2180
|
+
* queue. An absent target — including a one-shot or unknown id —
|
|
2181
|
+
* is an accepted no-op, as is a manager-less composition, which cannot own a
|
|
2182
|
+
* live Activation.
|
|
2183
|
+
* @param targetSessionId - the durable child session id to interrupt.
|
|
2184
|
+
* @param authority - the human parent address or exact live ancestor Agent.
|
|
2185
|
+
* @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the
|
|
2186
|
+
* live target.
|
|
2187
|
+
*/
|
|
2188
|
+
interrupt(targetSessionId, authority) {
|
|
2189
|
+
this.continuations?.interrupt(targetSessionId, authority);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Deliver selected content from one live continuable child to its durable
|
|
2193
|
+
* direct parent. The child is the authority credential; callers cannot name a
|
|
2194
|
+
* recipient. Reporting does not conclude the child's turn or Activation.
|
|
2195
|
+
* @param child - exact live reporting child.
|
|
2196
|
+
* @param content - selected model-facing content.
|
|
2197
|
+
* @param options - parent scheduling and pre-acceptance cancellation.
|
|
2198
|
+
* @returns the stable identity of the parent-accepted message.
|
|
2199
|
+
* @throws when continuation services are unavailable, sender authorization
|
|
2200
|
+
* fails, or the direct parent is not live.
|
|
2201
|
+
*/
|
|
2202
|
+
async reportFrom(child, content, options) {
|
|
2203
|
+
return this.requireContinuations().reportFrom(child, content, options);
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Compose one deployment capability into every continuable child's
|
|
2207
|
+
* unpublished creation context on fresh creation and cold resume. Grants wait
|
|
2208
|
+
* for the next Activation; removing the contribution revokes every resident
|
|
2209
|
+
* installation immediately.
|
|
2210
|
+
* @param contribution - synchronous child-scope installer.
|
|
2211
|
+
* @returns the exact Cordis effect disposer.
|
|
2212
|
+
*/
|
|
2213
|
+
registerContinuableSetup(contribution) {
|
|
2214
|
+
return this.ctx.effect(() => this.setupRegistry.register(contribution), "subagents.registerContinuableSetup()");
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Close continuable admission below exact live parent Agents, stop only their
|
|
2218
|
+
* visible descendant Activations synchronously, then await admitted scoped
|
|
2219
|
+
* materializations and release those forests child-first. The scoped cutoff
|
|
2220
|
+
* lasts until each exact parent leaves the registry; unrelated parent trees
|
|
2221
|
+
* remain live.
|
|
2222
|
+
* @param parents - exact host-owned parent Agents entering teardown.
|
|
2223
|
+
* @returns once every retained descendant Activation released its `AgentHandle`.
|
|
2224
|
+
* @throws an aggregate error after all branches settle when any failed.
|
|
2225
|
+
*/
|
|
2226
|
+
async drainContinuableDescendants(parents) {
|
|
2227
|
+
const manager = this.continuations;
|
|
2228
|
+
if (manager === void 0) return;
|
|
2229
|
+
await manager.drainDescendants(parents);
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Enumerate the parent's direct session-backed subagents without loading or
|
|
2233
|
+
* resuming an Agent and without any query service: the listing merges the live
|
|
2234
|
+
* session store with optional session persistence (live-preferred) and
|
|
2235
|
+
* serves each child's durable mode/label from the registered `subagent`
|
|
2236
|
+
* projection unit down a three-rung ladder — the registry's watermark
|
|
2237
|
+
* snapshot for a live child; for a cold one, a durable projection-cache
|
|
2238
|
+
* row when the optional cache serves an own-suffix identity (its `seq`
|
|
2239
|
+
* gate proves the value postdates the fork seed, where a child's own
|
|
2240
|
+
* descriptor is immutable once appended), else one persistence inspection
|
|
2241
|
+
* folded through the registry. The
|
|
2242
|
+
* projection fold is the single classification authority; per-child
|
|
2243
|
+
* diagnostics relay a fold that served no identity or a failed inspection,
|
|
2244
|
+
* never a list-time descriptor parse. Absent persistence, enumeration is
|
|
2245
|
+
* live-only (a cold child cannot be resumed then either, so its absence is
|
|
2246
|
+
* capability absence, not an error). This service consults no Agent
|
|
2247
|
+
* registrations, Activations, or providers.
|
|
2248
|
+
*
|
|
2249
|
+
* Every persistence read receives `signal`, and the listing rechecks
|
|
2250
|
+
* cancellation around each of those awaits. Read rejections that settle
|
|
2251
|
+
* after an abort become a stable `SubagentError` with code `CANCELLED`.
|
|
2252
|
+
* @param parentSessionId - parent session whose direct children are listed.
|
|
2253
|
+
* @param signal - caller-owned cancellation forwarded to persistence reads
|
|
2254
|
+
* and observed around every read await.
|
|
2255
|
+
* @returns children and per-child diagnostics ordered by `createdAt`, then id.
|
|
2256
|
+
* @throws {@link SubagentError} when the projection registry or the session
|
|
2257
|
+
* store is not mounted, or the caller cancels the listing.
|
|
2258
|
+
*/
|
|
2259
|
+
listChildren(parentSessionId, signal) {
|
|
2260
|
+
return listChildren(this.ctx, parentSessionId, signal);
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Enumerate the root's complete session-backed subagent tree in stable
|
|
2264
|
+
* pre-order from one live-preferred corpus, without loading or resuming an
|
|
2265
|
+
* Agent. Ordinary sessions and one-shot children remain traversal nodes so
|
|
2266
|
+
* continuable descendants below them are discovered; each returned entry
|
|
2267
|
+
* adds its durable `parentId` and root-relative `depth`. Identity resolution,
|
|
2268
|
+
* diagnostics, optional persistence, and cancellation follow the same
|
|
2269
|
+
* projection-backed contract as {@link listChildren}.
|
|
2270
|
+
* @param rootSessionId - session whose complete descendant tree is listed.
|
|
2271
|
+
* @param signal - caller-owned cancellation forwarded to persistence reads
|
|
2272
|
+
* and observed around every read await.
|
|
2273
|
+
* @returns children and per-candidate diagnostics with tree position, in
|
|
2274
|
+
* stable pre-order.
|
|
2275
|
+
* @throws {@link SubagentError} under the same conditions as {@link listChildren}.
|
|
2276
|
+
*/
|
|
2277
|
+
listDescendants(rootSessionId, signal) {
|
|
2278
|
+
return listDescendants(this.ctx, rootSessionId, signal);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Register a provider under its name. Registration is effect-scoped and HMR
|
|
2282
|
+
* safe; removing a provider blocks new starts but does not revoke runs that
|
|
2283
|
+
* were already returned to their holders.
|
|
2284
|
+
* @param provider - the trusted provider implementation.
|
|
2285
|
+
* @returns the exact Cordis effect disposer.
|
|
2286
|
+
*/
|
|
2287
|
+
registerProvider(provider) {
|
|
2288
|
+
const name = provider.name;
|
|
2289
|
+
return this.ctx.effect(function* () {
|
|
2290
|
+
if (this.providers.has(name)) throw new SubagentError(`a subagent provider named "${name}" is already registered`, "DUPLICATE_PROVIDER");
|
|
2291
|
+
this.providers.set(name, provider);
|
|
2292
|
+
yield () => {
|
|
2293
|
+
this.providers.delete(name);
|
|
2294
|
+
this.emitLifecycle("subagent/provider-removed", name);
|
|
2295
|
+
};
|
|
2296
|
+
this.ctx.emit("subagent/provider-added", provider);
|
|
2297
|
+
}.bind(this), "subagents.registerProvider()");
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Look up a provider by name.
|
|
2301
|
+
* @param name - the provider name.
|
|
2302
|
+
* @returns the provider, or undefined when absent.
|
|
2303
|
+
*/
|
|
2304
|
+
getProvider(name) {
|
|
2305
|
+
return this.providers.get(name);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* List registered provider names in insertion order.
|
|
2309
|
+
* @returns the registered names.
|
|
2310
|
+
*/
|
|
2311
|
+
list() {
|
|
2312
|
+
return [...this.providers.keys()];
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Establish a published child on the named provider. Capability and semantic
|
|
2316
|
+
* checks run before delegation. Provider ownership lasts until its promise
|
|
2317
|
+
* fulfills; a rejection therefore has no run for the caller to dispose and
|
|
2318
|
+
* emits no run lifecycle events. Post-publication turn and infrastructure
|
|
2319
|
+
* failures settle through the returned run.
|
|
2320
|
+
* @param name - the provider to use.
|
|
2321
|
+
* @param request - child label, prompt, parent, signal, and optional capabilities.
|
|
2322
|
+
* @returns the published holder-owned run.
|
|
2323
|
+
*/
|
|
2324
|
+
async start(name, request) {
|
|
2325
|
+
const provider = this.expectProvider(name);
|
|
2326
|
+
this.assertCapabilities(provider, request);
|
|
2327
|
+
assertSubagentMaxDepth(request.maxDepth);
|
|
2328
|
+
if (request.outputSchema !== void 0) assertObjectJsonSchema(request.outputSchema);
|
|
2329
|
+
const descriptor = snapshotSubagentDescriptor({
|
|
2330
|
+
mode: "one-shot",
|
|
2331
|
+
provider: name,
|
|
2332
|
+
...request.label !== void 0 ? { label: request.label } : {}
|
|
2333
|
+
});
|
|
2334
|
+
const resolved = {
|
|
2335
|
+
...request,
|
|
2336
|
+
descriptor
|
|
2337
|
+
};
|
|
2338
|
+
return observeRun(this.emitLifecycle, name, request.parent, await provider.start(resolved));
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Resolve one provider's detached continuable-creation contribution. Method
|
|
2342
|
+
* presence on the provider IS the capability, so a provider without it is
|
|
2343
|
+
* rejected before the manager reserves any child resources.
|
|
2344
|
+
*/
|
|
2345
|
+
async prepareContinuable(name, request) {
|
|
2346
|
+
const provider = this.expectProvider(name);
|
|
2347
|
+
if (provider.prepareContinuable === void 0) throw new SubagentError(`subagent provider "${provider.name}" does not support continuable children (no prepareContinuable capability)`, "UNSUPPORTED_CAPABILITY");
|
|
2348
|
+
return provider.prepareContinuable(request);
|
|
2349
|
+
}
|
|
2350
|
+
/** Look up a provider for dispatch or fail loud. */
|
|
2351
|
+
expectProvider(name) {
|
|
2352
|
+
const provider = this.providers.get(name);
|
|
2353
|
+
if (provider === void 0) throw new SubagentError(`no subagent provider registered for "${name}"`, "NO_PROVIDER");
|
|
2354
|
+
return provider;
|
|
2355
|
+
}
|
|
2356
|
+
/** Resolve the optional continuable-subagent manager or fail loud. */
|
|
2357
|
+
requireContinuations() {
|
|
2358
|
+
if (this.continuations === void 0) throw new SubagentError("continuable subagents require the agents service", "CONTINUATION_UNAVAILABLE");
|
|
2359
|
+
return this.continuations;
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Build the lifecycle observer for one continuable Activation's residency
|
|
2363
|
+
* epoch, so the manager publishes its edges without owning event dispatch.
|
|
2364
|
+
*/
|
|
2365
|
+
observeActivation(provider, childId, parent) {
|
|
2366
|
+
return createActivationObserver(this.emitLifecycle, provider, childId, parent);
|
|
2367
|
+
}
|
|
2368
|
+
/** Reject the first requested capability that the provider lacks. */
|
|
2369
|
+
assertCapabilities(provider, request) {
|
|
2370
|
+
const needs = [
|
|
2371
|
+
{
|
|
2372
|
+
when: request.outputSchema !== void 0,
|
|
2373
|
+
cap: "outputSchema"
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
when: request.maxDepth !== void 0,
|
|
2377
|
+
cap: "depthLimit"
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
when: request.toolFilter !== void 0,
|
|
2381
|
+
cap: "toolFilter"
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
when: request.persona !== void 0,
|
|
2385
|
+
cap: "persona"
|
|
2386
|
+
}
|
|
2387
|
+
];
|
|
2388
|
+
for (const { when, cap } of needs) if (when && !provider.capabilities[cap]) throw new SubagentError(`subagent provider "${provider.name}" does not support the "${cap}" capability`, "UNSUPPORTED_CAPABILITY");
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
//#endregion
|
|
2392
|
+
export { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentService, SubagentService as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, foldSubagentDescriptor, resolveChildAgentOptions, resolveChildCwd, resolveChildDepth, seedDescriptorTurn, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };
|