@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.
@@ -1,5 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  const RAW_MODEL_CONTEXT_PREVIEW_CHARACTERS = 256;
3
+ const EXEC_INPUT_COMPLETE_CHARACTERS = 512;
4
+ const EXEC_INPUT_PREVIEW_CHARACTERS = 256;
5
+ const EXEC_OUTPUT_PREVIEW_CHARACTERS = 512;
3
6
  export function contextGcPatchResultFromCompletedPayload(payload) {
4
7
  if (!isRecord(payload) || payload.type !== 'tool.completed')
5
8
  return undefined;
@@ -43,39 +46,55 @@ export function contextGcPatchResultFromCompletedPayload(payload) {
43
46
  }
44
47
  };
45
48
  }
46
- export function contextGcApplyPatchInputReplacement(arguments_, result, evidenceSequence) {
49
+ export function contextGcApplyPatchInputReplacement(arguments_, result, evidenceSequence, formatVersion = 2) {
47
50
  const original = typeof arguments_ === 'string' ? arguments_ : stableJson(arguments_);
48
51
  const summary = result.files
49
52
  .map((file) => `${file.operation} ${file.path}${file.targetPath ? ` -> ${file.targetPath}` : ''} (+${file.addedLines}/-${file.deletedLines})`)
50
53
  .join('; ');
51
- return `<context_gc kind="successful_apply_patch_input" originalBytes="${Buffer.byteLength(original)}" originalSha256="${sha256(original)}" evidenceSequence="${evidenceSequence}">Applied successfully: ${escapeXml(summary)}. Totals: ${result.totals.files} files, +${result.totals.addedLines}/-${result.totals.deletedLines}. The original patch body was reclaimed.</context_gc>`;
54
+ return `<context_gc kind="successful_apply_patch_input"${formatVersion === 2 ? ' formatVersion="2"' : ''} originalBytes="${Buffer.byteLength(original)}"${formatVersion === 1 ? ` originalSha256="${sha256(original)}"` : ''}${formatVersion === 1 ? ` evidenceSequence="${evidenceSequence}"` : ''}>Applied successfully: ${escapeXml(summary)}. Totals: ${result.totals.files} files, +${result.totals.addedLines}/-${result.totals.deletedLines}. The original patch body was reclaimed.</context_gc>`;
52
55
  }
53
56
  export function contextGcStaleReadReplacement(input) {
54
- return `<context_gc kind="stale_read" path="${escapeXml(input.path)}" originalBytes="${Buffer.byteLength(input.content)}" originalSha256="${sha256(input.content)}" supersededByCallId="${escapeXml(input.supersededByCallId)}" operation="${input.operation}" evidenceSequence="${input.evidenceSequence}">This read predates a successful apply_patch. Re-read the path before relying on its contents.</context_gc>`;
57
+ const formatVersion = input.formatVersion ?? 2;
58
+ return `<context_gc kind="stale_read" path="${escapeXml(input.path)}"${formatVersion === 2 ? ' formatVersion="2"' : ''} originalBytes="${Buffer.byteLength(input.content)}"${formatVersion === 1 ? ` originalSha256="${sha256(input.content)}"` : ''}${formatVersion === 1
59
+ ? ` supersededByCallId="${escapeXml(input.supersededByCallId)}" evidenceSequence="${input.evidenceSequence}"`
60
+ : ''} operation="${input.operation}">This read predates a successful apply_patch. Re-read the path before relying on its contents.</context_gc>`;
55
61
  }
56
- export function contextGcExecInputReplacement(arguments_) {
62
+ export function contextGcWebOutputReplacement(toolName, originalContent, truncated) {
63
+ const preview = unicodeHeadTail(originalContent, EXEC_OUTPUT_PREVIEW_CHARACTERS);
64
+ return JSON.stringify({
65
+ toolName,
66
+ originalBytes: Buffer.byteLength(originalContent),
67
+ lines: lineCount(originalContent),
68
+ truncated,
69
+ preview,
70
+ previewComplete: preview === originalContent,
71
+ contextGc: 'Detailed web tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete search result or page, and GC did not access the network.'
72
+ });
73
+ }
74
+ export function contextGcExecInputReplacement(arguments_, formatVersion = 3) {
57
75
  if (!isRecord(arguments_))
58
- return `<context_gc kind="terminal_exec_input" originalSha256="${sha256(stableJson(arguments_))}" />`;
76
+ return `<context_gc kind="terminal_exec_input"${formatVersion === 3 ? ' formatVersion="3"' : ''} originalBytes="${Buffer.byteLength(stableJson(arguments_))}"${formatVersion === 3 ? '' : ` originalSha256="${sha256(stableJson(arguments_))}"`} />`;
59
77
  const action = typeof arguments_.action === 'string' ? arguments_.action : 'unknown';
60
78
  const original = stableJson(arguments_);
61
79
  const retained = {
62
80
  action,
63
81
  contextGc: {
64
82
  kind: 'terminal_exec_input',
83
+ ...(formatVersion >= 2 ? { formatVersion } : {}),
65
84
  originalBytes: Buffer.byteLength(original),
66
- originalSha256: sha256(original)
85
+ ...(formatVersion <= 2 ? { originalSha256: sha256(original) } : {})
67
86
  }
68
87
  };
69
88
  for (const key of ['cwd', 'processId', 'timeoutMs', 'yieldMs'])
70
89
  if (arguments_[key] !== undefined)
71
90
  retained[key] = arguments_[key];
72
91
  if (typeof arguments_.command === 'string')
73
- retained.commandPrefix = [...arguments_.command].slice(0, 256).join('');
92
+ retainExecText(retained, 'command', arguments_.command, formatVersion);
74
93
  if (typeof arguments_.data === 'string')
75
- retained.dataPrefix = [...arguments_.data].slice(0, 256).join('');
94
+ retainExecText(retained, 'data', arguments_.data, formatVersion);
76
95
  return retained;
77
96
  }
78
- export function contextGcExecOutputReplacement(result) {
97
+ export function contextGcExecOutputReplacement(result, formatVersion = 3) {
79
98
  if (result.source === 'MODEL_CONTEXT_TRUNCATED')
80
99
  return JSON.stringify({
81
100
  status: result.status,
@@ -88,12 +107,15 @@ export function contextGcExecOutputReplacement(result) {
88
107
  stderrDroppedBytes: result.stderrDroppedBytes,
89
108
  modelContextTruncated: true,
90
109
  modelContextBytes: Buffer.byteLength(result.modelContextContent),
91
- modelContextSha256: sha256(result.modelContextContent),
92
110
  modelContextPreview: unicodeHeadTail(result.modelContextContent, RAW_MODEL_CONTEXT_PREVIEW_CHARACTERS),
111
+ ...(formatVersion >= 2 ? { contextGcFormatVersion: formatVersion } : {}),
112
+ ...(formatVersion <= 2 ? { modelContextSha256: sha256(result.modelContextContent) } : {}),
93
113
  contextGc: 'The model-visible terminal result was truncated after execution and is no longer valid JSON. Detailed terminal input/output was reclaimed after the process reached a stable terminal state. Re-run the command if full output is required.'
94
114
  });
95
115
  const stdout = result.stdout;
96
116
  const stderr = result.stderr;
117
+ const stdoutPreview = unicodeHeadTail(stdout, EXEC_OUTPUT_PREVIEW_CHARACTERS);
118
+ const stderrPreview = unicodeHeadTail(stderr, EXEC_OUTPUT_PREVIEW_CHARACTERS);
97
119
  return JSON.stringify({
98
120
  status: result.status,
99
121
  ...(result.processId === undefined ? {} : { processId: result.processId }),
@@ -103,11 +125,32 @@ export function contextGcExecOutputReplacement(result) {
103
125
  stderrBytes: nonnegativeNumber(result.stderrBytes) ?? Buffer.byteLength(stderr),
104
126
  stdoutDroppedBytes: nonnegativeNumber(result.stdoutDroppedBytes) ?? 0,
105
127
  stderrDroppedBytes: nonnegativeNumber(result.stderrDroppedBytes) ?? 0,
106
- stdoutSha256: sha256(stdout),
107
- stderrSha256: sha256(stderr),
108
- stdoutPreview: unicodeHeadTail(stdout, 512),
109
- stderrPreview: unicodeHeadTail(stderr, 512),
110
- contextGc: 'Detailed terminal input/output was reclaimed after the process reached a stable terminal state. Re-run the command if full output is required.'
128
+ ...(formatVersion <= 2
129
+ ? {
130
+ stdoutSha256: sha256(stdout),
131
+ stderrSha256: sha256(stderr)
132
+ }
133
+ : {}),
134
+ stdoutPreview,
135
+ stderrPreview,
136
+ ...(formatVersion >= 2
137
+ ? {
138
+ stdoutLines: lineCount(stdout),
139
+ stderrLines: lineCount(stderr),
140
+ stdoutPreviewComplete: result.truncated === false &&
141
+ result.stdoutDroppedBytes === 0 &&
142
+ result.stdoutBytes === Buffer.byteLength(stdout) &&
143
+ stdoutPreview === stdout,
144
+ stderrPreviewComplete: result.truncated === false &&
145
+ result.stderrDroppedBytes === 0 &&
146
+ result.stderrBytes === Buffer.byteLength(stderr) &&
147
+ stderrPreview === stderr,
148
+ contextGcFormatVersion: formatVersion
149
+ }
150
+ : {}),
151
+ contextGc: formatVersion >= 2
152
+ ? 'Detailed terminal input/output was reclaimed after the process reached a stable terminal state. An incomplete preview is not the complete result. Re-run the command if full output is required.'
153
+ : 'Detailed terminal input/output was reclaimed after the process reached a stable terminal state. Re-run the command if full output is required.'
111
154
  });
112
155
  }
113
156
  export function normalizeContextGcExecResult(modelContent, completedPayload) {
@@ -192,6 +235,23 @@ function unicodeHeadTail(value, count) {
192
235
  .slice(-count)
193
236
  .join('')}`;
194
237
  }
238
+ function retainExecText(retained, key, value, formatVersion) {
239
+ if (formatVersion === 1) {
240
+ retained[`${key}Prefix`] = [...value].slice(0, EXEC_INPUT_PREVIEW_CHARACTERS).join('');
241
+ return;
242
+ }
243
+ if ([...value].length <= EXEC_INPUT_COMPLETE_CHARACTERS) {
244
+ retained[key] = value;
245
+ return;
246
+ }
247
+ retained[`${key}Preview`] = unicodeHeadTail(value, EXEC_INPUT_PREVIEW_CHARACTERS);
248
+ retained[`${key}Bytes`] = Buffer.byteLength(value);
249
+ if (formatVersion === 2)
250
+ retained[`${key}Sha256`] = sha256(value);
251
+ }
252
+ function lineCount(value) {
253
+ return value.length === 0 ? 0 : value.split('\n').length;
254
+ }
195
255
  function nonnegativeNumber(value) {
196
256
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
197
257
  }
@@ -14,6 +14,16 @@ export interface ContextGcSubagentSnapshot {
14
14
  readonly timestamp: string;
15
15
  readonly truncated?: boolean;
16
16
  };
17
+ readonly latestActivity?: {
18
+ readonly kind: 'message';
19
+ readonly timestamp: string;
20
+ readonly phase: 'commentary' | 'final_answer';
21
+ } | {
22
+ readonly kind: 'tool';
23
+ readonly timestamp: string;
24
+ readonly toolName: string;
25
+ readonly status: 'started' | 'completed' | 'failed';
26
+ };
17
27
  readonly summary?: string;
18
28
  readonly changedFiles?: readonly string[];
19
29
  readonly verification?: readonly string[];
@@ -34,10 +44,12 @@ export declare function contextGcSubagentSnapshotTurnId(snapshot: ContextGcSubag
34
44
  export declare function contextGcSubagentNeedsRetainedPriorFinal(snapshot: ContextGcSubagentSnapshot): boolean;
35
45
  export declare function contextGcSubagentPriorFinalIsRetained(source: ContextGcSubagentSnapshot, retained: ContextGcSubagentSnapshot | undefined): boolean;
36
46
  export declare function contextGcSubagentSnapshotIsSuperseded(source: ContextGcSubagentSnapshot, terminal: ContextGcSubagentSnapshot, retainedPriorFinal?: ContextGcSubagentSnapshot): boolean;
47
+ export declare function contextGcSubagentSnapshotIsPreciselyCovered(source: ContextGcSubagentSnapshot, retained: ContextGcSubagentSnapshot): boolean;
37
48
  export declare function contextGcSubagentOutputReplacement(input: {
38
49
  readonly sourceContent: string;
39
50
  readonly source: ContextGcSubagentSnapshot;
40
51
  readonly terminal: ContextGcSubagentSnapshot;
41
52
  readonly terminalCallId: string;
42
53
  readonly evidenceSequence: number;
54
+ readonly formatVersion?: 1 | 2;
43
55
  }): string;
@@ -67,8 +67,32 @@ export function contextGcSubagentSnapshotIsSuperseded(source, terminal, retained
67
67
  stringArrayIsCovered(source.verification, terminal.verification) &&
68
68
  evidenceIsCovered(source.evidence, terminal.evidence));
69
69
  }
70
+ export function contextGcSubagentSnapshotIsPreciselyCovered(source, retained) {
71
+ if (retained.status !== 'cancelled' &&
72
+ retained.status !== 'failed' &&
73
+ retained.status !== 'interrupted')
74
+ return false;
75
+ if (source.agentId !== retained.agentId ||
76
+ source.description !== retained.description ||
77
+ contextGcSubagentSnapshotTurnId(source) !== contextGcSubagentSnapshotTurnId(retained) ||
78
+ !optionalStringMatches(source.modelId, retained.modelId) ||
79
+ !optionalStringMatches(source.modelName, retained.modelName) ||
80
+ !optionalStringMatches(source.reasoningEffort, retained.reasoningEffort) ||
81
+ !optionalStringMatches(source.summary, retained.summary) ||
82
+ !optionalStringMatches(source.errorCode, retained.errorCode) ||
83
+ !optionalStringMatches(source.errorMessage, retained.errorMessage) ||
84
+ !latestMessageIsCovered(source.latestMessage, retained.latestMessage) ||
85
+ !latestActivityIsCovered(source.latestActivity, retained.latestActivity) ||
86
+ !receiptIsCovered(source, retained) ||
87
+ !stringArrayIsCovered(source.changedFiles, retained.changedFiles) ||
88
+ !stringArrayIsCovered(source.verification, retained.verification) ||
89
+ !evidenceIsCovered(source.evidence, retained.evidence))
90
+ return false;
91
+ return (source.status === retained.status || source.status === 'queued' || source.status === 'running');
92
+ }
70
93
  export function contextGcSubagentOutputReplacement(input) {
71
94
  const latestMessage = input.source.latestMessage;
95
+ const formatVersion = input.formatVersion ?? 2;
72
96
  return JSON.stringify({
73
97
  agentId: input.source.agentId,
74
98
  description: input.source.description,
@@ -87,26 +111,37 @@ export function contextGcSubagentOutputReplacement(input) {
87
111
  ? {}
88
112
  : {
89
113
  latestMessage: {
90
- itemId: latestMessage.itemId,
91
114
  turnId: latestMessage.turnId,
92
115
  phase: latestMessage.phase,
93
116
  timestamp: latestMessage.timestamp,
117
+ ...(formatVersion === 1 ? { itemId: latestMessage.itemId } : {}),
94
118
  ...(latestMessage.truncated === undefined
95
119
  ? {}
96
120
  : { truncated: latestMessage.truncated }),
97
121
  textBytes: Buffer.byteLength(latestMessage.text),
98
- textSha256: sha256(latestMessage.text)
122
+ ...(formatVersion === 1 ? { textSha256: sha256(latestMessage.text) } : {})
99
123
  }
100
124
  }),
125
+ ...(input.source.latestActivity === undefined
126
+ ? {}
127
+ : { latestActivity: input.source.latestActivity }),
101
128
  contextGc: {
102
129
  kind: 'superseded_subagent_snapshot',
130
+ ...(formatVersion === 2 ? { formatVersion } : {}),
103
131
  originalBytes: Buffer.byteLength(input.sourceContent),
104
- originalSha256: sha256(input.sourceContent),
132
+ ...(formatVersion === 1 ? { originalSha256: sha256(input.sourceContent) } : {}),
105
133
  terminalStatus: input.terminal.status,
106
- terminalTurnId: contextGcCompletedSubagentTurnId(input.terminal),
107
- terminalCallId: input.terminalCallId,
108
- evidenceSequence: input.evidenceSequence,
109
- note: 'This intermediate child-agent snapshot was superseded by retained completed evidence; complete Turn results remain in retained completed snapshots.'
134
+ terminalTurnId: contextGcCompletedSubagentTurnId(input.terminal) ??
135
+ contextGcSubagentSnapshotTurnId(input.terminal),
136
+ ...(formatVersion === 1
137
+ ? {
138
+ terminalCallId: input.terminalCallId,
139
+ evidenceSequence: input.evidenceSequence
140
+ }
141
+ : {}),
142
+ note: input.terminal.status === 'completed'
143
+ ? 'This intermediate child-agent snapshot was superseded by retained completed evidence; complete Turn results remain in retained completed snapshots.'
144
+ : 'This duplicate child-agent snapshot was superseded by a retained non-completed terminal snapshot. Only fields exactly covered by the retained snapshot were reclaimed.'
110
145
  }
111
146
  });
112
147
  }
@@ -128,6 +163,7 @@ function parseContextGcSubagentSnapshot(value) {
128
163
  'modelName',
129
164
  'reasoningEffort',
130
165
  'latestMessage',
166
+ 'latestActivity',
131
167
  'summary',
132
168
  'changedFiles',
133
169
  'verification',
@@ -169,6 +205,9 @@ function parseContextGcSubagentSnapshot(value) {
169
205
  const latestMessage = parseContextGcSubagentLatestMessage(parsed.latestMessage);
170
206
  if (parsed.latestMessage !== undefined && !latestMessage)
171
207
  return undefined;
208
+ const latestActivity = parseContextGcSubagentLatestActivity(parsed.latestActivity);
209
+ if (parsed.latestActivity !== undefined && !latestActivity)
210
+ return undefined;
172
211
  return {
173
212
  agentId: parsed.agentId,
174
213
  description: parsed.description,
@@ -179,6 +218,7 @@ function parseContextGcSubagentSnapshot(value) {
179
218
  ? { reasoningEffort: parsed.reasoningEffort }
180
219
  : {}),
181
220
  ...(latestMessage ? { latestMessage } : {}),
221
+ ...(latestActivity ? { latestActivity } : {}),
182
222
  ...(typeof parsed.summary === 'string' ? { summary: parsed.summary } : {}),
183
223
  ...(Array.isArray(parsed.changedFiles) ? { changedFiles: [...parsed.changedFiles] } : {}),
184
224
  ...(Array.isArray(parsed.verification) ? { verification: [...parsed.verification] } : {}),
@@ -215,6 +255,25 @@ function parseContextGcSubagentLatestMessage(value) {
215
255
  ...(typeof value.truncated === 'boolean' ? { truncated: value.truncated } : {})
216
256
  };
217
257
  }
258
+ function parseContextGcSubagentLatestActivity(value) {
259
+ if (!isRecord(value) || !isNonemptyString(value.timestamp))
260
+ return undefined;
261
+ if (value.kind === 'message' &&
262
+ Object.keys(value).every((key) => ['kind', 'timestamp', 'phase'].includes(key)) &&
263
+ (value.phase === 'commentary' || value.phase === 'final_answer'))
264
+ return { kind: 'message', timestamp: value.timestamp, phase: value.phase };
265
+ if (value.kind === 'tool' &&
266
+ Object.keys(value).every((key) => ['kind', 'timestamp', 'toolName', 'status'].includes(key)) &&
267
+ isNonemptyString(value.toolName) &&
268
+ (value.status === 'started' || value.status === 'completed' || value.status === 'failed'))
269
+ return {
270
+ kind: 'tool',
271
+ timestamp: value.timestamp,
272
+ toolName: value.toolName,
273
+ status: value.status
274
+ };
275
+ return undefined;
276
+ }
218
277
  function isOptionalSubagentEvidence(value) {
219
278
  return (value === undefined ||
220
279
  (Array.isArray(value) &&
@@ -236,6 +295,19 @@ function isOptionalNonemptyString(value) {
236
295
  function optionalStringMatches(source, terminal) {
237
296
  return source === undefined || source === terminal;
238
297
  }
298
+ function latestMessageIsCovered(source, retained) {
299
+ return (source === undefined || (retained !== undefined && stableJson(source) === stableJson(retained)));
300
+ }
301
+ function latestActivityIsCovered(source, retained) {
302
+ return (source === undefined || (retained !== undefined && stableJson(source) === stableJson(retained)));
303
+ }
304
+ function receiptIsCovered(source, retained) {
305
+ if (source.messageId === undefined)
306
+ return true;
307
+ return (source.messageId === retained.messageId &&
308
+ source.turnId === retained.turnId &&
309
+ source.deliveryStatus === retained.deliveryStatus);
310
+ }
239
311
  function stringArrayIsCovered(source, terminal) {
240
312
  return source === undefined || source.every((item) => terminal?.includes(item) === true);
241
313
  }
@@ -184,6 +184,21 @@ export declare class JsonlSessionStore {
184
184
  source: SessionSource;
185
185
  }[];
186
186
  }>;
187
+ listSubagentChildren(parentSessionId: string): Promise<{
188
+ id: string;
189
+ title: string;
190
+ workspace: string;
191
+ createdAt: string;
192
+ lastActivityAt: string;
193
+ source: {
194
+ readonly type: "subagent";
195
+ readonly parentSessionId: string;
196
+ readonly rootSessionId: string;
197
+ readonly parentExecutionId: string;
198
+ readonly role: "general-purpose" | "explore";
199
+ readonly description: string;
200
+ };
201
+ }[]>;
187
202
  sessionTree(sessionId: string): Promise<readonly string[]>;
188
203
  delete(sessionId: string): Promise<{
189
204
  readonly deleted: boolean;
@@ -841,6 +841,21 @@ export class JsonlSessionStore {
841
841
  : undefined;
842
842
  return { sessions, ...(next ? { nextCursor: next } : {}) };
843
843
  }
844
+ async listSubagentChildren(parentSessionId) {
845
+ this.#assertOpen();
846
+ await this.#refreshIndex();
847
+ return this.#shared.index.sessions
848
+ .map((session) => ({ session, source: normalizeSessionSource(session.source) }))
849
+ .filter((item) => item.source.type === 'subagent' && item.source.parentSessionId === parentSessionId)
850
+ .map(({ session, source }) => ({
851
+ id: session.id,
852
+ title: session.title,
853
+ workspace: session.workspace,
854
+ createdAt: session.createdAt,
855
+ lastActivityAt: session.lastActivityAt,
856
+ source
857
+ }));
858
+ }
844
859
  async sessionTree(sessionId) {
845
860
  this.#assertOpen();
846
861
  await this.#refreshIndex();
@@ -26,6 +26,16 @@ export interface SubagentLatestMessage {
26
26
  timestamp: string;
27
27
  truncated: boolean;
28
28
  }
29
+ export type SubagentLatestActivity = {
30
+ kind: 'message';
31
+ timestamp: string;
32
+ phase: 'commentary' | 'final_answer';
33
+ } | {
34
+ kind: 'tool';
35
+ timestamp: string;
36
+ toolName: string;
37
+ status: 'started' | 'completed' | 'failed';
38
+ };
29
39
  export interface SubagentTaskSnapshot extends Record<string, unknown> {
30
40
  agentId: string;
31
41
  description: string;
@@ -34,6 +44,7 @@ export interface SubagentTaskSnapshot extends Record<string, unknown> {
34
44
  modelName?: string;
35
45
  reasoningEffort?: MarAgentReasoningEffort;
36
46
  latestMessage?: SubagentLatestMessage;
47
+ latestActivity?: SubagentLatestActivity;
37
48
  summary?: string;
38
49
  changedFiles?: string[];
39
50
  verification?: string[];
@@ -64,6 +75,7 @@ export interface SubagentController {
64
75
  beginExecution?(runner: (task: SubagentTaskInput, signal: AbortSignal) => Promise<SubagentTaskResult>): void;
65
76
  run(input: SubagentTaskInput, options?: SubagentRunOptions): Promise<SubagentTaskSnapshot>;
66
77
  output(agentId: string, options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
78
+ outputAny(options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
67
79
  message(agentId: string, message: string, options?: SubagentMessageOptions): Promise<SubagentTaskSnapshot>;
68
80
  cancel(agentId: string): Promise<SubagentTaskSnapshot>;
69
81
  listSessionResources(): readonly {
@@ -86,11 +98,12 @@ export declare class SubagentScheduler implements SubagentController {
86
98
  beginExecution(runner: (task: SubagentTaskInput, signal: AbortSignal) => Promise<SubagentTaskResult>): void;
87
99
  run(input: SubagentTaskInput, options?: SubagentRunOptions): Promise<SubagentTaskSnapshot>;
88
100
  output(agentId: string, options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
101
+ outputAny(options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
89
102
  cancel(agentId: string): Promise<SubagentTaskSnapshot>;
90
103
  message(_agentId: string, _message: string): Promise<SubagentTaskSnapshot>;
91
104
  listSessionResources(): {
92
105
  agentId: string;
93
- status: "completed" | "cancelled" | "running" | "queued" | "failed";
106
+ status: "completed" | "cancelled" | "running" | "failed" | "queued";
94
107
  description: string;
95
108
  }[];
96
109
  dispose(): Promise<void>;
@@ -6,6 +6,7 @@ export class SubagentScheduler {
6
6
  limits;
7
7
  #tasks = new Map();
8
8
  #queue = [];
9
+ #completionWaiters = new Set();
9
10
  #running = 0;
10
11
  #disposed = false;
11
12
  #createdInExecution = 0;
@@ -92,9 +93,26 @@ export class SubagentScheduler {
92
93
  ? undefined
93
94
  : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
94
95
  if (waitMs !== undefined && !['completed', 'failed', 'cancelled'].includes(task.status))
95
- await waitForCompletion(task.completion, waitMs, options.signal);
96
+ await this.#waitForCompletion(task, waitMs, options.signal);
96
97
  return this.#public(task);
97
98
  }
99
+ async outputAny(options = {}) {
100
+ options.signal?.throwIfAborted();
101
+ validateOutputOptions(options);
102
+ const tasks = [...this.#tasks.values()];
103
+ if (tasks.length === 0)
104
+ throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent task was not found.');
105
+ 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));
115
+ }
98
116
  async cancel(agentId) {
99
117
  const task = this.#tasks.get(agentId);
100
118
  if (!task)
@@ -115,6 +133,7 @@ export class SubagentScheduler {
115
133
  this.#queue.splice(index, 1);
116
134
  task.controller.abort(reason);
117
135
  task.resolve();
136
+ this.#notifyCompletion(task);
118
137
  this.#drain();
119
138
  }
120
139
  else if (task.status === 'running')
@@ -145,6 +164,7 @@ export class SubagentScheduler {
145
164
  .finally(() => {
146
165
  this.#running--;
147
166
  task.resolve();
167
+ this.#notifyCompletion(task);
148
168
  this.#drain();
149
169
  });
150
170
  }
@@ -175,6 +195,7 @@ export class SubagentScheduler {
175
195
  task.status = 'cancelled';
176
196
  task.controller.abort();
177
197
  task.resolve();
198
+ this.#notifyCompletion(task);
178
199
  }
179
200
  else if (task.status === 'running')
180
201
  task.controller.abort();
@@ -183,6 +204,16 @@ export class SubagentScheduler {
183
204
  this.#queue.length = 0;
184
205
  this.#tasks.clear();
185
206
  }
207
+ #waitForAnyCompletion(candidates, waitMs, signal) {
208
+ return waitForCompletionNotification(this.#completionWaiters, (task) => candidates.includes(task), waitMs, signal);
209
+ }
210
+ #waitForCompletion(task, waitMs, signal) {
211
+ return waitForCompletionNotification(this.#completionWaiters, (completed) => completed === task, waitMs, signal);
212
+ }
213
+ #notifyCompletion(task) {
214
+ for (const waiter of this.#completionWaiters)
215
+ waiter(task);
216
+ }
186
217
  }
187
218
  function normalizeResult(result) {
188
219
  return {
@@ -217,7 +248,7 @@ function truncateUtf8(value, maximumBytes) {
217
248
  end--;
218
249
  return bytes.subarray(0, end).toString('utf8');
219
250
  }
220
- function waitForCompletion(completion, waitMs, signal) {
251
+ function waitForCompletionNotification(waiters, accepts, waitMs, signal) {
221
252
  return new Promise((resolve, reject) => {
222
253
  let settled = false;
223
254
  const finish = (error) => {
@@ -226,20 +257,32 @@ function waitForCompletion(completion, waitMs, signal) {
226
257
  settled = true;
227
258
  clearTimeout(timer);
228
259
  signal?.removeEventListener('abort', abort);
260
+ waiters.delete(notify);
229
261
  if (error === undefined)
230
262
  resolve();
231
263
  else
232
264
  reject(error);
233
265
  };
266
+ const notify = (value) => {
267
+ if (accepts(value))
268
+ finish();
269
+ };
234
270
  const abort = () => finish(signal?.reason ?? new MarAgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
235
271
  const timer = setTimeout(finish, waitMs);
236
272
  timer.unref();
273
+ waiters.add(notify);
237
274
  signal?.addEventListener('abort', abort, { once: true });
238
- void completion.then(() => finish());
239
275
  if (signal?.aborted)
240
276
  abort();
241
277
  });
242
278
  }
279
+ 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))
284
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Task wait is invalid.');
285
+ }
243
286
  function linkAbort(signal, abort) {
244
287
  const listener = () => abort(signal.reason);
245
288
  signal.addEventListener('abort', listener, { once: true });
@@ -42,6 +42,7 @@ export interface SubagentParentExecution {
42
42
  }
43
43
  export interface PersistedSubagentSession {
44
44
  readonly id: string;
45
+ readonly createdAt: string;
45
46
  readonly source: Extract<SessionSource, {
46
47
  type: 'subagent';
47
48
  }>;
@@ -57,6 +58,7 @@ export interface SubagentSessionControllerBackend {
57
58
  readonly id: string;
58
59
  }>;
59
60
  deleteSession(sessionId: string): Promise<void>;
61
+ /** Returns children in durable creation order. */
60
62
  listChildren(parentSessionId: string): Promise<readonly PersistedSubagentSession[]>;
61
63
  loadChild(parentSessionId: string, sessionId: string): Promise<PersistedSubagentSession | undefined>;
62
64
  }
@@ -83,11 +85,12 @@ export declare class SubagentSessionController implements SubagentController {
83
85
  endParentExecution(executionId: string): void;
84
86
  run(input: SubagentTaskInput, options?: SubagentRunOptions): Promise<SubagentTaskSnapshot>;
85
87
  output(agentId: string, options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
88
+ outputAny(options?: SubagentOutputOptions): Promise<SubagentTaskSnapshot>;
86
89
  message(agentId: string, message: string, options?: SubagentMessageOptions): Promise<SubagentTaskSnapshot>;
87
90
  cancel(agentId: string): Promise<SubagentTaskSnapshot>;
88
91
  listSessionResources(): {
89
92
  agentId: string;
90
- status: "completed" | "cancelled" | "running" | "queued" | "failed" | "interrupted";
93
+ status: "completed" | "cancelled" | "running" | "failed" | "queued" | "interrupted";
91
94
  description: string;
92
95
  }[];
93
96
  dispose(): Promise<void>;