@deepseek-ai/dsh-subagent 0.1.2-alpha.2 → 0.1.2-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +12 -11
- package/README.zh.md +12 -11
- package/lib/index.js +238 -324
- package/lib/typert.host.js +52 -157
- package/lib/typert.remote-client.js +8 -91
- package/lib/types/child-agent.d.ts +2 -2
- package/lib/types/child-agent.js +3 -3
- package/lib/types/continuation.d.ts +50 -69
- package/lib/types/continuation.js +173 -105
- package/lib/types/control-types.d.ts +11 -27
- package/lib/types/control.d.ts +1 -17
- package/lib/types/control.js +6 -24
- package/lib/types/descriptor-seed.d.ts +1 -1
- package/lib/types/descriptor-seed.js +1 -1
- package/lib/types/index.d.ts +32 -43
- package/lib/types/index.js +51 -50
- package/lib/types/internal.d.ts +46 -0
- package/lib/types/internal.js +45 -0
- package/lib/types/lifecycle.js +4 -3
- package/lib/types/list-children.d.ts +6 -5
- package/lib/types/list-children.js +23 -22
- package/lib/types/projection-types.d.ts +4 -3
- package/lib/types/projection.js +3 -2
- package/package.json +48 -44
- package/lib/types/activation-setup-registry.d.ts +0 -57
- package/lib/types/activation-setup-registry.js +0 -148
|
@@ -27,27 +27,18 @@ import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
27
27
|
import type { SubagentDescriptorData } from './descriptor.ts';
|
|
28
28
|
import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentStartRequest } from './types.ts';
|
|
29
29
|
import type { ActivationObserver } from './lifecycle.ts';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
readonly kind: 'coordinator';
|
|
30
|
+
/** Durable attribution for one model-authored message between adjacent Agents. */
|
|
31
|
+
export interface AgentMessageSource {
|
|
32
|
+
readonly kind: 'agent-message';
|
|
34
33
|
/** A message another agent addressed to this one (`relay` context form). */
|
|
35
34
|
readonly form: 'relay';
|
|
36
|
-
/** Session id of the
|
|
37
|
-
readonly senderSessionId: SessionId;
|
|
38
|
-
}
|
|
39
|
-
/** Durable attribution for a continuable child's explicit parent report. */
|
|
40
|
-
export interface SubagentReportMessageSource {
|
|
41
|
-
readonly kind: 'subagent-report';
|
|
42
|
-
/** A message another agent addressed to this one (`relay` context form). */
|
|
43
|
-
readonly form: 'relay';
|
|
44
|
-
/** Session id of the reporting child. */
|
|
35
|
+
/** Session id of the Agent whose tool call produced the message. */
|
|
45
36
|
readonly senderSessionId: SessionId;
|
|
46
37
|
}
|
|
47
38
|
/**
|
|
48
39
|
* Durable attribution for the runtime's own account of a continuable child
|
|
49
40
|
* settling. Deliberately a different kind from
|
|
50
|
-
* {@link
|
|
41
|
+
* {@link AgentMessageSource}: an Agent message is content the sender chose,
|
|
51
42
|
* while this message is the manager stating what became of the child, and a
|
|
52
43
|
* transcript that merged them would credit the child with words it never wrote.
|
|
53
44
|
*/
|
|
@@ -62,20 +53,10 @@ export interface SubagentSettledMessageSource {
|
|
|
62
53
|
}
|
|
63
54
|
declare module '@deepseek-ai/dsh-llm' {
|
|
64
55
|
interface MessageSourceMap {
|
|
65
|
-
|
|
66
|
-
'subagent-report': SubagentReportMessageSource;
|
|
56
|
+
'agent-message': AgentMessageSource;
|
|
67
57
|
'subagent-settled': SubagentSettledMessageSource;
|
|
68
58
|
}
|
|
69
59
|
}
|
|
70
|
-
/** Deployment scheduling policy for accepted child reports. */
|
|
71
|
-
export type SubagentReportDelivery = 'quiet' | 'next-step';
|
|
72
|
-
/** Options for one continuable child's report to its direct parent. */
|
|
73
|
-
export interface SubagentReportOptions {
|
|
74
|
-
/** Already-resolved parent scheduling policy. */
|
|
75
|
-
readonly delivery: SubagentReportDelivery;
|
|
76
|
-
/** Caller cancellation, owning authorization and admission until acceptance. */
|
|
77
|
-
readonly signal: AbortSignal;
|
|
78
|
-
}
|
|
79
60
|
/** What a caller asks for when starting a continuable background child. */
|
|
80
61
|
export interface ContinuableStartSpec {
|
|
81
62
|
/** The `ctx.subagents` provider whose continuable-creation capability establishes the child. */
|
|
@@ -115,10 +96,8 @@ export type SubagentInterruptAuthority = {
|
|
|
115
96
|
readonly kind: 'ancestor';
|
|
116
97
|
readonly agent: Agent;
|
|
117
98
|
};
|
|
118
|
-
/** Options for
|
|
119
|
-
export interface
|
|
120
|
-
/** Durable attribution retained on the delivered message; it grants no authority. */
|
|
121
|
-
readonly source: MessageSource;
|
|
99
|
+
/** Options for one model-authored message between adjacent Agents. */
|
|
100
|
+
export interface SubagentSendMessageOptions {
|
|
122
101
|
/** Caller cancellation, owning the operation only until inbox acceptance. */
|
|
123
102
|
readonly signal: AbortSignal;
|
|
124
103
|
}
|
|
@@ -155,7 +134,6 @@ interface ContinuationHost {
|
|
|
155
134
|
export declare class SubagentContinuationManager {
|
|
156
135
|
private readonly ctx;
|
|
157
136
|
private readonly host;
|
|
158
|
-
private readonly setupRegistry;
|
|
159
137
|
/** Child session id → its live Activation. Process-local, never durable. */
|
|
160
138
|
private activations;
|
|
161
139
|
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
@@ -171,7 +149,7 @@ export declare class SubagentContinuationManager {
|
|
|
171
149
|
*/
|
|
172
150
|
private readonly closingScopes;
|
|
173
151
|
private draining;
|
|
174
|
-
constructor(ctx: Context, host: ContinuationHost
|
|
152
|
+
constructor(ctx: Context, host: ContinuationHost);
|
|
175
153
|
/**
|
|
176
154
|
* Start one continuable background child: reserve its durable identity,
|
|
177
155
|
* resolve the provider's detached creation spec, create the child Agent
|
|
@@ -191,23 +169,31 @@ export declare class SubagentContinuationManager {
|
|
|
191
169
|
/** Reject one child identity already owned by a live Agent or Session. */
|
|
192
170
|
private assertChildIdAvailable;
|
|
193
171
|
/**
|
|
194
|
-
* Deliver one
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* @
|
|
204
|
-
* @
|
|
205
|
-
|
|
206
|
-
|
|
172
|
+
* Deliver one model-authored message to a direct continuable child or to the
|
|
173
|
+
* sender's direct parent. Both directions use Steer: a running target admits
|
|
174
|
+
* the message at its nearest step boundary, while an idle target starts a
|
|
175
|
+
* turn. A missing direct child cold-resumes through the ordinary continuation
|
|
176
|
+
* lifecycle. The caller signal owns the operation only until inbox acceptance.
|
|
177
|
+
* @param sender - exact live Agent authorizing and originating the message.
|
|
178
|
+
* @param targetId - durable direct-parent or direct-child session id.
|
|
179
|
+
* @param content - model-authored content to deliver.
|
|
180
|
+
* @param options - caller cancellation before acceptance.
|
|
181
|
+
* @returns the accepted message's inbox id.
|
|
182
|
+
* @throws when adjacency, availability, or admission rejects delivery.
|
|
183
|
+
*/
|
|
184
|
+
sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;
|
|
185
|
+
/**
|
|
186
|
+
* Queue one human-authored prompt as a distinct direct-child turn.
|
|
187
|
+
* @param parent - exact live direct parent authorizing delivery.
|
|
188
|
+
* @param childId - durable direct-child session id.
|
|
189
|
+
* @param content - human-authored content to deliver.
|
|
190
|
+
* @param source - durable host-protocol provenance.
|
|
191
|
+
* @param signal - caller cancellation before inbox acceptance.
|
|
207
192
|
* @returns the accepted message's inbox id.
|
|
208
|
-
* @throws when parent authority, availability, or admission rejects the delivery.
|
|
209
193
|
*/
|
|
210
|
-
|
|
194
|
+
queuePrompt(parent: Agent, childId: SessionId, content: ContentBlock[], source: MessageSource, signal: AbortSignal): Promise<MessageId>;
|
|
195
|
+
/** Route one parent-originated delivery through residency and cold resume. */
|
|
196
|
+
private deliverToChild;
|
|
211
197
|
/**
|
|
212
198
|
* Interrupt one live continuable child's current turn. Admission is
|
|
213
199
|
* synchronous and the effect is asynchronous: this authorizes the caller,
|
|
@@ -230,25 +216,8 @@ export declare class SubagentContinuationManager {
|
|
|
230
216
|
* an ancestor outside the target's recorded live lineage.
|
|
231
217
|
*/
|
|
232
218
|
interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
* its durable direct parent. Sender authorization, parent resolution, and
|
|
236
|
-
* send acceptance share one no-await span. Reporting neither concludes the
|
|
237
|
-
* child's turn nor changes its Activation lifetime.
|
|
238
|
-
* @param child - exact live reporting child; this is the authority credential.
|
|
239
|
-
* @param content - selected model-facing content.
|
|
240
|
-
* @param options - scheduling policy and pre-acceptance cancellation.
|
|
241
|
-
* @returns the stable identity of the message accepted by the parent.
|
|
242
|
-
* @throws {SubagentError} when the sender is unauthorized, the parent is not
|
|
243
|
-
* live, or continuation admission is closing.
|
|
244
|
-
*/
|
|
245
|
-
reportFrom(child: Agent, content: ContentBlock[], options: SubagentReportOptions): Promise<MessageId>;
|
|
246
|
-
/** Authorize only the exact Agent of one resident Activation. */
|
|
247
|
-
private authorizeReporter;
|
|
248
|
-
/** Resolve the reporting child's live direct parent from durable lineage. */
|
|
249
|
-
private resolveReportParent;
|
|
250
|
-
/** Deliver one framed report through the selected parent scheduling preset. */
|
|
251
|
-
private deliverReport;
|
|
219
|
+
/** Deliver one resident continuable child's message to its live direct parent. */
|
|
220
|
+
private sendToParent;
|
|
252
221
|
/**
|
|
253
222
|
* Perform one waking send to a parent, accounted against that parent's own
|
|
254
223
|
* Activation when it has one. Registering the id before the send is what
|
|
@@ -259,8 +228,8 @@ export declare class SubagentContinuationManager {
|
|
|
259
228
|
* @param send - the synchronous waking send to perform.
|
|
260
229
|
*/
|
|
261
230
|
private sendWaking;
|
|
262
|
-
/** Send one
|
|
263
|
-
private
|
|
231
|
+
/** Send one Agent message while translating only the target's own rejection. */
|
|
232
|
+
private sendAgentMessage;
|
|
264
233
|
/**
|
|
265
234
|
* Close admission, await every already-admitted materialization through
|
|
266
235
|
* publication or rollback, then dispose the stable live Activation forest
|
|
@@ -334,12 +303,24 @@ export declare class SubagentContinuationManager {
|
|
|
334
303
|
* Submit to a freshly materialized Activation or roll it back completely.
|
|
335
304
|
* @param activation - the just-published Activation to admit or release.
|
|
336
305
|
* @param content - the initial or resumed message content.
|
|
337
|
-
* @param
|
|
306
|
+
* @param options - durable source, scheduling, and pre-acceptance cancellation.
|
|
338
307
|
* @param parent - the live direct parent authorizing admission.
|
|
339
|
-
* @param signal - caller cancellation owning admission until acceptance.
|
|
340
308
|
* @returns the accepted inbox message id.
|
|
341
309
|
*/
|
|
342
310
|
private submitMaterialized;
|
|
311
|
+
/**
|
|
312
|
+
* Refuse image content addressed to a child whose model accepts text only.
|
|
313
|
+
* Callers guard with `contentHasImage`, so text-only delivery never awaits.
|
|
314
|
+
* The check runs inside the per-child delivery lock, before the message
|
|
315
|
+
* exists, so a rejection leaves no partial user message. When the child's
|
|
316
|
+
* route is not fixed by its options (a request-waterfall listener owns it)
|
|
317
|
+
* or no LLM registry is composed, delivery proceeds and the LLM layer's
|
|
318
|
+
* text-only projection replaces each image with its stable placeholder.
|
|
319
|
+
* @param agent - the live or freshly materialized child agent.
|
|
320
|
+
* @param signal - caller cancellation bounding the model-info read.
|
|
321
|
+
* @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
|
|
322
|
+
*/
|
|
323
|
+
private assertImageCapable;
|
|
343
324
|
/**
|
|
344
325
|
* Create or resume the child Agent through the private activation-owner
|
|
345
326
|
* scope, install the handle in a fresh Activation, and register ownership on
|
|
@@ -74,12 +74,14 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
74
74
|
});
|
|
75
75
|
import { randomUUID } from 'node:crypto';
|
|
76
76
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
77
|
-
import { ReasoningEffortId, boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
77
|
+
import { ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
78
|
+
import { SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
78
79
|
import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
|
|
79
80
|
import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
|
|
80
81
|
import { assertSubagentMaxDepth } from "./depth.js";
|
|
81
82
|
import { seedDescriptorTurn } from "./descriptor-seed.js";
|
|
82
83
|
import { SubagentError } from "./error.js";
|
|
84
|
+
import { isAdjacentAgentSendMessageTool } from "./internal.js";
|
|
83
85
|
/**
|
|
84
86
|
* Read one Activation's current disposal transaction. This indirection exists
|
|
85
87
|
* because TypeScript would otherwise narrow repeated reads of the mutable field
|
|
@@ -90,6 +92,39 @@ import { SubagentError } from "./error.js";
|
|
|
90
92
|
function disposalOf(activation) {
|
|
91
93
|
return activation.disposal;
|
|
92
94
|
}
|
|
95
|
+
/** Build durable attribution for one adjacent-Agent message. */
|
|
96
|
+
function agentMessageSource(sender) {
|
|
97
|
+
return {
|
|
98
|
+
kind: 'agent-message',
|
|
99
|
+
form: 'relay',
|
|
100
|
+
senderSessionId: sender.id,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Build the model-visible and durable representation of one adjacent-Agent message. */
|
|
104
|
+
function agentMessage(sender, content) {
|
|
105
|
+
return createUserMessage({
|
|
106
|
+
content: [
|
|
107
|
+
{ type: 'text', text: `Agent ${sender.id} sent a message:` },
|
|
108
|
+
...content,
|
|
109
|
+
],
|
|
110
|
+
source: agentMessageSource(sender),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Append adjacent-Agent return guidance to a continuable child's initial task. */
|
|
114
|
+
function continuableInitialPrompt(parentId, prompt) {
|
|
115
|
+
const encodedParentId = JSON.stringify(parentId);
|
|
116
|
+
return [
|
|
117
|
+
...prompt,
|
|
118
|
+
{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with `
|
|
121
|
+
+ `send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares `
|
|
122
|
+
+ 'your workspace but does not automatically receive your transcript, tool output, or reasoning. Send '
|
|
123
|
+
+ 'earlier messages as well when a finding changes what the parent should do next; sending a message '
|
|
124
|
+
+ 'does not end your turn.',
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
}
|
|
93
128
|
/**
|
|
94
129
|
* One line telling a parent that a background child is finished and why, in
|
|
95
130
|
* the parent's own task vocabulary.
|
|
@@ -151,7 +186,6 @@ class ChildLock {
|
|
|
151
186
|
export class SubagentContinuationManager {
|
|
152
187
|
ctx;
|
|
153
188
|
host;
|
|
154
|
-
setupRegistry;
|
|
155
189
|
/** Child session id → its live Activation. Process-local, never durable. */
|
|
156
190
|
activations = new Map();
|
|
157
191
|
/** Materializations admitted before drain, tracked through publication or rollback. */
|
|
@@ -167,10 +201,9 @@ export class SubagentContinuationManager {
|
|
|
167
201
|
*/
|
|
168
202
|
closingScopes = new Map();
|
|
169
203
|
draining = false;
|
|
170
|
-
constructor(ctx, host
|
|
204
|
+
constructor(ctx, host) {
|
|
171
205
|
this.ctx = ctx;
|
|
172
206
|
this.host = host;
|
|
173
|
-
this.setupRegistry = setupRegistry;
|
|
174
207
|
// Ordinary Cordis owner effects unwind in reverse registration order, which
|
|
175
208
|
// cannot express the dynamic child graph. Register the private scope's
|
|
176
209
|
// structural disposer FIRST and the drain SECOND, so reverse unwind invokes
|
|
@@ -237,7 +270,7 @@ export class SubagentContinuationManager {
|
|
|
237
270
|
});
|
|
238
271
|
spec.signal.throwIfAborted();
|
|
239
272
|
this.assertAdmitting(parent);
|
|
240
|
-
const
|
|
273
|
+
const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
|
|
241
274
|
const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
|
|
242
275
|
const messageId = await this.locks.run(childId, async () => {
|
|
243
276
|
spec.signal.throwIfAborted();
|
|
@@ -256,12 +289,19 @@ export class SubagentContinuationManager {
|
|
|
256
289
|
childId,
|
|
257
290
|
provider: spec.provider,
|
|
258
291
|
parent,
|
|
259
|
-
create: {
|
|
292
|
+
create: {
|
|
293
|
+
seed,
|
|
294
|
+
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined),
|
|
295
|
+
inheritedEventCount,
|
|
296
|
+
delegatedPolicies,
|
|
297
|
+
},
|
|
260
298
|
agentOptions,
|
|
261
299
|
composition: { persona: request.persona, toolFilter: request.toolFilter },
|
|
262
300
|
signal: spec.signal,
|
|
263
301
|
});
|
|
264
|
-
return this.submitMaterialized(activation,
|
|
302
|
+
return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent))
|
|
303
|
+
? continuableInitialPrompt(parent.id, request.prompt)
|
|
304
|
+
: request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent);
|
|
265
305
|
});
|
|
266
306
|
return { childId, messageId };
|
|
267
307
|
}
|
|
@@ -272,23 +312,52 @@ export class SubagentContinuationManager {
|
|
|
272
312
|
}
|
|
273
313
|
}
|
|
274
314
|
/**
|
|
275
|
-
* Deliver one
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* @param parent - the exact live direct parent authorizing this delivery.
|
|
285
|
-
* @param childId - the durable child session id.
|
|
286
|
-
* @param content - the user-role content to deliver.
|
|
287
|
-
* @param options - the message source fields and caller cancellation.
|
|
315
|
+
* Deliver one model-authored message to a direct continuable child or to the
|
|
316
|
+
* sender's direct parent. Both directions use Steer: a running target admits
|
|
317
|
+
* the message at its nearest step boundary, while an idle target starts a
|
|
318
|
+
* turn. A missing direct child cold-resumes through the ordinary continuation
|
|
319
|
+
* lifecycle. The caller signal owns the operation only until inbox acceptance.
|
|
320
|
+
* @param sender - exact live Agent authorizing and originating the message.
|
|
321
|
+
* @param targetId - durable direct-parent or direct-child session id.
|
|
322
|
+
* @param content - model-authored content to deliver.
|
|
323
|
+
* @param options - caller cancellation before acceptance.
|
|
288
324
|
* @returns the accepted message's inbox id.
|
|
289
|
-
* @throws when
|
|
325
|
+
* @throws when adjacency, availability, or admission rejects delivery.
|
|
290
326
|
*/
|
|
291
|
-
async
|
|
327
|
+
async sendMessage(sender, targetId, content, options) {
|
|
328
|
+
if (this.ctx.agents.get(sender.id) !== sender) {
|
|
329
|
+
throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
|
|
330
|
+
}
|
|
331
|
+
this.assertAdmitting(sender);
|
|
332
|
+
const senderActivation = this.activations.get(sender.id);
|
|
333
|
+
if (senderActivation !== undefined
|
|
334
|
+
&& senderActivation.handle.agent === sender
|
|
335
|
+
&& senderActivation.parentSession === targetId) {
|
|
336
|
+
options.signal.throwIfAborted();
|
|
337
|
+
return this.sendToParent(senderActivation, sender, content);
|
|
338
|
+
}
|
|
339
|
+
if (sender.session.header.parentSession === targetId) {
|
|
340
|
+
throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, 'UNAUTHORIZED');
|
|
341
|
+
}
|
|
342
|
+
return this.deliverToChild(sender, targetId, content, {
|
|
343
|
+
signal: options.signal,
|
|
344
|
+
delivery: 'steer',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Queue one human-authored prompt as a distinct direct-child turn.
|
|
349
|
+
* @param parent - exact live direct parent authorizing delivery.
|
|
350
|
+
* @param childId - durable direct-child session id.
|
|
351
|
+
* @param content - human-authored content to deliver.
|
|
352
|
+
* @param source - durable host-protocol provenance.
|
|
353
|
+
* @param signal - caller cancellation before inbox acceptance.
|
|
354
|
+
* @returns the accepted message's inbox id.
|
|
355
|
+
*/
|
|
356
|
+
async queuePrompt(parent, childId, content, source, signal) {
|
|
357
|
+
return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
|
|
358
|
+
}
|
|
359
|
+
/** Route one parent-originated delivery through residency and cold resume. */
|
|
360
|
+
async deliverToChild(parent, childId, content, options) {
|
|
292
361
|
this.assertAdmitting(parent);
|
|
293
362
|
while (true) {
|
|
294
363
|
const live = await this.locks.run(childId, async () => {
|
|
@@ -297,14 +366,27 @@ export class SubagentContinuationManager {
|
|
|
297
366
|
return this.coldResume(parent, childId, content, options);
|
|
298
367
|
// A delivery that arrives after the disposal transaction began must not
|
|
299
368
|
// reach a handle being torn down; wait for release, then cold-resume.
|
|
369
|
+
const disposal = activation.disposal;
|
|
300
370
|
/* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
|
|
301
371
|
* delivery to observe the transaction inside the same critical section that opened it,
|
|
302
372
|
* which no test can schedule deterministically. The behavior is covered end-to-end by
|
|
303
373
|
* "cold-resumes a delivery that lost the race with final disposal". */
|
|
304
|
-
if (
|
|
305
|
-
return
|
|
374
|
+
if (disposal !== undefined) {
|
|
375
|
+
return disposal.then(() => undefined, () => undefined);
|
|
306
376
|
}
|
|
307
|
-
|
|
377
|
+
// Text-only delivery stays await-free, so the disposal-cutoff check
|
|
378
|
+
// above and the submit share one critical window. The image path
|
|
379
|
+
// awaits a capability read, so it re-checks the cutoff afterwards; a
|
|
380
|
+
// disposal that began during the read is waited out and retried like
|
|
381
|
+
// one observed on entry.
|
|
382
|
+
if (contentHasImage(content)) {
|
|
383
|
+
await this.assertImageCapable(activation.handle.agent, options.signal);
|
|
384
|
+
if (activation.disposal !== undefined) {
|
|
385
|
+
await Promise.allSettled([activation.disposal]);
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return this.submitAdmitted(activation, content, options, parent);
|
|
308
390
|
});
|
|
309
391
|
/* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
|
|
310
392
|
* race reaches the retry below, which then cold-resumes a new Activation. */
|
|
@@ -365,68 +447,19 @@ export class SubagentContinuationManager {
|
|
|
365
447
|
return;
|
|
366
448
|
activation.handle.agent.cancel(authority.kind === 'user' ? { kind: 'user' } : { kind: 'parent' }, { keepInbox: true });
|
|
367
449
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
* child's turn nor changes its Activation lifetime.
|
|
373
|
-
* @param child - exact live reporting child; this is the authority credential.
|
|
374
|
-
* @param content - selected model-facing content.
|
|
375
|
-
* @param options - scheduling policy and pre-acceptance cancellation.
|
|
376
|
-
* @returns the stable identity of the message accepted by the parent.
|
|
377
|
-
* @throws {SubagentError} when the sender is unauthorized, the parent is not
|
|
378
|
-
* live, or continuation admission is closing.
|
|
379
|
-
*/
|
|
380
|
-
// oxlint-disable-next-line typescript/require-await -- keep rejection semantics without yielding during admission
|
|
381
|
-
async reportFrom(child, content, options) {
|
|
382
|
-
options.signal.throwIfAborted();
|
|
383
|
-
this.assertAdmitting(child);
|
|
384
|
-
const activation = this.authorizeReporter(child);
|
|
385
|
-
const parent = this.resolveReportParent(child);
|
|
386
|
-
return this.deliverReport(activation, parent, content, options.delivery);
|
|
387
|
-
}
|
|
388
|
-
/** Authorize only the exact Agent of one resident Activation. */
|
|
389
|
-
authorizeReporter(child) {
|
|
390
|
-
const activation = this.activations.get(child.id);
|
|
391
|
-
if (activation === undefined || activation.handle.agent !== child) {
|
|
392
|
-
throw new SubagentError(`agent "${child.id}" is not a live continuable subagent and cannot report`, 'UNAUTHORIZED');
|
|
393
|
-
}
|
|
394
|
-
/* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
|
|
395
|
-
* transaction between exact-agent authorization and this no-await cutoff. */
|
|
450
|
+
/** Deliver one resident continuable child's message to its live direct parent. */
|
|
451
|
+
sendToParent(activation, sender, content) {
|
|
452
|
+
/* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
|
|
453
|
+
* transaction between exact-agent authorization and this no-await span. */
|
|
396
454
|
if (activation.disposal !== undefined) {
|
|
397
|
-
throw new SubagentError(`subagent "${
|
|
455
|
+
throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
|
|
398
456
|
}
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
/** Resolve the reporting child's live direct parent from durable lineage. */
|
|
402
|
-
resolveReportParent(child) {
|
|
403
|
-
const parentId = child.session.header.parentSession;
|
|
404
|
-
/* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
|
|
405
|
-
const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId);
|
|
457
|
+
const parent = this.ctx.agents.get(activation.parentSession);
|
|
406
458
|
if (parent === undefined) {
|
|
407
|
-
throw new SubagentError('direct parent is not live;
|
|
408
|
-
}
|
|
409
|
-
return parent;
|
|
410
|
-
}
|
|
411
|
-
/** Deliver one framed report through the selected parent scheduling preset. */
|
|
412
|
-
deliverReport(activation, parent, content, delivery) {
|
|
413
|
-
const message = createUserMessage({
|
|
414
|
-
content: [
|
|
415
|
-
{ type: 'text', text: `Background subagent ${activation.childId} reported:` },
|
|
416
|
-
...content,
|
|
417
|
-
],
|
|
418
|
-
source: {
|
|
419
|
-
kind: 'subagent-report',
|
|
420
|
-
form: 'relay',
|
|
421
|
-
senderSessionId: activation.childId,
|
|
422
|
-
},
|
|
423
|
-
});
|
|
424
|
-
if (delivery === 'next-step') {
|
|
425
|
-
this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery); });
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
this.sendReport(parent, message, delivery);
|
|
459
|
+
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
|
|
429
460
|
}
|
|
461
|
+
const message = agentMessage(sender, content);
|
|
462
|
+
this.sendWaking(parent, message, () => { this.sendAgentMessage(parent, message); });
|
|
430
463
|
return message.id;
|
|
431
464
|
}
|
|
432
465
|
/**
|
|
@@ -447,16 +480,13 @@ export class SubagentContinuationManager {
|
|
|
447
480
|
send();
|
|
448
481
|
}
|
|
449
482
|
}
|
|
450
|
-
/** Send one
|
|
451
|
-
|
|
483
|
+
/** Send one Agent message while translating only the target's own rejection. */
|
|
484
|
+
sendAgentMessage(parent, message) {
|
|
452
485
|
try {
|
|
453
|
-
|
|
454
|
-
parent.steer(message);
|
|
455
|
-
else
|
|
456
|
-
parent.inject(message);
|
|
486
|
+
parent.steer(message);
|
|
457
487
|
}
|
|
458
488
|
catch (error) {
|
|
459
|
-
throw new SubagentError('direct parent is not live;
|
|
489
|
+
throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
|
|
460
490
|
}
|
|
461
491
|
}
|
|
462
492
|
/**
|
|
@@ -693,10 +723,9 @@ export class SubagentContinuationManager {
|
|
|
693
723
|
// Fold only the child's own suffix: a fork seed replays the parent's log,
|
|
694
724
|
// which may carry an ANCESTOR's descriptor when the parent is itself a
|
|
695
725
|
// continuable child.
|
|
696
|
-
const descriptor = foldSubagentDescriptor(source.events.slice(source.
|
|
726
|
+
const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
|
|
697
727
|
if (descriptor === undefined || descriptor.mode !== 'continuable') {
|
|
698
|
-
throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed;
|
|
699
|
-
+ 'do not retry send_message with this id', 'NOT_RESUMABLE');
|
|
728
|
+
throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
|
|
700
729
|
}
|
|
701
730
|
let activation;
|
|
702
731
|
try {
|
|
@@ -721,7 +750,7 @@ export class SubagentContinuationManager {
|
|
|
721
750
|
throw error;
|
|
722
751
|
throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
|
|
723
752
|
}
|
|
724
|
-
return await this.submitMaterialized(activation, content, options
|
|
753
|
+
return await this.submitMaterialized(activation, content, options, parent);
|
|
725
754
|
}
|
|
726
755
|
catch (e_1) {
|
|
727
756
|
env_1.error = e_1;
|
|
@@ -735,14 +764,22 @@ export class SubagentContinuationManager {
|
|
|
735
764
|
* Submit to a freshly materialized Activation or roll it back completely.
|
|
736
765
|
* @param activation - the just-published Activation to admit or release.
|
|
737
766
|
* @param content - the initial or resumed message content.
|
|
738
|
-
* @param
|
|
767
|
+
* @param options - durable source, scheduling, and pre-acceptance cancellation.
|
|
739
768
|
* @param parent - the live direct parent authorizing admission.
|
|
740
|
-
* @param signal - caller cancellation owning admission until acceptance.
|
|
741
769
|
* @returns the accepted inbox message id.
|
|
742
770
|
*/
|
|
743
|
-
async submitMaterialized(activation, content,
|
|
771
|
+
async submitMaterialized(activation, content, options, parent) {
|
|
744
772
|
try {
|
|
745
|
-
|
|
773
|
+
if (contentHasImage(content)) {
|
|
774
|
+
// The capability read awaits with the activation already published, so
|
|
775
|
+
// the disposal cutoff is re-checked before the submit; a drain that
|
|
776
|
+
// began during the read turns into a clean closing rejection.
|
|
777
|
+
await this.assertImageCapable(activation.handle.agent, options.signal);
|
|
778
|
+
if (activation.disposal !== undefined) {
|
|
779
|
+
throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return this.submitAdmitted(activation, content, options, parent);
|
|
746
783
|
}
|
|
747
784
|
catch (error) {
|
|
748
785
|
/* v8 ignore next -- rollback disposal failures must not mask the
|
|
@@ -751,6 +788,32 @@ export class SubagentContinuationManager {
|
|
|
751
788
|
throw error;
|
|
752
789
|
}
|
|
753
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Refuse image content addressed to a child whose model accepts text only.
|
|
793
|
+
* Callers guard with `contentHasImage`, so text-only delivery never awaits.
|
|
794
|
+
* The check runs inside the per-child delivery lock, before the message
|
|
795
|
+
* exists, so a rejection leaves no partial user message. When the child's
|
|
796
|
+
* route is not fixed by its options (a request-waterfall listener owns it)
|
|
797
|
+
* or no LLM registry is composed, delivery proceeds and the LLM layer's
|
|
798
|
+
* text-only projection replaces each image with its stable placeholder.
|
|
799
|
+
* @param agent - the live or freshly materialized child agent.
|
|
800
|
+
* @param signal - caller cancellation bounding the model-info read.
|
|
801
|
+
* @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
|
|
802
|
+
*/
|
|
803
|
+
async assertImageCapable(agent, signal) {
|
|
804
|
+
const { provider, model } = agent.options;
|
|
805
|
+
if (provider === undefined || model === undefined)
|
|
806
|
+
return;
|
|
807
|
+
const llm = this.ctx.get('llm');
|
|
808
|
+
/* v8 ignore next -- a deployment without the LLM registry serves no model
|
|
809
|
+
* to refuse against; delivery then defers to the text-only projection. */
|
|
810
|
+
if (llm === undefined)
|
|
811
|
+
return;
|
|
812
|
+
const info = await llm.resolveModelInfo(provider, model, signal);
|
|
813
|
+
if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
|
|
814
|
+
throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
754
817
|
/**
|
|
755
818
|
* Create or resume the child Agent through the private activation-owner
|
|
756
819
|
* scope, install the handle in a fresh Activation, and register ownership on
|
|
@@ -791,7 +854,6 @@ export class SubagentContinuationManager {
|
|
|
791
854
|
appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
|
|
792
855
|
}
|
|
793
856
|
applyChildComposition(childCtx, parent, inputs.composition);
|
|
794
|
-
return this.setupRegistry.apply(childCtx);
|
|
795
857
|
};
|
|
796
858
|
const observer = this.host.observeActivation(provider, childId, parent);
|
|
797
859
|
// Agent creation owns rollback before handle transfer. A rejection leaves
|
|
@@ -807,6 +869,7 @@ export class SubagentContinuationManager {
|
|
|
807
869
|
sessionId: childId,
|
|
808
870
|
meta: create.meta,
|
|
809
871
|
seed: create.seed,
|
|
872
|
+
inheritedEventCount: create.inheritedEventCount,
|
|
810
873
|
agentOptions: inputs.agentOptions,
|
|
811
874
|
signal: inputs.signal,
|
|
812
875
|
setup,
|
|
@@ -914,13 +977,18 @@ export class SubagentContinuationManager {
|
|
|
914
977
|
* inbox id. Acceptance is the operation's success boundary; the manager owns
|
|
915
978
|
* the Activation independently afterwards.
|
|
916
979
|
*/
|
|
917
|
-
submit(activation, content,
|
|
980
|
+
submit(activation, content, options, parent) {
|
|
918
981
|
// Parent-originated delivery keeps the parent live through ownership, so
|
|
919
982
|
// establish it before the message can enter the child's inbox.
|
|
920
983
|
this.acquireOwnership(parent, activation.childId);
|
|
921
|
-
const message =
|
|
984
|
+
const message = options.delivery === 'steer'
|
|
985
|
+
? agentMessage(parent, content)
|
|
986
|
+
: createUserMessage({ content, source: options.source });
|
|
922
987
|
const accepted = this.admitWaking(activation, message.id, () => {
|
|
923
|
-
|
|
988
|
+
if (options.delivery === 'steer')
|
|
989
|
+
activation.handle.agent.steer(message);
|
|
990
|
+
else
|
|
991
|
+
activation.handle.agent.followup(message);
|
|
924
992
|
});
|
|
925
993
|
// Past this point the caller has an id for this child, so its eventual
|
|
926
994
|
// settlement is something the parent is owed an account of.
|
|
@@ -955,8 +1023,8 @@ export class SubagentContinuationManager {
|
|
|
955
1023
|
* manager drain, or Activation disposal that wins before this synchronous
|
|
956
1024
|
* span rejects without inbox acceptance.
|
|
957
1025
|
*/
|
|
958
|
-
submitAdmitted(activation, content,
|
|
959
|
-
signal.throwIfAborted();
|
|
1026
|
+
submitAdmitted(activation, content, options, parent) {
|
|
1027
|
+
options.signal.throwIfAborted();
|
|
960
1028
|
this.assertAdmitting(parent);
|
|
961
1029
|
/* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
|
|
962
1030
|
* this field between the caller's live check and this no-await boundary. */
|
|
@@ -964,7 +1032,7 @@ export class SubagentContinuationManager {
|
|
|
964
1032
|
throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, 'ACTIVATION_CLOSING');
|
|
965
1033
|
}
|
|
966
1034
|
this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
|
|
967
|
-
return this.submit(activation, content,
|
|
1035
|
+
return this.submit(activation, content, options, parent);
|
|
968
1036
|
}
|
|
969
1037
|
/**
|
|
970
1038
|
* Authorize one operation against the durable direct-parent lineage. Other
|