@myagentroam/agent 0.9.71 → 0.9.72

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.
@@ -2,7 +2,7 @@ import type { ExecutionMode } from '../sdk/types.js';
2
2
  import { type ExecutionBudgetPromptState } from './execution-budget.js';
3
3
  import { type RetainedSessionResources } from './resources.js';
4
4
  import { type CurrentAgentModel, type SubagentModelOption } from './subagent.js';
5
- export declare const MAR_AGENT_PROMPT_VERSION = "1.41";
5
+ export declare const MAR_AGENT_PROMPT_VERSION = "1.42";
6
6
  export declare function buildSystemPrompt(input: {
7
7
  mode: ExecutionMode;
8
8
  platform: string;
@@ -6,7 +6,7 @@ import { outputStylePrompt } from './output.js';
6
6
  import { retainedSessionResourcesPrompt } from './resources.js';
7
7
  import { subagentModelOptionsPrompt, subagentPrompt, currentAgentModelPrompt } from './subagent.js';
8
8
  import { interactionPrompt, longRunningPrompt, safetyPrompt, toolUsagePrompt, workflowPrompt } from './workflow.js';
9
- export const MAR_AGENT_PROMPT_VERSION = '1.41';
9
+ export const MAR_AGENT_PROMPT_VERSION = '1.42';
10
10
  export function buildSystemPrompt(input) {
11
11
  const toolNames = new Set(input.tools);
12
12
  const hasLongRunningCapability = [
@@ -3,14 +3,14 @@ export function toolUsagePrompt(tools) {
3
3
  const names = new Set(tools);
4
4
  const clauses = [
5
5
  `# Tools and execution\nAvailable client tools: ${tools.join(', ')}.`,
6
- 'Inspect before editing, do not manufacture file contents from memory, and treat tool success as evidence only for what its result actually proves. A model response may request multiple independent tools; the Runtime may parallelize calls declared safe while preserving serial barriers and model call order.'
6
+ 'Inspect before editing, do not manufacture file contents from memory, and treat tool success as evidence only for what its result actually proves. Start exploration with the smallest evidence that can change the next decision: locate relevant symbols, headings, or files before reading bodies, and expand only when current evidence is insufficient. Do not request multiple broad outputs merely because tools can run in parallel. A model response may request multiple independent tools; the Runtime may parallelize calls declared safe while preserving serial barriers and model call order.'
7
7
  ];
8
8
  if (names.has('skill'))
9
9
  clauses.push('If the request names or clearly matches an available Skill description, call skill before generic tools and follow the loaded workflow; semantic matches do not require the user to know the Skill name. Do not load unrelated Skills or claim to use one without calling skill.');
10
10
  if (names.has('exec'))
11
11
  clauses.push('Use exec for platform-native commands, scripts, verification, and mechanical edits. Prefer rg or rg --files for search. Batch independent lookups and already-decided commands, preserving failure ordering. Use scripts for mechanical substitutions instead of reproducing long unchanged text in a patch; inspect actual effects afterward.');
12
12
  if (names.has('read'))
13
- clauses.push('Use read to batch known files and ranges. Read only what can affect the next decision, use returned continuation metadata when needed, and avoid rereading unchanged content.');
13
+ clauses.push('Use read to batch known files and ranges. Batch only already-located, bounded ranges that are likely to fit together; for large files or documents, locate relevant sections before reading them. Read only what can affect the next decision, continue only relevant incomplete results using returned metadata, and avoid rereading unchanged content.');
14
14
  if (names.has('apply_patch'))
15
15
  clauses.push('Prefer apply_patch for precise local edits. After a conflict, use the reported evidence to refresh only the affected context before retrying.');
16
16
  if (names.has('apply_patch') || names.has('exec'))
@@ -459,6 +459,8 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
459
459
  if (!parsed)
460
460
  continue;
461
461
  const selected = parsed.items.flatMap((item) => {
462
+ if (item.pathTruncated)
463
+ return [];
462
464
  const itemKey = `${read.result.record.recordId}:${item.index}`;
463
465
  if (claimedReadItems.has(itemKey))
464
466
  return [];
@@ -1,6 +1,7 @@
1
1
  export declare const MODEL_TOKEN_ESTIMATOR: "UTF8_BYTES_V1";
2
2
  /** Local budget estimate only; provider usage remains authoritative. */
3
3
  export declare function countModelTokens(value: string): number;
4
+ export declare function modelTokenBudgetBytes(maximumTokens: number): number;
4
5
  export declare function truncateModelText(value: string, maximumTokens: number, omittedLabel?: string): {
5
6
  content: string;
6
7
  truncated: boolean;
@@ -4,12 +4,16 @@ const BYTES_PER_TOKEN = 4;
4
4
  export function countModelTokens(value) {
5
5
  return Math.ceil(Buffer.byteLength(value, 'utf8') / BYTES_PER_TOKEN);
6
6
  }
7
+ export function modelTokenBudgetBytes(maximumTokens) {
8
+ return Math.max(0, Math.floor(maximumTokens)) * BYTES_PER_TOKEN;
9
+ }
7
10
  export function truncateModelText(value, maximumTokens, omittedLabel = 'tool output') {
8
11
  const tokens = countModelTokens(value);
9
12
  if (tokens <= maximumTokens)
10
13
  return { content: value, truncated: false, omittedTokens: 0 };
11
- const maximumBytes = Math.max(0, Math.floor(maximumTokens)) * BYTES_PER_TOKEN;
14
+ const maximumBytes = modelTokenBudgetBytes(maximumTokens);
12
15
  const bytes = Buffer.from(value, 'utf8');
16
+ const warning = `Warning: truncated output (original token count: ${tokens})\nTotal output lines: ${lineCount(value)}\n\n`;
13
17
  let retainedBytes = Math.max(0, maximumBytes - 16 * BYTES_PER_TOKEN);
14
18
  for (let attempt = 0; attempt < 8; attempt++) {
15
19
  let head = Math.ceil(retainedBytes / 2);
@@ -21,10 +25,14 @@ export function truncateModelText(value, maximumTokens, omittedLabel = 'tool out
21
25
  tail++;
22
26
  const omittedTokens = Math.ceil((tail - head) / BYTES_PER_TOKEN);
23
27
  const marker = `\n...[${omittedTokens} tokens of ${omittedLabel} omitted]...\n`;
24
- const candidateBytes = head + Buffer.byteLength(marker, 'utf8') + bytes.length - tail;
28
+ const candidateBytes = Buffer.byteLength(warning, 'utf8') +
29
+ head +
30
+ Buffer.byteLength(marker, 'utf8') +
31
+ bytes.length -
32
+ tail;
25
33
  if (candidateBytes <= maximumBytes)
26
34
  return {
27
- content: `${bytes.toString('utf8', 0, head)}${marker}${bytes.toString('utf8', tail)}`,
35
+ content: `${warning}${bytes.toString('utf8', 0, head)}${marker}${bytes.toString('utf8', tail)}`,
28
36
  truncated: true,
29
37
  omittedTokens
30
38
  };
@@ -33,12 +41,22 @@ export function truncateModelText(value, maximumTokens, omittedLabel = 'tool out
33
41
  retainedBytes = Math.max(0, retainedBytes - (candidateBytes - maximumBytes));
34
42
  }
35
43
  const marker = `\n...[${tokens} tokens of ${omittedLabel} omitted]...\n`;
44
+ const fallback = `${warning}${marker}`;
36
45
  return {
37
- content: countModelTokens(marker) <= maximumTokens ? marker : '',
46
+ content: countModelTokens(fallback) <= maximumTokens ? fallback : '',
38
47
  truncated: true,
39
48
  omittedTokens: tokens
40
49
  };
41
50
  }
51
+ function lineCount(value) {
52
+ let lines = value.length === 0 ? 0 : 1;
53
+ for (let index = 0; index < value.length; index++)
54
+ if (value.charCodeAt(index) === 10)
55
+ lines++;
56
+ if (value.endsWith('\n'))
57
+ lines--;
58
+ return lines;
59
+ }
42
60
  function isContinuationByte(byte) {
43
61
  return byte !== undefined && (byte & 0xc0) === 0x80;
44
62
  }
package/dist/sdk/agent.js CHANGED
@@ -931,14 +931,17 @@ export async function createMarAgent(options) {
931
931
  })
932
932
  });
933
933
  const modelOutput = boundedModelToolOutput(output.content);
934
+ const eventSummary = output.content.slice(0, AGENT_EXECUTION_POLICY.toolEventSummaryCharacters);
934
935
  const structuredData = structuredToolEventData(call.name, output.data);
935
936
  const artifacts = structuredToolArtifacts(call.name, output.artifacts);
936
937
  await emit({
937
938
  type: 'tool.completed',
938
939
  toolName: call.name,
939
940
  callId: call.callId,
940
- summary: output.content.slice(0, AGENT_EXECUTION_POLICY.toolEventSummaryCharacters),
941
- truncated: output.truncated === true || modelOutput.truncated,
941
+ summary: eventSummary,
942
+ truncated: output.truncated === true ||
943
+ modelOutput.truncated ||
944
+ eventSummary.length < output.content.length,
942
945
  ...(artifacts === undefined ? {} : { artifacts }),
943
946
  ...(structuredData === undefined ? {} : { data: structuredData })
944
947
  });
@@ -243,7 +243,8 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
243
243
  return replacement.items.every((item) => {
244
244
  const original = originalItems.get(item.index);
245
245
  const evidence = successfulPatchEvidenceForPath(records, recordsBySequence, item.evidenceSequence, item.path, workspace, platform);
246
- return (original?.path === item.path &&
246
+ return (original?.pathTruncated === false &&
247
+ original?.path === item.path &&
247
248
  evidence !== undefined &&
248
249
  item.replacement ===
249
250
  contextGcStaleReadReplacement({
@@ -77,7 +77,8 @@ export class SubagentSessionController {
77
77
  changedFiles: [],
78
78
  verification: [],
79
79
  evidence: [],
80
- terminalPending: false
80
+ terminalPending: false,
81
+ terminalObserved: false
81
82
  };
82
83
  this.#createdInExecution++;
83
84
  this.#agents.set(state.agentId, state);
@@ -96,7 +97,7 @@ export class SubagentSessionController {
96
97
  this.#setFailure(state, error);
97
98
  }
98
99
  try {
99
- await options.onCreated?.(this.#snapshot(state));
100
+ await options.onCreated?.(this.#receiptSnapshot(state));
100
101
  if (this.#disposed)
101
102
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
102
103
  options.signal?.throwIfAborted();
@@ -106,6 +107,7 @@ export class SubagentSessionController {
106
107
  }
107
108
  if (state.status === 'failed') {
108
109
  await this.#publishTerminal(state);
110
+ state.terminalObserved = true;
109
111
  return this.#snapshot(state);
110
112
  }
111
113
  this.#queueTurn(state, input.prompt, execution.startTurn, []);
@@ -115,9 +117,13 @@ export class SubagentSessionController {
115
117
  })
116
118
  : undefined;
117
119
  try {
118
- if (!input.background)
119
- await state.turn.completion;
120
- return this.#snapshot(state);
120
+ if (input.background)
121
+ return this.#receiptSnapshot(state);
122
+ await state.turn.completion;
123
+ const includeLatestMessage = this.#hasUnreadMessage(state);
124
+ const includeTerminalDetails = this.#observeTerminal(state);
125
+ this.#observeLatestMessage(state);
126
+ return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
121
127
  }
122
128
  finally {
123
129
  detachParentAbort?.();
@@ -129,10 +135,21 @@ export class SubagentSessionController {
129
135
  options.signal?.throwIfAborted();
130
136
  validateOutputOptions(options);
131
137
  if (options.waitMs === undefined || options.waitMs === 0)
132
- return { ...this.#snapshot(state), waitOutcome: 'snapshot' };
138
+ return {
139
+ ...this.#snapshot(state, {
140
+ includeLatestMessage: false,
141
+ includeTerminalDetails: false
142
+ }),
143
+ waitOutcome: 'snapshot'
144
+ };
133
145
  if (isTerminalState(state)) {
146
+ const includeLatestMessage = this.#hasUnreadMessage(state);
147
+ const includeTerminalDetails = this.#observeTerminal(state);
134
148
  this.#observeLatestMessage(state);
135
- return { ...this.#snapshot(state), waitOutcome: 'completion' };
149
+ return {
150
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
151
+ waitOutcome: 'completion'
152
+ };
136
153
  }
137
154
  const waitFor = options.waitFor ?? 'completion';
138
155
  if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
@@ -143,19 +160,27 @@ export class SubagentSessionController {
143
160
  if (waitFor === 'completion') {
144
161
  const completed = await this.#waitForCompletion(state, waitMs, options.signal);
145
162
  options.signal?.throwIfAborted();
163
+ const includeLatestMessage = completed ? this.#hasUnreadMessage(state) : false;
164
+ const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
146
165
  if (completed)
147
166
  this.#observeLatestMessage(state);
148
167
  return {
149
- ...this.#snapshot(state),
168
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
150
169
  waitOutcome: completed ? 'completion' : 'timeout'
151
170
  };
152
171
  }
153
172
  const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal);
154
173
  options.signal?.throwIfAborted();
174
+ const includeLatestMessage = activity?.kind === 'message' ||
175
+ (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
176
+ const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
155
177
  if (activity)
156
178
  this.#observeLatestMessage(activity.state);
157
179
  return {
158
- ...this.#snapshot(activity?.state ?? state),
180
+ ...this.#snapshot(activity?.state ?? state, {
181
+ includeLatestMessage,
182
+ includeTerminalDetails
183
+ }),
159
184
  waitOutcome: activity?.kind === 'message'
160
185
  ? 'message'
161
186
  : activity?.kind === 'completion'
@@ -172,13 +197,21 @@ export class SubagentSessionController {
172
197
  const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
173
198
  if (options.waitMs === undefined || options.waitMs === 0)
174
199
  return {
175
- ...this.#snapshot(live.at(-1) ?? states.at(-1)),
200
+ ...this.#snapshot(live.at(-1) ?? states.at(-1), {
201
+ includeLatestMessage: false,
202
+ includeTerminalDetails: false
203
+ }),
176
204
  waitOutcome: 'snapshot'
177
205
  };
178
206
  if (live.length === 0) {
179
207
  const state = states.at(-1);
208
+ const includeLatestMessage = this.#hasUnreadMessage(state);
209
+ const includeTerminalDetails = this.#observeTerminal(state);
180
210
  this.#observeLatestMessage(state);
181
- return { ...this.#snapshot(state), waitOutcome: 'completion' };
211
+ return {
212
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
213
+ waitOutcome: 'completion'
214
+ };
182
215
  }
183
216
  const waitFor = options.waitFor ?? 'completion';
184
217
  if (waitFor === 'message') {
@@ -192,10 +225,16 @@ export class SubagentSessionController {
192
225
  if (waitFor === 'message') {
193
226
  const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal);
194
227
  options.signal?.throwIfAborted();
228
+ const includeLatestMessage = activity?.kind === 'message' ||
229
+ (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
230
+ const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
195
231
  if (activity)
196
232
  this.#observeLatestMessage(activity.state);
197
233
  return {
198
- ...this.#snapshot(activity?.state ?? live.at(-1)),
234
+ ...this.#snapshot(activity?.state ?? live.at(-1), {
235
+ includeLatestMessage,
236
+ includeTerminalDetails
237
+ }),
199
238
  waitOutcome: activity?.kind === 'message'
200
239
  ? 'message'
201
240
  : activity?.kind === 'completion'
@@ -205,10 +244,15 @@ export class SubagentSessionController {
205
244
  }
206
245
  const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal);
207
246
  options.signal?.throwIfAborted();
247
+ const includeLatestMessage = completed ? this.#hasUnreadMessage(completed) : false;
248
+ const includeTerminalDetails = completed ? this.#observeTerminal(completed) : false;
208
249
  if (completed)
209
250
  this.#observeLatestMessage(completed);
210
251
  return {
211
- ...this.#snapshot(completed ?? live.at(-1)),
252
+ ...this.#snapshot(completed ?? live.at(-1), {
253
+ includeLatestMessage,
254
+ includeTerminalDetails
255
+ }),
212
256
  waitOutcome: completed ? 'completion' : 'timeout'
213
257
  };
214
258
  }
@@ -250,14 +294,14 @@ export class SubagentSessionController {
250
294
  state.turn === turn &&
251
295
  turn.handle?.enqueueMessage(message, delivery)) {
252
296
  state.receipt = mutableReceipt;
253
- return this.#snapshot(state);
297
+ return this.#messageReceiptSnapshot(state);
254
298
  }
255
299
  await turn.completion;
256
300
  }
257
301
  else if (state.status === 'queued' && state.turn) {
258
302
  state.turn.pendingMessages.push({ text: message, delivery });
259
303
  state.receipt = mutableReceipt;
260
- return this.#snapshot(state);
304
+ return this.#messageReceiptSnapshot(state);
261
305
  }
262
306
  this.#requireExecution();
263
307
  options.signal?.throwIfAborted();
@@ -268,12 +312,16 @@ export class SubagentSessionController {
268
312
  mutableReceipt.turnId = randomUUID();
269
313
  state.receipt = mutableReceipt;
270
314
  this.#queueTurn(state, message, execution.startTurn, [], mutableReceipt.turnId, delivery);
271
- return this.#snapshot(state);
315
+ return this.#messageReceiptSnapshot(state);
272
316
  });
273
317
  }
274
318
  async cancel(agentId) {
275
319
  const state = await this.#find(agentId);
276
- return this.#control(state, () => this.#cancelState(state));
320
+ await this.#control(state, () => this.#cancelState(state));
321
+ const includeLatestMessage = this.#hasUnreadMessage(state);
322
+ const includeTerminalDetails = this.#observeTerminal(state);
323
+ this.#observeLatestMessage(state);
324
+ return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
277
325
  }
278
326
  #control(state, operation) {
279
327
  const result = (state.controlTail ?? Promise.resolve()).then(operation);
@@ -304,7 +352,6 @@ export class SubagentSessionController {
304
352
  }
305
353
  await turn.completion;
306
354
  }
307
- return this.#snapshot(state);
308
355
  }
309
356
  listSessionResources() {
310
357
  return [...this.#agents.values()].map((state) => ({
@@ -493,6 +540,12 @@ export class SubagentSessionController {
493
540
  #observeLatestMessage(state) {
494
541
  state.observedMessageRevision = state.messageRevision;
495
542
  }
543
+ #observeTerminal(state) {
544
+ if (!isTerminalState(state) || state.terminalObserved)
545
+ return false;
546
+ state.terminalObserved = true;
547
+ return true;
548
+ }
496
549
  #closeReceipt(state) {
497
550
  if (state.receipt?.deliveryStatus === 'accepted' && state.receipt.turnId === state.turn?.turnId)
498
551
  state.receipt.deliveryStatus = state.status === 'cancelled' ? 'cancelled' : 'failed';
@@ -527,12 +580,28 @@ export class SubagentSessionController {
527
580
  throw new MarAgentError('MAR_AGENT_SUBAGENT_FAILED', 'Subagent control requires an active parent execution.');
528
581
  return this.#execution;
529
582
  }
530
- #snapshot(state) {
583
+ #receiptSnapshot(state) {
584
+ return this.#snapshot(state, {
585
+ includeLatestMessage: false,
586
+ includeLatestActivity: false,
587
+ includeTerminalDetails: false
588
+ });
589
+ }
590
+ #messageReceiptSnapshot(state) {
591
+ // A follow-up starts a new observation boundary; messages from the preceding state
592
+ // must not be returned later as fresh progress for the newly requested work.
593
+ this.#observeLatestMessage(state);
594
+ return this.#receiptSnapshot(state);
595
+ }
596
+ #snapshot(state, options = {}) {
531
597
  const terminal = state.status === 'completed' ||
532
598
  state.status === 'failed' ||
533
599
  state.status === 'cancelled' ||
534
600
  state.status === 'interrupted';
535
- const latestMessage = state.latestMessage === undefined
601
+ const includeLatestMessage = options.includeLatestMessage ?? true;
602
+ const includeLatestActivity = options.includeLatestActivity ?? true;
603
+ const includeTerminalDetails = options.includeTerminalDetails ?? true;
604
+ const latestMessage = !includeLatestMessage || state.latestMessage === undefined
536
605
  ? undefined
537
606
  : terminal
538
607
  ? { ...state.latestMessage }
@@ -546,17 +615,17 @@ export class SubagentSessionController {
546
615
  ...(state.modelName === undefined ? {} : { modelName: state.modelName }),
547
616
  ...(state.reasoningEffort === undefined ? {} : { reasoningEffort: state.reasoningEffort }),
548
617
  ...(latestMessage === undefined ? {} : { latestMessage }),
549
- ...(state.latestActivity === undefined
618
+ ...(!includeLatestActivity || state.latestActivity === undefined
550
619
  ? {}
551
620
  : { latestActivity: { ...state.latestActivity } }),
552
- ...(state.summary === undefined ? {} : { summary: state.summary }),
553
- ...(!terminal || state.changedFiles.length === 0
621
+ ...(!includeTerminalDetails || state.summary === undefined ? {} : { summary: state.summary }),
622
+ ...(!includeTerminalDetails || !terminal || state.changedFiles.length === 0
554
623
  ? {}
555
624
  : { changedFiles: [...state.changedFiles] }),
556
- ...(!terminal || state.verification.length === 0
625
+ ...(!includeTerminalDetails || !terminal || state.verification.length === 0
557
626
  ? {}
558
627
  : { verification: [...state.verification] }),
559
- ...(!terminal || state.evidence.length === 0
628
+ ...(!includeTerminalDetails || !terminal || state.evidence.length === 0
560
629
  ? {}
561
630
  : { evidence: state.evidence.map((item) => ({ ...item })) }),
562
631
  ...(state.errorCode === undefined ? {} : { errorCode: state.errorCode }),
@@ -667,7 +736,8 @@ function restoreAgentState(session) {
667
736
  changedFiles: [],
668
737
  verification: [],
669
738
  evidence: [],
670
- terminalPending: false
739
+ terminalPending: false,
740
+ terminalObserved: false
671
741
  };
672
742
  for (const record of currentRecords) {
673
743
  if (!isRecord(record.payload) ||
@@ -3,7 +3,7 @@ import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
3
3
  export const agentMessageToolDefinition = {
4
4
  name: 'agent_message',
5
5
  parallelSafety: 'serial',
6
- description: 'Send a real user message to an existing child agent. Default delivery=append inserts guidance into a running child turn at the next safe boundary after the current model response or complete tool batch. Explicit delivery=replace immediately requests cancellation, waits for the old turn to close, then starts a new turn in the same Session. When idle, either mode starts a new turn using persisted history. Returns messageId, turnId and deliveryStatus: accepted means queued in memory; applied means persisted in model context, not that the guidance has been executed. Use agent_wait for the latest receipt and task status. Does not wait for the new task to complete or roll back existing side effects.',
6
+ description: 'Send a real user message to an existing child agent. Default delivery=append inserts guidance into a running child turn at the next safe boundary after the current model response or complete tool batch. Explicit delivery=replace immediately requests cancellation, waits for the old turn to close, then starts a new turn in the same Session. When idle, either mode starts a new turn using persisted history. Returns messageId, turnId, deliveryStatus, current status, and model metadata without repeating the child’s previous latestMessage, latestActivity, summary, or verification evidence. accepted means queued in memory; applied means persisted in model context, not that the guidance has been executed. Use agent_wait for subsequent progress or completion. Does not wait for the new task to complete or roll back existing side effects.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
@@ -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. 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.',
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. A message or first terminal completion result includes the newly observed latest public commentary/final message; snapshot, timeout, and repeated terminal results 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.',
7
7
  inputSchema: {
8
8
  type: 'object',
9
9
  properties: {
@@ -1,8 +1,9 @@
1
1
  import { MarAgentError } from '../error.js';
2
+ import { AGENT_EXECUTION_POLICY } from '../runtime/execution-policy.js';
3
+ import { modelTokenBudgetBytes } from '../runtime/token-budget.js';
2
4
  import { ApplyPatchTool } from './apply-patch.js';
3
5
  import { ExecTool } from './exec.js';
4
6
  import { CodeModeTool } from './code-mode.js';
5
- import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
6
7
  import { ReadTool } from './read.js';
7
8
  import { WorkspacePathResolver } from './shared/path-resolver.js';
8
9
  import { ViewImageTool } from './view-image.js';
@@ -188,46 +189,106 @@ async function fetchWebUrl(url, signal, allowUntrustedTls, redirects = 0) {
188
189
  function escapeAttribute(value) {
189
190
  return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;');
190
191
  }
191
- function formatBatchReadResult(result, index) {
192
- const path = escapeAttribute(result.path);
193
- if ('error' in result)
194
- return `<read_error index="${index}" path="${path}" code="${escapeAttribute(result.error.code)}">${result.error.message}</read_error>`;
195
- const metadata = {
196
- path: result.path,
197
- encoding: result.encoding,
198
- newline: result.newline,
199
- startLine: result.startLine,
200
- ...(result.endLine === undefined ? {} : { endLine: result.endLine }),
201
- eof: result.eof,
202
- truncated: result.truncated,
203
- ...(result.nextOffset === undefined ? {} : { nextOffset: result.nextOffset })
192
+ function escapeText(value) {
193
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;');
194
+ }
195
+ function boundedUtf8Prefix(value, maximumBytes) {
196
+ if (Buffer.byteLength(value) <= maximumBytes)
197
+ return { value, truncated: false };
198
+ const marker = '…';
199
+ const contentBudget = Math.max(0, maximumBytes - Buffer.byteLength(marker));
200
+ let result = '';
201
+ let bytes = 0;
202
+ for (const character of value) {
203
+ const characterBytes = Buffer.byteLength(character);
204
+ if (bytes + characterBytes > contentBudget)
205
+ break;
206
+ result += character;
207
+ bytes += characterBytes;
208
+ }
209
+ return { value: `${result}${marker}`, truncated: true };
210
+ }
211
+ function formatBatchReadResult(result, index, maximumBytes) {
212
+ const visiblePath = boundedUtf8Prefix(result.path, 128);
213
+ const path = escapeAttribute(visiblePath.value);
214
+ const pathTruncatedAttribute = visiblePath.truncated ? ' pathTruncated="true"' : '';
215
+ if ('error' in result) {
216
+ const message = boundedUtf8Prefix(result.error.message, 256);
217
+ const detailsTruncatedAttribute = message.truncated ? ' detailsTruncated="true"' : '';
218
+ const content = `<read_error index="${index}" path="${path}"${pathTruncatedAttribute} code="${escapeAttribute(result.error.code)}"${detailsTruncatedAttribute}>${escapeText(message.value)}</read_error>`;
219
+ if (Buffer.byteLength(content) <= maximumBytes)
220
+ return { content, previewComplete: true };
221
+ return {
222
+ content: `<read_error index="${index}" code="MAR_AGENT_TOOL_OUTPUT_LIMIT">Model-visible error metadata exceeds this item's output share.</read_error>`,
223
+ previewComplete: false
224
+ };
225
+ }
226
+ const lines = result.content ? result.content.split('\n') : [];
227
+ const render = (visibleLines) => {
228
+ const previewComplete = visibleLines === lines.length;
229
+ const endLine = visibleLines === 0 ? undefined : result.startLine + Math.max(0, visibleLines - 1);
230
+ const metadata = {
231
+ path: visiblePath.value,
232
+ ...(visiblePath.truncated ? { pathTruncated: true } : {}),
233
+ encoding: result.encoding,
234
+ newline: result.newline,
235
+ startLine: result.startLine,
236
+ ...(endLine === undefined ? {} : { endLine }),
237
+ eof: previewComplete ? result.eof : false,
238
+ truncated: previewComplete ? result.truncated : true,
239
+ ...(previewComplete
240
+ ? result.nextOffset === undefined
241
+ ? {}
242
+ : { nextOffset: result.nextOffset }
243
+ : { nextOffset: result.startLine + visibleLines }),
244
+ previewComplete
245
+ };
246
+ return [
247
+ `<read_result index="${index}" path="${path}"${pathTruncatedAttribute}>`,
248
+ lines.slice(0, visibleLines).join('\n'),
249
+ `<read_metadata>${JSON.stringify(metadata)}</read_metadata>`,
250
+ '</read_result>'
251
+ ]
252
+ .filter(Boolean)
253
+ .join('\n');
254
+ };
255
+ const full = render(lines.length);
256
+ if (Buffer.byteLength(full) <= maximumBytes)
257
+ return { content: full, previewComplete: true };
258
+ let low = 0;
259
+ let high = lines.length;
260
+ while (low < high) {
261
+ const middle = Math.ceil((low + high) / 2);
262
+ if (Buffer.byteLength(render(middle)) <= maximumBytes)
263
+ low = middle;
264
+ else
265
+ high = middle - 1;
266
+ }
267
+ const content = render(low);
268
+ if (Buffer.byteLength(content) <= maximumBytes)
269
+ return { content, previewComplete: low === lines.length };
270
+ return {
271
+ content: `<read_error index="${index}" code="MAR_AGENT_TOOL_OUTPUT_LIMIT">Model-visible read metadata exceeds this item's output share.</read_error>`,
272
+ previewComplete: false
204
273
  };
205
- return [
206
- `<read_result index="${index}" path="${path}">`,
207
- result.content,
208
- `<read_metadata>${JSON.stringify(metadata)}</read_metadata>`,
209
- '</read_result>'
210
- ]
211
- .filter(Boolean)
212
- .join('\n');
213
274
  }
214
275
  function formatBatchReadOutput(results) {
215
276
  const blocks = [];
216
277
  let bytes = 0;
217
278
  let truncated = false;
279
+ const maximumBytes = modelTokenBudgetBytes(AGENT_EXECUTION_POLICY.modelToolOutputTokens);
218
280
  for (const [index, result] of results.entries()) {
219
- let block = formatBatchReadResult(result, index);
220
281
  const separatorBytes = blocks.length === 0 ? 0 : 1;
221
- if (bytes + separatorBytes + Buffer.byteLength(block) >
222
- TOOL_EXECUTION_LIMITS.readMaxBatchBytes) {
223
- truncated = true;
224
- block = `<read_error index="${index}" path="${escapeAttribute(result.path)}" code="MAR_AGENT_TOOL_OUTPUT_LIMIT">Batch output budget exhausted; read this range separately.</read_error>`;
225
- }
226
- const blockBytes = Buffer.byteLength(block);
227
- if (bytes + separatorBytes + blockBytes > TOOL_EXECUTION_LIMITS.readMaxBatchBytes)
282
+ const remainingItems = results.length - index;
283
+ const itemBudget = Math.floor((maximumBytes - bytes - separatorBytes) / remainingItems);
284
+ const block = formatBatchReadResult(result, index, itemBudget);
285
+ const blockBytes = Buffer.byteLength(block.content);
286
+ if (bytes + separatorBytes + blockBytes > maximumBytes)
228
287
  break;
229
- blocks.push(block);
288
+ blocks.push(block.content);
230
289
  bytes += separatorBytes + blockBytes;
290
+ if (!block.previewComplete)
291
+ truncated = true;
231
292
  }
232
293
  return { content: blocks.join('\n'), truncated };
233
294
  }
@@ -1,6 +1,7 @@
1
1
  export interface ParsedReadContextItem {
2
2
  readonly index: number;
3
3
  readonly path: string;
4
+ readonly pathTruncated: boolean;
4
5
  readonly content: string;
5
6
  readonly metadata: Record<string, unknown>;
6
7
  readonly contentStart: number;
@@ -5,7 +5,7 @@ export function parseReadContextOutput(value) {
5
5
  let offset = 0;
6
6
  while (offset < value.length) {
7
7
  if (value.startsWith('<read_result ', offset)) {
8
- const header = /^<read_result index="(\d+)" path="([^"]*)">\n/u.exec(value.slice(offset));
8
+ const header = /^<read_result index="(\d+)" path="([^"]*)"( pathTruncated="true")?>\n/u.exec(value.slice(offset));
9
9
  if (!header)
10
10
  return undefined;
11
11
  const contentStart = offset + header[0].length;
@@ -27,15 +27,18 @@ export function parseReadContextOutput(value) {
27
27
  }
28
28
  const index = Number(header[1]);
29
29
  const path = unescapeAttribute(header[2]);
30
+ const pathTruncated = header[3] !== undefined;
30
31
  if (!Number.isSafeInteger(index) ||
31
32
  index < 0 ||
32
33
  !isRecord(metadata) ||
33
- metadata.path !== path)
34
+ metadata.path !== path ||
35
+ (metadata.pathTruncated === true) !== pathTruncated)
34
36
  return undefined;
35
37
  const blockEnd = metadataEnd + metadataEndMarker.length;
36
38
  items.push({
37
39
  index,
38
40
  path,
41
+ pathTruncated,
39
42
  content: value.slice(contentStart, metadataStart),
40
43
  metadata,
41
44
  contentStart,
@@ -44,7 +47,7 @@ export function parseReadContextOutput(value) {
44
47
  offset = blockEnd;
45
48
  }
46
49
  else if (value.startsWith('<read_error ', offset)) {
47
- const error = /^<read_error index="\d+" path="[^"]*" code="[^"]*">[^\n]*<\/read_error>/u.exec(value.slice(offset));
50
+ const error = /^<read_error index="\d+" path="[^"]*"(?: pathTruncated="true")? code="[^"]*"(?: detailsTruncated="true")?>[^\n]*<\/read_error>/u.exec(value.slice(offset));
48
51
  if (!error)
49
52
  return undefined;
50
53
  offset += error[0].length;
@@ -46,7 +46,7 @@ export const readToolDefinition = {
46
46
  }
47
47
  }
48
48
  },
49
- description: `Read one or more known text files/ranges. Strict UTF-8 with optional BOM is the default; BOM-declared UTF-16LE/UTF-16BE is supported. Invalid text returns an item error instead of replacement characters. Always pass reads; use one item for a single file and up to ${TOOL_EXECUTION_LIMITS.readMaxBatchItems} items for a batch. Results retain input order, use 1-based line numbers, and include <read_metadata> with path, encoding, newline, exact range, eof, truncation, and nextOffset. Items fail independently and run with bounded concurrency. Each item returns at most ${TOOL_EXECUTION_LIMITS.readMaxLines} lines/${TOOL_EXECUTION_LIMITS.readMaxBytes} bytes; the call shares a ${TOOL_EXECUTION_LIMITS.readMaxBatchBytes}-byte budget. Structured data.results items provide text as decoded source for the returned range, preserving line endings and excluding BOM; content is numbered display text. Large files support bounded range reads. Relative paths use the workspace; absolute paths are allowed. Prefer exec with rg for search and read for known files.`,
49
+ description: `Read one or more known text files/ranges. Strict UTF-8 with optional BOM is the default; BOM-declared UTF-16LE/UTF-16BE is supported. Invalid text returns an item error instead of replacement characters. Always pass reads; use one item for a single file and up to ${TOOL_EXECUTION_LIMITS.readMaxBatchItems} items for a batch. Batch only already-located, bounded ranges whose combined content is likely to fit the model-visible output budget; for exploration, locate relevant files, symbols, or headings first, then read the smallest useful ranges. Results retain input order, use 1-based line numbers, and include <read_metadata> with path, encoding, newline, exact visible range, eof, truncation, nextOffset, and whether previewComplete. Items fail independently and run with bounded concurrency. Each item returns at most ${TOOL_EXECUTION_LIMITS.readMaxLines} lines/${TOOL_EXECUTION_LIMITS.readMaxBytes} bytes; the structured call shares a ${TOOL_EXECUTION_LIMITS.readMaxBatchBytes}-byte budget, while ordinary model-visible content fairly shares the common tool-output budget across all items. Structured data.results items provide text as decoded source for the returned range, preserving line endings and excluding BOM; content is numbered display text. Large files support bounded range reads. If previewComplete is false, continue only the still-relevant item from nextOffset. Relative paths use the workspace; absolute paths are allowed. Prefer exec with rg for search and read for known files.`,
50
50
  inputSchema: {
51
51
  type: 'object',
52
52
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.71",
3
+ "version": "0.9.72",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",