@myagentroam/agent 0.10.51 → 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.
@@ -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) => wait({ ...options, agentId: id }),
99
- outputAny: (options) => wait({ ...options }),
100
- outputMany: (ids, options) => wait({ ...options, agentIds: ids }),
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
@@ -16,4 +16,5 @@ export declare function createNativeSteer(input: {
16
16
  status: 'accepted';
17
17
  }>['entry']): Promise<unknown>;
18
18
  diagnose(): void;
19
+ wake(): void;
19
20
  }): (request: SteerInput) => Promise<SteerResult>;
@@ -60,6 +60,7 @@ export function createNativeSteer(input) {
60
60
  void input.notify(historyEntry).catch(() => input.diagnose());
61
61
  ready.resolve();
62
62
  result.resolve({ status: 'accepted', entry: historyEntry });
63
+ input.wake();
63
64
  return {};
64
65
  })));
65
66
  void admission
@@ -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 = 'snapshot' | 'message' | 'completion' | 'timeout';
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" | "running" | "interrupted" | "queued";
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, waitDeadline } from './wait.js';
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
- waitOutcome: completed ? 'completion' : 'timeout'
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
- waitOutcome: activity?.kind === 'message'
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
- waitOutcome: activity?.kind === 'message'
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
- waitOutcome: completed ? 'completion' : 'timeout'
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
- : 'timeout';
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
- waitOutcome: outcome
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
- waitOutcome: snapshot
488
+ ...waitMetadata(snapshot
491
489
  ? 'snapshot'
492
490
  : !satisfied
493
- ? 'timeout'
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 waitForCompletionNotification(this.#completionWaiters, (state) => candidates.includes(state), waitMs, signal);
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 waitForCompletionNotification(this.#completionWaiters, (completed) => completed === state, waitMs, signal);
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 waitForCompletionNotification(waiters, accepts, waitMs, signal) {
1305
- return new Promise((resolve, reject) => {
1306
- let settled = false;
1307
- const finish = (error) => {
1308
- if (settled)
1309
- return;
1310
- settled = true;
1311
- cancelTimer();
1312
- signal?.removeEventListener('abort', abort);
1313
- waiters.delete(notify);
1314
- if (error === undefined)
1315
- resolve(undefined);
1316
- else
1317
- reject(error);
1318
- };
1319
- const notify = (value) => {
1320
- if (!accepts(value) || settled)
1321
- return;
1322
- settled = true;
1323
- cancelTimer();
1324
- signal?.removeEventListener('abort', abort);
1325
- waiters.delete(notify);
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.signal?.throwIfAborted();
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
- if (cell.status === 'running' && !cell.yielded) {
183
- await new Promise((resolve, reject) => {
184
- const finish = () => {
185
- clearTimeout(timer);
186
- cell.listeners.delete(finish);
187
- signal.removeEventListener('abort', abort);
188
- resolve();
189
- };
190
- const abort = () => {
191
- clearTimeout(timer);
192
- cell.listeners.delete(finish);
193
- signal.removeEventListener('abort', abort);
194
- reject(signal.reason);
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']>): Promise<ExecResult>;
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 { ...completed.result, cached: true };
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 result;
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
- await waitForExitOrDelay(managed.settled, Math.min(input.yieldMs ?? TOOL_EXECUTION_LIMITS.execDefaultYieldMs, TOOL_EXECUTION_LIMITS.execMaxYieldMs), signal);
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 result;
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
- if (managed.status !== 'running' || waitMs === 0)
784
- return;
785
- if (signal?.aborted)
786
- throw executionAbortReason(signal);
787
- let timer;
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
- aborted,
799
- new Promise((resolve) => {
800
- timer = setTimeout(resolve, waitMs);
801
- timer.unref();
802
- })
803
- ]);
804
- }
805
- finally {
806
- managed.waiters.delete(resolveChange);
807
- signal?.removeEventListener('abort', abort);
808
- if (timer)
809
- clearTimeout(timer);
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.'));
@@ -90,7 +90,7 @@ export class LocalToolRegistry {
90
90
  ? {}
91
91
  : { ownerInstanceId: context.operation.ownerInstanceId })
92
92
  })
93
- });
93
+ }, context.waitInterrupt);
94
94
  return formatExecResult(result);
95
95
  }
96
96
  ])
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.51",
3
+ "version": "0.10.53",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",