@myagentroam/agent 0.10.60 → 0.10.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-core/sdk/agent.js +1 -1
- package/dist/agent-core/sdk/extension-agents.js +1 -1
- package/dist/agent-core/sdk/session.d.ts +18 -12
- package/dist/agent-core/sdk/session.js +42 -39
- package/dist/agent-core/sdk/subagent-backend.js +20 -17
- package/dist/agent-core/session/store/directory-index.d.ts +2 -2
- package/dist/agent-core/session/store/directory-index.js +17 -11
- package/dist/agent-core/session/store/execution-state.d.ts +1 -0
- package/dist/agent-core/session/store/execution-state.js +9 -0
- package/dist/agent-core/session/store/jsonl-store.d.ts +2 -2
- package/dist/agent-core/session/store/jsonl-store.js +217 -218
- package/dist/agent-core/session/store/types.d.ts +1 -0
- package/dist/agent-core/subagent/scheduler.js +1 -1
- package/dist/agent-core/subagent/session-controller.d.ts +7 -1
- package/dist/agent-core/subagent/session-controller.js +560 -352
- package/package.json +1 -1
|
@@ -4,12 +4,28 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { MarAgentError, describeMarAgentError } from '../error.js';
|
|
5
5
|
import { TOOL_EXECUTION_LIMITS } from '../tools/execution-limits.js';
|
|
6
6
|
import { parseAgentWaitTargets, waitForAgentCondition } from './wait.js';
|
|
7
|
+
const RECENT_TERMINAL_RESOURCE_LIMIT = 16;
|
|
8
|
+
const DETAIL_FIELDS = [
|
|
9
|
+
'latestMessage',
|
|
10
|
+
'latestActivity',
|
|
11
|
+
'summary',
|
|
12
|
+
'changedFiles',
|
|
13
|
+
'verification',
|
|
14
|
+
'evidence',
|
|
15
|
+
'permissionStop',
|
|
16
|
+
'errorMessage'
|
|
17
|
+
];
|
|
7
18
|
export class SubagentSessionController {
|
|
8
19
|
parentSessionId;
|
|
9
20
|
rootSessionId;
|
|
10
21
|
backend;
|
|
11
22
|
limits;
|
|
12
23
|
#agents = new Map();
|
|
24
|
+
// Discovery order is independent of the order in which history is requested.
|
|
25
|
+
#discovered = new Map();
|
|
26
|
+
#loading = new Map();
|
|
27
|
+
#readers = new Map();
|
|
28
|
+
#allReaders = 0;
|
|
13
29
|
#queue = [];
|
|
14
30
|
#completionWaiters = new Set();
|
|
15
31
|
#activityWaiters = new Set();
|
|
@@ -35,7 +51,7 @@ export class SubagentSessionController {
|
|
|
35
51
|
#retainedExecution = new AsyncLocalStorage();
|
|
36
52
|
#execution;
|
|
37
53
|
#pendingDeliveries = new Set();
|
|
38
|
-
constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks:
|
|
54
|
+
constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks: 16, maxConcurrent: 16 }) {
|
|
39
55
|
this.parentSessionId = parentSessionId;
|
|
40
56
|
this.rootSessionId = rootSessionId;
|
|
41
57
|
this.backend = backend;
|
|
@@ -43,11 +59,12 @@ export class SubagentSessionController {
|
|
|
43
59
|
}
|
|
44
60
|
static async open(input) {
|
|
45
61
|
const controller = new SubagentSessionController(input.parentSessionId, input.rootSessionId, input.backend, input.limits);
|
|
46
|
-
const sessions =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
const sessions = await input.backend.listChildren(input.parentSessionId);
|
|
63
|
+
// Map insertion order is creation order; values independently rank activity.
|
|
64
|
+
for (const session of sessions)
|
|
65
|
+
controller.#discovered.set(session.id, 0);
|
|
66
|
+
for (const session of [...sessions].sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt))) {
|
|
67
|
+
controller.#discovered.set(session.id, ++controller.#resourceRevision);
|
|
51
68
|
}
|
|
52
69
|
return controller;
|
|
53
70
|
}
|
|
@@ -58,6 +75,7 @@ export class SubagentSessionController {
|
|
|
58
75
|
this.#creationBudget = { count: 0 };
|
|
59
76
|
for (const state of this.#agents.values())
|
|
60
77
|
await this.#publishTerminal(state);
|
|
78
|
+
this.#releaseOldDetails();
|
|
61
79
|
}
|
|
62
80
|
/** A retained cell must not borrow a later model Execution's identity or task budget. */
|
|
63
81
|
captureExecution(executionId) {
|
|
@@ -93,12 +111,63 @@ export class SubagentSessionController {
|
|
|
93
111
|
return this.#retainedExecution.run({ execution: operation, budget: { count: 0 }, native: true }, run);
|
|
94
112
|
}
|
|
95
113
|
async inspect(agentId) {
|
|
96
|
-
return this.#
|
|
114
|
+
return this.#using([agentId], async () => {
|
|
115
|
+
return this.#snapshot(await this.#find(agentId), { full: true });
|
|
116
|
+
});
|
|
97
117
|
}
|
|
98
118
|
inspectAll() {
|
|
99
119
|
if (this.#disposed)
|
|
100
120
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
101
|
-
return
|
|
121
|
+
return this.#orderedStates().map((state) => this.#snapshot(state, { full: true }));
|
|
122
|
+
}
|
|
123
|
+
async list(signal) {
|
|
124
|
+
return this.#using(undefined, async () => {
|
|
125
|
+
await this.prepareAll(signal);
|
|
126
|
+
return this.inspectAll();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Full listing is explicit; ordinary opens and executions never require it. */
|
|
130
|
+
async prepareAll(signal) {
|
|
131
|
+
for (const id of this.#discovered.keys()) {
|
|
132
|
+
signal?.throwIfAborted();
|
|
133
|
+
await this.#restoreDiscovered(id);
|
|
134
|
+
}
|
|
135
|
+
signal?.throwIfAborted();
|
|
136
|
+
}
|
|
137
|
+
async prepareSessionResources(signal) {
|
|
138
|
+
const candidates = [...this.#discovered]
|
|
139
|
+
.filter(([id]) => {
|
|
140
|
+
const state = this.#agents.get(id);
|
|
141
|
+
return !state || isTerminalState(state);
|
|
142
|
+
})
|
|
143
|
+
.sort(([a, ar], [b, br]) => (this.#agents.get(b)?.resourceRevision ?? br) -
|
|
144
|
+
(this.#agents.get(a)?.resourceRevision ?? ar));
|
|
145
|
+
let prepared = 0;
|
|
146
|
+
for (const [id] of candidates) {
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
const state = await this.#restoreDiscovered(id);
|
|
149
|
+
if (state && isTerminalState(state) && ++prepared === RECENT_TERMINAL_RESOURCE_LIMIT)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
signal?.throwIfAborted();
|
|
153
|
+
this.#releaseOldDetails();
|
|
154
|
+
}
|
|
155
|
+
async #restoreDiscovered(agentId) {
|
|
156
|
+
try {
|
|
157
|
+
return await this.#find(agentId);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_SUBAGENT_NOT_FOUND')
|
|
161
|
+
throw error;
|
|
162
|
+
this.#discovered.delete(agentId);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
#orderedStates() {
|
|
167
|
+
return [...this.#discovered.keys()].flatMap((id) => {
|
|
168
|
+
const state = this.#agents.get(id);
|
|
169
|
+
return state ? [state] : [];
|
|
170
|
+
});
|
|
102
171
|
}
|
|
103
172
|
collectModelUpdates() {
|
|
104
173
|
return [...this.#agents.values()]
|
|
@@ -203,173 +272,125 @@ export class SubagentSessionController {
|
|
|
203
272
|
catch (error) {
|
|
204
273
|
await this.#deleteUnstartedSession(created.id, error);
|
|
205
274
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.#agents.set(state.agentId, state);
|
|
226
|
-
const knownModel = execution.models.get(input.modelId ?? execution.modelId);
|
|
227
|
-
if (knownModel) {
|
|
228
|
-
state.modelId = knownModel.id;
|
|
229
|
-
state.modelName = knownModel.name;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const selected = resolveModel(execution, input);
|
|
233
|
-
state.modelId = selected.id;
|
|
234
|
-
state.modelName = selected.name;
|
|
235
|
-
state.reasoningEffort = selected.reasoningEffort;
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
this.#setFailure(state, error);
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
await options.onCreated?.(this.#receiptSnapshot(state));
|
|
242
|
-
if (this.#disposed)
|
|
243
|
-
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
244
|
-
this.#requireExecution();
|
|
245
|
-
options.signal?.throwIfAborted();
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
await this.#deleteUnstartedSession(state.agentId, error);
|
|
249
|
-
}
|
|
250
|
-
if (state.status === 'failed') {
|
|
251
|
-
await this.#publishTerminal(state);
|
|
252
|
-
this.#observeTerminal(state);
|
|
253
|
-
return this.#snapshot(state);
|
|
254
|
-
}
|
|
255
|
-
this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
|
|
256
|
-
if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
|
|
257
|
-
return this.#receiptSnapshot(state);
|
|
258
|
-
await waitForPromise(state.turn.completion, options.signal);
|
|
259
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
260
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
261
|
-
this.#observeLatestMessage(state);
|
|
262
|
-
return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
|
|
263
|
-
}
|
|
264
|
-
async output(agentId, options = {}) {
|
|
265
|
-
options.signal?.throwIfAborted();
|
|
266
|
-
const state = await this.#find(agentId);
|
|
267
|
-
options.signal?.throwIfAborted();
|
|
268
|
-
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
269
|
-
if (options.untilSettled) {
|
|
270
|
-
if (state.turn)
|
|
271
|
-
await waitForPromise(state.turn.completion, options.signal);
|
|
272
|
-
options.signal?.throwIfAborted();
|
|
273
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
274
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
275
|
-
this.#observeLatestMessage(state);
|
|
276
|
-
return {
|
|
277
|
-
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
278
|
-
waitOutcome: 'completion'
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
if (options.waitMs === undefined || options.waitMs === 0)
|
|
282
|
-
return {
|
|
283
|
-
...this.#snapshot(state, {
|
|
284
|
-
includeLatestMessage: false,
|
|
285
|
-
includeTerminalDetails: false
|
|
286
|
-
}),
|
|
287
|
-
waitOutcome: 'snapshot'
|
|
275
|
+
return this.#using([created.id], async () => {
|
|
276
|
+
const state = {
|
|
277
|
+
agentId: created.id,
|
|
278
|
+
description: source.description,
|
|
279
|
+
role: input.role,
|
|
280
|
+
...(source.ownerInstanceId === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { ownerInstanceId: source.ownerInstanceId }),
|
|
283
|
+
...(source.extensionId === undefined ? {} : { extensionId: source.extensionId }),
|
|
284
|
+
status: 'queued',
|
|
285
|
+
messageRevision: 0,
|
|
286
|
+
observedMessageRevision: 0,
|
|
287
|
+
latestMessageActivityRevision: 0,
|
|
288
|
+
changedFiles: [],
|
|
289
|
+
verification: [],
|
|
290
|
+
evidence: [],
|
|
291
|
+
terminalPending: false,
|
|
292
|
+
observedTerminalRevision: -1,
|
|
293
|
+
resourceRevision: ++this.#resourceRevision
|
|
288
294
|
};
|
|
289
|
-
|
|
295
|
+
if (!native)
|
|
296
|
+
(this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
|
|
297
|
+
this.#agents.set(state.agentId, state);
|
|
298
|
+
this.#discovered.set(state.agentId, state.resourceRevision);
|
|
299
|
+
const knownModel = execution.models.get(input.modelId ?? execution.modelId);
|
|
300
|
+
if (knownModel) {
|
|
301
|
+
state.modelId = knownModel.id;
|
|
302
|
+
state.modelName = knownModel.name;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const selected = resolveModel(execution, input);
|
|
306
|
+
state.modelId = selected.id;
|
|
307
|
+
state.modelName = selected.name;
|
|
308
|
+
state.reasoningEffort = selected.reasoningEffort;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
this.#setFailure(state, error);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
await options.onCreated?.(this.#receiptSnapshot(state));
|
|
315
|
+
if (this.#disposed)
|
|
316
|
+
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
317
|
+
this.#requireExecution();
|
|
318
|
+
options.signal?.throwIfAborted();
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
await this.#deleteUnstartedSession(state.agentId, error);
|
|
322
|
+
}
|
|
323
|
+
if (state.status === 'failed') {
|
|
324
|
+
await this.#publishTerminal(state);
|
|
325
|
+
this.#observeTerminal(state);
|
|
326
|
+
return this.#snapshot(state);
|
|
327
|
+
}
|
|
328
|
+
this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
|
|
329
|
+
if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
|
|
330
|
+
return this.#receiptSnapshot(state);
|
|
331
|
+
await waitForPromise(state.turn.completion, options.signal);
|
|
290
332
|
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
291
333
|
const includeTerminalDetails = this.#observeTerminal(state);
|
|
292
334
|
this.#observeLatestMessage(state);
|
|
293
|
-
return {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const waitFor = options.waitFor ?? 'completion';
|
|
299
|
-
if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
|
|
300
|
-
this.#observeLatestMessage(state);
|
|
301
|
-
return { ...this.#snapshot(state), waitOutcome: 'message' };
|
|
302
|
-
}
|
|
303
|
-
const waitMs = this.#retainedExecution.getStore()?.native
|
|
304
|
-
? options.waitMs
|
|
305
|
-
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
306
|
-
if (waitFor === 'completion') {
|
|
307
|
-
const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
|
|
335
|
+
return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async output(agentId, options = {}) {
|
|
339
|
+
return this.#using([agentId], async () => {
|
|
308
340
|
options.signal?.throwIfAborted();
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
341
|
+
const state = await this.#find(agentId);
|
|
342
|
+
options.signal?.throwIfAborted();
|
|
343
|
+
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
344
|
+
if (options.untilSettled) {
|
|
345
|
+
if (state.turn)
|
|
346
|
+
await waitForPromise(state.turn.completion, options.signal);
|
|
347
|
+
options.signal?.throwIfAborted();
|
|
348
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
349
|
+
const includeTerminalDetails = this.#observeTerminal(state);
|
|
312
350
|
this.#observeLatestMessage(state);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
|
|
319
|
-
options.signal?.throwIfAborted();
|
|
320
|
-
const includeLatestMessage = activity?.kind === 'message' ||
|
|
321
|
-
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
322
|
-
const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
|
|
323
|
-
if (activity)
|
|
324
|
-
this.#observeLatestMessage(activity.state);
|
|
325
|
-
return {
|
|
326
|
-
...this.#snapshot(activity?.state ?? state, {
|
|
327
|
-
includeLatestMessage,
|
|
328
|
-
includeTerminalDetails
|
|
329
|
-
}),
|
|
330
|
-
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
async outputAny(options = {}) {
|
|
334
|
-
options.signal?.throwIfAborted();
|
|
335
|
-
if (options.untilSettled)
|
|
336
|
-
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
|
|
337
|
-
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
338
|
-
const states = [...this.#agents.values()];
|
|
339
|
-
if (states.length === 0)
|
|
340
|
-
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
341
|
-
const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
|
|
342
|
-
if (options.waitMs === undefined || options.waitMs === 0)
|
|
343
|
-
return {
|
|
344
|
-
...this.#snapshot(live.at(-1) ?? states.at(-1), {
|
|
345
|
-
includeLatestMessage: false,
|
|
346
|
-
includeTerminalDetails: false
|
|
347
|
-
}),
|
|
348
|
-
waitOutcome: 'snapshot'
|
|
349
|
-
};
|
|
350
|
-
if (live.length === 0) {
|
|
351
|
-
const state = states.at(-1);
|
|
352
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
353
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
354
|
-
this.#observeLatestMessage(state);
|
|
355
|
-
return {
|
|
356
|
-
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
357
|
-
waitOutcome: 'completion'
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
const waitFor = options.waitFor ?? 'completion';
|
|
361
|
-
if (waitFor === 'message') {
|
|
362
|
-
const pending = this.#oldestUnreadMessage(live);
|
|
363
|
-
if (pending) {
|
|
364
|
-
this.#observeLatestMessage(pending);
|
|
365
|
-
return { ...this.#snapshot(pending), waitOutcome: 'message' };
|
|
351
|
+
return {
|
|
352
|
+
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
353
|
+
waitOutcome: 'completion'
|
|
354
|
+
};
|
|
366
355
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
356
|
+
if (options.waitMs === undefined || options.waitMs === 0)
|
|
357
|
+
return {
|
|
358
|
+
...this.#snapshot(state, {
|
|
359
|
+
includeLatestMessage: false,
|
|
360
|
+
includeTerminalDetails: false
|
|
361
|
+
}),
|
|
362
|
+
waitOutcome: 'snapshot'
|
|
363
|
+
};
|
|
364
|
+
if (isTerminalState(state)) {
|
|
365
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
366
|
+
const includeTerminalDetails = this.#observeTerminal(state);
|
|
367
|
+
this.#observeLatestMessage(state);
|
|
368
|
+
return {
|
|
369
|
+
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
370
|
+
waitOutcome: 'completion'
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const waitFor = options.waitFor ?? 'completion';
|
|
374
|
+
if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
|
|
375
|
+
this.#observeLatestMessage(state);
|
|
376
|
+
return { ...this.#snapshot(state), waitOutcome: 'message' };
|
|
377
|
+
}
|
|
378
|
+
const waitMs = this.#retainedExecution.getStore()?.native
|
|
379
|
+
? options.waitMs
|
|
380
|
+
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
381
|
+
if (waitFor === 'completion') {
|
|
382
|
+
const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
|
|
383
|
+
options.signal?.throwIfAborted();
|
|
384
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
385
|
+
const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
|
|
386
|
+
if (includeLatestMessage)
|
|
387
|
+
this.#observeLatestMessage(state);
|
|
388
|
+
return {
|
|
389
|
+
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
390
|
+
...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
|
|
373
394
|
options.signal?.throwIfAborted();
|
|
374
395
|
const includeLatestMessage = activity?.kind === 'message' ||
|
|
375
396
|
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
@@ -377,27 +398,93 @@ export class SubagentSessionController {
|
|
|
377
398
|
if (activity)
|
|
378
399
|
this.#observeLatestMessage(activity.state);
|
|
379
400
|
return {
|
|
380
|
-
...this.#snapshot(activity?.state ??
|
|
401
|
+
...this.#snapshot(activity?.state ?? state, {
|
|
381
402
|
includeLatestMessage,
|
|
382
403
|
includeTerminalDetails
|
|
383
404
|
}),
|
|
384
405
|
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
385
406
|
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.#
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async outputAny(options = {}) {
|
|
410
|
+
return this.#using(undefined, async () => {
|
|
411
|
+
options.signal?.throwIfAborted();
|
|
412
|
+
if (options.untilSettled)
|
|
413
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
|
|
414
|
+
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
415
|
+
const hasLive = [...this.#agents.values()].some((state) => !isTerminalState(state));
|
|
416
|
+
if (!hasLive) {
|
|
417
|
+
for (const id of [...this.#discovered.keys()].reverse()) {
|
|
418
|
+
options.signal?.throwIfAborted();
|
|
419
|
+
if (await this.#restoreDiscovered(id))
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
options.signal?.throwIfAborted();
|
|
424
|
+
const states = this.#orderedStates();
|
|
425
|
+
if (states.length === 0)
|
|
426
|
+
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
427
|
+
const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
|
|
428
|
+
if (options.waitMs === undefined || options.waitMs === 0)
|
|
429
|
+
return {
|
|
430
|
+
...this.#snapshot(live.at(-1) ?? states.at(-1), {
|
|
431
|
+
includeLatestMessage: false,
|
|
432
|
+
includeTerminalDetails: false
|
|
433
|
+
}),
|
|
434
|
+
waitOutcome: 'snapshot'
|
|
435
|
+
};
|
|
436
|
+
if (live.length === 0) {
|
|
437
|
+
const state = states.at(-1);
|
|
438
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
439
|
+
const includeTerminalDetails = this.#observeTerminal(state);
|
|
440
|
+
this.#observeLatestMessage(state);
|
|
441
|
+
return {
|
|
442
|
+
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
443
|
+
waitOutcome: 'completion'
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const waitFor = options.waitFor ?? 'completion';
|
|
447
|
+
if (waitFor === 'message') {
|
|
448
|
+
const pending = this.#oldestUnreadMessage(live);
|
|
449
|
+
if (pending) {
|
|
450
|
+
this.#observeLatestMessage(pending);
|
|
451
|
+
return { ...this.#snapshot(pending), waitOutcome: 'message' };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const waitMs = this.#retainedExecution.getStore()?.native
|
|
455
|
+
? options.waitMs
|
|
456
|
+
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
457
|
+
if (waitFor === 'message') {
|
|
458
|
+
const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal, options.waitInterrupt);
|
|
459
|
+
options.signal?.throwIfAborted();
|
|
460
|
+
const includeLatestMessage = activity?.kind === 'message' ||
|
|
461
|
+
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
462
|
+
const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
|
|
463
|
+
if (activity)
|
|
464
|
+
this.#observeLatestMessage(activity.state);
|
|
465
|
+
return {
|
|
466
|
+
...this.#snapshot(activity?.state ?? live.at(-1), {
|
|
467
|
+
includeLatestMessage,
|
|
468
|
+
includeTerminalDetails
|
|
469
|
+
}),
|
|
470
|
+
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal, options.waitInterrupt);
|
|
474
|
+
options.signal?.throwIfAborted();
|
|
475
|
+
const state = completed ?? this.#oldestUnreadMessage(live) ?? live.at(-1);
|
|
476
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
477
|
+
const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
|
|
478
|
+
if (includeLatestMessage)
|
|
479
|
+
this.#observeLatestMessage(state);
|
|
480
|
+
return {
|
|
481
|
+
...this.#snapshot(state, {
|
|
482
|
+
includeLatestMessage,
|
|
483
|
+
includeTerminalDetails
|
|
484
|
+
}),
|
|
485
|
+
...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
|
|
486
|
+
};
|
|
487
|
+
});
|
|
401
488
|
}
|
|
402
489
|
async outputMany(agentIds, options = {}) {
|
|
403
490
|
options.signal?.throwIfAborted();
|
|
@@ -407,187 +494,198 @@ export class SubagentSessionController {
|
|
|
407
494
|
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
408
495
|
if (!ids || 'untilSettled' in options)
|
|
409
496
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Group waits require a bounded deadline.');
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
state
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
(target.
|
|
423
|
-
(target.state.turn ===
|
|
424
|
-
|
|
497
|
+
return this.#using(ids, async () => {
|
|
498
|
+
const states = await Promise.all(ids.map((id) => this.#find(id)));
|
|
499
|
+
options.signal?.throwIfAborted();
|
|
500
|
+
const waitFor = options.waitFor ?? 'completion';
|
|
501
|
+
const targets = states.map((state) => ({
|
|
502
|
+
state,
|
|
503
|
+
turn: state.turn,
|
|
504
|
+
view: copyWaitState(state),
|
|
505
|
+
observation: this.#nativeObservation(state)
|
|
506
|
+
}));
|
|
507
|
+
const refresh = () => {
|
|
508
|
+
for (const target of targets) {
|
|
509
|
+
if (!isTerminalState(target.view) &&
|
|
510
|
+
(target.state.turn === target.turn ||
|
|
511
|
+
(target.state.turn === undefined && isTerminalState(target.state))))
|
|
512
|
+
target.view = copyWaitState(target.state);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const unread = (target) => target.view.messageRevision >
|
|
516
|
+
(target.observation?.messageRevision ?? target.state.observedMessageRevision);
|
|
517
|
+
const ready = (target) => isTerminalState(target.view) || (waitFor === 'message' && unread(target));
|
|
518
|
+
const snapshot = options.waitMs === undefined || options.waitMs === 0;
|
|
519
|
+
let satisfied = false;
|
|
520
|
+
if (!snapshot) {
|
|
521
|
+
const waitMs = this.#retainedExecution.getStore()?.native
|
|
522
|
+
? options.waitMs
|
|
523
|
+
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
524
|
+
satisfied = await waitForAgentCondition({
|
|
525
|
+
waitMs,
|
|
526
|
+
signal: options.signal,
|
|
527
|
+
interrupt: options.waitInterrupt,
|
|
528
|
+
ready: () => {
|
|
529
|
+
refresh();
|
|
530
|
+
return mode === 'all' ? targets.every(ready) : targets.some(ready);
|
|
531
|
+
},
|
|
532
|
+
subscribe: (notify) => {
|
|
533
|
+
const listener = () => notify();
|
|
534
|
+
this.#activityWaiters.add(listener);
|
|
535
|
+
return () => this.#activityWaiters.delete(listener);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
options.signal?.throwIfAborted();
|
|
425
539
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
540
|
+
refresh();
|
|
541
|
+
if (!snapshot)
|
|
542
|
+
satisfied = mode === 'all' ? targets.every(ready) : targets.some(ready);
|
|
543
|
+
const pendingAgentIds = targets
|
|
544
|
+
.filter((target) => !ready(target))
|
|
545
|
+
.map((target) => target.state.agentId);
|
|
546
|
+
const agents = targets.map((target) => {
|
|
547
|
+
const { state, view, observation } = target;
|
|
548
|
+
const terminal = isTerminalState(view);
|
|
549
|
+
const outcome = snapshot
|
|
550
|
+
? 'snapshot'
|
|
551
|
+
: terminal
|
|
552
|
+
? 'completion'
|
|
553
|
+
: ready(target)
|
|
554
|
+
? 'message'
|
|
555
|
+
: satisfied
|
|
556
|
+
? 'snapshot'
|
|
557
|
+
: options.waitInterrupt?.pending()
|
|
558
|
+
? 'interrupted'
|
|
559
|
+
: 'timeout';
|
|
560
|
+
const includeLatestMessage = outcome !== 'snapshot' && unread(target);
|
|
561
|
+
const includeTerminalDetails = outcome === 'completion' &&
|
|
562
|
+
(observation
|
|
563
|
+
? observation.terminalRevision < view.resourceRevision
|
|
564
|
+
: state.observedTerminalRevision < view.resourceRevision);
|
|
565
|
+
const result = {
|
|
566
|
+
...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
|
|
567
|
+
...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
|
|
568
|
+
...waitMetadata(outcome)
|
|
569
|
+
};
|
|
570
|
+
if (includeLatestMessage) {
|
|
571
|
+
if (observation)
|
|
572
|
+
observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
|
|
573
|
+
else
|
|
574
|
+
state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
|
|
575
|
+
}
|
|
576
|
+
if (includeTerminalDetails) {
|
|
577
|
+
if (observation)
|
|
578
|
+
observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
|
|
579
|
+
else
|
|
580
|
+
state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
|
|
448
581
|
}
|
|
582
|
+
return result;
|
|
449
583
|
});
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
? 'completion'
|
|
465
|
-
: ready(target)
|
|
466
|
-
? 'message'
|
|
467
|
-
: satisfied
|
|
468
|
-
? 'snapshot'
|
|
469
|
-
: options.waitInterrupt?.pending()
|
|
470
|
-
? 'interrupted'
|
|
471
|
-
: 'timeout';
|
|
472
|
-
const includeLatestMessage = outcome !== 'snapshot' && unread(target);
|
|
473
|
-
const includeTerminalDetails = outcome === 'completion' &&
|
|
474
|
-
(observation
|
|
475
|
-
? observation.terminalRevision < view.resourceRevision
|
|
476
|
-
: state.observedTerminalRevision < view.resourceRevision);
|
|
477
|
-
const result = {
|
|
478
|
-
...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
|
|
479
|
-
...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
|
|
480
|
-
...waitMetadata(outcome)
|
|
584
|
+
return {
|
|
585
|
+
mode,
|
|
586
|
+
waitFor,
|
|
587
|
+
agents,
|
|
588
|
+
pendingAgentIds,
|
|
589
|
+
...waitMetadata(snapshot
|
|
590
|
+
? 'snapshot'
|
|
591
|
+
: !satisfied
|
|
592
|
+
? options.waitInterrupt?.pending()
|
|
593
|
+
? 'interrupted'
|
|
594
|
+
: 'timeout'
|
|
595
|
+
: agents.some((agent) => agent.waitOutcome === 'message')
|
|
596
|
+
? 'message'
|
|
597
|
+
: 'completion')
|
|
481
598
|
};
|
|
482
|
-
if (includeLatestMessage) {
|
|
483
|
-
if (observation)
|
|
484
|
-
observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
|
|
485
|
-
else
|
|
486
|
-
state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
|
|
487
|
-
}
|
|
488
|
-
if (includeTerminalDetails) {
|
|
489
|
-
if (observation)
|
|
490
|
-
observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
|
|
491
|
-
else
|
|
492
|
-
state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
|
|
493
|
-
}
|
|
494
|
-
return result;
|
|
495
599
|
});
|
|
496
|
-
return {
|
|
497
|
-
mode,
|
|
498
|
-
waitFor,
|
|
499
|
-
agents,
|
|
500
|
-
pendingAgentIds,
|
|
501
|
-
...waitMetadata(snapshot
|
|
502
|
-
? 'snapshot'
|
|
503
|
-
: !satisfied
|
|
504
|
-
? options.waitInterrupt?.pending()
|
|
505
|
-
? 'interrupted'
|
|
506
|
-
: 'timeout'
|
|
507
|
-
: agents.some((agent) => agent.waitOutcome === 'message')
|
|
508
|
-
? 'message'
|
|
509
|
-
: 'completion')
|
|
510
|
-
};
|
|
511
600
|
}
|
|
512
601
|
async message(agentId, message, options = {}) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
options.delivery !==
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
this.#
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
state.
|
|
546
|
-
|
|
602
|
+
return this.#using([agentId], async () => {
|
|
603
|
+
const execution = this.#requireExecution();
|
|
604
|
+
if (typeof message !== 'string' ||
|
|
605
|
+
message.trim().length === 0 ||
|
|
606
|
+
(this.#retainedExecution.getStore()?.native !== true &&
|
|
607
|
+
Buffer.byteLength(message) > TOOL_EXECUTION_LIMITS.subagentMessageMaxBytes) ||
|
|
608
|
+
(options.delivery !== undefined &&
|
|
609
|
+
options.delivery !== 'append' &&
|
|
610
|
+
options.delivery !== 'replace'))
|
|
611
|
+
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent message input is invalid.');
|
|
612
|
+
const state = await this.#find(agentId);
|
|
613
|
+
return this.#control(state, async () => {
|
|
614
|
+
this.#requireExecution();
|
|
615
|
+
options.signal?.throwIfAborted();
|
|
616
|
+
// Resolve before cancelling so an unavailable model does not stop valid work.
|
|
617
|
+
const selected = resolveExistingModel(execution, state);
|
|
618
|
+
if (options.delivery === 'replace')
|
|
619
|
+
await this.#cancelState(state);
|
|
620
|
+
this.#requireExecution();
|
|
621
|
+
options.signal?.throwIfAborted();
|
|
622
|
+
const turnId = state.turn?.turnId ?? randomUUID();
|
|
623
|
+
const receipt = { messageId: randomUUID(), turnId, deliveryStatus: 'accepted' };
|
|
624
|
+
const mutableReceipt = receipt;
|
|
625
|
+
const delivery = this.#createDelivery(state, execution, mutableReceipt, options.signal);
|
|
626
|
+
try {
|
|
627
|
+
if (state.status === 'running' && state.turn) {
|
|
628
|
+
const turn = state.turn;
|
|
629
|
+
if (!turn.handle)
|
|
630
|
+
await turn.handleReady;
|
|
631
|
+
this.#requireExecution();
|
|
632
|
+
options.signal?.throwIfAborted();
|
|
633
|
+
delivery.signal?.throwIfAborted();
|
|
634
|
+
if (state.status === 'running' &&
|
|
635
|
+
state.turn === turn &&
|
|
636
|
+
turn.handle?.enqueueMessage(message, delivery)) {
|
|
637
|
+
delivery.accepted();
|
|
638
|
+
state.receipt = mutableReceipt;
|
|
639
|
+
return this.#messageReceiptSnapshot(state);
|
|
640
|
+
}
|
|
641
|
+
await turn.completion;
|
|
642
|
+
}
|
|
643
|
+
else if (state.status === 'queued' && state.turn) {
|
|
547
644
|
delivery.accepted();
|
|
645
|
+
state.turn.pendingMessages.push({ text: message, delivery });
|
|
548
646
|
state.receipt = mutableReceipt;
|
|
549
647
|
return this.#messageReceiptSnapshot(state);
|
|
550
648
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
649
|
+
this.#requireExecution();
|
|
650
|
+
options.signal?.throwIfAborted();
|
|
651
|
+
delivery.signal?.throwIfAborted();
|
|
652
|
+
state.modelId = selected.id;
|
|
653
|
+
state.modelName = selected.name;
|
|
654
|
+
state.reasoningEffort = selected.reasoningEffort;
|
|
655
|
+
// A closing turn may have rejected append; reserve a fresh identity for its successor.
|
|
656
|
+
mutableReceipt.turnId = randomUUID();
|
|
554
657
|
delivery.accepted();
|
|
555
|
-
state.turn.pendingMessages.push({ text: message, delivery });
|
|
556
658
|
state.receipt = mutableReceipt;
|
|
659
|
+
this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
|
|
557
660
|
return this.#messageReceiptSnapshot(state);
|
|
558
661
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
mutableReceipt.turnId = randomUUID();
|
|
567
|
-
delivery.accepted();
|
|
568
|
-
state.receipt = mutableReceipt;
|
|
569
|
-
this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
|
|
570
|
-
return this.#messageReceiptSnapshot(state);
|
|
571
|
-
}
|
|
572
|
-
catch (error) {
|
|
573
|
-
delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
|
|
574
|
-
? 'cancelled'
|
|
575
|
-
: 'failed');
|
|
576
|
-
throw error;
|
|
577
|
-
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
|
|
664
|
+
? 'cancelled'
|
|
665
|
+
: 'failed');
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
578
669
|
});
|
|
579
670
|
}
|
|
580
671
|
async cancel(agentId) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
672
|
+
return this.#using([agentId], async () => {
|
|
673
|
+
const state = await this.#find(agentId);
|
|
674
|
+
await this.#control(state, () => this.#cancelState(state));
|
|
675
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
676
|
+
const includeTerminalDetails = this.#observeTerminal(state);
|
|
677
|
+
this.#observeLatestMessage(state);
|
|
678
|
+
return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
|
|
679
|
+
});
|
|
587
680
|
}
|
|
588
681
|
#control(state, operation) {
|
|
589
682
|
const result = (state.controlTail ?? Promise.resolve()).then(operation);
|
|
590
|
-
|
|
683
|
+
const tail = result.then(() => undefined, () => undefined);
|
|
684
|
+
state.controlTail = tail;
|
|
685
|
+
void tail.then(() => {
|
|
686
|
+
if (state.controlTail === tail)
|
|
687
|
+
delete state.controlTail;
|
|
688
|
+
});
|
|
591
689
|
return result;
|
|
592
690
|
}
|
|
593
691
|
async #cancelState(state) {
|
|
@@ -625,7 +723,7 @@ export class SubagentSessionController {
|
|
|
625
723
|
const terminal = states
|
|
626
724
|
.filter((state) => state.status !== 'queued' && state.status !== 'running')
|
|
627
725
|
.sort((left, right) => right.resourceRevision - left.resourceRevision)
|
|
628
|
-
.slice(0,
|
|
726
|
+
.slice(0, RECENT_TERMINAL_RESOURCE_LIMIT);
|
|
629
727
|
return [...active, ...terminal].map((state) => ({
|
|
630
728
|
agentId: state.agentId,
|
|
631
729
|
status: state.status,
|
|
@@ -673,6 +771,7 @@ export class SubagentSessionController {
|
|
|
673
771
|
? native.signal
|
|
674
772
|
: AbortSignal.any([native.signal, signal]));
|
|
675
773
|
state.status = 'queued';
|
|
774
|
+
state.restorable = false;
|
|
676
775
|
this.#touchResource(state);
|
|
677
776
|
delete state.summary;
|
|
678
777
|
delete state.permissionStop;
|
|
@@ -750,9 +849,18 @@ export class SubagentSessionController {
|
|
|
750
849
|
await this.#consumeTurn(state, turn);
|
|
751
850
|
}
|
|
752
851
|
async #consumeTurn(state, turn) {
|
|
852
|
+
const previousActivity = state.latestActivity;
|
|
853
|
+
const previousMessageRevision = state.messageRevision;
|
|
854
|
+
let terminal;
|
|
753
855
|
try {
|
|
754
856
|
const consume = (async () => {
|
|
755
857
|
for await (const event of turn.handle) {
|
|
858
|
+
if (event.type === 'execution.completed')
|
|
859
|
+
terminal = 'completed';
|
|
860
|
+
else if (event.type === 'execution.failed')
|
|
861
|
+
terminal = 'failed';
|
|
862
|
+
else if (event.type === 'execution.cancelled')
|
|
863
|
+
terminal = 'cancelled';
|
|
756
864
|
if (!observeAgentEvent(state, event))
|
|
757
865
|
continue;
|
|
758
866
|
state.messageRevision++;
|
|
@@ -778,6 +886,14 @@ export class SubagentSessionController {
|
|
|
778
886
|
state.errorCode = result.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED';
|
|
779
887
|
state.errorMessage = result.errorMessage ?? 'Subagent execution failed.';
|
|
780
888
|
}
|
|
889
|
+
state.restorable =
|
|
890
|
+
turn.handle.persistedEvents === true &&
|
|
891
|
+
terminal === state.status &&
|
|
892
|
+
(previousActivity === undefined || state.latestActivity !== previousActivity) &&
|
|
893
|
+
(state.latestMessage === undefined || state.messageRevision > previousMessageRevision) &&
|
|
894
|
+
(state.status !== 'completed' ||
|
|
895
|
+
(state.latestMessage?.phase === 'final_answer' &&
|
|
896
|
+
state.summary === state.latestMessage.text));
|
|
781
897
|
state.terminalPending = true;
|
|
782
898
|
this.#touchResource(state, true);
|
|
783
899
|
this.#closeReceipt(state);
|
|
@@ -812,6 +928,7 @@ export class SubagentSessionController {
|
|
|
812
928
|
#setFailure(state, error) {
|
|
813
929
|
const diagnostic = describeMarAgentError(error, 'MAR_AGENT_SUBAGENT_FAILED');
|
|
814
930
|
state.status = 'failed';
|
|
931
|
+
state.restorable = false;
|
|
815
932
|
state.errorCode = diagnostic.code;
|
|
816
933
|
state.errorMessage = diagnostic.message;
|
|
817
934
|
state.terminalPending = true;
|
|
@@ -946,6 +1063,7 @@ export class SubagentSessionController {
|
|
|
946
1063
|
}
|
|
947
1064
|
async #deleteUnstartedSession(agentId, error) {
|
|
948
1065
|
this.#agents.delete(agentId);
|
|
1066
|
+
this.#discovered.delete(agentId);
|
|
949
1067
|
try {
|
|
950
1068
|
await this.backend.deleteSession(agentId);
|
|
951
1069
|
}
|
|
@@ -956,16 +1074,105 @@ export class SubagentSessionController {
|
|
|
956
1074
|
}
|
|
957
1075
|
throw error;
|
|
958
1076
|
}
|
|
1077
|
+
async #using(ids, operation) {
|
|
1078
|
+
if (ids === undefined)
|
|
1079
|
+
this.#allReaders++;
|
|
1080
|
+
else
|
|
1081
|
+
for (const id of ids)
|
|
1082
|
+
this.#readers.set(id, (this.#readers.get(id) ?? 0) + 1);
|
|
1083
|
+
try {
|
|
1084
|
+
return await operation();
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
if (ids === undefined)
|
|
1088
|
+
this.#allReaders--;
|
|
1089
|
+
else
|
|
1090
|
+
for (const id of ids) {
|
|
1091
|
+
const count = this.#readers.get(id) - 1;
|
|
1092
|
+
if (count)
|
|
1093
|
+
this.#readers.set(id, count);
|
|
1094
|
+
else
|
|
1095
|
+
this.#readers.delete(id);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
#releaseOldDetails() {
|
|
1100
|
+
if (this.#allReaders)
|
|
1101
|
+
return;
|
|
1102
|
+
const terminal = [...this.#agents.values()]
|
|
1103
|
+
.filter(isTerminalState)
|
|
1104
|
+
.sort((a, b) => b.resourceRevision - a.resourceRevision);
|
|
1105
|
+
for (const state of terminal.slice(RECENT_TERMINAL_RESOURCE_LIMIT)) {
|
|
1106
|
+
if (state.detailsEvicted ||
|
|
1107
|
+
!state.restorable ||
|
|
1108
|
+
state.turn ||
|
|
1109
|
+
state.controlTail ||
|
|
1110
|
+
this.#readers.has(state.agentId) ||
|
|
1111
|
+
this.#loading.has(state.agentId) ||
|
|
1112
|
+
state.terminalPending ||
|
|
1113
|
+
state.messageRevision > state.observedMessageRevision ||
|
|
1114
|
+
(state.terminalRevision !== undefined &&
|
|
1115
|
+
state.terminalRevision > state.observedTerminalRevision) ||
|
|
1116
|
+
[...this.#pendingDeliveries].some((pending) => pending.state === state))
|
|
1117
|
+
continue;
|
|
1118
|
+
// Keep identity and cursors for old acknowledgements; only the disk-backed body unloads.
|
|
1119
|
+
for (const key of DETAIL_FIELDS)
|
|
1120
|
+
delete state[key];
|
|
1121
|
+
state.changedFiles = [];
|
|
1122
|
+
state.verification = [];
|
|
1123
|
+
state.evidence = [];
|
|
1124
|
+
state.detailsEvicted = true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
959
1127
|
async #find(agentId) {
|
|
1128
|
+
if (this.#disposed)
|
|
1129
|
+
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
960
1130
|
const known = this.#agents.get(agentId);
|
|
961
|
-
if (known)
|
|
1131
|
+
if (known && !known.detailsEvicted)
|
|
962
1132
|
return known;
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
1133
|
+
const pending = this.#loading.get(agentId);
|
|
1134
|
+
if (pending)
|
|
1135
|
+
return pending;
|
|
1136
|
+
const load = (async () => {
|
|
1137
|
+
const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
|
|
1138
|
+
if (this.#disposed)
|
|
1139
|
+
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
1140
|
+
if (!persisted) {
|
|
1141
|
+
this.#agents.delete(agentId);
|
|
1142
|
+
this.#discovered.delete(agentId);
|
|
1143
|
+
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
1144
|
+
}
|
|
1145
|
+
const existing = this.#agents.get(agentId);
|
|
1146
|
+
if (existing && !existing.detailsEvicted)
|
|
1147
|
+
return existing;
|
|
1148
|
+
const restored = restoreAgentState(persisted);
|
|
1149
|
+
if (existing) {
|
|
1150
|
+
for (const key of DETAIL_FIELDS) {
|
|
1151
|
+
if (key in restored)
|
|
1152
|
+
Object.assign(existing, { [key]: restored[key] });
|
|
1153
|
+
else
|
|
1154
|
+
delete existing[key];
|
|
1155
|
+
}
|
|
1156
|
+
delete existing.detailsEvicted;
|
|
1157
|
+
return existing;
|
|
1158
|
+
}
|
|
1159
|
+
let revision = this.#discovered.get(agentId);
|
|
1160
|
+
if (revision === undefined) {
|
|
1161
|
+
revision = ++this.#resourceRevision;
|
|
1162
|
+
this.#discovered.set(agentId, revision);
|
|
1163
|
+
}
|
|
1164
|
+
restored.resourceRevision = revision;
|
|
1165
|
+
this.#agents.set(agentId, restored);
|
|
1166
|
+
return restored;
|
|
1167
|
+
})();
|
|
1168
|
+
this.#loading.set(agentId, load);
|
|
1169
|
+
try {
|
|
1170
|
+
return await load;
|
|
1171
|
+
}
|
|
1172
|
+
finally {
|
|
1173
|
+
if (this.#loading.get(agentId) === load)
|
|
1174
|
+
this.#loading.delete(agentId);
|
|
1175
|
+
}
|
|
969
1176
|
}
|
|
970
1177
|
#requireExecution() {
|
|
971
1178
|
if (this.#disposed)
|
|
@@ -1139,6 +1346,7 @@ function restoreAgentState(session) {
|
|
|
1139
1346
|
const terminal = currentRecords.findLast((record) => ['execution.completed', 'execution.failed', 'execution.cancelled'].includes(eventType(record) ?? ''));
|
|
1140
1347
|
const terminalType = eventType(terminal);
|
|
1141
1348
|
const state = {
|
|
1349
|
+
restorable: true,
|
|
1142
1350
|
agentId: session.id,
|
|
1143
1351
|
description: session.source.description,
|
|
1144
1352
|
role: session.source.role,
|