@myagentroam/agent 0.9.66 → 0.9.68

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.
@@ -1,4 +1,4 @@
1
- export declare const subagentPrompt = "# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. The parent retains responsibility for the final result.";
1
+ export declare const subagentPrompt = "# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.";
2
2
  export interface SubagentModelOption {
3
3
  readonly id: string;
4
4
  readonly name: string;
@@ -1,4 +1,4 @@
1
- export const subagentPrompt = '# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. The parent retains responsibility for the final result.';
1
+ export const subagentPrompt = '# Subagents\nUse your own direct tools by default. Delegate only when a bounded task can proceed independently and parallelism has real value; do not delegate ordinary local search, a known-file read, a single verification command, a small edit, or sequential debugging. Give each child a self-contained objective, relevant paths and constraints, expected evidence, and a clear read/write boundary. Subagents cannot recurse and share the real filesystem; do not assign conflicting writes or assume isolation. Track background agent IDs, wait or cancel them, inspect their actual workspace effects, and verify every claim before incorporating it. Continue meaningful non-overlapping work after starting a background child. Wait for completion only when the next critical-path action is blocked on the final result, and then prefer one long wait measured in minutes rather than repeated short waits. Wait for a public message only when a fresh progress update would change the next decision; do not use message waits as heartbeat checks or request progress merely to confirm that a child is still running. The parent retains responsibility for the final result.';
2
2
  export function currentAgentModelPrompt(model) {
3
3
  if (!model)
4
4
  return '';
package/dist/sdk/agent.js CHANGED
@@ -1841,6 +1841,11 @@ function isSubagentTaskSnapshot(value) {
1841
1841
  (value.reasoningEffort === undefined ||
1842
1842
  (typeof value.reasoningEffort === 'string' &&
1843
1843
  ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(value.reasoningEffort))) &&
1844
+ (value.waitOutcome === undefined ||
1845
+ value.waitOutcome === 'snapshot' ||
1846
+ value.waitOutcome === 'message' ||
1847
+ value.waitOutcome === 'completion' ||
1848
+ value.waitOutcome === 'timeout') &&
1844
1849
  (value.latestMessage === undefined ||
1845
1850
  (isRecord(value.latestMessage) &&
1846
1851
  typeof value.latestMessage.itemId === 'string' &&
@@ -37,6 +37,7 @@ export interface ContextGcSubagentSnapshot {
37
37
  readonly messageId?: string;
38
38
  readonly turnId?: string;
39
39
  readonly deliveryStatus?: 'accepted' | 'applied' | 'cancelled' | 'failed';
40
+ readonly waitOutcome?: 'snapshot' | 'message' | 'completion' | 'timeout';
40
41
  }
41
42
  export declare function normalizeContextGcSubagentSnapshot(modelContent: unknown, completedPayload: unknown, toolName: ContextGcSubagentToolName): ContextGcSubagentSnapshot | undefined;
42
43
  export declare function contextGcCompletedSubagentTurnId(snapshot: ContextGcSubagentSnapshot): string | undefined;
@@ -107,6 +107,7 @@ export function contextGcSubagentOutputReplacement(input) {
107
107
  ...(input.source.deliveryStatus === undefined
108
108
  ? {}
109
109
  : { deliveryStatus: input.source.deliveryStatus }),
110
+ ...(input.source.waitOutcome === undefined ? {} : { waitOutcome: input.source.waitOutcome }),
110
111
  ...(latestMessage === undefined
111
112
  ? {}
112
113
  : {
@@ -172,7 +173,8 @@ function parseContextGcSubagentSnapshot(value) {
172
173
  'errorMessage',
173
174
  'messageId',
174
175
  'turnId',
175
- 'deliveryStatus'
176
+ 'deliveryStatus',
177
+ 'waitOutcome'
176
178
  ].includes(key)) ||
177
179
  !isNonemptyString(parsed.agentId) ||
178
180
  !isNonemptyString(parsed.description) ||
@@ -195,6 +197,11 @@ function parseContextGcSubagentSnapshot(value) {
195
197
  parsed.deliveryStatus !== 'applied' &&
196
198
  parsed.deliveryStatus !== 'cancelled' &&
197
199
  parsed.deliveryStatus !== 'failed') ||
200
+ (parsed.waitOutcome !== undefined &&
201
+ parsed.waitOutcome !== 'snapshot' &&
202
+ parsed.waitOutcome !== 'message' &&
203
+ parsed.waitOutcome !== 'completion' &&
204
+ parsed.waitOutcome !== 'timeout') ||
198
205
  !isOptionalStringArray(parsed.changedFiles) ||
199
206
  !isOptionalStringArray(parsed.verification) ||
200
207
  !isOptionalSubagentEvidence(parsed.evidence))
@@ -233,6 +240,11 @@ function parseContextGcSubagentSnapshot(value) {
233
240
  ? {
234
241
  deliveryStatus: parsed.deliveryStatus
235
242
  }
243
+ : {}),
244
+ ...(typeof parsed.waitOutcome === 'string'
245
+ ? {
246
+ waitOutcome: parsed.waitOutcome
247
+ }
236
248
  : {})
237
249
  };
238
250
  }
@@ -58,7 +58,10 @@ export interface SubagentTaskSnapshot extends Record<string, unknown> {
58
58
  messageId?: string;
59
59
  turnId?: string;
60
60
  deliveryStatus?: 'accepted' | 'applied' | 'cancelled' | 'failed';
61
+ waitOutcome?: SubagentWaitOutcome;
61
62
  }
63
+ export type SubagentWaitFor = 'completion' | 'message';
64
+ export type SubagentWaitOutcome = 'snapshot' | 'message' | 'completion' | 'timeout';
62
65
  export interface SubagentMessageOptions {
63
66
  delivery?: 'append' | 'replace';
64
67
  signal?: AbortSignal;
@@ -69,6 +72,7 @@ export interface SubagentRunOptions {
69
72
  }
70
73
  export interface SubagentOutputOptions {
71
74
  waitMs?: number;
75
+ waitFor?: SubagentWaitFor;
72
76
  signal?: AbortSignal;
73
77
  }
74
78
  export interface SubagentController {
@@ -97,8 +101,100 @@ export declare class SubagentScheduler implements SubagentController {
97
101
  });
98
102
  beginExecution(runner: (task: SubagentTaskInput, signal: AbortSignal) => Promise<SubagentTaskResult>): void;
99
103
  run(input: SubagentTaskInput, options?: SubagentRunOptions): Promise<SubagentTaskSnapshot>;
100
- output(agentId: string, options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
101
- outputAny(options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
104
+ output(agentId: string, options?: SubagentOutputOptions): Promise<{
105
+ waitOutcome: "snapshot";
106
+ agentId: string;
107
+ description: string;
108
+ status: "queued" | "running" | "completed" | "failed" | "cancelled" | "interrupted";
109
+ modelId?: string;
110
+ modelName?: string;
111
+ reasoningEffort?: MarAgentReasoningEffort;
112
+ latestMessage?: SubagentLatestMessage;
113
+ latestActivity?: SubagentLatestActivity;
114
+ summary?: string;
115
+ changedFiles?: string[];
116
+ verification?: string[];
117
+ evidence?: Array<{
118
+ path: string;
119
+ startLine?: number;
120
+ endLine?: number;
121
+ }>;
122
+ errorCode?: string;
123
+ errorMessage?: string;
124
+ messageId?: string;
125
+ turnId?: string;
126
+ deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
127
+ } | {
128
+ waitOutcome: "timeout" | "completion";
129
+ agentId: string;
130
+ description: string;
131
+ status: "queued" | "running" | "completed" | "failed" | "cancelled" | "interrupted";
132
+ modelId?: string;
133
+ modelName?: string;
134
+ reasoningEffort?: MarAgentReasoningEffort;
135
+ latestMessage?: SubagentLatestMessage;
136
+ latestActivity?: SubagentLatestActivity;
137
+ summary?: string;
138
+ changedFiles?: string[];
139
+ verification?: string[];
140
+ evidence?: Array<{
141
+ path: string;
142
+ startLine?: number;
143
+ endLine?: number;
144
+ }>;
145
+ errorCode?: string;
146
+ errorMessage?: string;
147
+ messageId?: string;
148
+ turnId?: string;
149
+ deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
150
+ }>;
151
+ outputAny(options?: SubagentOutputOptions): Promise<{
152
+ waitOutcome: "snapshot";
153
+ agentId: string;
154
+ description: string;
155
+ status: "queued" | "running" | "completed" | "failed" | "cancelled" | "interrupted";
156
+ modelId?: string;
157
+ modelName?: string;
158
+ reasoningEffort?: MarAgentReasoningEffort;
159
+ latestMessage?: SubagentLatestMessage;
160
+ latestActivity?: SubagentLatestActivity;
161
+ summary?: string;
162
+ changedFiles?: string[];
163
+ verification?: string[];
164
+ evidence?: Array<{
165
+ path: string;
166
+ startLine?: number;
167
+ endLine?: number;
168
+ }>;
169
+ errorCode?: string;
170
+ errorMessage?: string;
171
+ messageId?: string;
172
+ turnId?: string;
173
+ deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
174
+ } | {
175
+ waitOutcome: "timeout" | "completion";
176
+ agentId: string;
177
+ description: string;
178
+ status: "queued" | "running" | "completed" | "failed" | "cancelled" | "interrupted";
179
+ modelId?: string;
180
+ modelName?: string;
181
+ reasoningEffort?: MarAgentReasoningEffort;
182
+ latestMessage?: SubagentLatestMessage;
183
+ latestActivity?: SubagentLatestActivity;
184
+ summary?: string;
185
+ changedFiles?: string[];
186
+ verification?: string[];
187
+ evidence?: Array<{
188
+ path: string;
189
+ startLine?: number;
190
+ endLine?: number;
191
+ }>;
192
+ errorCode?: string;
193
+ errorMessage?: string;
194
+ messageId?: string;
195
+ turnId?: string;
196
+ deliveryStatus?: "accepted" | "applied" | "cancelled" | "failed";
197
+ }>;
102
198
  cancel(agentId: string): Promise<SubagentTaskSnapshot>;
103
199
  message(_agentId: string, _message: string): Promise<SubagentTaskSnapshot>;
104
200
  listSessionResources(): {
@@ -81,37 +81,43 @@ export class SubagentScheduler {
81
81
  }
82
82
  async output(agentId, options = {}) {
83
83
  options.signal?.throwIfAborted();
84
+ rejectUnsupportedMessageWait(options);
84
85
  const task = this.#tasks.get(agentId);
85
86
  if (!task)
86
87
  throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent task was not found.');
87
- if (options.waitMs !== undefined &&
88
- (!Number.isInteger(options.waitMs) ||
89
- options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
90
- options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs))
91
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
92
- const waitMs = options.waitMs === undefined || options.waitMs === 0
93
- ? undefined
94
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
95
- if (waitMs !== undefined && !['completed', 'failed', 'cancelled'].includes(task.status))
96
- await this.#waitForCompletion(task, waitMs, options.signal);
97
- return this.#public(task);
88
+ validateOutputOptions(options);
89
+ const terminal = ['completed', 'failed', 'cancelled'].includes(task.status);
90
+ if (options.waitMs === undefined || options.waitMs === 0)
91
+ return { ...this.#public(task), waitOutcome: 'snapshot' };
92
+ if (terminal)
93
+ return { ...this.#public(task), waitOutcome: 'completion' };
94
+ const waitMs = Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
95
+ const completed = await this.#waitForCompletion(task, waitMs, options.signal);
96
+ options.signal?.throwIfAborted();
97
+ return {
98
+ ...this.#public(task),
99
+ waitOutcome: completed ? 'completion' : 'timeout'
100
+ };
98
101
  }
99
102
  async outputAny(options = {}) {
100
103
  options.signal?.throwIfAborted();
101
104
  validateOutputOptions(options);
105
+ rejectUnsupportedMessageWait(options);
102
106
  const tasks = [...this.#tasks.values()];
103
107
  if (tasks.length === 0)
104
108
  throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent task was not found.');
105
109
  const live = tasks.filter((task) => task.status === 'queued' || task.status === 'running');
106
- if (live.length === 0 || options.waitMs === 0)
107
- return this.#public(live.at(-1) ?? tasks.at(-1));
108
- const waitMs = options.waitMs === undefined
109
- ? undefined
110
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
111
- if (waitMs !== undefined)
112
- await this.#waitForAnyCompletion(live, waitMs, options.signal);
113
- const completed = live.find((task) => task.status !== 'queued' && task.status !== 'running');
114
- return this.#public(completed ?? live.at(-1));
110
+ if (options.waitMs === undefined || options.waitMs === 0)
111
+ return { ...this.#public(live.at(-1) ?? tasks.at(-1)), waitOutcome: 'snapshot' };
112
+ if (live.length === 0)
113
+ return { ...this.#public(tasks.at(-1)), waitOutcome: 'completion' };
114
+ const waitMs = Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
115
+ const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal);
116
+ options.signal?.throwIfAborted();
117
+ return {
118
+ ...this.#public(completed ?? live.at(-1)),
119
+ waitOutcome: completed ? 'completion' : 'timeout'
120
+ };
115
121
  }
116
122
  async cancel(agentId) {
117
123
  const task = this.#tasks.get(agentId);
@@ -259,13 +265,20 @@ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
259
265
  signal?.removeEventListener('abort', abort);
260
266
  waiters.delete(notify);
261
267
  if (error === undefined)
262
- resolve();
268
+ resolve(undefined);
263
269
  else
264
270
  reject(error);
265
271
  };
266
272
  const notify = (value) => {
267
- if (accepts(value))
268
- finish();
273
+ if (accepts(value)) {
274
+ if (settled)
275
+ return;
276
+ settled = true;
277
+ clearTimeout(timer);
278
+ signal?.removeEventListener('abort', abort);
279
+ waiters.delete(notify);
280
+ resolve(value);
281
+ }
269
282
  };
270
283
  const abort = () => finish(signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
271
284
  const timer = setTimeout(finish, waitMs);
@@ -277,12 +290,19 @@ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
277
290
  });
278
291
  }
279
292
  function validateOutputOptions(options) {
280
- if (options.waitMs !== undefined &&
281
- (!Number.isInteger(options.waitMs) ||
282
- options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
283
- options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs))
293
+ if ((options.waitFor !== undefined &&
294
+ options.waitFor !== 'completion' &&
295
+ options.waitFor !== 'message') ||
296
+ (options.waitMs !== undefined &&
297
+ (!Number.isInteger(options.waitMs) ||
298
+ options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
299
+ options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
284
300
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
285
301
  }
302
+ function rejectUnsupportedMessageWait(options) {
303
+ if (options.waitFor === 'message')
304
+ throw new MarAgentError('MAR_AGENT_TOOL_NOT_AVAILABLE', 'Message waits require a session-backed subagent controller.');
305
+ }
286
306
  function linkAbort(signal, abort) {
287
307
  const listener = () => abort(signal.reason);
288
308
  signal.addEventListener('abort', listener, { once: true });
@@ -9,6 +9,8 @@ export class SubagentSessionController {
9
9
  #agents = new Map();
10
10
  #queue = [];
11
11
  #completionWaiters = new Set();
12
+ #activityWaiters = new Set();
13
+ #activityRevision = 0;
12
14
  #running = 0;
13
15
  #disposed = false;
14
16
  #createdInExecution = 0;
@@ -69,6 +71,9 @@ export class SubagentSessionController {
69
71
  description: source.description,
70
72
  role: input.role,
71
73
  status: 'queued',
74
+ messageRevision: 0,
75
+ observedMessageRevision: 0,
76
+ latestMessageActivityRevision: 0,
72
77
  changedFiles: [],
73
78
  verification: [],
74
79
  evidence: [],
@@ -121,19 +126,42 @@ export class SubagentSessionController {
121
126
  async output(agentId, options = {}) {
122
127
  options.signal?.throwIfAborted();
123
128
  const state = await this.#find(agentId);
124
- if (options.waitMs !== undefined &&
125
- (!Number.isInteger(options.waitMs) ||
126
- options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
127
- options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs))
128
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
129
- const waitMs = options.waitMs === undefined || options.waitMs === 0
130
- ? undefined
131
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
132
- if (waitMs !== undefined &&
133
- state.turn &&
134
- (state.status === 'queued' || state.status === 'running'))
135
- await this.#waitForCompletion(state, waitMs, options.signal);
136
- return this.#snapshot(state);
129
+ options.signal?.throwIfAborted();
130
+ validateOutputOptions(options);
131
+ if (options.waitMs === undefined || options.waitMs === 0)
132
+ return { ...this.#snapshot(state), waitOutcome: 'snapshot' };
133
+ if (isTerminalState(state)) {
134
+ this.#observeLatestMessage(state);
135
+ return { ...this.#snapshot(state), waitOutcome: 'completion' };
136
+ }
137
+ const waitFor = options.waitFor ?? 'completion';
138
+ if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
139
+ this.#observeLatestMessage(state);
140
+ return { ...this.#snapshot(state), waitOutcome: 'message' };
141
+ }
142
+ const waitMs = Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
143
+ if (waitFor === 'completion') {
144
+ const completed = await this.#waitForCompletion(state, waitMs, options.signal);
145
+ options.signal?.throwIfAborted();
146
+ if (completed)
147
+ this.#observeLatestMessage(state);
148
+ return {
149
+ ...this.#snapshot(state),
150
+ waitOutcome: completed ? 'completion' : 'timeout'
151
+ };
152
+ }
153
+ const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal);
154
+ options.signal?.throwIfAborted();
155
+ if (activity)
156
+ this.#observeLatestMessage(activity.state);
157
+ return {
158
+ ...this.#snapshot(activity?.state ?? state),
159
+ waitOutcome: activity?.kind === 'message'
160
+ ? 'message'
161
+ : activity?.kind === 'completion'
162
+ ? 'completion'
163
+ : 'timeout'
164
+ };
137
165
  }
138
166
  async outputAny(options = {}) {
139
167
  options.signal?.throwIfAborted();
@@ -142,16 +170,47 @@ export class SubagentSessionController {
142
170
  if (states.length === 0)
143
171
  throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
144
172
  const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
145
- if (live.length === 0 || options.waitMs === 0)
146
- return this.#snapshot(live.at(-1) ?? states.at(-1));
147
- const waitMs = options.waitMs === undefined
148
- ? undefined
149
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
150
- if (waitMs !== undefined) {
151
- await this.#waitForAnyCompletion(live, waitMs, options.signal);
173
+ if (options.waitMs === undefined || options.waitMs === 0)
174
+ return {
175
+ ...this.#snapshot(live.at(-1) ?? states.at(-1)),
176
+ waitOutcome: 'snapshot'
177
+ };
178
+ if (live.length === 0) {
179
+ const state = states.at(-1);
180
+ this.#observeLatestMessage(state);
181
+ return { ...this.#snapshot(state), waitOutcome: 'completion' };
152
182
  }
153
- const completed = live.find((state) => state.status !== 'queued' && state.status !== 'running');
154
- return this.#snapshot(completed ?? live.at(-1));
183
+ const waitFor = options.waitFor ?? 'completion';
184
+ if (waitFor === 'message') {
185
+ const pending = this.#oldestUnreadMessage(live);
186
+ if (pending) {
187
+ this.#observeLatestMessage(pending);
188
+ return { ...this.#snapshot(pending), waitOutcome: 'message' };
189
+ }
190
+ }
191
+ const waitMs = Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
192
+ if (waitFor === 'message') {
193
+ const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal);
194
+ options.signal?.throwIfAborted();
195
+ if (activity)
196
+ this.#observeLatestMessage(activity.state);
197
+ return {
198
+ ...this.#snapshot(activity?.state ?? live.at(-1)),
199
+ waitOutcome: activity?.kind === 'message'
200
+ ? 'message'
201
+ : activity?.kind === 'completion'
202
+ ? 'completion'
203
+ : 'timeout'
204
+ };
205
+ }
206
+ const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal);
207
+ options.signal?.throwIfAborted();
208
+ if (completed)
209
+ this.#observeLatestMessage(completed);
210
+ return {
211
+ ...this.#snapshot(completed ?? live.at(-1)),
212
+ waitOutcome: completed ? 'completion' : 'timeout'
213
+ };
155
214
  }
156
215
  async message(agentId, message, options = {}) {
157
216
  const execution = this.#requireExecution();
@@ -340,8 +399,13 @@ export class SubagentSessionController {
340
399
  async #consumeTurn(state, turn) {
341
400
  try {
342
401
  const consume = (async () => {
343
- for await (const event of turn.handle)
344
- observeAgentEvent(state, event);
402
+ for await (const event of turn.handle) {
403
+ if (!observeAgentEvent(state, event))
404
+ continue;
405
+ state.messageRevision++;
406
+ state.latestMessageActivityRevision = ++this.#activityRevision;
407
+ this.#notifyActivity({ state, kind: 'message' });
408
+ }
345
409
  })();
346
410
  const result = await turn.handle.result();
347
411
  await consume;
@@ -402,6 +466,32 @@ export class SubagentSessionController {
402
466
  #notifyCompletion(state) {
403
467
  for (const waiter of this.#completionWaiters)
404
468
  waiter(state);
469
+ this.#notifyActivity({ state, kind: 'completion' });
470
+ }
471
+ #waitForMessageOrCompletion(candidates, waitMs, signal) {
472
+ return waitForNotification(this.#activityWaiters, (activity) => candidates.includes(activity.state), () => {
473
+ const pending = this.#oldestUnreadMessage(candidates);
474
+ if (pending)
475
+ return { state: pending, kind: 'message' };
476
+ const terminal = candidates.find(isTerminalState);
477
+ return terminal ? { state: terminal, kind: 'completion' } : undefined;
478
+ }, waitMs, signal);
479
+ }
480
+ #notifyActivity(activity) {
481
+ for (const waiter of this.#activityWaiters)
482
+ waiter(activity);
483
+ }
484
+ #hasUnreadMessage(state) {
485
+ return state.messageRevision > state.observedMessageRevision;
486
+ }
487
+ #oldestUnreadMessage(states) {
488
+ return states
489
+ .filter((state) => this.#hasUnreadMessage(state))
490
+ .sort((left, right) => left.latestMessageActivityRevision - right.latestMessageActivityRevision)
491
+ .at(0);
492
+ }
493
+ #observeLatestMessage(state) {
494
+ state.observedMessageRevision = state.messageRevision;
405
495
  }
406
496
  #closeReceipt(state) {
407
497
  if (state.receipt?.deliveryStatus === 'accepted' && state.receipt.turnId === state.turn?.turnId)
@@ -571,6 +661,9 @@ function restoreAgentState(session) {
571
661
  ...(isReasoningEffort(header?.reasoningEffort)
572
662
  ? { reasoningEffort: header.reasoningEffort }
573
663
  : {}),
664
+ messageRevision: 0,
665
+ observedMessageRevision: 0,
666
+ latestMessageActivityRevision: 0,
574
667
  changedFiles: [],
575
668
  verification: [],
576
669
  evidence: [],
@@ -581,8 +674,10 @@ function restoreAgentState(session) {
581
674
  !['message.completed', 'tool.started', 'tool.completed', 'tool.failed'].includes(eventType(record) ?? ''))
582
675
  continue;
583
676
  const event = record.payload;
584
- observeAgentEvent(state, event);
677
+ if (observeAgentEvent(state, event))
678
+ state.messageRevision++;
585
679
  }
680
+ state.observedMessageRevision = state.messageRevision;
586
681
  if (state.status === 'completed' && state.latestMessage?.phase === 'final_answer')
587
682
  state.summary = truncateUtf8(state.latestMessage.text, TOOL_EXECUTION_LIMITS.subagentSummaryMaxBytes);
588
683
  if (state.status === 'failed' && terminal && isRecord(terminal.payload)) {
@@ -601,7 +696,7 @@ function observeAgentEvent(state, event) {
601
696
  timestamp: event.timestamp,
602
697
  phase: event.phase
603
698
  };
604
- return;
699
+ return true;
605
700
  }
606
701
  if (event.type === 'tool.started') {
607
702
  state.latestActivity = {
@@ -610,7 +705,7 @@ function observeAgentEvent(state, event) {
610
705
  toolName: event.toolName,
611
706
  status: 'started'
612
707
  };
613
- return;
708
+ return false;
614
709
  }
615
710
  if (event.type === 'tool.failed') {
616
711
  state.latestActivity = {
@@ -619,10 +714,10 @@ function observeAgentEvent(state, event) {
619
714
  toolName: event.toolName,
620
715
  status: 'failed'
621
716
  };
622
- return;
717
+ return false;
623
718
  }
624
719
  if (event.type !== 'tool.completed')
625
- return;
720
+ return false;
626
721
  state.latestActivity = {
627
722
  kind: 'tool',
628
723
  timestamp: event.timestamp,
@@ -664,6 +759,7 @@ function observeAgentEvent(state, event) {
664
759
  else if (event.toolName === 'exec' &&
665
760
  state.verification.length < TOOL_EXECUTION_LIMITS.subagentVerificationMaxItems)
666
761
  state.verification.push(truncateUtf8(event.summary, 2_000));
762
+ return false;
667
763
  }
668
764
  function eventType(record) {
669
765
  return record && isRecord(record.payload) && typeof record.payload.type === 'string'
@@ -738,13 +834,18 @@ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
738
834
  signal?.removeEventListener('abort', abort);
739
835
  waiters.delete(notify);
740
836
  if (error === undefined)
741
- resolve();
837
+ resolve(undefined);
742
838
  else
743
839
  reject(error);
744
840
  };
745
841
  const notify = (value) => {
746
- if (accepts(value))
747
- finish();
842
+ if (!accepts(value) || settled)
843
+ return;
844
+ settled = true;
845
+ clearTimeout(timer);
846
+ signal?.removeEventListener('abort', abort);
847
+ waiters.delete(notify);
848
+ resolve(value);
748
849
  };
749
850
  const abort = () => finish(signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
750
851
  const timer = setTimeout(finish, waitMs);
@@ -755,13 +856,55 @@ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
755
856
  abort();
756
857
  });
757
858
  }
859
+ function waitForNotification(waiters, accepts, current, waitMs, signal) {
860
+ return new Promise((resolve, reject) => {
861
+ let settled = false;
862
+ const finish = (value, error) => {
863
+ if (settled)
864
+ return;
865
+ settled = true;
866
+ clearTimeout(timer);
867
+ signal?.removeEventListener('abort', abort);
868
+ waiters.delete(notify);
869
+ if (error === undefined)
870
+ resolve(value);
871
+ else
872
+ reject(error);
873
+ };
874
+ const notify = (value) => {
875
+ if (accepts(value))
876
+ finish(value);
877
+ };
878
+ const abort = () => finish(undefined, signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
879
+ const timer = setTimeout(() => finish(), waitMs);
880
+ timer.unref();
881
+ waiters.add(notify);
882
+ signal?.addEventListener('abort', abort, { once: true });
883
+ if (signal?.aborted)
884
+ abort();
885
+ else {
886
+ const pending = current();
887
+ if (pending !== undefined)
888
+ finish(pending);
889
+ }
890
+ });
891
+ }
758
892
  function validateOutputOptions(options) {
759
- if (options.waitMs !== undefined &&
760
- (!Number.isInteger(options.waitMs) ||
761
- options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
762
- options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs))
893
+ if ((options.waitFor !== undefined &&
894
+ options.waitFor !== 'completion' &&
895
+ options.waitFor !== 'message') ||
896
+ (options.waitMs !== undefined &&
897
+ (!Number.isInteger(options.waitMs) ||
898
+ options.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
899
+ options.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
763
900
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
764
901
  }
902
+ function isTerminalState(state) {
903
+ return (state.status === 'completed' ||
904
+ state.status === 'failed' ||
905
+ state.status === 'cancelled' ||
906
+ state.status === 'interrupted');
907
+ }
765
908
  function linkAbort(signal, abort) {
766
909
  const listener = () => abort();
767
910
  signal.addEventListener('abort', listener, { once: true });
@@ -3,11 +3,17 @@ import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
3
3
  export const agentWaitToolDefinition = {
4
4
  name: 'agent_wait',
5
5
  parallelSafety: 'serial',
6
- description: 'Read or wait a bounded time for a child agent result. 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 with action:"poll" instead. 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. Every result includes the real agentId and description. latestMessage is the most recent public assistant commentary/final message, never hidden reasoning or a tool result; latestActivity identifies whether the latest observed activity was a public message or a bounded tool started/completed/failed status. To request a fresh textual progress report, send an append agent_message and then wait. A queued or running result is not completion; wait again only when useful, or cancel if no longer needed. 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. Avoid busy polling.',
6
+ description: 'Read or wait a bounded time for a child agent result. 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 with action:"poll" instead. 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. Every result includes waitOutcome as snapshot, message, completion, or timeout plus the real agentId and description. latestMessage is the most recent public assistant commentary/final message, never hidden reasoning or a tool result; latestActivity identifies whether the latest observed activity was a public message or a 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. 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.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
10
10
  agentId: { type: 'string' },
11
+ waitFor: {
12
+ type: 'string',
13
+ enum: ['completion', 'message'],
14
+ default: 'completion',
15
+ description: 'Wait for terminal completion or for the latest unread public message. Terminal state ends either wait.'
16
+ },
11
17
  waitMs: {
12
18
  type: 'integer',
13
19
  minimum: TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs,
@@ -26,6 +32,7 @@ export async function executeAgentWait(arguments_, context) {
26
32
  throw new MarAgentError('MAR_AGENT_SUBAGENT_RECURSION_DISABLED', 'Subagents cannot recurse.');
27
33
  const options = {
28
34
  waitMs: value.waitMs ?? TOOL_EXECUTION_LIMITS.agentWaitDefaultWaitMs,
35
+ waitFor: value.waitFor ?? 'completion',
29
36
  signal: context.signal
30
37
  };
31
38
  const task = value.agentId === undefined
@@ -37,18 +44,22 @@ function parseAgentWaitInput(value) {
37
44
  if (!value || typeof value !== 'object' || Array.isArray(value))
38
45
  return invalidAgentWait();
39
46
  const input = value;
40
- if (Object.keys(input).some((key) => !['agentId', 'waitMs'].includes(key)) ||
47
+ if (Object.keys(input).some((key) => !['agentId', 'waitFor', 'waitMs'].includes(key)) ||
41
48
  (input.agentId !== undefined &&
42
- input.agentId !== null &&
43
49
  (typeof input.agentId !== 'string' || input.agentId.trim().length === 0)) ||
50
+ (input.waitFor !== undefined &&
51
+ input.waitFor !== 'completion' &&
52
+ input.waitFor !== 'message') ||
44
53
  (input.waitMs !== undefined &&
45
- input.waitMs !== null &&
46
54
  (!Number.isInteger(input.waitMs) ||
47
55
  input.waitMs < TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs ||
48
56
  input.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
49
57
  return invalidAgentWait();
50
58
  return {
51
59
  ...(typeof input.agentId === 'string' ? { agentId: input.agentId.trim() } : {}),
60
+ ...(input.waitFor === 'completion' || input.waitFor === 'message'
61
+ ? { waitFor: input.waitFor }
62
+ : {}),
52
63
  ...(typeof input.waitMs === 'number' ? { waitMs: input.waitMs } : {})
53
64
  };
54
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.66",
3
+ "version": "0.9.68",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",