@myagentroam/agent 0.10.52 → 0.10.53
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/extensions/agents.js +16 -6
- package/dist/agent-core/sdk/native-steer.d.ts +1 -0
- package/dist/agent-core/sdk/native-steer.js +1 -0
- package/dist/agent-core/sdk/session.js +5 -0
- package/dist/agent-core/subagent/contracts.d.ts +4 -1
- package/dist/agent-core/subagent/scheduler.d.ts +4 -0
- package/dist/agent-core/subagent/session-controller.d.ts +1 -1
- package/dist/agent-core/subagent/session-controller.js +51 -90
- package/dist/agent-core/subagent/wait.d.ts +3 -8
- package/dist/agent-core/subagent/wait.js +4 -49
- package/dist/agent-core/tools/agent/agent-wait.js +3 -2
- package/dist/agent-core/tools/code-mode/cells.js +31 -25
- package/dist/agent-core/tools/code-mode/index.js +2 -1
- package/dist/agent-core/tools/contracts.d.ts +5 -0
- package/dist/agent-core/tools/local/exec.d.ts +2 -1
- package/dist/agent-core/tools/local/exec.js +74 -61
- package/dist/agent-core/tools/local/registry.js +1 -1
- package/dist/agent-core/tools/output-projection.js +9 -2
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/agent-core/tools/runtime.js +1 -0
- package/dist/agent-core/tools/wait.d.ts +25 -0
- package/dist/agent-core/tools/wait.js +82 -0
- package/package.json +1 -1
|
@@ -51,17 +51,18 @@ function resolveAgentSpawn(input) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
export function bindAgentController(controller, action) {
|
|
54
|
-
const wait = (input) => action('agent.wait', input, (target, call) => {
|
|
54
|
+
const wait = (input, waitInterrupt) => action('agent.wait', input, (target, call) => {
|
|
55
55
|
const selection = parseAgentWaitTargets(target);
|
|
56
56
|
return selection.agentIds !== undefined
|
|
57
57
|
? controller.outputMany(selection.agentIds, {
|
|
58
58
|
...target,
|
|
59
|
+
waitInterrupt,
|
|
59
60
|
mode: selection.mode ?? 'any',
|
|
60
61
|
signal: call.signal
|
|
61
62
|
})
|
|
62
63
|
: target.agentId === undefined
|
|
63
|
-
? controller.outputAny({ ...target, signal: call.signal })
|
|
64
|
-
: controller.output(target.agentId, { ...target, signal: call.signal });
|
|
64
|
+
? controller.outputAny({ ...target, waitInterrupt, signal: call.signal })
|
|
65
|
+
: controller.output(target.agentId, { ...target, waitInterrupt, signal: call.signal });
|
|
65
66
|
});
|
|
66
67
|
return {
|
|
67
68
|
run: (input, options) => {
|
|
@@ -95,9 +96,18 @@ export function bindAgentController(controller, action) {
|
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
98
|
},
|
|
98
|
-
output: (id, options) =>
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
output: (id, options) => {
|
|
100
|
+
const { waitInterrupt, ...input } = options ?? {};
|
|
101
|
+
return wait({ ...input, agentId: id }, waitInterrupt);
|
|
102
|
+
},
|
|
103
|
+
outputAny: (options) => {
|
|
104
|
+
const { waitInterrupt, ...input } = options ?? {};
|
|
105
|
+
return wait(input, waitInterrupt);
|
|
106
|
+
},
|
|
107
|
+
outputMany: (ids, options) => {
|
|
108
|
+
const { waitInterrupt, ...input } = options ?? {};
|
|
109
|
+
return wait({ ...input, agentIds: ids }, waitInterrupt);
|
|
110
|
+
},
|
|
101
111
|
message: (id, message, options) => action('agent.message', { ...options, agentId: id, message }, (target, call) => controller.message(target.agentId, target.message, {
|
|
102
112
|
...(target.delivery === undefined ? {} : { delivery: target.delivery }),
|
|
103
113
|
signal: call.signal
|
|
@@ -10,6 +10,7 @@ import { ExecutionLoop } from '../runtime/execution.js';
|
|
|
10
10
|
import { closeModelAdapters, createModelTurnSession } from './model-runtime.js';
|
|
11
11
|
import { createSubagentBackend } from './subagent-backend.js';
|
|
12
12
|
import { ExecutionToolPermissions, projectPermissionTool, resolvePermissionMode, withToolPermissions } from '../tools/permissions.js';
|
|
13
|
+
import { createToolWaitInterrupt } from '../tools/wait.js';
|
|
13
14
|
import { discoveredMcpToolNames, rebindMcpToolSearchOutputs } from '../tools/mcp/history.js';
|
|
14
15
|
import { currentExecutionImages } from '../tools/output-projection.js';
|
|
15
16
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -414,6 +415,7 @@ export function createSessionOpener(services) {
|
|
|
414
415
|
});
|
|
415
416
|
const messages = restored.messages;
|
|
416
417
|
const pendingInputs = restored.pendingInputs;
|
|
418
|
+
const waitInterrupt = createToolWaitInterrupt(() => pendingInputs.size > 0);
|
|
417
419
|
liveModelMessages = messages;
|
|
418
420
|
const restoredRecordIds = new Set(contextRecords.map((record) => record.recordId));
|
|
419
421
|
for (const pending of pendingAppends)
|
|
@@ -1009,6 +1011,7 @@ export function createSessionOpener(services) {
|
|
|
1009
1011
|
executionId,
|
|
1010
1012
|
modelId: selectedModel.id,
|
|
1011
1013
|
signal: controller.signal,
|
|
1014
|
+
waitInterrupt,
|
|
1012
1015
|
emit,
|
|
1013
1016
|
skills: instructionContext?.skills ?? new Map(),
|
|
1014
1017
|
invokeTool: (call, context, caller) => {
|
|
@@ -1051,6 +1054,7 @@ export function createSessionOpener(services) {
|
|
|
1051
1054
|
recentImages,
|
|
1052
1055
|
captureCodeContext: () => ({
|
|
1053
1056
|
...toolContext,
|
|
1057
|
+
waitInterrupt: undefined,
|
|
1054
1058
|
recentImages: recentImages()
|
|
1055
1059
|
}),
|
|
1056
1060
|
codeModeContext: async (call) => {
|
|
@@ -1223,6 +1227,7 @@ export function createSessionOpener(services) {
|
|
|
1223
1227
|
return record;
|
|
1224
1228
|
}),
|
|
1225
1229
|
notify: (entry) => emit({ type: 'input.received', entry }),
|
|
1230
|
+
wake: () => waitInterrupt.notify(),
|
|
1226
1231
|
diagnose: () => coreExtensions.diagnose({ code: 'MAR_AGENT_STEER_POST_COMMIT_FAILED', sessionId })
|
|
1227
1232
|
});
|
|
1228
1233
|
}
|
|
@@ -62,9 +62,10 @@ export interface SubagentTaskSnapshot extends Record<string, unknown> {
|
|
|
62
62
|
turnId?: string;
|
|
63
63
|
deliveryStatus?: 'accepted' | 'applied' | 'cancelled' | 'failed';
|
|
64
64
|
waitOutcome?: SubagentWaitOutcome;
|
|
65
|
+
reason?: 'steer';
|
|
65
66
|
}
|
|
66
67
|
export type SubagentWaitFor = 'completion' | 'message';
|
|
67
|
-
export type SubagentWaitOutcome = '
|
|
68
|
+
export type SubagentWaitOutcome = Exclude<import('../tools/wait.js').ToolWaitOutcome, 'yield'>;
|
|
68
69
|
export interface SubagentMessageOptions {
|
|
69
70
|
delivery?: 'append' | 'replace';
|
|
70
71
|
signal?: AbortSignal;
|
|
@@ -76,6 +77,7 @@ export interface SubagentRunOptions {
|
|
|
76
77
|
onCreated?: (task: SubagentTaskSnapshot) => Promise<void> | void;
|
|
77
78
|
}
|
|
78
79
|
export interface SubagentOutputOptions {
|
|
80
|
+
waitInterrupt?: import('../tools/wait.js').ToolWaitInterrupt | undefined;
|
|
79
81
|
untilSettled?: true;
|
|
80
82
|
waitMs?: number;
|
|
81
83
|
waitFor?: SubagentWaitFor;
|
|
@@ -89,6 +91,7 @@ export interface SubagentManyOutput {
|
|
|
89
91
|
mode: SubagentWaitMode;
|
|
90
92
|
waitFor: SubagentWaitFor;
|
|
91
93
|
waitOutcome: SubagentWaitOutcome;
|
|
94
|
+
reason?: 'steer';
|
|
92
95
|
agents: SubagentTaskSnapshot[];
|
|
93
96
|
pendingAgentIds: string[];
|
|
94
97
|
}
|
|
@@ -39,6 +39,7 @@ export declare class SubagentScheduler implements SubagentController {
|
|
|
39
39
|
messageId?: string;
|
|
40
40
|
turnId?: string;
|
|
41
41
|
deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
|
|
42
|
+
reason?: "steer";
|
|
42
43
|
} | {
|
|
43
44
|
waitOutcome: "completion" | "timeout";
|
|
44
45
|
permissionStop?: import("../tools/permissions.js").PermissionStop;
|
|
@@ -65,6 +66,7 @@ export declare class SubagentScheduler implements SubagentController {
|
|
|
65
66
|
messageId?: string;
|
|
66
67
|
turnId?: string;
|
|
67
68
|
deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
|
|
69
|
+
reason?: "steer";
|
|
68
70
|
}>;
|
|
69
71
|
outputAny(options?: SubagentOutputOptions): Promise<{
|
|
70
72
|
waitOutcome: "snapshot";
|
|
@@ -92,6 +94,7 @@ export declare class SubagentScheduler implements SubagentController {
|
|
|
92
94
|
messageId?: string;
|
|
93
95
|
turnId?: string;
|
|
94
96
|
deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
|
|
97
|
+
reason?: "steer";
|
|
95
98
|
} | {
|
|
96
99
|
waitOutcome: "completion" | "timeout";
|
|
97
100
|
permissionStop?: import("../tools/permissions.js").PermissionStop;
|
|
@@ -118,6 +121,7 @@ export declare class SubagentScheduler implements SubagentController {
|
|
|
118
121
|
messageId?: string;
|
|
119
122
|
turnId?: string;
|
|
120
123
|
deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
|
|
124
|
+
reason?: "steer";
|
|
121
125
|
}>;
|
|
122
126
|
outputMany(agentIds: readonly string[], options?: SubagentManyOutputOptions): Promise<SubagentManyOutput>;
|
|
123
127
|
cancel(agentId: string): Promise<SubagentTaskSnapshot>;
|
|
@@ -123,7 +123,7 @@ export declare class SubagentSessionController implements SubagentController {
|
|
|
123
123
|
cancel(agentId: string): Promise<SubagentTaskSnapshot>;
|
|
124
124
|
listSessionResources(): {
|
|
125
125
|
agentId: string;
|
|
126
|
-
status: "completed" | "failed" | "cancelled" | "
|
|
126
|
+
status: "completed" | "failed" | "cancelled" | "interrupted" | "running" | "queued";
|
|
127
127
|
description: string;
|
|
128
128
|
}[];
|
|
129
129
|
dispose(): Promise<void>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { waitForToolCondition, waitMetadata } from '../tools/wait.js';
|
|
1
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { MarAgentError, describeMarAgentError } from '../error.js';
|
|
4
5
|
import { TOOL_EXECUTION_LIMITS } from '../tools/execution-limits.js';
|
|
5
|
-
import { parseAgentWaitTargets, waitForAgentCondition
|
|
6
|
+
import { parseAgentWaitTargets, waitForAgentCondition } from './wait.js';
|
|
6
7
|
export class SubagentSessionController {
|
|
7
8
|
parentSessionId;
|
|
8
9
|
rootSessionId;
|
|
@@ -290,7 +291,7 @@ export class SubagentSessionController {
|
|
|
290
291
|
? options.waitMs
|
|
291
292
|
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
292
293
|
if (waitFor === 'completion') {
|
|
293
|
-
const completed = await this.#waitForCompletion(state, waitMs, options.signal);
|
|
294
|
+
const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
|
|
294
295
|
options.signal?.throwIfAborted();
|
|
295
296
|
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
296
297
|
const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
|
|
@@ -298,10 +299,10 @@ export class SubagentSessionController {
|
|
|
298
299
|
this.#observeLatestMessage(state);
|
|
299
300
|
return {
|
|
300
301
|
...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
|
|
301
|
-
|
|
302
|
+
...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
|
-
const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal);
|
|
305
|
+
const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
|
|
305
306
|
options.signal?.throwIfAborted();
|
|
306
307
|
const includeLatestMessage = activity?.kind === 'message' ||
|
|
307
308
|
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
@@ -313,11 +314,7 @@ export class SubagentSessionController {
|
|
|
313
314
|
includeLatestMessage,
|
|
314
315
|
includeTerminalDetails
|
|
315
316
|
}),
|
|
316
|
-
|
|
317
|
-
? 'message'
|
|
318
|
-
: activity?.kind === 'completion'
|
|
319
|
-
? 'completion'
|
|
320
|
-
: 'timeout'
|
|
317
|
+
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
321
318
|
};
|
|
322
319
|
}
|
|
323
320
|
async outputAny(options = {}) {
|
|
@@ -359,7 +356,7 @@ export class SubagentSessionController {
|
|
|
359
356
|
? options.waitMs
|
|
360
357
|
: Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
|
|
361
358
|
if (waitFor === 'message') {
|
|
362
|
-
const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal);
|
|
359
|
+
const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal, options.waitInterrupt);
|
|
363
360
|
options.signal?.throwIfAborted();
|
|
364
361
|
const includeLatestMessage = activity?.kind === 'message' ||
|
|
365
362
|
(activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
|
|
@@ -371,14 +368,10 @@ export class SubagentSessionController {
|
|
|
371
368
|
includeLatestMessage,
|
|
372
369
|
includeTerminalDetails
|
|
373
370
|
}),
|
|
374
|
-
|
|
375
|
-
? 'message'
|
|
376
|
-
: activity?.kind === 'completion'
|
|
377
|
-
? 'completion'
|
|
378
|
-
: 'timeout'
|
|
371
|
+
...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
|
|
379
372
|
};
|
|
380
373
|
}
|
|
381
|
-
const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal);
|
|
374
|
+
const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal, options.waitInterrupt);
|
|
382
375
|
options.signal?.throwIfAborted();
|
|
383
376
|
const state = completed ?? this.#oldestUnreadMessage(live) ?? live.at(-1);
|
|
384
377
|
const includeLatestMessage = this.#hasUnreadMessage(state);
|
|
@@ -390,7 +383,7 @@ export class SubagentSessionController {
|
|
|
390
383
|
includeLatestMessage,
|
|
391
384
|
includeTerminalDetails
|
|
392
385
|
}),
|
|
393
|
-
|
|
386
|
+
...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
|
|
394
387
|
};
|
|
395
388
|
}
|
|
396
389
|
async outputMany(agentIds, options = {}) {
|
|
@@ -430,6 +423,7 @@ export class SubagentSessionController {
|
|
|
430
423
|
satisfied = await waitForAgentCondition({
|
|
431
424
|
waitMs,
|
|
432
425
|
signal: options.signal,
|
|
426
|
+
interrupt: options.waitInterrupt,
|
|
433
427
|
ready: () => {
|
|
434
428
|
refresh();
|
|
435
429
|
return mode === 'all' ? targets.every(ready) : targets.some(ready);
|
|
@@ -443,6 +437,8 @@ export class SubagentSessionController {
|
|
|
443
437
|
options.signal?.throwIfAborted();
|
|
444
438
|
}
|
|
445
439
|
refresh();
|
|
440
|
+
if (!snapshot)
|
|
441
|
+
satisfied = mode === 'all' ? targets.every(ready) : targets.some(ready);
|
|
446
442
|
const pendingAgentIds = targets
|
|
447
443
|
.filter((target) => !ready(target))
|
|
448
444
|
.map((target) => target.state.agentId);
|
|
@@ -457,7 +453,9 @@ export class SubagentSessionController {
|
|
|
457
453
|
? 'message'
|
|
458
454
|
: satisfied
|
|
459
455
|
? 'snapshot'
|
|
460
|
-
:
|
|
456
|
+
: options.waitInterrupt?.pending()
|
|
457
|
+
? 'interrupted'
|
|
458
|
+
: 'timeout';
|
|
461
459
|
const includeLatestMessage = outcome !== 'snapshot' && unread(target);
|
|
462
460
|
const includeTerminalDetails = outcome === 'completion' &&
|
|
463
461
|
(observation
|
|
@@ -466,7 +464,7 @@ export class SubagentSessionController {
|
|
|
466
464
|
const result = {
|
|
467
465
|
...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
|
|
468
466
|
...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
|
|
469
|
-
|
|
467
|
+
...waitMetadata(outcome)
|
|
470
468
|
};
|
|
471
469
|
if (includeLatestMessage) {
|
|
472
470
|
if (observation)
|
|
@@ -487,13 +485,15 @@ export class SubagentSessionController {
|
|
|
487
485
|
waitFor,
|
|
488
486
|
agents,
|
|
489
487
|
pendingAgentIds,
|
|
490
|
-
|
|
488
|
+
...waitMetadata(snapshot
|
|
491
489
|
? 'snapshot'
|
|
492
490
|
: !satisfied
|
|
493
|
-
?
|
|
491
|
+
? options.waitInterrupt?.pending()
|
|
492
|
+
? 'interrupted'
|
|
493
|
+
: 'timeout'
|
|
494
494
|
: agents.some((agent) => agent.waitOutcome === 'message')
|
|
495
495
|
? 'message'
|
|
496
|
-
: 'completion'
|
|
496
|
+
: 'completion')
|
|
497
497
|
};
|
|
498
498
|
}
|
|
499
499
|
async message(agentId, message, options = {}) {
|
|
@@ -809,25 +809,25 @@ export class SubagentSessionController {
|
|
|
809
809
|
if (terminal)
|
|
810
810
|
state.terminalRevision = state.resourceRevision;
|
|
811
811
|
}
|
|
812
|
-
#waitForAnyCompletion(candidates, waitMs, signal) {
|
|
813
|
-
return
|
|
812
|
+
#waitForAnyCompletion(candidates, waitMs, signal, interrupt) {
|
|
813
|
+
return waitForNotification(this.#completionWaiters, (state) => candidates.includes(state), () => candidates.find(isTerminalState), waitMs, signal, interrupt);
|
|
814
814
|
}
|
|
815
|
-
#waitForCompletion(state, waitMs, signal) {
|
|
816
|
-
return
|
|
815
|
+
#waitForCompletion(state, waitMs, signal, interrupt) {
|
|
816
|
+
return waitForNotification(this.#completionWaiters, (completed) => completed === state, () => (isTerminalState(state) ? state : undefined), waitMs, signal, interrupt);
|
|
817
817
|
}
|
|
818
818
|
#notifyCompletion(state) {
|
|
819
819
|
for (const waiter of this.#completionWaiters)
|
|
820
820
|
waiter(state);
|
|
821
821
|
this.#notifyActivity({ state, kind: 'completion' });
|
|
822
822
|
}
|
|
823
|
-
#waitForMessageOrCompletion(candidates, waitMs, signal) {
|
|
823
|
+
#waitForMessageOrCompletion(candidates, waitMs, signal, interrupt) {
|
|
824
824
|
return waitForNotification(this.#activityWaiters, (activity) => candidates.includes(activity.state), () => {
|
|
825
825
|
const pending = this.#oldestUnreadMessage(candidates);
|
|
826
826
|
if (pending)
|
|
827
827
|
return { state: pending, kind: 'message' };
|
|
828
828
|
const terminal = candidates.find(isTerminalState);
|
|
829
829
|
return terminal ? { state: terminal, kind: 'completion' } : undefined;
|
|
830
|
-
}, waitMs, signal);
|
|
830
|
+
}, waitMs, signal, interrupt);
|
|
831
831
|
}
|
|
832
832
|
#notifyActivity(activity) {
|
|
833
833
|
for (const waiter of this.#activityWaiters)
|
|
@@ -1301,69 +1301,30 @@ function truncateCharacters(value, maximumCharacters) {
|
|
|
1301
1301
|
? value
|
|
1302
1302
|
: characters.slice(0, maximumCharacters).join('');
|
|
1303
1303
|
}
|
|
1304
|
-
function
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
resolve(value);
|
|
1327
|
-
};
|
|
1328
|
-
const abort = () => finish(signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
|
|
1329
|
-
const cancelTimer = waitDeadline(waitMs, () => finish());
|
|
1330
|
-
waiters.add(notify);
|
|
1331
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
1332
|
-
if (signal?.aborted)
|
|
1333
|
-
abort();
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
function waitForNotification(waiters, accepts, current, waitMs, signal) {
|
|
1337
|
-
return new Promise((resolve, reject) => {
|
|
1338
|
-
let settled = false;
|
|
1339
|
-
const finish = (value, error) => {
|
|
1340
|
-
if (settled)
|
|
1341
|
-
return;
|
|
1342
|
-
settled = true;
|
|
1343
|
-
cancelTimer();
|
|
1344
|
-
signal?.removeEventListener('abort', abort);
|
|
1345
|
-
waiters.delete(notify);
|
|
1346
|
-
if (error === undefined)
|
|
1347
|
-
resolve(value);
|
|
1348
|
-
else
|
|
1349
|
-
reject(error);
|
|
1350
|
-
};
|
|
1351
|
-
const notify = (value) => {
|
|
1352
|
-
if (accepts(value))
|
|
1353
|
-
finish(value);
|
|
1354
|
-
};
|
|
1355
|
-
const abort = () => finish(undefined, signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
|
|
1356
|
-
const cancelTimer = waitDeadline(waitMs, () => finish());
|
|
1357
|
-
waiters.add(notify);
|
|
1358
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
1359
|
-
if (signal?.aborted)
|
|
1360
|
-
abort();
|
|
1361
|
-
else {
|
|
1362
|
-
const pending = current();
|
|
1363
|
-
if (pending !== undefined)
|
|
1364
|
-
finish(pending);
|
|
1365
|
-
}
|
|
1304
|
+
async function waitForNotification(waiters, accepts, current, waitMs, signal, interrupt) {
|
|
1305
|
+
let value;
|
|
1306
|
+
await waitForToolCondition({
|
|
1307
|
+
ready: () => {
|
|
1308
|
+
value ??= current();
|
|
1309
|
+
return value !== undefined;
|
|
1310
|
+
},
|
|
1311
|
+
subscribe: (notify) => {
|
|
1312
|
+
const listener = (next) => {
|
|
1313
|
+
if (accepts(next)) {
|
|
1314
|
+
value = next;
|
|
1315
|
+
notify();
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
waiters.add(listener);
|
|
1319
|
+
return () => {
|
|
1320
|
+
waiters.delete(listener);
|
|
1321
|
+
};
|
|
1322
|
+
},
|
|
1323
|
+
waitMs,
|
|
1324
|
+
signal,
|
|
1325
|
+
interrupt
|
|
1366
1326
|
});
|
|
1327
|
+
return value;
|
|
1367
1328
|
}
|
|
1368
1329
|
function validateOutputOptions(options, native = false) {
|
|
1369
1330
|
if ((options.untilSettled !== undefined && options.untilSettled !== true) ||
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { waitForToolCondition } from '../tools/wait.js';
|
|
1
2
|
import type { SubagentWaitMode } from './contracts.js';
|
|
2
3
|
/** Validate the entire target set before any observation can be consumed. */
|
|
3
4
|
export declare function parseAgentWaitTargets(input: {
|
|
@@ -8,12 +9,6 @@ export declare function parseAgentWaitTargets(input: {
|
|
|
8
9
|
agentIds?: string[];
|
|
9
10
|
mode?: SubagentWaitMode;
|
|
10
11
|
};
|
|
12
|
+
export { waitDeadline } from '../tools/wait.js';
|
|
11
13
|
/** A group owns one listener and one deadline; readiness never consumes observations. */
|
|
12
|
-
export declare function waitForAgentCondition(input:
|
|
13
|
-
ready(): boolean;
|
|
14
|
-
subscribe(notify: () => void): () => void;
|
|
15
|
-
waitMs: number;
|
|
16
|
-
signal?: AbortSignal | undefined;
|
|
17
|
-
}): Promise<boolean>;
|
|
18
|
-
/** Explicit waits retain their deadline without overflowing the platform timer. */
|
|
19
|
-
export declare function waitDeadline(waitMs: number, finish: () => void): () => void;
|
|
14
|
+
export declare function waitForAgentCondition(input: Parameters<typeof waitForToolCondition>[0]): Promise<boolean>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { waitForToolCondition } from '../tools/wait.js';
|
|
1
2
|
import { MarAgentError } from '../error.js';
|
|
2
3
|
/** Validate the entire target set before any observation can be consumed. */
|
|
3
4
|
export function parseAgentWaitTargets(input, maxAgents = Infinity) {
|
|
@@ -24,54 +25,8 @@ export function parseAgentWaitTargets(input, maxAgents = Infinity) {
|
|
|
24
25
|
function invalid() {
|
|
25
26
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent wait targets are invalid.');
|
|
26
27
|
}
|
|
28
|
+
export { waitDeadline } from '../tools/wait.js';
|
|
27
29
|
/** A group owns one listener and one deadline; readiness never consumes observations. */
|
|
28
|
-
export function waitForAgentCondition(input) {
|
|
29
|
-
input
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
let settled = false;
|
|
32
|
-
let unsubscribe = () => { };
|
|
33
|
-
let cancelTimer = () => { };
|
|
34
|
-
const finish = (ready, error) => {
|
|
35
|
-
if (settled)
|
|
36
|
-
return;
|
|
37
|
-
settled = true;
|
|
38
|
-
unsubscribe();
|
|
39
|
-
cancelTimer();
|
|
40
|
-
input.signal?.removeEventListener('abort', abort);
|
|
41
|
-
if (error === undefined)
|
|
42
|
-
resolve(ready);
|
|
43
|
-
else
|
|
44
|
-
reject(error);
|
|
45
|
-
};
|
|
46
|
-
const notify = () => {
|
|
47
|
-
if (input.ready())
|
|
48
|
-
finish(true);
|
|
49
|
-
};
|
|
50
|
-
const abort = () => finish(false, input.signal?.reason ??
|
|
51
|
-
new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
|
|
52
|
-
unsubscribe = input.subscribe(notify);
|
|
53
|
-
cancelTimer = waitDeadline(input.waitMs, () => finish(input.ready()));
|
|
54
|
-
input.signal?.addEventListener('abort', abort, { once: true });
|
|
55
|
-
if (input.signal?.aborted)
|
|
56
|
-
abort();
|
|
57
|
-
else
|
|
58
|
-
notify();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
/** Explicit waits retain their deadline without overflowing the platform timer. */
|
|
62
|
-
export function waitDeadline(waitMs, finish) {
|
|
63
|
-
const deadline = performance.now() + waitMs;
|
|
64
|
-
let timer;
|
|
65
|
-
const tick = () => {
|
|
66
|
-
const remaining = deadline - performance.now();
|
|
67
|
-
if (remaining <= 0)
|
|
68
|
-
finish();
|
|
69
|
-
else
|
|
70
|
-
arm(remaining);
|
|
71
|
-
};
|
|
72
|
-
const arm = (remaining) => {
|
|
73
|
-
timer = setTimeout(tick, Math.min(remaining, 2_147_483_647));
|
|
74
|
-
};
|
|
75
|
-
arm(waitMs);
|
|
76
|
-
return () => clearTimeout(timer);
|
|
30
|
+
export async function waitForAgentCondition(input) {
|
|
31
|
+
return (await waitForToolCondition(input)) === 'ready';
|
|
77
32
|
}
|
|
@@ -5,7 +5,7 @@ export const agentWaitToolDefinition = {
|
|
|
5
5
|
name: 'agent_wait',
|
|
6
6
|
allowedCallers: ['direct'],
|
|
7
7
|
parallelSafety: 'serial',
|
|
8
|
-
description: 'Read or wait a bounded time for a child agent result. Unread child messages and terminal results are supplied automatically before the next model request. Wait only when the next local step depends on the child result; otherwise continue useful non-overlapping work. waitFor defaults to "completion"; use "message" only when a fresh public commentary/final message would change the next decision. Omit agentId to wait for whichever live child returns first, matching Codex-style untargeted waiting and avoiding UUID transcription; provide the exact agentId returned by agent_start only when observing a specific child. If no child is live, untargeted waiting reads the most recently created child. For an exec processId, use exec_wait instead (tools.exec_wait in Code Mode). Omitting waitMs waits up to 30 seconds by default; waitMs=0 returns the current snapshot immediately. Positive waits range up to 5 minutes, and positive values below 10 seconds are raised to 10 seconds. When blocked on a live child result, prefer one 300000 ms completion wait rather than repeated short waits. Use agentIds with mode any (default) or all to wait for an explicit non-empty set of up to 8 distinct child IDs; never combine agentId and agentIds or use mode without agentIds. mode and waitFor are independent. A group uses one deadline and returns mode, waitFor, waitOutcome, agents in input order, and pendingAgentIds. Failed, cancelled and interrupted turns also satisfy completion; terminal state satisfies message waits too. Group waitMs=0 returns only snapshots without consuming messages. Single-target and untargeted results include waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. A message, timeout with unread progress, or first terminal completion result includes the newly observed latest public commentary/final message; snapshots and repeated observations omit previously observed message text. latestActivity can still identify a public message or bounded tool started/completed/failed status. A queued or running result is not completion. A message wait consumes only the latest unread public message in the current Session runtime; tool activity does not wake it, and parent cancellation preserves unread progress for a later Execution. Do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. First terminal results include bounded summary, changedFiles, verification, and read evidence with path/line ranges; use that evidence to avoid repeating the full exploration, while independently checking consequential edits and claims.',
|
|
8
|
+
description: 'Read or wait a bounded time for a child agent result. Unread child messages and terminal results are supplied automatically before the next model request. Wait only when the next local step depends on the child result; otherwise continue useful non-overlapping work. waitFor defaults to "completion"; use "message" only when a fresh public commentary/final message would change the next decision. Omit agentId to wait for whichever live child returns first, matching Codex-style untargeted waiting and avoiding UUID transcription; provide the exact agentId returned by agent_start only when observing a specific child. If no child is live, untargeted waiting reads the most recently created child. For an exec processId, use exec_wait instead (tools.exec_wait in Code Mode). Omitting waitMs waits up to 30 seconds by default; waitMs=0 returns the current snapshot immediately. Positive waits range up to 5 minutes, and positive values below 10 seconds are raised to 10 seconds. When blocked on a live child result, prefer one 300000 ms completion wait rather than repeated short waits. Use agentIds with mode any (default) or all to wait for an explicit non-empty set of up to 8 distinct child IDs; never combine agentId and agentIds or use mode without agentIds. mode and waitFor are independent. A group uses one deadline and returns mode, waitFor, waitOutcome, agents in input order, and pendingAgentIds. Failed, cancelled and interrupted turns also satisfy completion; terminal state satisfies message waits too. Group waitMs=0 returns only snapshots without consuming messages. Single-target and untargeted results include waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. A message, timeout with unread progress, or first terminal completion result includes the newly observed latest public commentary/final message; snapshots and repeated observations omit previously observed message text. latestActivity can still identify a public message or bounded tool started/completed/failed status. A queued or running result is not completion. A message wait consumes only the latest unread public message in the current Session runtime; tool activity does not wake it, and parent cancellation preserves unread progress for a later Execution. waitOutcome distinguishes snapshot, message, completion, and timeout. A foreground steer may return interrupted with reason=steer without cancelling any child. Do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. First terminal results include bounded summary, changedFiles, verification, and read evidence with path/line ranges; use that evidence to avoid repeating the full exploration, while independently checking consequential edits and claims.',
|
|
9
9
|
inputSchema: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
@@ -49,7 +49,8 @@ export async function executeAgentWait(arguments_, context) {
|
|
|
49
49
|
const options = {
|
|
50
50
|
waitMs: value.waitMs ?? TOOL_EXECUTION_LIMITS.agentWaitDefaultWaitMs,
|
|
51
51
|
waitFor: value.waitFor ?? 'completion',
|
|
52
|
-
signal: context.signal
|
|
52
|
+
signal: context.signal,
|
|
53
|
+
waitInterrupt: context.waitInterrupt
|
|
53
54
|
};
|
|
54
55
|
const task = value.agentIds !== undefined
|
|
55
56
|
? await subagents.outputMany(value.agentIds, { ...options, mode: value.mode ?? 'any' })
|
|
@@ -3,6 +3,7 @@ import { permissionStopFromError } from '../permissions.js';
|
|
|
3
3
|
import { MarAgentError, describeMarAgentError } from '../../error.js';
|
|
4
4
|
import { truncateModelText } from '../../runtime/token-budget.js';
|
|
5
5
|
import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
|
|
6
|
+
import { waitForToolCondition, waitMetadata } from '../wait.js';
|
|
6
7
|
import { BoundedTextOutput, renderCapturedText } from './text-output.js';
|
|
7
8
|
import { boundObservations, selectObservationImages, newObservationId, visibleObservation } from './observations.js';
|
|
8
9
|
/** Keeps a failed tool call observable without discarding earlier explicit output. */
|
|
@@ -100,7 +101,7 @@ export class CodeCells {
|
|
|
100
101
|
cell.done = Promise.resolve()
|
|
101
102
|
.then(async () => {
|
|
102
103
|
releaseSession = await context.codeMode?.retainSession?.();
|
|
103
|
-
return run(source, { ...context, signal: controller.signal }, {
|
|
104
|
+
return run(source, { ...context, signal: controller.signal, waitInterrupt: undefined }, {
|
|
104
105
|
call: (value) => cell.calls.set(value.callId, value),
|
|
105
106
|
observe: (value) => cell.observations.push(value),
|
|
106
107
|
text: (text, originalBytes) => cell.text.append(text, originalBytes),
|
|
@@ -155,7 +156,7 @@ export class CodeCells {
|
|
|
155
156
|
}
|
|
156
157
|
notify();
|
|
157
158
|
});
|
|
158
|
-
return this.#wait(cell, settings, context.signal, true, context.codeMode?.supportsImages === true);
|
|
159
|
+
return this.#wait(cell, settings, context.signal, true, context.codeMode?.supportsImages === true, context.waitInterrupt);
|
|
159
160
|
}
|
|
160
161
|
async wait(value, context) {
|
|
161
162
|
const input = options(value, true);
|
|
@@ -176,30 +177,32 @@ export class CodeCells {
|
|
|
176
177
|
abort();
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
|
-
return this.#wait(cell, input, context.signal, false, context.codeMode?.supportsImages === true);
|
|
180
|
+
return this.#wait(cell, input, context.signal, false, context.codeMode?.supportsImages === true, input.action === 'poll' ? context.waitInterrupt : undefined);
|
|
180
181
|
}
|
|
181
|
-
async #wait(cell, input, signal, initial, supportsImages) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
const timer = setTimeout(finish, input.yieldMs);
|
|
197
|
-
cell.listeners.add(finish);
|
|
198
|
-
signal.addEventListener('abort', abort, { once: true });
|
|
199
|
-
if (signal.aborted)
|
|
200
|
-
abort();
|
|
182
|
+
async #wait(cell, input, signal, initial, supportsImages, interrupt) {
|
|
183
|
+
const outcome = input.yieldMs === 0
|
|
184
|
+
? undefined
|
|
185
|
+
: await waitForToolCondition({
|
|
186
|
+
ready: () => cell.status !== 'running' || cell.yielded,
|
|
187
|
+
subscribe: (notify) => {
|
|
188
|
+
cell.listeners.add(notify);
|
|
189
|
+
return () => {
|
|
190
|
+
cell.listeners.delete(notify);
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
waitMs: input.yieldMs,
|
|
194
|
+
signal,
|
|
195
|
+
interrupt
|
|
201
196
|
});
|
|
202
|
-
|
|
197
|
+
const wait = waitMetadata(input.yieldMs === 0
|
|
198
|
+
? 'snapshot'
|
|
199
|
+
: cell.status !== 'running'
|
|
200
|
+
? 'completion'
|
|
201
|
+
: cell.yielded
|
|
202
|
+
? 'yield'
|
|
203
|
+
: outcome === 'interrupted'
|
|
204
|
+
? 'interrupted'
|
|
205
|
+
: 'timeout');
|
|
203
206
|
signal.throwIfAborted();
|
|
204
207
|
if (!supportsImages && (cell.images.length || cell.terminal?.images?.length))
|
|
205
208
|
throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'The current model cannot receive this cell image output.');
|
|
@@ -213,6 +216,7 @@ export class CodeCells {
|
|
|
213
216
|
output: truncateModelText(original.output ?? '', limits.codeMaxOutputTokens).content,
|
|
214
217
|
observations: visible.observations,
|
|
215
218
|
cached: true,
|
|
219
|
+
...wait,
|
|
216
220
|
truncated: cell.terminal.truncated === true || bounded.truncated
|
|
217
221
|
};
|
|
218
222
|
return {
|
|
@@ -235,7 +239,8 @@ export class CodeCells {
|
|
|
235
239
|
...snapshot,
|
|
236
240
|
output: bounded.content,
|
|
237
241
|
truncated: cell.terminal.truncated || bounded.truncated,
|
|
238
|
-
cached: true
|
|
242
|
+
cached: true,
|
|
243
|
+
...wait
|
|
239
244
|
};
|
|
240
245
|
return {
|
|
241
246
|
...cell.terminal,
|
|
@@ -270,6 +275,7 @@ export class CodeCells {
|
|
|
270
275
|
const data = {
|
|
271
276
|
cellId: cell.cellId,
|
|
272
277
|
status: cell.status,
|
|
278
|
+
...wait,
|
|
273
279
|
...(permissionStopFromError(cell.error)
|
|
274
280
|
? { permissionStop: permissionStopFromError(cell.error) }
|
|
275
281
|
: {}),
|
|
@@ -45,6 +45,7 @@ Built-in namespaces:
|
|
|
45
45
|
- await cell.sleep(ms) performs a cancellable delay up to ${limits.codeSleepMaxMs} ms.
|
|
46
46
|
- await cell.yield_control() returns accumulated output to the model immediately while this cell continues running.
|
|
47
47
|
- Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and controls this call's wait window (0..${limits.codeMaxYieldMs}), not nested tool timeouts or wait windows. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
|
|
48
|
+
- waitOutcome separates snapshot, completion, timeout, and yield from the cell status. A foreground steer returns interrupted with reason=steer without stopping the cell or its internal tool waits.
|
|
48
49
|
- Registered cells are Session warm resources: ending or cancelling the model Execution cancels its wait, not the cell. Reuse retained cellId; do not start duplicate work. Host cleanup or explicit termination stops the cell.
|
|
49
50
|
- tools, observe, state, catalog and cell are frozen bindings. Do not shadow their names.
|
|
50
51
|
|
|
@@ -78,7 +79,7 @@ observe.result(patched);
|
|
|
78
79
|
};
|
|
79
80
|
export const codeExecWaitToolDefinition = {
|
|
80
81
|
name: 'code_exec_wait',
|
|
81
|
-
description: 'Wait for a retained Code Exec cell by cellId. It cannot start new work; use code_exec for that. A missing cellId does not mean code_exec is unavailable. Returns only new output, or a bounded terminal snapshot marked cached on repeat reads. yieldMs is a wait window, not a timeout. action defaults to poll (wait); action cancel stops the cell and waits for cleanup. Cancelling this wait does not cancel the cell. Cells belong to the current Session and cannot survive Host restart. Call directly, never inside code_exec.',
|
|
82
|
+
description: 'Wait for a retained Code Exec cell by cellId. It cannot start new work; use code_exec for that. A missing cellId does not mean code_exec is unavailable. Returns only new output, or a bounded terminal snapshot marked cached on repeat reads. yieldMs is a wait window, not a timeout. action defaults to poll (wait); action cancel stops the cell and waits for cleanup. Cancelling this wait does not cancel the cell. Cells belong to the current Session and cannot survive Host restart. Call directly, never inside code_exec. Poll results include waitOutcome; interrupted with reason=steer ends only the foreground wait and preserves the cell.',
|
|
82
83
|
inputSchema: {
|
|
83
84
|
type: 'object',
|
|
84
85
|
properties: {
|
|
@@ -27,6 +27,7 @@ export interface ToolCall {
|
|
|
27
27
|
export interface ToolExecutionContext {
|
|
28
28
|
mode: ExecutionMode;
|
|
29
29
|
signal: AbortSignal;
|
|
30
|
+
waitInterrupt?: import('./wait.js').ToolWaitInterrupt | undefined;
|
|
30
31
|
sessionId: string;
|
|
31
32
|
executionId: string;
|
|
32
33
|
/** Current model identity for neutral Host-owned tool policies. */
|
|
@@ -108,11 +109,15 @@ export interface CodeCellEventData {
|
|
|
108
109
|
calls?: readonly import('./code-mode/observations.js').CodeToolCallRecord[];
|
|
109
110
|
cellId: string;
|
|
110
111
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
|
112
|
+
waitOutcome?: import('./wait.js').ToolWaitOutcome;
|
|
113
|
+
reason?: 'steer';
|
|
111
114
|
cached?: boolean;
|
|
112
115
|
observations?: readonly import('./code-mode/observations.js').CodeObservation[];
|
|
113
116
|
}
|
|
114
117
|
export interface ExecResult {
|
|
115
118
|
status: 'running' | 'completed' | 'cancelled';
|
|
119
|
+
waitOutcome?: import('./wait.js').ToolWaitOutcome;
|
|
120
|
+
reason?: 'steer';
|
|
116
121
|
processId?: string;
|
|
117
122
|
stdout: string;
|
|
118
123
|
stderr: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type LocalProcessExecutor } from '../../host/local-process-executor.js';
|
|
2
2
|
import type { ClientToolDefinition } from '../../model/contracts.js';
|
|
3
3
|
import type { ExecResult, ToolExecutionResult } from '../contracts.js';
|
|
4
|
+
import { type ToolWaitInterrupt } from '../wait.js';
|
|
4
5
|
import { WorkspacePathResolver } from '../shared/path-resolver.js';
|
|
5
6
|
import { type CoreProcessInput, type CoreProcessHandle } from '../../extensions/processes.js';
|
|
6
7
|
import type { CoreOperationContext } from '../../extensions/operation.js';
|
|
@@ -39,7 +40,7 @@ export declare class ExecTool {
|
|
|
39
40
|
private readonly shell?;
|
|
40
41
|
private readonly processExecutor;
|
|
41
42
|
constructor(paths: WorkspacePathResolver, maxOutputBytes?: number, maxProcesses?: number, environment?: NodeJS.ProcessEnv, shell?: string | undefined, processExecutor?: LocalProcessExecutor);
|
|
42
|
-
execute(arguments_: unknown, signal?: AbortSignal, owner?: NonNullable<import('../../host/local-process-executor.js').LocalProcessSpawnRequest['owner']
|
|
43
|
+
execute(arguments_: unknown, signal?: AbortSignal, owner?: NonNullable<import('../../host/local-process-executor.js').LocalProcessSpawnRequest['owner']>, waitInterrupt?: ToolWaitInterrupt): Promise<ExecResult>;
|
|
43
44
|
spawn(input: CoreProcessInput, call: CoreOperationContext): Promise<CoreProcessHandle>;
|
|
44
45
|
listSessionResources(sessionId: string): {
|
|
45
46
|
processId: string;
|
|
@@ -3,6 +3,7 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
3
3
|
import { MarAgentError } from '../../error.js';
|
|
4
4
|
import { directLocalProcessExecutor, signalProcessTree } from '../../host/local-process-executor.js';
|
|
5
5
|
import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
|
|
6
|
+
import { waitForToolCondition, waitMetadata } from '../wait.js';
|
|
6
7
|
import { validateCoreProcessInput } from '../../extensions/processes.js';
|
|
7
8
|
/** Share the model-facing body across direct calls and original Code Mode observations. */
|
|
8
9
|
export function formatExecResult(result) {
|
|
@@ -29,7 +30,7 @@ export const execToolDefinition = {
|
|
|
29
30
|
name: 'exec',
|
|
30
31
|
allowedCallers: ['direct', 'programmatic'],
|
|
31
32
|
parallelSafety: 'safe',
|
|
32
|
-
description: 'Start a platform-native command, script, or program in workspace/default cwd. Returns a stable processId, status, bounded stdout/stderr, cumulative byte counts, dropped-byte counts, exitCode and truncated. A completed status does not imply exitCode 0; inspect stderr/exitCode. For subsequent waiting, stdin writes or cancellation, call exec_wait directly with processId, not exec. Truncation does not stop the process. Cancelling the initial wait does not terminate the process.',
|
|
33
|
+
description: 'Start a platform-native command, script, or program in workspace/default cwd. Returns a stable processId, status, bounded stdout/stderr, cumulative byte counts, dropped-byte counts, exitCode and truncated. A completed status does not imply exitCode 0; inspect stderr/exitCode. For subsequent waiting, stdin writes or cancellation, call exec_wait directly with processId, not exec. Truncation does not stop the process. Cancelling the initial wait does not terminate the process. waitOutcome distinguishes snapshot, completion, timeout, and yield. A foreground wait may return interrupted with reason=steer; the process keeps running.',
|
|
33
34
|
outputSchema: {
|
|
34
35
|
type: 'object',
|
|
35
36
|
required: ['content', 'data'],
|
|
@@ -49,6 +50,8 @@ export const execToolDefinition = {
|
|
|
49
50
|
],
|
|
50
51
|
properties: {
|
|
51
52
|
status: { enum: ['running', 'completed', 'cancelled'] },
|
|
53
|
+
waitOutcome: { enum: ['snapshot', 'completion', 'timeout', 'yield', 'interrupted'] },
|
|
54
|
+
reason: { enum: ['steer'] },
|
|
52
55
|
processId: { type: 'string' },
|
|
53
56
|
stdout: { type: 'string' },
|
|
54
57
|
stderr: { type: 'string' },
|
|
@@ -101,7 +104,7 @@ export const execWaitToolDefinition = {
|
|
|
101
104
|
name: 'exec_wait',
|
|
102
105
|
allowedCallers: ['direct', 'programmatic'],
|
|
103
106
|
parallelSafety: 'safe',
|
|
104
|
-
description: 'Wait for or control a managed process by processId. action defaults to poll: wait for new output or termination and return incremental stdout/stderr. write sends data to stdin; cancel terminates the process or removes its cached result. Terminal polls return a bounded snapshot marked cached for up to five minutes. Cancelling a wait does not terminate the process. Uses the same Session-owned processes as exec; cannot start a process or extend its timeout. Inspect stderr/exitCode; completed does not imply success. Supports direct and programmatic calls; in Code Mode use tools.exec_wait.',
|
|
107
|
+
description: 'Wait for or control a managed process by processId. action defaults to poll: wait for new output or termination and return incremental stdout/stderr. write sends data to stdin; cancel terminates the process or removes its cached result. Terminal polls return a bounded snapshot marked cached for up to five minutes. Cancelling a wait does not terminate the process. Uses the same Session-owned processes as exec; cannot start a process or extend its timeout. Inspect stderr/exitCode; completed does not imply success. Supports direct and programmatic calls; in Code Mode use tools.exec_wait. Poll results include waitOutcome; interrupted with reason=steer ends only a foreground wait, never the process.',
|
|
105
108
|
outputSchema: execToolDefinition.outputSchema,
|
|
106
109
|
inputSchema: {
|
|
107
110
|
type: 'object',
|
|
@@ -160,7 +163,7 @@ export class ExecTool {
|
|
|
160
163
|
this.shell = shell;
|
|
161
164
|
this.processExecutor = processExecutor;
|
|
162
165
|
}
|
|
163
|
-
async execute(arguments_, signal, owner) {
|
|
166
|
+
async execute(arguments_, signal, owner, waitInterrupt) {
|
|
164
167
|
if (signal?.aborted)
|
|
165
168
|
throw executionAbortReason(signal);
|
|
166
169
|
const input = parseExecInput(arguments_);
|
|
@@ -174,14 +177,21 @@ export class ExecTool {
|
|
|
174
177
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Completed process cannot accept input.');
|
|
175
178
|
if (input.action === 'cancel')
|
|
176
179
|
this.#removeCompleted(input.processId);
|
|
177
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
...completed.result,
|
|
182
|
+
cached: true,
|
|
183
|
+
...(input.action === 'poll'
|
|
184
|
+
? waitMetadata(input.yieldMs === 0 ? 'snapshot' : 'completion')
|
|
185
|
+
: {})
|
|
186
|
+
};
|
|
178
187
|
}
|
|
179
188
|
if (!process ||
|
|
180
189
|
process.native ||
|
|
181
190
|
(owner !== undefined && process.ownerSessionId !== owner.sessionId))
|
|
182
191
|
throw new MarAgentError('MAR_AGENT_PROCESS_NOT_FOUND', 'Managed process was not found.');
|
|
192
|
+
let outcome;
|
|
183
193
|
if (input.action === 'poll' && input.yieldMs)
|
|
184
|
-
await waitForProcessChange(process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxPollYieldMs), signal);
|
|
194
|
+
outcome = await waitForProcessChange(process, Math.min(Math.max(input.yieldMs, 0), TOOL_EXECUTION_LIMITS.execMaxPollYieldMs), signal, waitInterrupt);
|
|
185
195
|
if (input.action === 'write') {
|
|
186
196
|
if (process.status !== 'running')
|
|
187
197
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Completed process cannot accept input.');
|
|
@@ -195,7 +205,20 @@ export class ExecTool {
|
|
|
195
205
|
if (input.action === 'cancel')
|
|
196
206
|
this.#removeCompleted(input.processId);
|
|
197
207
|
}
|
|
198
|
-
return
|
|
208
|
+
return {
|
|
209
|
+
...result,
|
|
210
|
+
...(input.action === 'poll'
|
|
211
|
+
? waitMetadata(input.yieldMs === 0
|
|
212
|
+
? 'snapshot'
|
|
213
|
+
: result.status !== 'running'
|
|
214
|
+
? 'completion'
|
|
215
|
+
: outcome === 'interrupted'
|
|
216
|
+
? 'interrupted'
|
|
217
|
+
: outcome === 'ready'
|
|
218
|
+
? 'yield'
|
|
219
|
+
: 'timeout')
|
|
220
|
+
: {})
|
|
221
|
+
};
|
|
199
222
|
}
|
|
200
223
|
const { id, managed } = await this.#startManaged(async () => {
|
|
201
224
|
const cwd = await this.paths.resolveWithDisplay(input.cwd ?? '.');
|
|
@@ -217,11 +240,34 @@ export class ExecTool {
|
|
|
217
240
|
}, Math.min(input.timeoutMs, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs));
|
|
218
241
|
managed.timer.unref();
|
|
219
242
|
}
|
|
220
|
-
|
|
243
|
+
const yieldMs = Math.min(input.yieldMs ?? TOOL_EXECUTION_LIMITS.execDefaultYieldMs, TOOL_EXECUTION_LIMITS.execMaxYieldMs);
|
|
244
|
+
const outcome = yieldMs === 0
|
|
245
|
+
? undefined
|
|
246
|
+
: await waitForToolCondition({
|
|
247
|
+
ready: () => managed.finalized,
|
|
248
|
+
subscribe: (notify) => {
|
|
249
|
+
managed.waiters.add(notify);
|
|
250
|
+
return () => {
|
|
251
|
+
managed.waiters.delete(notify);
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
waitMs: yieldMs,
|
|
255
|
+
signal,
|
|
256
|
+
interrupt: waitInterrupt
|
|
257
|
+
});
|
|
221
258
|
const result = this.#result(id, managed);
|
|
222
259
|
if (managed.status !== 'running')
|
|
223
260
|
this.#retainCompleted(id, managed);
|
|
224
|
-
return
|
|
261
|
+
return {
|
|
262
|
+
...result,
|
|
263
|
+
...waitMetadata(yieldMs === 0
|
|
264
|
+
? 'snapshot'
|
|
265
|
+
: result.status !== 'running'
|
|
266
|
+
? 'completion'
|
|
267
|
+
: outcome === 'interrupted'
|
|
268
|
+
? 'interrupted'
|
|
269
|
+
: 'timeout')
|
|
270
|
+
};
|
|
225
271
|
}
|
|
226
272
|
async #startManaged(request, ownerSessionId, native) {
|
|
227
273
|
if (this.#processes.size + this.#startingProcesses.size >= this.maxProcesses)
|
|
@@ -734,30 +780,6 @@ function optionalBoundedInteger(value, minimum, maximum) {
|
|
|
734
780
|
function invalidExecInput() {
|
|
735
781
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Exec input is invalid.');
|
|
736
782
|
}
|
|
737
|
-
async function waitForExitOrDelay(exited, delayMs, signal) {
|
|
738
|
-
if (signal?.aborted)
|
|
739
|
-
throw executionAbortReason(signal);
|
|
740
|
-
let timer;
|
|
741
|
-
let rejectAbort;
|
|
742
|
-
const aborted = new Promise((_resolve, reject) => (rejectAbort = reject));
|
|
743
|
-
const abort = () => rejectAbort(executionAbortReason(signal));
|
|
744
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
745
|
-
try {
|
|
746
|
-
await Promise.race([
|
|
747
|
-
exited,
|
|
748
|
-
aborted,
|
|
749
|
-
new Promise((resolve) => {
|
|
750
|
-
timer = setTimeout(resolve, delayMs);
|
|
751
|
-
timer.unref();
|
|
752
|
-
})
|
|
753
|
-
]);
|
|
754
|
-
}
|
|
755
|
-
finally {
|
|
756
|
-
signal?.removeEventListener('abort', abort);
|
|
757
|
-
if (timer)
|
|
758
|
-
clearTimeout(timer);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
783
|
async function waitForExit(exited, delayMs) {
|
|
762
784
|
let timer;
|
|
763
785
|
try {
|
|
@@ -777,37 +799,28 @@ async function waitForExit(exited, delayMs) {
|
|
|
777
799
|
function notifyProcessChange(managed) {
|
|
778
800
|
for (const resolve of managed.waiters)
|
|
779
801
|
resolve();
|
|
780
|
-
managed.waiters.clear();
|
|
781
802
|
}
|
|
782
|
-
async function waitForProcessChange(managed, waitMs, signal) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
let resolveChange;
|
|
789
|
-
const changed = new Promise((resolve) => (resolveChange = resolve));
|
|
790
|
-
let rejectAbort;
|
|
791
|
-
const aborted = new Promise((_resolve, reject) => (rejectAbort = reject));
|
|
792
|
-
const abort = () => rejectAbort(executionAbortReason(signal));
|
|
793
|
-
managed.waiters.add(resolveChange);
|
|
794
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
795
|
-
try {
|
|
796
|
-
await Promise.race([
|
|
803
|
+
async function waitForProcessChange(managed, waitMs, signal, interrupt) {
|
|
804
|
+
let changed = false;
|
|
805
|
+
return waitForToolCondition({
|
|
806
|
+
ready: () => managed.status !== 'running' ||
|
|
807
|
+
managed.stdout.bytes > 0 ||
|
|
808
|
+
managed.stderr.bytes > 0 ||
|
|
797
809
|
changed,
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
810
|
+
subscribe: (notify) => {
|
|
811
|
+
const listener = () => {
|
|
812
|
+
changed = true;
|
|
813
|
+
notify();
|
|
814
|
+
};
|
|
815
|
+
managed.waiters.add(listener);
|
|
816
|
+
return () => {
|
|
817
|
+
managed.waiters.delete(listener);
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
waitMs,
|
|
821
|
+
signal,
|
|
822
|
+
interrupt
|
|
823
|
+
});
|
|
811
824
|
}
|
|
812
825
|
function executionAbortReason(signal) {
|
|
813
826
|
return (signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
|
|
@@ -38,7 +38,7 @@ export function structuredToolEventData(toolName, value) {
|
|
|
38
38
|
if (toolName === 'agent_wait' &&
|
|
39
39
|
(value.mode === 'any' || value.mode === 'all') &&
|
|
40
40
|
(value.waitFor === 'completion' || value.waitFor === 'message') &&
|
|
41
|
-
['snapshot', 'message', 'completion', 'timeout'].includes(String(value.waitOutcome)) &&
|
|
41
|
+
['snapshot', 'message', 'completion', 'timeout', 'interrupted'].includes(String(value.waitOutcome)) &&
|
|
42
42
|
Array.isArray(value.agents) &&
|
|
43
43
|
value.agents.length > 0 &&
|
|
44
44
|
value.agents.length <= TOOL_EXECUTION_LIMITS.agentWaitMaxAgents &&
|
|
@@ -54,6 +54,12 @@ export function structuredToolEventData(toolName, value) {
|
|
|
54
54
|
cellId: value.cellId,
|
|
55
55
|
status: value.status,
|
|
56
56
|
cached: value.cached === true,
|
|
57
|
+
...(['snapshot', 'completion', 'timeout', 'yield', 'interrupted'].includes(String(value.waitOutcome))
|
|
58
|
+
? { waitOutcome: value.waitOutcome }
|
|
59
|
+
: {}),
|
|
60
|
+
...(value.waitOutcome === 'interrupted' && value.reason === 'steer'
|
|
61
|
+
? { reason: 'steer' }
|
|
62
|
+
: {}),
|
|
57
63
|
...(isRecord(value.permissionStop)
|
|
58
64
|
? {
|
|
59
65
|
permissionStop: value.permissionStop
|
|
@@ -207,7 +213,8 @@ function isSubagentTaskSnapshot(value) {
|
|
|
207
213
|
value.waitOutcome === 'snapshot' ||
|
|
208
214
|
value.waitOutcome === 'message' ||
|
|
209
215
|
value.waitOutcome === 'completion' ||
|
|
210
|
-
value.waitOutcome === 'timeout'
|
|
216
|
+
value.waitOutcome === 'timeout' ||
|
|
217
|
+
value.waitOutcome === 'interrupted') &&
|
|
211
218
|
(value.latestMessage === undefined ||
|
|
212
219
|
(isRecord(value.latestMessage) &&
|
|
213
220
|
typeof value.latestMessage.itemId === 'string' &&
|
|
@@ -47,6 +47,7 @@ export interface ToolRuntimeContext {
|
|
|
47
47
|
executionId: string;
|
|
48
48
|
modelId?: string;
|
|
49
49
|
signal: AbortSignal;
|
|
50
|
+
waitInterrupt?: import('./wait.js').ToolWaitInterrupt | undefined;
|
|
50
51
|
permissionPolicy?: import('./permissions.js').ExecutionToolPermissions;
|
|
51
52
|
parentCallId?: string;
|
|
52
53
|
writeToolResultResource?: import('./result-extensions.js').ToolResultResourceWriter;
|
|
@@ -36,6 +36,7 @@ export async function executeCapturedTool(kind, call, context) {
|
|
|
36
36
|
return context.host.executeTool(call, {
|
|
37
37
|
mode: context.mode,
|
|
38
38
|
signal: context.signal,
|
|
39
|
+
waitInterrupt: context.waitInterrupt,
|
|
39
40
|
sessionId: context.sessionId,
|
|
40
41
|
executionId: context.executionId,
|
|
41
42
|
...(context.modelId === undefined ? {} : { modelId: context.modelId }),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ToolWaitOutcome = 'snapshot' | 'message' | 'completion' | 'timeout' | 'yield' | 'interrupted';
|
|
2
|
+
/** A wake-up source, deliberately independent of resource cancellation. */
|
|
3
|
+
export interface ToolWaitInterrupt {
|
|
4
|
+
pending(): boolean;
|
|
5
|
+
subscribe(listener: () => void): () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createToolWaitInterrupt(pending: () => boolean): {
|
|
8
|
+
pending: () => boolean;
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
notify(): void;
|
|
11
|
+
};
|
|
12
|
+
export declare function waitMetadata<T extends ToolWaitOutcome>(waitOutcome: T): {
|
|
13
|
+
waitOutcome: T;
|
|
14
|
+
reason?: 'steer';
|
|
15
|
+
};
|
|
16
|
+
/** All exits release listeners; a ready result wins over steer and the deadline. */
|
|
17
|
+
export declare function waitForToolCondition(input: {
|
|
18
|
+
ready(): boolean;
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
waitMs: number;
|
|
21
|
+
signal?: AbortSignal | undefined;
|
|
22
|
+
interrupt?: ToolWaitInterrupt | undefined;
|
|
23
|
+
}): Promise<'ready' | 'timeout' | 'interrupted'>;
|
|
24
|
+
/** Explicit waits retain their deadline without overflowing the platform timer. */
|
|
25
|
+
export declare function waitDeadline(waitMs: number, finish: () => void): () => void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { MarAgentError } from '../error.js';
|
|
2
|
+
export function createToolWaitInterrupt(pending) {
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
return {
|
|
5
|
+
pending,
|
|
6
|
+
subscribe(listener) {
|
|
7
|
+
listeners.add(listener);
|
|
8
|
+
return () => {
|
|
9
|
+
listeners.delete(listener);
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
notify() {
|
|
13
|
+
for (const listener of listeners)
|
|
14
|
+
listener();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function waitMetadata(waitOutcome) {
|
|
19
|
+
return { waitOutcome, ...(waitOutcome === 'interrupted' ? { reason: 'steer' } : {}) };
|
|
20
|
+
}
|
|
21
|
+
/** All exits release listeners; a ready result wins over steer and the deadline. */
|
|
22
|
+
export function waitForToolCondition(input) {
|
|
23
|
+
input.signal?.throwIfAborted();
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
let unsubscribe = () => { };
|
|
27
|
+
let unsubscribeInterrupt = () => { };
|
|
28
|
+
let cancelTimer = () => { };
|
|
29
|
+
const finish = (outcome, error) => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
unsubscribe();
|
|
34
|
+
unsubscribeInterrupt();
|
|
35
|
+
cancelTimer();
|
|
36
|
+
input.signal?.removeEventListener('abort', abort);
|
|
37
|
+
if (error === undefined)
|
|
38
|
+
resolve(outcome);
|
|
39
|
+
else
|
|
40
|
+
reject(error);
|
|
41
|
+
};
|
|
42
|
+
const check = () => {
|
|
43
|
+
if (input.signal?.aborted)
|
|
44
|
+
abort();
|
|
45
|
+
else if (input.ready())
|
|
46
|
+
finish('ready');
|
|
47
|
+
else if (input.interrupt?.pending())
|
|
48
|
+
finish('interrupted');
|
|
49
|
+
};
|
|
50
|
+
const abort = () => finish('interrupted', input.signal?.reason ??
|
|
51
|
+
new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
|
|
52
|
+
unsubscribe = input.subscribe(check);
|
|
53
|
+
unsubscribeInterrupt = input.interrupt?.subscribe(check) ?? (() => { });
|
|
54
|
+
cancelTimer = waitDeadline(input.waitMs, () => {
|
|
55
|
+
check();
|
|
56
|
+
finish('timeout');
|
|
57
|
+
});
|
|
58
|
+
input.signal?.addEventListener('abort', abort, { once: true });
|
|
59
|
+
check();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Explicit waits retain their deadline without overflowing the platform timer. */
|
|
63
|
+
export function waitDeadline(waitMs, finish) {
|
|
64
|
+
if (waitMs <= 2_147_483_647) {
|
|
65
|
+
const timer = setTimeout(finish, waitMs);
|
|
66
|
+
return () => clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
const deadline = performance.now() + waitMs;
|
|
69
|
+
let timer;
|
|
70
|
+
const tick = () => {
|
|
71
|
+
const remaining = deadline - performance.now();
|
|
72
|
+
if (remaining <= 0)
|
|
73
|
+
finish();
|
|
74
|
+
else
|
|
75
|
+
arm(remaining);
|
|
76
|
+
};
|
|
77
|
+
const arm = (remaining) => {
|
|
78
|
+
timer = setTimeout(tick, Math.min(remaining, 2_147_483_647));
|
|
79
|
+
};
|
|
80
|
+
arm(waitMs);
|
|
81
|
+
return () => clearTimeout(timer);
|
|
82
|
+
}
|