@myagentroam/agent 0.10.61 → 0.10.63
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.
|
@@ -28,11 +28,7 @@ export function createExtensionAgentsBackend(input) {
|
|
|
28
28
|
throw new MarAgentError(created.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED', created.errorMessage ?? 'Agent could not start.');
|
|
29
29
|
}
|
|
30
30
|
})),
|
|
31
|
-
list: (call) => withController(call,
|
|
32
|
-
await controller.prepareAll(call.signal);
|
|
33
|
-
call.signal.throwIfAborted();
|
|
34
|
-
return controller.inspectAll();
|
|
35
|
-
}),
|
|
31
|
+
list: (call) => withController(call, (controller) => controller.list(call.signal)),
|
|
36
32
|
inspect: (target, call) => withController(call, (controller) => controller.inspect(target.agentId)),
|
|
37
33
|
wait: (target, call) => withController(call, (controller) => {
|
|
38
34
|
const selection = parseAgentWaitTargets(target);
|
|
@@ -282,6 +282,7 @@ export function createSessionOpener(services) {
|
|
|
282
282
|
let releaseSession;
|
|
283
283
|
let exhaustedModelAttempt;
|
|
284
284
|
// Serialize durable events before either the local stream or Host observes them.
|
|
285
|
+
let persistedTerminal = false;
|
|
285
286
|
const emit = (event, assertLive) => {
|
|
286
287
|
assertLive?.();
|
|
287
288
|
const full = {
|
|
@@ -303,6 +304,10 @@ export function createSessionOpener(services) {
|
|
|
303
304
|
executionId,
|
|
304
305
|
turnId
|
|
305
306
|
});
|
|
307
|
+
if (full.type === 'execution.completed' ||
|
|
308
|
+
full.type === 'execution.failed' ||
|
|
309
|
+
full.type === 'execution.cancelled')
|
|
310
|
+
persistedTerminal = true;
|
|
306
311
|
if (full.type === 'message.completed') {
|
|
307
312
|
const message = liveModelMessages.findLast((item) => item.role === 'assistant' && item.content === full.text);
|
|
308
313
|
if (message)
|
|
@@ -1953,6 +1958,9 @@ export function createSessionOpener(services) {
|
|
|
1953
1958
|
executionResults.delete(executionKey);
|
|
1954
1959
|
});
|
|
1955
1960
|
return {
|
|
1961
|
+
get persistedEvents() {
|
|
1962
|
+
return persistedTerminal;
|
|
1963
|
+
},
|
|
1956
1964
|
executionId,
|
|
1957
1965
|
turnId,
|
|
1958
1966
|
[Symbol.asyncIterator]: () => queue[Symbol.asyncIterator](),
|
|
@@ -16,6 +16,8 @@ export interface SubagentModelOption {
|
|
|
16
16
|
export interface SubagentTurnHandle extends AsyncIterable<MarAgentEvent> {
|
|
17
17
|
readonly executionId: string;
|
|
18
18
|
readonly turnId: string;
|
|
19
|
+
/** True only after the child terminal event was durably appended. */
|
|
20
|
+
readonly persistedEvents?: boolean;
|
|
19
21
|
result(): Promise<ExecutionResult>;
|
|
20
22
|
cancel(): void;
|
|
21
23
|
enqueueMessage(message: string, delivery?: SubagentMessageDelivery): boolean;
|
|
@@ -109,6 +111,7 @@ export declare class SubagentSessionController implements SubagentController {
|
|
|
109
111
|
withOperation<T>(operation: SubagentParentOperation, run: () => T): T;
|
|
110
112
|
inspect(agentId: string): Promise<SubagentTaskSnapshot>;
|
|
111
113
|
inspectAll(): readonly SubagentTaskSnapshot[];
|
|
114
|
+
list(signal?: AbortSignal): Promise<readonly SubagentTaskSnapshot[]>;
|
|
112
115
|
/** Full listing is explicit; ordinary opens and executions never require it. */
|
|
113
116
|
prepareAll(signal?: AbortSignal): Promise<void>;
|
|
114
117
|
prepareSessionResources(signal?: AbortSignal): Promise<void>;
|
|
@@ -5,6 +5,16 @@ 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
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
|
+
];
|
|
8
18
|
export class SubagentSessionController {
|
|
9
19
|
parentSessionId;
|
|
10
20
|
rootSessionId;
|
|
@@ -14,6 +24,8 @@ export class SubagentSessionController {
|
|
|
14
24
|
// Discovery order is independent of the order in which history is requested.
|
|
15
25
|
#discovered = new Map();
|
|
16
26
|
#loading = new Map();
|
|
27
|
+
#readers = new Map();
|
|
28
|
+
#allReaders = 0;
|
|
17
29
|
#queue = [];
|
|
18
30
|
#completionWaiters = new Set();
|
|
19
31
|
#activityWaiters = new Set();
|
|
@@ -63,6 +75,7 @@ export class SubagentSessionController {
|
|
|
63
75
|
this.#creationBudget = { count: 0 };
|
|
64
76
|
for (const state of this.#agents.values())
|
|
65
77
|
await this.#publishTerminal(state);
|
|
78
|
+
this.#releaseOldDetails();
|
|
66
79
|
}
|
|
67
80
|
/** A retained cell must not borrow a later model Execution's identity or task budget. */
|
|
68
81
|
captureExecution(executionId) {
|
|
@@ -98,13 +111,21 @@ export class SubagentSessionController {
|
|
|
98
111
|
return this.#retainedExecution.run({ execution: operation, budget: { count: 0 }, native: true }, run);
|
|
99
112
|
}
|
|
100
113
|
async inspect(agentId) {
|
|
101
|
-
return this.#
|
|
114
|
+
return this.#using([agentId], async () => {
|
|
115
|
+
return this.#snapshot(await this.#find(agentId), { full: true });
|
|
116
|
+
});
|
|
102
117
|
}
|
|
103
118
|
inspectAll() {
|
|
104
119
|
if (this.#disposed)
|
|
105
120
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
106
121
|
return this.#orderedStates().map((state) => this.#snapshot(state, { full: true }));
|
|
107
122
|
}
|
|
123
|
+
async list(signal) {
|
|
124
|
+
return this.#using(undefined, async () => {
|
|
125
|
+
await this.prepareAll(signal);
|
|
126
|
+
return this.inspectAll();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
108
129
|
/** Full listing is explicit; ordinary opens and executions never require it. */
|
|
109
130
|
async prepareAll(signal) {
|
|
110
131
|
for (const id of this.#discovered.keys()) {
|
|
@@ -129,6 +150,7 @@ export class SubagentSessionController {
|
|
|
129
150
|
break;
|
|
130
151
|
}
|
|
131
152
|
signal?.throwIfAborted();
|
|
153
|
+
this.#releaseOldDetails();
|
|
132
154
|
}
|
|
133
155
|
async #restoreDiscovered(agentId) {
|
|
134
156
|
try {
|
|
@@ -250,183 +272,125 @@ export class SubagentSessionController {
|
|
|
250
272
|
catch (error) {
|
|
251
273
|
await this.#deleteUnstartedSession(created.id, error);
|
|
252
274
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.#agents.set(state.agentId, state);
|
|
273
|
-
this.#discovered.set(state.agentId, state.resourceRevision);
|
|
274
|
-
const knownModel = execution.models.get(input.modelId ?? execution.modelId);
|
|
275
|
-
if (knownModel) {
|
|
276
|
-
state.modelId = knownModel.id;
|
|
277
|
-
state.modelName = knownModel.name;
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
const selected = resolveModel(execution, input);
|
|
281
|
-
state.modelId = selected.id;
|
|
282
|
-
state.modelName = selected.name;
|
|
283
|
-
state.reasoningEffort = selected.reasoningEffort;
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
this.#setFailure(state, error);
|
|
287
|
-
}
|
|
288
|
-
try {
|
|
289
|
-
await options.onCreated?.(this.#receiptSnapshot(state));
|
|
290
|
-
if (this.#disposed)
|
|
291
|
-
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
292
|
-
this.#requireExecution();
|
|
293
|
-
options.signal?.throwIfAborted();
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
await this.#deleteUnstartedSession(state.agentId, error);
|
|
297
|
-
}
|
|
298
|
-
if (state.status === 'failed') {
|
|
299
|
-
await this.#publishTerminal(state);
|
|
300
|
-
this.#observeTerminal(state);
|
|
301
|
-
return this.#snapshot(state);
|
|
302
|
-
}
|
|
303
|
-
this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
|
|
304
|
-
if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
|
|
305
|
-
return this.#receiptSnapshot(state);
|
|
306
|
-
await waitForPromise(state.turn.completion, options.signal);
|
|
307
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
308
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
309
|
-
this.#observeLatestMessage(state);
|
|
310
|
-
return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
|
|
311
|
-
}
|
|
312
|
-
async output(agentId, options = {}) {
|
|
313
|
-
options.signal?.throwIfAborted();
|
|
314
|
-
const state = await this.#find(agentId);
|
|
315
|
-
options.signal?.throwIfAborted();
|
|
316
|
-
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
317
|
-
if (options.untilSettled) {
|
|
318
|
-
if (state.turn)
|
|
319
|
-
await waitForPromise(state.turn.completion, options.signal);
|
|
320
|
-
options.signal?.throwIfAborted();
|
|
321
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
322
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
323
|
-
this.#observeLatestMessage(state);
|
|
324
|
-
return {
|
|
325
|
-
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
326
|
-
waitOutcome: 'completion'
|
|
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
|
|
327
294
|
};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
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);
|
|
338
332
|
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
339
333
|
const includeTerminalDetails = this.#observeTerminal(state);
|
|
340
334
|
this.#observeLatestMessage(state);
|
|
341
|
-
return {
|
|
342
|
-
|
|
343
|
-
waitOutcome: 'completion'
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
const waitFor = options.waitFor ?? 'completion';
|
|
347
|
-
if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
|
|
348
|
-
this.#observeLatestMessage(state);
|
|
349
|
-
return { ...this.#snapshot(state), waitOutcome: 'message' };
|
|
350
|
-
}
|
|
351
|
-
const waitMs = this.#retainedExecution.getStore()?.native
|
|
352
|
-
? options.waitMs
|
|
353
|
-
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
354
|
-
if (waitFor === 'completion') {
|
|
355
|
-
const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
|
|
356
|
-
options.signal?.throwIfAborted();
|
|
357
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
358
|
-
const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
|
|
359
|
-
if (includeLatestMessage)
|
|
360
|
-
this.#observeLatestMessage(state);
|
|
361
|
-
return {
|
|
362
|
-
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
363
|
-
...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
|
|
367
|
-
options.signal?.throwIfAborted();
|
|
368
|
-
const includeLatestMessage = activity?.kind === 'message' ||
|
|
369
|
-
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
370
|
-
const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
|
|
371
|
-
if (activity)
|
|
372
|
-
this.#observeLatestMessage(activity.state);
|
|
373
|
-
return {
|
|
374
|
-
...this.#snapshot(activity?.state ?? state, {
|
|
375
|
-
includeLatestMessage,
|
|
376
|
-
includeTerminalDetails
|
|
377
|
-
}),
|
|
378
|
-
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
379
|
-
};
|
|
335
|
+
return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
|
|
336
|
+
});
|
|
380
337
|
}
|
|
381
|
-
async
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
338
|
+
async output(agentId, options = {}) {
|
|
339
|
+
return this.#using([agentId], async () => {
|
|
340
|
+
options.signal?.throwIfAborted();
|
|
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);
|
|
389
347
|
options.signal?.throwIfAborted();
|
|
390
|
-
|
|
391
|
-
|
|
348
|
+
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
349
|
+
const includeTerminalDetails = this.#observeTerminal(state);
|
|
350
|
+
this.#observeLatestMessage(state);
|
|
351
|
+
return {
|
|
352
|
+
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
353
|
+
waitOutcome: 'completion'
|
|
354
|
+
};
|
|
392
355
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
410
|
-
const includeTerminalDetails = this.#observeTerminal(state);
|
|
411
|
-
this.#observeLatestMessage(state);
|
|
412
|
-
return {
|
|
413
|
-
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
414
|
-
waitOutcome: 'completion'
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
const waitFor = options.waitFor ?? 'completion';
|
|
418
|
-
if (waitFor === 'message') {
|
|
419
|
-
const pending = this.#oldestUnreadMessage(live);
|
|
420
|
-
if (pending) {
|
|
421
|
-
this.#observeLatestMessage(pending);
|
|
422
|
-
return { ...this.#snapshot(pending), waitOutcome: 'message' };
|
|
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
|
+
};
|
|
423
372
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
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);
|
|
430
394
|
options.signal?.throwIfAborted();
|
|
431
395
|
const includeLatestMessage = activity?.kind === 'message' ||
|
|
432
396
|
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
@@ -434,27 +398,93 @@ export class SubagentSessionController {
|
|
|
434
398
|
if (activity)
|
|
435
399
|
this.#observeLatestMessage(activity.state);
|
|
436
400
|
return {
|
|
437
|
-
...this.#snapshot(activity?.state ??
|
|
401
|
+
...this.#snapshot(activity?.state ?? state, {
|
|
438
402
|
includeLatestMessage,
|
|
439
403
|
includeTerminalDetails
|
|
440
404
|
}),
|
|
441
405
|
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
442
406
|
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.#
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
});
|
|
458
488
|
}
|
|
459
489
|
async outputMany(agentIds, options = {}) {
|
|
460
490
|
options.signal?.throwIfAborted();
|
|
@@ -464,187 +494,198 @@ export class SubagentSessionController {
|
|
|
464
494
|
validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
|
|
465
495
|
if (!ids || 'untilSettled' in options)
|
|
466
496
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Group waits require a bounded deadline.');
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
state
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
(target.
|
|
480
|
-
(target.state.turn ===
|
|
481
|
-
|
|
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();
|
|
482
539
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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);
|
|
505
581
|
}
|
|
582
|
+
return result;
|
|
506
583
|
});
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
? 'completion'
|
|
522
|
-
: ready(target)
|
|
523
|
-
? 'message'
|
|
524
|
-
: satisfied
|
|
525
|
-
? 'snapshot'
|
|
526
|
-
: options.waitInterrupt?.pending()
|
|
527
|
-
? 'interrupted'
|
|
528
|
-
: 'timeout';
|
|
529
|
-
const includeLatestMessage = outcome !== 'snapshot' && unread(target);
|
|
530
|
-
const includeTerminalDetails = outcome === 'completion' &&
|
|
531
|
-
(observation
|
|
532
|
-
? observation.terminalRevision < view.resourceRevision
|
|
533
|
-
: state.observedTerminalRevision < view.resourceRevision);
|
|
534
|
-
const result = {
|
|
535
|
-
...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
|
|
536
|
-
...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
|
|
537
|
-
...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')
|
|
538
598
|
};
|
|
539
|
-
if (includeLatestMessage) {
|
|
540
|
-
if (observation)
|
|
541
|
-
observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
|
|
542
|
-
else
|
|
543
|
-
state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
|
|
544
|
-
}
|
|
545
|
-
if (includeTerminalDetails) {
|
|
546
|
-
if (observation)
|
|
547
|
-
observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
|
|
548
|
-
else
|
|
549
|
-
state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
|
|
550
|
-
}
|
|
551
|
-
return result;
|
|
552
599
|
});
|
|
553
|
-
return {
|
|
554
|
-
mode,
|
|
555
|
-
waitFor,
|
|
556
|
-
agents,
|
|
557
|
-
pendingAgentIds,
|
|
558
|
-
...waitMetadata(snapshot
|
|
559
|
-
? 'snapshot'
|
|
560
|
-
: !satisfied
|
|
561
|
-
? options.waitInterrupt?.pending()
|
|
562
|
-
? 'interrupted'
|
|
563
|
-
: 'timeout'
|
|
564
|
-
: agents.some((agent) => agent.waitOutcome === 'message')
|
|
565
|
-
? 'message'
|
|
566
|
-
: 'completion')
|
|
567
|
-
};
|
|
568
600
|
}
|
|
569
601
|
async message(agentId, message, options = {}) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
options.delivery !==
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.#
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
state.
|
|
603
|
-
|
|
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) {
|
|
604
644
|
delivery.accepted();
|
|
645
|
+
state.turn.pendingMessages.push({ text: message, delivery });
|
|
605
646
|
state.receipt = mutableReceipt;
|
|
606
647
|
return this.#messageReceiptSnapshot(state);
|
|
607
648
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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();
|
|
611
657
|
delivery.accepted();
|
|
612
|
-
state.turn.pendingMessages.push({ text: message, delivery });
|
|
613
658
|
state.receipt = mutableReceipt;
|
|
659
|
+
this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
|
|
614
660
|
return this.#messageReceiptSnapshot(state);
|
|
615
661
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
mutableReceipt.turnId = randomUUID();
|
|
624
|
-
delivery.accepted();
|
|
625
|
-
state.receipt = mutableReceipt;
|
|
626
|
-
this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
|
|
627
|
-
return this.#messageReceiptSnapshot(state);
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
|
|
631
|
-
? 'cancelled'
|
|
632
|
-
: 'failed');
|
|
633
|
-
throw error;
|
|
634
|
-
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
|
|
664
|
+
? 'cancelled'
|
|
665
|
+
: 'failed');
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
635
669
|
});
|
|
636
670
|
}
|
|
637
671
|
async cancel(agentId) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
});
|
|
644
680
|
}
|
|
645
681
|
#control(state, operation) {
|
|
646
682
|
const result = (state.controlTail ?? Promise.resolve()).then(operation);
|
|
647
|
-
|
|
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
|
+
});
|
|
648
689
|
return result;
|
|
649
690
|
}
|
|
650
691
|
async #cancelState(state) {
|
|
@@ -730,6 +771,7 @@ export class SubagentSessionController {
|
|
|
730
771
|
? native.signal
|
|
731
772
|
: AbortSignal.any([native.signal, signal]));
|
|
732
773
|
state.status = 'queued';
|
|
774
|
+
state.restorable = false;
|
|
733
775
|
this.#touchResource(state);
|
|
734
776
|
delete state.summary;
|
|
735
777
|
delete state.permissionStop;
|
|
@@ -807,9 +849,18 @@ export class SubagentSessionController {
|
|
|
807
849
|
await this.#consumeTurn(state, turn);
|
|
808
850
|
}
|
|
809
851
|
async #consumeTurn(state, turn) {
|
|
852
|
+
const previousActivity = state.latestActivity;
|
|
853
|
+
const previousMessageRevision = state.messageRevision;
|
|
854
|
+
let terminal;
|
|
810
855
|
try {
|
|
811
856
|
const consume = (async () => {
|
|
812
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';
|
|
813
864
|
if (!observeAgentEvent(state, event))
|
|
814
865
|
continue;
|
|
815
866
|
state.messageRevision++;
|
|
@@ -835,6 +886,14 @@ export class SubagentSessionController {
|
|
|
835
886
|
state.errorCode = result.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED';
|
|
836
887
|
state.errorMessage = result.errorMessage ?? 'Subagent execution failed.';
|
|
837
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));
|
|
838
897
|
state.terminalPending = true;
|
|
839
898
|
this.#touchResource(state, true);
|
|
840
899
|
this.#closeReceipt(state);
|
|
@@ -869,6 +928,7 @@ export class SubagentSessionController {
|
|
|
869
928
|
#setFailure(state, error) {
|
|
870
929
|
const diagnostic = describeMarAgentError(error, 'MAR_AGENT_SUBAGENT_FAILED');
|
|
871
930
|
state.status = 'failed';
|
|
931
|
+
state.restorable = false;
|
|
872
932
|
state.errorCode = diagnostic.code;
|
|
873
933
|
state.errorMessage = diagnostic.message;
|
|
874
934
|
state.terminalPending = true;
|
|
@@ -1014,11 +1074,61 @@ export class SubagentSessionController {
|
|
|
1014
1074
|
}
|
|
1015
1075
|
throw error;
|
|
1016
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
|
+
}
|
|
1017
1127
|
async #find(agentId) {
|
|
1018
1128
|
if (this.#disposed)
|
|
1019
1129
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
1020
1130
|
const known = this.#agents.get(agentId);
|
|
1021
|
-
if (known)
|
|
1131
|
+
if (known && !known.detailsEvicted)
|
|
1022
1132
|
return known;
|
|
1023
1133
|
const pending = this.#loading.get(agentId);
|
|
1024
1134
|
if (pending)
|
|
@@ -1027,12 +1137,25 @@ export class SubagentSessionController {
|
|
|
1027
1137
|
const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
|
|
1028
1138
|
if (this.#disposed)
|
|
1029
1139
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
|
|
1030
|
-
if (!persisted)
|
|
1140
|
+
if (!persisted) {
|
|
1141
|
+
this.#agents.delete(agentId);
|
|
1142
|
+
this.#discovered.delete(agentId);
|
|
1031
1143
|
throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
|
|
1144
|
+
}
|
|
1032
1145
|
const existing = this.#agents.get(agentId);
|
|
1033
|
-
if (existing)
|
|
1146
|
+
if (existing && !existing.detailsEvicted)
|
|
1034
1147
|
return existing;
|
|
1035
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
|
+
}
|
|
1036
1159
|
let revision = this.#discovered.get(agentId);
|
|
1037
1160
|
if (revision === undefined) {
|
|
1038
1161
|
revision = ++this.#resourceRevision;
|
|
@@ -1223,6 +1346,7 @@ function restoreAgentState(session) {
|
|
|
1223
1346
|
const terminal = currentRecords.findLast((record) => ['execution.completed', 'execution.failed', 'execution.cancelled'].includes(eventType(record) ?? ''));
|
|
1224
1347
|
const terminalType = eventType(terminal);
|
|
1225
1348
|
const state = {
|
|
1349
|
+
restorable: true,
|
|
1226
1350
|
agentId: session.id,
|
|
1227
1351
|
description: session.source.description,
|
|
1228
1352
|
role: session.source.role,
|