@myagentroam/agent 0.9.65 → 0.9.66

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.
@@ -8,6 +8,7 @@ export class SubagentSessionController {
8
8
  limits;
9
9
  #agents = new Map();
10
10
  #queue = [];
11
+ #completionWaiters = new Set();
11
12
  #running = 0;
12
13
  #disposed = false;
13
14
  #createdInExecution = 0;
@@ -20,7 +21,8 @@ export class SubagentSessionController {
20
21
  }
21
22
  static async open(input) {
22
23
  const controller = new SubagentSessionController(input.parentSessionId, input.rootSessionId, input.backend, input.limits);
23
- for (const session of await input.backend.listChildren(input.parentSessionId))
24
+ const sessions = await input.backend.listChildren(input.parentSessionId);
25
+ for (const session of sessions)
24
26
  controller.#agents.set(session.id, restoreAgentState(session));
25
27
  return controller;
26
28
  }
@@ -130,9 +132,27 @@ export class SubagentSessionController {
130
132
  if (waitMs !== undefined &&
131
133
  state.turn &&
132
134
  (state.status === 'queued' || state.status === 'running'))
133
- await waitForCompletion(state.turn.completion, waitMs, options.signal);
135
+ await this.#waitForCompletion(state, waitMs, options.signal);
134
136
  return this.#snapshot(state);
135
137
  }
138
+ async outputAny(options = {}) {
139
+ options.signal?.throwIfAborted();
140
+ validateOutputOptions(options);
141
+ const states = [...this.#agents.values()];
142
+ if (states.length === 0)
143
+ throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
144
+ 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);
152
+ }
153
+ const completed = live.find((state) => state.status !== 'queued' && state.status !== 'running');
154
+ return this.#snapshot(completed ?? live.at(-1));
155
+ }
136
156
  async message(agentId, message, options = {}) {
137
157
  const execution = this.#requireExecution();
138
158
  if (typeof message !== 'string' ||
@@ -211,6 +231,7 @@ export class SubagentSessionController {
211
231
  state.terminalPending = true;
212
232
  await this.#publishTerminal(state);
213
233
  state.turn.resolve();
234
+ this.#notifyCompletion(state);
214
235
  delete state.turn;
215
236
  this.#drain();
216
237
  }
@@ -241,6 +262,7 @@ export class SubagentSessionController {
241
262
  state.status = 'cancelled';
242
263
  this.#closeReceipt(state);
243
264
  state.turn?.resolve();
265
+ this.#notifyCompletion(state);
244
266
  delete state.turn;
245
267
  }
246
268
  for (const state of this.#agents.values()) {
@@ -347,6 +369,7 @@ export class SubagentSessionController {
347
369
  this.#closeReceipt(state);
348
370
  this.#running--;
349
371
  turn.resolve();
372
+ this.#notifyCompletion(state);
350
373
  if (state.turn === turn)
351
374
  delete state.turn;
352
375
  this.#drain();
@@ -359,6 +382,7 @@ export class SubagentSessionController {
359
382
  delete state.turn;
360
383
  this.#running--;
361
384
  turn?.resolve();
385
+ this.#notifyCompletion(state);
362
386
  await this.#publishTerminal(state);
363
387
  this.#drain();
364
388
  }
@@ -369,6 +393,16 @@ export class SubagentSessionController {
369
393
  state.errorMessage = diagnostic.message;
370
394
  state.terminalPending = true;
371
395
  }
396
+ #waitForAnyCompletion(candidates, waitMs, signal) {
397
+ return waitForCompletionNotification(this.#completionWaiters, (state) => candidates.includes(state), waitMs, signal);
398
+ }
399
+ #waitForCompletion(state, waitMs, signal) {
400
+ return waitForCompletionNotification(this.#completionWaiters, (completed) => completed === state, waitMs, signal);
401
+ }
402
+ #notifyCompletion(state) {
403
+ for (const waiter of this.#completionWaiters)
404
+ waiter(state);
405
+ }
372
406
  #closeReceipt(state) {
373
407
  if (state.receipt?.deliveryStatus === 'accepted' && state.receipt.turnId === state.turn?.turnId)
374
408
  state.receipt.deliveryStatus = state.status === 'cancelled' ? 'cancelled' : 'failed';
@@ -404,6 +438,15 @@ export class SubagentSessionController {
404
438
  return this.#execution;
405
439
  }
406
440
  #snapshot(state) {
441
+ const terminal = state.status === 'completed' ||
442
+ state.status === 'failed' ||
443
+ state.status === 'cancelled' ||
444
+ state.status === 'interrupted';
445
+ const latestMessage = state.latestMessage === undefined
446
+ ? undefined
447
+ : terminal
448
+ ? { ...state.latestMessage }
449
+ : compactRunningLatestMessage(state.latestMessage);
407
450
  return {
408
451
  agentId: state.agentId,
409
452
  description: state.description,
@@ -412,11 +455,18 @@ export class SubagentSessionController {
412
455
  ...(state.modelId === undefined ? {} : { modelId: state.modelId }),
413
456
  ...(state.modelName === undefined ? {} : { modelName: state.modelName }),
414
457
  ...(state.reasoningEffort === undefined ? {} : { reasoningEffort: state.reasoningEffort }),
415
- ...(state.latestMessage === undefined ? {} : { latestMessage: { ...state.latestMessage } }),
458
+ ...(latestMessage === undefined ? {} : { latestMessage }),
459
+ ...(state.latestActivity === undefined
460
+ ? {}
461
+ : { latestActivity: { ...state.latestActivity } }),
416
462
  ...(state.summary === undefined ? {} : { summary: state.summary }),
417
- ...(state.changedFiles.length === 0 ? {} : { changedFiles: [...state.changedFiles] }),
418
- ...(state.verification.length === 0 ? {} : { verification: [...state.verification] }),
419
- ...(state.evidence.length === 0
463
+ ...(!terminal || state.changedFiles.length === 0
464
+ ? {}
465
+ : { changedFiles: [...state.changedFiles] }),
466
+ ...(!terminal || state.verification.length === 0
467
+ ? {}
468
+ : { verification: [...state.verification] }),
469
+ ...(!terminal || state.evidence.length === 0
420
470
  ? {}
421
471
  : { evidence: state.evidence.map((item) => ({ ...item })) }),
422
472
  ...(state.errorCode === undefined ? {} : { errorCode: state.errorCode }),
@@ -435,6 +485,16 @@ export class SubagentSessionController {
435
485
  }
436
486
  }
437
487
  }
488
+ function compactRunningLatestMessage(message) {
489
+ if (message.phase !== 'commentary')
490
+ return { ...message };
491
+ const text = truncateCharacters(message.text, TOOL_EXECUTION_LIMITS.subagentRunningLatestMessageMaxCharacters);
492
+ return {
493
+ ...message,
494
+ text,
495
+ truncated: message.truncated === true || text !== message.text
496
+ };
497
+ }
438
498
  function validateTaskInput(input) {
439
499
  if (input.parentSubagentId ||
440
500
  !input.description.trim() ||
@@ -486,10 +546,6 @@ function restoreAgentState(session) {
486
546
  const header = session.records.findLast((record) => record.type === 'execution.header' && isRecord(record.payload))?.payload;
487
547
  const terminal = currentRecords.findLast((record) => ['execution.completed', 'execution.failed', 'execution.cancelled'].includes(eventType(record) ?? ''));
488
548
  const terminalType = eventType(terminal);
489
- const latestMessageRecord = session.records.findLast((record) => eventType(record) === 'message.completed');
490
- const latestMessage = latestMessageRecord && isRecord(latestMessageRecord.payload)
491
- ? latestMessageFromPayload(latestMessageRecord.payload)
492
- : undefined;
493
549
  const state = {
494
550
  agentId: session.id,
495
551
  description: session.source.description,
@@ -515,36 +571,64 @@ function restoreAgentState(session) {
515
571
  ...(isReasoningEffort(header?.reasoningEffort)
516
572
  ? { reasoningEffort: header.reasoningEffort }
517
573
  : {}),
518
- ...(latestMessage === undefined ? {} : { latestMessage }),
519
574
  changedFiles: [],
520
575
  verification: [],
521
576
  evidence: [],
522
577
  terminalPending: false
523
578
  };
524
- if (state.status === 'completed' && latestMessage?.phase === 'final_answer')
525
- state.summary = truncateUtf8(latestMessage.text, TOOL_EXECUTION_LIMITS.subagentSummaryMaxBytes);
579
+ for (const record of currentRecords) {
580
+ if (!isRecord(record.payload) ||
581
+ !['message.completed', 'tool.started', 'tool.completed', 'tool.failed'].includes(eventType(record) ?? ''))
582
+ continue;
583
+ const event = record.payload;
584
+ observeAgentEvent(state, event);
585
+ }
586
+ if (state.status === 'completed' && state.latestMessage?.phase === 'final_answer')
587
+ state.summary = truncateUtf8(state.latestMessage.text, TOOL_EXECUTION_LIMITS.subagentSummaryMaxBytes);
526
588
  if (state.status === 'failed' && terminal && isRecord(terminal.payload)) {
527
589
  if (typeof terminal.payload.code === 'string')
528
590
  state.errorCode = terminal.payload.code;
529
591
  if (typeof terminal.payload.message === 'string')
530
592
  state.errorMessage = terminal.payload.message;
531
593
  }
532
- for (const record of currentRecords) {
533
- if (!isRecord(record.payload) || eventType(record) !== 'tool.completed')
534
- continue;
535
- const event = record.payload;
536
- if (event.type === 'tool.completed')
537
- observeAgentEvent(state, event);
538
- }
539
594
  return state;
540
595
  }
541
596
  function observeAgentEvent(state, event) {
542
597
  if (event.type === 'message.completed') {
543
598
  state.latestMessage = latestMessageFromEvent(event);
599
+ state.latestActivity = {
600
+ kind: 'message',
601
+ timestamp: event.timestamp,
602
+ phase: event.phase
603
+ };
604
+ return;
605
+ }
606
+ if (event.type === 'tool.started') {
607
+ state.latestActivity = {
608
+ kind: 'tool',
609
+ timestamp: event.timestamp,
610
+ toolName: event.toolName,
611
+ status: 'started'
612
+ };
613
+ return;
614
+ }
615
+ if (event.type === 'tool.failed') {
616
+ state.latestActivity = {
617
+ kind: 'tool',
618
+ timestamp: event.timestamp,
619
+ toolName: event.toolName,
620
+ status: 'failed'
621
+ };
544
622
  return;
545
623
  }
546
624
  if (event.type !== 'tool.completed')
547
625
  return;
626
+ state.latestActivity = {
627
+ kind: 'tool',
628
+ timestamp: event.timestamp,
629
+ toolName: event.toolName,
630
+ status: 'completed'
631
+ };
548
632
  if (event.toolName === 'apply_patch' && isRecord(event.data)) {
549
633
  const data = event.data;
550
634
  const files = Array.isArray(data.files) ? data.files : [];
@@ -643,7 +727,7 @@ function truncateCharacters(value, maximumCharacters) {
643
727
  ? value
644
728
  : characters.slice(0, maximumCharacters).join('');
645
729
  }
646
- function waitForCompletion(completion, waitMs, signal) {
730
+ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
647
731
  return new Promise((resolve, reject) => {
648
732
  let settled = false;
649
733
  const finish = (error) => {
@@ -652,20 +736,32 @@ function waitForCompletion(completion, waitMs, signal) {
652
736
  settled = true;
653
737
  clearTimeout(timer);
654
738
  signal?.removeEventListener('abort', abort);
739
+ waiters.delete(notify);
655
740
  if (error === undefined)
656
741
  resolve();
657
742
  else
658
743
  reject(error);
659
744
  };
745
+ const notify = (value) => {
746
+ if (accepts(value))
747
+ finish();
748
+ };
660
749
  const abort = () => finish(signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
661
750
  const timer = setTimeout(finish, waitMs);
662
751
  timer.unref();
752
+ waiters.add(notify);
663
753
  signal?.addEventListener('abort', abort, { once: true });
664
- void completion.then(() => finish());
665
754
  if (signal?.aborted)
666
755
  abort();
667
756
  });
668
757
  }
758
+ 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))
763
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
764
+ }
669
765
  function linkAbort(signal, abort) {
670
766
  const listener = () => abort();
671
767
  signal.addEventListener('abort', listener, { once: true });
@@ -3,7 +3,7 @@ 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 using the agentId returned by agent_start. 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 agentId and description. 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. Preserve agentId and avoid busy polling.',
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.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
@@ -16,18 +16,21 @@ export const agentWaitToolDefinition = {
16
16
  description: `Timeout in milliseconds. Defaults to ${TOOL_EXECUTION_LIMITS.agentWaitDefaultWaitMs}, min ${TOOL_EXECUTION_LIMITS.agentWaitMinWaitMs}, max ${TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs}. Zero returns immediately. Positive values below ${TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs} are raised to ${TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs}.`
17
17
  }
18
18
  },
19
- required: ['agentId'],
20
19
  additionalProperties: false
21
20
  }
22
21
  };
23
22
  export async function executeAgentWait(arguments_, context) {
24
23
  const value = parseAgentWaitInput(arguments_);
25
- if (!context.subagents)
24
+ const subagents = context.subagents;
25
+ if (!subagents)
26
26
  throw new MarAgentError('MAR_AGENT_SUBAGENT_RECURSION_DISABLED', 'Subagents cannot recurse.');
27
- const task = await context.subagents.output(value.agentId, {
27
+ const options = {
28
28
  waitMs: value.waitMs ?? TOOL_EXECUTION_LIMITS.agentWaitDefaultWaitMs,
29
29
  signal: context.signal
30
- });
30
+ };
31
+ const task = value.agentId === undefined
32
+ ? await subagents.outputAny(options)
33
+ : await subagents.output(value.agentId, options);
31
34
  return { content: JSON.stringify(task), data: task };
32
35
  }
33
36
  function parseAgentWaitInput(value) {
@@ -35,8 +38,9 @@ function parseAgentWaitInput(value) {
35
38
  return invalidAgentWait();
36
39
  const input = value;
37
40
  if (Object.keys(input).some((key) => !['agentId', 'waitMs'].includes(key)) ||
38
- typeof input.agentId !== 'string' ||
39
- input.agentId.trim().length === 0 ||
41
+ (input.agentId !== undefined &&
42
+ input.agentId !== null &&
43
+ (typeof input.agentId !== 'string' || input.agentId.trim().length === 0)) ||
40
44
  (input.waitMs !== undefined &&
41
45
  input.waitMs !== null &&
42
46
  (!Number.isInteger(input.waitMs) ||
@@ -44,7 +48,7 @@ function parseAgentWaitInput(value) {
44
48
  input.waitMs > TOOL_EXECUTION_LIMITS.agentWaitMaxWaitMs)))
45
49
  return invalidAgentWait();
46
50
  return {
47
- agentId: input.agentId,
51
+ ...(typeof input.agentId === 'string' ? { agentId: input.agentId.trim() } : {}),
48
52
  ...(typeof input.waitMs === 'number' ? { waitMs: input.waitMs } : {})
49
53
  };
50
54
  }
@@ -34,6 +34,7 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
34
34
  subagentMessageMaxBytes: 131072;
35
35
  subagentReasoningEffortMaxCharacters: 64;
36
36
  subagentSummaryMaxBytes: 16384;
37
+ subagentRunningLatestMessageMaxCharacters: 2000;
37
38
  subagentEvidenceMaxItems: 64;
38
39
  subagentVerificationMaxItems: 16;
39
40
  subagentChangedFilesMaxItems: 200;
@@ -34,6 +34,7 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
34
34
  subagentMessageMaxBytes: 131_072,
35
35
  subagentReasoningEffortMaxCharacters: 64,
36
36
  subagentSummaryMaxBytes: 16_384,
37
+ subagentRunningLatestMessageMaxCharacters: 2_000,
37
38
  subagentEvidenceMaxItems: 64,
38
39
  subagentVerificationMaxItems: 16,
39
40
  subagentChangedFilesMaxItems: 200,
@@ -44,7 +44,7 @@ export class ViewImageTool {
44
44
  sha256: createHash('sha256').update(data).digest('hex')
45
45
  };
46
46
  return {
47
- content: `Viewed image ${input.path} (${mimeType}, ${data.byteLength} bytes, sha256 ${metadata.sha256}).`,
47
+ content: `Viewed image ${input.path} (${mimeType}, ${data.byteLength} bytes).`,
48
48
  data: metadata,
49
49
  images: [{ mimeType, dataBase64: data.toString('base64') }]
50
50
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.65",
3
+ "version": "0.9.66",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",