@myagentroam/agent 0.11.42 → 0.11.44

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.
@@ -169,7 +169,8 @@ export class ExecTool {
169
169
  ...(child.signalCode == null ? {} : { exitSignal: child.signalCode }),
170
170
  finalized: false,
171
171
  terminalResultReturned: false,
172
- waiters: new Set()
172
+ waiters: new Set(),
173
+ outputListeners: new Set()
173
174
  };
174
175
  this.#processes.set(id, managed);
175
176
  if (native) {
@@ -188,6 +189,8 @@ export class ExecTool {
188
189
  return;
189
190
  const decoded = Buffer.from(managed[name].decoder.write(chunk));
190
191
  appendDisplay(managed[name === 'stdout' ? 'stdoutDisplay' : 'stderrDisplay'], decoded);
192
+ for (const listener of managed.outputListeners)
193
+ listener(name, decoded);
191
194
  appendOutput(managed[name], decoded, this.maxOutputBytes, chunk.length);
192
195
  appendOutput(managed[name === 'stdout' ? 'stdoutHistory' : 'stderrHistory'], decoded, this.maxOutputBytes, chunk.length);
193
196
  notifyProcessChange(managed);
@@ -226,6 +229,8 @@ export class ExecTool {
226
229
  else {
227
230
  const message = Buffer.from(error.message);
228
231
  appendDisplay(managed.stderrDisplay, message);
232
+ for (const listener of managed.outputListeners)
233
+ listener('stderr', message);
229
234
  appendOutput(managed.stderr, message, this.maxOutputBytes);
230
235
  appendOutput(managed.stderrHistory, message, this.maxOutputBytes);
231
236
  notifyProcessChange(managed);
@@ -421,7 +426,13 @@ export class ExecTool {
421
426
  stderrBytes: process.stderr.totalBytes,
422
427
  stdoutDroppedBytes: stdout.droppedBytes,
423
428
  stderrDroppedBytes: stderr.droppedBytes,
424
- displayOutput: displayOutput(process)
429
+ displayOutput: displayOutput(process),
430
+ executionSource: {
431
+ kind: 'SHELL',
432
+ text: process.command.slice(0, 10_000),
433
+ cwd: process.cwd.slice(0, 10_000),
434
+ truncated: process.command.length > 10_000 || process.cwd.length > 10_000
435
+ }
425
436
  }
426
437
  });
427
438
  while (this.#completed.size > TOOL_EXECUTION_LIMITS.execMaxCompletedResults)
@@ -484,7 +495,13 @@ export class ExecTool {
484
495
  stderrBytes: process.stderr.totalBytes,
485
496
  stdoutDroppedBytes: stdout.droppedBytes,
486
497
  stderrDroppedBytes: stderr.droppedBytes,
487
- displayOutput: displayOutput(process)
498
+ displayOutput: displayOutput(process),
499
+ executionSource: {
500
+ kind: 'SHELL',
501
+ text: process.command.slice(0, 10_000),
502
+ cwd: process.cwd.slice(0, 10_000),
503
+ truncated: process.command.length > 10_000 || process.cwd.length > 10_000
504
+ }
488
505
  };
489
506
  }
490
507
  async dispose() {
@@ -529,6 +546,10 @@ function finalizeManagedProcess(managed, maximumOutputBytes, exitCode) {
529
546
  const stderrEnd = Buffer.from(managed.stderr.decoder.end());
530
547
  appendDisplay(managed.stdoutDisplay, stdoutEnd);
531
548
  appendDisplay(managed.stderrDisplay, stderrEnd);
549
+ for (const listener of managed.outputListeners) {
550
+ listener('stdout', stdoutEnd);
551
+ listener('stderr', stderrEnd);
552
+ }
532
553
  appendOutput(managed.stdout, stdoutEnd, maximumOutputBytes);
533
554
  appendOutput(managed.stderr, stderrEnd, maximumOutputBytes);
534
555
  appendOutput(managed.stdoutHistory, stdoutEnd, maximumOutputBytes);
@@ -642,34 +663,36 @@ async function waitForProcessCompletion(processId, managed, waitMs, signal, inte
642
663
  stopOutput?.();
643
664
  }
644
665
  }
645
- /** Coalesce display snapshots without consuming output or queueing behind a slow observer. */
666
+ /** Coalesce decoded chunks into bounded per-call frames without blocking the process. */
646
667
  function watchProcessOutput(processId, managed, onOutput) {
647
668
  let stopped = false;
648
- let dirty = false;
669
+ let stdout = { value: Buffer.alloc(0), droppedBytes: 0 };
670
+ let stderr = { value: Buffer.alloc(0), droppedBytes: 0 };
649
671
  let timer;
650
672
  let inFlight;
651
673
  let failed = false;
674
+ let initialSnapshot = managed.stdoutDisplay.value.length > 0 || managed.stderrDisplay.value.length > 0;
652
675
  const publish = () => {
653
- dirty = false;
654
- const preview = (output) => {
655
- const bounded = createOutputBuffer();
656
- for (const chunk of output.chunks)
657
- appendOutput(bounded, chunk, TOOL_EXECUTION_LIMITS.execProgressMaxOutputBytes);
658
- const result = consumeOutput(bounded);
659
- return { ...result, droppedBytes: result.droppedBytes + output.droppedBytes };
660
- };
661
- const stdout = preview(managed.stdout);
662
- const stderr = preview(managed.stderr);
676
+ const frameOut = stdout;
677
+ const frameErr = stderr;
678
+ stdout = { value: Buffer.alloc(0), droppedBytes: 0 };
679
+ stderr = { value: Buffer.alloc(0), droppedBytes: 0 };
680
+ const snapshot = displayOutput(managed);
681
+ const snapshotFrame = initialSnapshot || frameOut.droppedBytes > 0 || frameErr.droppedBytes > 0;
682
+ initialSnapshot = false;
663
683
  inFlight = Promise.resolve()
664
684
  .then(() => {
665
685
  if (stopped)
666
686
  return;
667
687
  return onOutput({
668
688
  processId,
669
- stdout: stdout.value,
670
- stderr: stderr.value,
671
- truncated: stdout.droppedBytes > 0 || stderr.droppedBytes > 0,
672
- displayOutput: displayOutput(managed)
689
+ mode: snapshotFrame ? 'snapshot' : 'delta',
690
+ stdout: snapshotFrame ? snapshot.stdout : frameOut.value.toString('utf8'),
691
+ stderr: snapshotFrame ? snapshot.stderr : frameErr.value.toString('utf8'),
692
+ truncated: snapshotFrame
693
+ ? snapshot.stdoutDroppedBytes > 0 || snapshot.stderrDroppedBytes > 0
694
+ : frameOut.droppedBytes > 0 || frameErr.droppedBytes > 0,
695
+ displayOutput: snapshot
673
696
  });
674
697
  })
675
698
  .catch(() => {
@@ -678,7 +701,7 @@ function watchProcessOutput(processId, managed, onOutput) {
678
701
  })
679
702
  .finally(() => {
680
703
  inFlight = undefined;
681
- if (dirty)
704
+ if (stdout.value.length || stderr.value.length)
682
705
  schedule();
683
706
  });
684
707
  };
@@ -691,17 +714,18 @@ function watchProcessOutput(processId, managed, onOutput) {
691
714
  }, TOOL_EXECUTION_LIMITS.execProgressIntervalMs);
692
715
  timer.unref();
693
716
  };
694
- const changed = () => {
695
- if (managed.stdout.bytes === 0 && managed.stderr.bytes === 0)
717
+ const changed = (channel, chunk) => {
718
+ if (chunk.length === 0)
696
719
  return;
697
- dirty = true;
720
+ appendDisplay(channel === 'stdout' ? stdout : stderr, chunk);
698
721
  schedule();
699
722
  };
700
- managed.waiters.add(changed);
701
- changed();
723
+ managed.outputListeners.add(changed);
724
+ if (initialSnapshot)
725
+ schedule();
702
726
  return () => {
703
727
  stopped = true;
704
- managed.waiters.delete(changed);
728
+ managed.outputListeners.delete(changed);
705
729
  if (timer)
706
730
  clearTimeout(timer);
707
731
  };
@@ -1,7 +1,7 @@
1
1
  export declare class AsyncQueue<T> implements AsyncIterable<T> {
2
2
  #private;
3
3
  /** Best-effort live values are delivered only to an actively waiting consumer. */
4
- pushIfWaiting(value: T): void;
4
+ pushIfWaiting(value: T): boolean;
5
5
  push(value: T): void;
6
6
  close(): void;
7
7
  discardIfUnobserved(): boolean;
@@ -8,8 +8,10 @@ export class AsyncQueue {
8
8
  #discarded = false;
9
9
  /** Best-effort live values are delivered only to an actively waiting consumer. */
10
10
  pushIfWaiting(value) {
11
- if (this.#waiterOffset < this.#waiters.length)
12
- this.push(value);
11
+ if (this.#waiterOffset >= this.#waiters.length || this.#closed || this.#discarded)
12
+ return false;
13
+ this.push(value);
14
+ return true;
13
15
  }
14
16
  push(value) {
15
17
  if (this.#closed || this.#discarded)
@@ -91,7 +91,7 @@ export type AgentEvent = AgentEventBase & ({
91
91
  truncated?: boolean;
92
92
  } | {
93
93
  type: 'tool.output';
94
- toolName: 'exec' | 'exec_wait';
94
+ toolName: 'exec' | 'exec_wait' | 'code_exec_wait';
95
95
  callId: string;
96
96
  parentCallId?: string;
97
97
  output: import('../tools/contracts.js').ExecOutputSnapshot;
@@ -40,12 +40,19 @@ export function waitForToolCondition(input) {
40
40
  reject(error);
41
41
  };
42
42
  const check = () => {
43
- if (input.signal?.aborted)
44
- abort();
45
- else if (input.ready())
46
- finish('ready');
47
- else if (input.interrupt?.pending())
48
- finish('interrupted');
43
+ if (settled)
44
+ return;
45
+ try {
46
+ if (input.signal?.aborted)
47
+ abort();
48
+ else if (input.ready())
49
+ finish('ready');
50
+ else if (input.interrupt?.pending())
51
+ finish('interrupted');
52
+ }
53
+ catch (error) {
54
+ finish('interrupted', error);
55
+ }
49
56
  };
50
57
  const abort = () => finish('interrupted', input.signal?.reason ??
51
58
  new AgentError('MAR_AGENT_EXECUTION_CANCELLED', 'Execution cancelled.'));
@@ -105,6 +105,7 @@ export declare class SessionExecution {
105
105
  private terminalAdmitted;
106
106
  private releaseLease;
107
107
  private eventSequence;
108
+ private readonly missedOutput;
108
109
  private persistedTerminal;
109
110
  private completionAdmitted;
110
111
  private appendedMessages;
@@ -65,6 +65,7 @@ export class SessionExecution {
65
65
  terminalAdmitted = false;
66
66
  releaseLease;
67
67
  eventSequence = 0;
68
+ missedOutput = new Set();
68
69
  persistedTerminal = false;
69
70
  completionAdmitted = false;
70
71
  appendedMessages = [];
@@ -235,10 +236,32 @@ export class SessionExecution {
235
236
  full.type === 'execution.failed' ||
236
237
  full.type === 'execution.cancelled')
237
238
  this.#observedTerminal = structuredClone(full);
238
- if (full.type === 'tool.output')
239
- this.events.pushIfWaiting(full);
240
- else
239
+ if (full.type === 'tool.output') {
240
+ const missed = this.missedOutput.has(full.callId);
241
+ const deliver = missed && full.output.displayOutput
242
+ ? {
243
+ ...full,
244
+ output: {
245
+ ...full.output,
246
+ mode: 'snapshot',
247
+ stdout: full.output.displayOutput.stdout,
248
+ stderr: full.output.displayOutput.stderr,
249
+ truncated: full.output.truncated ||
250
+ full.output.displayOutput.stdoutDroppedBytes > 0 ||
251
+ full.output.displayOutput.stderrDroppedBytes > 0
252
+ }
253
+ }
254
+ : full;
255
+ if (this.events.pushIfWaiting(deliver))
256
+ this.missedOutput.delete(full.callId);
257
+ else
258
+ this.missedOutput.add(full.callId);
259
+ }
260
+ else {
261
+ if (full.type === 'tool.completed' || full.type === 'tool.failed')
262
+ this.missedOutput.delete(full.callId);
241
263
  this.events.push(full);
264
+ }
242
265
  ports.observe?.(full);
243
266
  this.compactRequests.publish(full);
244
267
  try {
@@ -3,7 +3,7 @@ import type { ToolExecutionContext, ToolExecutionResult } from '../contracts.js'
3
3
  import type { CodeObservation, CodeToolCallRecord } from '../../session/code-observation-contract.js';
4
4
  export interface CellOutput {
5
5
  call(value: CodeToolCallRecord): void;
6
- observe(value: CodeObservation): void;
6
+ observe(value: CodeObservation, display?: CodeObservation, displayHeadTruncated?: boolean): void;
7
7
  text(value: string, originalBytes?: number): void;
8
8
  image(value: NonNullable<ToolExecutionResult['images']>[number]): void;
9
9
  yield(): void;
@@ -6,6 +6,7 @@ import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
6
6
  import { waitForToolCondition, waitMetadata } from '../wait.js';
7
7
  import { BoundedTextOutput, renderCapturedText } from './text-output.js';
8
8
  import { boundObservations, newObservationId } from './observations.js';
9
+ import { publicCodeObservations } from '../output-projection.js';
9
10
  import { selectObservationImages, visibleObservation } from '../../session/code-observation-projection.js';
10
11
  /** Keeps a failed tool call observable without discarding earlier explicit output. */
11
12
  export class CodeCellExecutionError extends AgentError {
@@ -20,6 +21,110 @@ export class CodeCellExecutionError extends AgentError {
20
21
  this.result = result;
21
22
  }
22
23
  }
24
+ /** A waiting call owns its bounded display window; delivery never delays the cell. */
25
+ function forwardCellOutput(publish) {
26
+ let stopped = false;
27
+ let failed = false;
28
+ let inFlight = false;
29
+ let pending = false;
30
+ let latest;
31
+ // Source metadata is bounded by the cell's tool-call budget; text shares one window.
32
+ const sources = new Map();
33
+ const trim = (channel) => {
34
+ let excess = [...sources.values()].reduce((bytes, source) => bytes + Buffer.byteLength(source[channel]), 0) - limits.execProgressMaxOutputBytes;
35
+ for (const source of sources.values()) {
36
+ if (excess <= 0)
37
+ break;
38
+ const bytes = Buffer.from(source[channel]);
39
+ let removed = Math.min(excess, bytes.length);
40
+ while (removed < bytes.length && (bytes[removed] & 0xc0) === 0x80)
41
+ removed++;
42
+ source[channel] = bytes.subarray(removed).toString('utf8');
43
+ excess -= removed;
44
+ }
45
+ };
46
+ const snapshot = () => {
47
+ const entries = [...sources.values()];
48
+ const stdout = entries.map((source) => source.stdout).join('');
49
+ const stderr = entries.map((source) => source.stderr).join('');
50
+ const stdoutDroppedBytes = entries.reduce((bytes, source) => bytes + source.stdoutBytes, 0) - Buffer.byteLength(stdout);
51
+ const stderrDroppedBytes = entries.reduce((bytes, source) => bytes + source.stderrBytes, 0) - Buffer.byteLength(stderr);
52
+ return {
53
+ ...latest,
54
+ mode: 'snapshot',
55
+ stdout,
56
+ stderr,
57
+ truncated: entries.some((source) => source.truncated) ||
58
+ stdoutDroppedBytes > 0 ||
59
+ stderrDroppedBytes > 0,
60
+ displayOutput: {
61
+ stdout,
62
+ stderr,
63
+ stdoutDroppedBytes,
64
+ stderrDroppedBytes
65
+ }
66
+ };
67
+ };
68
+ const send = (output) => {
69
+ inFlight = true;
70
+ void Promise.resolve()
71
+ .then(() => (stopped ? undefined : publish(output)))
72
+ .catch(() => {
73
+ failed = true;
74
+ })
75
+ .finally(() => {
76
+ inFlight = false;
77
+ if (!stopped && !failed && pending) {
78
+ pending = false;
79
+ send(snapshot());
80
+ }
81
+ });
82
+ };
83
+ return {
84
+ onOutput(output) {
85
+ if (stopped || failed)
86
+ return;
87
+ const isSnapshot = output.mode !== 'delta';
88
+ const source = sources.get(output.processId) ?? {
89
+ stdout: '',
90
+ stderr: '',
91
+ stdoutBytes: 0,
92
+ stderrBytes: 0,
93
+ truncated: false
94
+ };
95
+ if (isSnapshot) {
96
+ source.stdout = output.stdout;
97
+ source.stderr = output.stderr;
98
+ source.stdoutBytes =
99
+ Buffer.byteLength(output.stdout) + (output.displayOutput?.stdoutDroppedBytes ?? 0);
100
+ source.stderrBytes =
101
+ Buffer.byteLength(output.stderr) + (output.displayOutput?.stderrDroppedBytes ?? 0);
102
+ source.truncated = output.truncated;
103
+ }
104
+ else {
105
+ source.stdout += output.stdout;
106
+ source.stderr += output.stderr;
107
+ source.stdoutBytes += Buffer.byteLength(output.stdout);
108
+ source.stderrBytes += Buffer.byteLength(output.stderr);
109
+ source.truncated ||= output.truncated;
110
+ }
111
+ sources.set(output.processId, source);
112
+ trim('stdout');
113
+ trim('stderr');
114
+ latest = output;
115
+ if (inFlight)
116
+ pending = true;
117
+ else
118
+ send(isSnapshot ? snapshot() : { ...output, displayOutput: snapshot().displayOutput });
119
+ },
120
+ stop() {
121
+ stopped = true;
122
+ pending = false;
123
+ sources.clear();
124
+ latest = undefined;
125
+ }
126
+ };
127
+ }
23
128
  function options(value, wait) {
24
129
  if (!value || typeof value !== 'object' || Array.isArray(value))
25
130
  throw new AgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Exec options must be an object.');
@@ -77,6 +182,7 @@ export class CodeCells {
77
182
  throw new AgentError('MAR_AGENT_PROCESS_LIMIT', `The Local Host shared running-cell limit (${limits.codeMaxConcurrentCells}) was reached.`);
78
183
  const controller = new AbortController();
79
184
  const cell = {
185
+ source: { kind: 'CODE', text: source.slice(0, 10_000), truncated: source.length > 10_000 },
80
186
  cellId: this.#handles.create(),
81
187
  sessionId: context.sessionId,
82
188
  executionId: context.executionId,
@@ -90,9 +196,14 @@ export class CodeCells {
90
196
  yielded: false,
91
197
  truncated: false,
92
198
  listeners: new Set(),
199
+ outputListeners: new Set(),
93
200
  done: Promise.resolve()
94
201
  };
95
202
  this.#cells.set(cell.cellId, cell);
203
+ const unsubscribeOutput = context.codeMode?.subscribeToolOutput?.((output) => {
204
+ for (const listener of cell.outputListeners)
205
+ listener(output);
206
+ });
96
207
  const notify = () => {
97
208
  for (const listener of cell.listeners)
98
209
  listener();
@@ -104,7 +215,18 @@ export class CodeCells {
104
215
  releaseSession = await context.codeMode?.retainSession?.();
105
216
  return run(source, { ...context, signal: controller.signal, waitInterrupt: undefined }, {
106
217
  call: (value) => cell.calls.set(value.callId, value),
107
- observe: (value) => cell.observations.push(value),
218
+ observe: (value, display = value, displayHeadTruncated = false) => {
219
+ cell.observations.push(value);
220
+ const observations = [...(cell.display?.observations ?? []), display];
221
+ cell.display = publicCodeObservations(observations, {
222
+ cellId: cell.cellId,
223
+ status: cell.status,
224
+ observations,
225
+ ...(cell.display?.outputHeadTruncated || displayHeadTruncated
226
+ ? { outputHeadTruncated: true }
227
+ : {})
228
+ });
229
+ },
108
230
  text: (text, originalBytes) => cell.text.append(text, originalBytes),
109
231
  image: (image) => cell.images.push(image),
110
232
  yield: () => {
@@ -124,6 +246,7 @@ export class CodeCells {
124
246
  outcome = controller.signal.aborted ? 'cancelled' : 'failed';
125
247
  })
126
248
  .then(async () => {
249
+ unsubscribeOutput?.();
127
250
  try {
128
251
  await context.codeMode?.onSettled?.({
129
252
  cellId: cell.cellId,
@@ -179,7 +302,23 @@ export class CodeCells {
179
302
  abort();
180
303
  });
181
304
  }
182
- return this.#wait(cell, input, context.signal, false, context.codeMode?.supportsImages === true, input.action === 'poll' ? context.waitInterrupt : undefined);
305
+ const forward = cell.executionId !== context.executionId &&
306
+ input.action === 'poll' &&
307
+ input.yieldMs > 0 &&
308
+ context.onOutput
309
+ ? forwardCellOutput(context.onOutput)
310
+ : undefined;
311
+ if (forward)
312
+ cell.outputListeners.add(forward.onOutput);
313
+ try {
314
+ return await this.#wait(cell, input, context.signal, false, context.codeMode?.supportsImages === true, input.action === 'poll' ? context.waitInterrupt : undefined);
315
+ }
316
+ finally {
317
+ if (forward) {
318
+ cell.outputListeners.delete(forward.onOutput);
319
+ forward.stop();
320
+ }
321
+ }
183
322
  }
184
323
  async #wait(cell, input, signal, initial, supportsImages, interrupt) {
185
324
  const outcome = input.yieldMs === 0
@@ -232,6 +371,14 @@ export class CodeCells {
232
371
  observations: visible.observations.map(visibleObservation)
233
372
  }),
234
373
  data,
374
+ eventData: {
375
+ ...data,
376
+ ...cell.display,
377
+ status: cell.status,
378
+ cached: true,
379
+ ...wait,
380
+ executionSource: cell.source
381
+ },
235
382
  truncated: data.truncated
236
383
  };
237
384
  }
@@ -247,6 +394,7 @@ export class CodeCells {
247
394
  return {
248
395
  ...cell.terminal,
249
396
  data,
397
+ eventData: { ...data, executionSource: cell.source },
250
398
  content: JSON.stringify(data),
251
399
  truncated: cell.terminal.truncated || bounded.truncated
252
400
  };
@@ -301,6 +449,12 @@ export class CodeCells {
301
449
  calls: [...cell.calls.values()],
302
450
  observations: remapped,
303
451
  data,
452
+ eventData: {
453
+ ...data,
454
+ ...cell.display,
455
+ status: cell.status,
456
+ executionSource: cell.source
457
+ },
304
458
  truncated: cell.truncated || structured.truncated,
305
459
  ...(visibleImages.length ? { images: visibleImages } : {})
306
460
  };
@@ -49,6 +49,9 @@ export function createCodeToolDispatch(input) {
49
49
  const { eventData: _eventData, ...guestResult } = result;
50
50
  return {
51
51
  ...guestResult,
52
+ ...((call.name === 'exec' || call.name === 'exec_wait') && data !== undefined
53
+ ? { eventData: { ...data } }
54
+ : {}),
52
55
  content: bounded.content,
53
56
  truncated: result.truncated === true || bounded.truncated
54
57
  };
@@ -41,6 +41,8 @@ export interface CodeModeEngine {
41
41
  format: 'text' | 'json';
42
42
  value: string;
43
43
  originalBytes: number;
44
+ /** Public display tail, independent of the model capture budget. */
45
+ displayValue?: string;
44
46
  } | {
45
47
  type: 'image';
46
48
  value: unknown;
@@ -300,7 +300,7 @@ export class CodeModeTool {
300
300
  const images = [];
301
301
  const observations = [];
302
302
  let observationBytes = 0;
303
- const appendObservation = (value, originalBytes) => {
303
+ const appendObservation = (value, originalBytes, display) => {
304
304
  const capture = new BoundedTextOutput(Math.max(0, limits.codeMaxOutputBytes - observationBytes), '');
305
305
  const body = value.type === 'tool_result' ? value.result.content : value.value;
306
306
  capture.append(body);
@@ -323,7 +323,10 @@ export class CodeModeTool {
323
323
  truncated: value.truncated === true || bounded.droppedBytes > 0
324
324
  };
325
325
  observations.push(observation);
326
- output?.observe(observation);
326
+ output?.observe(observation, display ?? value, display !== undefined &&
327
+ (display.type === 'tool_result'
328
+ ? display.result.truncated === true
329
+ : display.truncated === true));
327
330
  };
328
331
  const toolResults = new Map();
329
332
  let transportBytes = 0;
@@ -389,6 +392,7 @@ export class CodeModeTool {
389
392
  return index;
390
393
  };
391
394
  let observation;
395
+ let displayObservation;
392
396
  if (value.type === 'tool_result' && typeof value.operationId === 'string') {
393
397
  const original = toolResults.get(value.operationId);
394
398
  if (!original)
@@ -423,6 +427,23 @@ export class CodeModeTool {
423
427
  ...(indices?.length ? { imageIndices: indices } : {})
424
428
  }
425
429
  };
430
+ const display = original.result.eventData?.displayOutput;
431
+ if ((original.source.toolName === 'exec' || original.source.toolName === 'exec_wait') &&
432
+ display !== null &&
433
+ typeof display === 'object' &&
434
+ 'stdout' in display &&
435
+ 'stderr' in display &&
436
+ typeof display.stdout === 'string' &&
437
+ typeof display.stderr === 'string')
438
+ displayObservation = {
439
+ ...observation,
440
+ result: {
441
+ content: '',
442
+ data: { stdout: display.stdout, stderr: display.stderr },
443
+ truncated: ('stdoutDroppedBytes' in display && Number(display.stdoutDroppedBytes) > 0) ||
444
+ ('stderrDroppedBytes' in display && Number(display.stderrDroppedBytes) > 0)
445
+ }
446
+ };
426
447
  }
427
448
  else if (value.type === 'image') {
428
449
  observation = {
@@ -444,12 +465,22 @@ export class CodeModeTool {
444
465
  truncated: typeof value.originalBytes === 'number' &&
445
466
  value.originalBytes > Buffer.byteLength(value.value)
446
467
  };
468
+ if (typeof value.displayValue === 'string') {
469
+ if (Buffer.byteLength(value.displayValue) > 10_000)
470
+ throw new AgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode sent oversized display text.');
471
+ displayObservation = {
472
+ ...observation,
473
+ value: value.displayValue,
474
+ truncated: typeof value.originalBytes === 'number' &&
475
+ value.originalBytes > Buffer.byteLength(value.displayValue)
476
+ };
477
+ }
447
478
  }
448
479
  else
449
480
  throw new AgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Invalid observation frame.');
450
481
  appendObservation(observation, value.type === 'value' && typeof value.originalBytes === 'number'
451
482
  ? value.originalBytes
452
- : undefined);
483
+ : undefined, displayObservation);
453
484
  }
454
485
  else if (frame.kind === 'text' && typeof frame.value === 'string') {
455
486
  const originalBytes = frame.originalBytes ?? Buffer.byteLength(frame.value);
@@ -527,10 +558,11 @@ export class CodeModeTool {
527
558
  : undefined
528
559
  }
529
560
  });
530
- const { content, ...structured } = output;
561
+ const { eventData: _eventData, ...guestOutput } = output;
562
+ const { content, ...structured } = guestOutput;
531
563
  const projected = output.data !== undefined && content === JSON.stringify(output.data)
532
564
  ? structured
533
- : output;
565
+ : guestOutput;
534
566
  await write({ id, ok: true, output: projected });
535
567
  }
536
568
  catch (error) {
@@ -354,6 +354,7 @@ export async function createCodeModeEngine(options) {
354
354
  }
355
355
  const separatorBytes = textCalls ? 1 : 0;
356
356
  const capture = new BoundedTextOutput(Math.max(0, input.limits.outputBytes - outputBytes - separatorBytes), '');
357
+ const display = new BoundedTextOutput(10_000, '', 'tail');
357
358
  const emit = vm.newFunction('textChunk', (chunk) => {
358
359
  if (!chunk || vm.typeof(chunk) !== 'string')
359
360
  return {
@@ -366,7 +367,9 @@ export async function createCodeModeEngine(options) {
366
367
  return {
367
368
  error: guestError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Text serialization chunk limit exceeded.')
368
369
  };
369
- capture.append(vm.getString(chunk));
370
+ const text = vm.getString(chunk);
371
+ capture.append(text);
372
+ display.append(text);
370
373
  return vm.undefined;
371
374
  });
372
375
  const result = callGuest(formatText, vm.undefined, handle ?? vm.undefined, emit);
@@ -381,6 +384,7 @@ export async function createCodeModeEngine(options) {
381
384
  type: 'value',
382
385
  format: handle && vm.typeof(handle) === 'object' ? 'json' : 'text',
383
386
  value: captured.text,
387
+ displayValue: display.snapshot().text,
384
388
  originalBytes: captured.originalBytes
385
389
  });
386
390
  }
@@ -1,9 +1,10 @@
1
1
  export function renderCapturedText(snapshot: any, imageCount?: number): string;
2
2
  /** Bounded UTF-8 capture shared by trusted engines, the bridge and retained cells. */
3
3
  export class BoundedTextOutput {
4
- constructor(maximumBytes: any, separator?: string);
4
+ constructor(maximumBytes: any, separator?: string, retention?: string);
5
5
  maximumBytes: any;
6
6
  separator: string;
7
+ retention: string;
7
8
  append(value: any, originalBytes?: number): void;
8
9
  snapshot(): {
9
10
  text: string;
@@ -6,9 +6,10 @@ export class BoundedTextOutput {
6
6
  #retained = 0;
7
7
  #hasValue = false;
8
8
  #sealed = false;
9
- constructor(maximumBytes, separator = '\n') {
9
+ constructor(maximumBytes, separator = '\n', retention = 'head') {
10
10
  this.maximumBytes = maximumBytes;
11
11
  this.separator = separator;
12
+ this.retention = retention;
12
13
  }
13
14
  append(value, originalBytes = Buffer.byteLength(value)) {
14
15
  const separator = this.#hasValue ? this.separator : '';
@@ -16,10 +17,22 @@ export class BoundedTextOutput {
16
17
  this.#original += Buffer.byteLength(separator) + originalBytes;
17
18
  this.#capture(separator);
18
19
  this.#capture(value);
19
- if (originalBytes > Buffer.byteLength(value))
20
+ if (this.retention === 'head' && originalBytes > Buffer.byteLength(value))
20
21
  this.#sealed = true;
21
22
  }
22
23
  #capture(value) {
24
+ if (this.retention === 'tail') {
25
+ let start = Math.max(0, value.length - this.maximumBytes);
26
+ if (start > 0 && /[\uDC00-\uDFFF]/u.test(value[start]))
27
+ start++;
28
+ const bytes = Buffer.from(this.#chunks.join('') + value.slice(start));
29
+ let offset = Math.max(0, bytes.length - this.maximumBytes);
30
+ while (offset < bytes.length && (bytes[offset] & 0xc0) === 0x80)
31
+ offset++;
32
+ this.#chunks = [bytes.subarray(offset).toString('utf8')];
33
+ this.#retained = bytes.length - offset;
34
+ return;
35
+ }
23
36
  const remaining = Math.max(0, this.maximumBytes - this.#used);
24
37
  if (this.#sealed || !value.length)
25
38
  return;
@@ -41,6 +41,7 @@ export interface ToolExecutionContext {
41
41
  readonly data: Buffer;
42
42
  }[];
43
43
  codeMode?: {
44
+ subscribeToolOutput?(listener: (output: ExecOutputSnapshot) => void): () => void;
44
45
  retainSession?(): Promise<() => Promise<void>>;
45
46
  onSettled?(result: CodeCellEventData): Promise<void>;
46
47
  supportsImages?: boolean;
@@ -94,6 +95,8 @@ export interface AgentToolImageArtifact {
94
95
  export type AgentToolArtifact = AgentToolImageArtifact;
95
96
  export interface ExecOutputSnapshot {
96
97
  processId: string;
98
+ /** Omitted for legacy snapshot sources; delta is a real decoded process chunk. */
99
+ mode?: 'delta' | 'snapshot';
97
100
  stdout: string;
98
101
  stderr: string;
99
102
  truncated: boolean;
@@ -106,6 +109,13 @@ export interface ExecDisplayOutput {
106
109
  stdoutDroppedBytes: number;
107
110
  stderrDroppedBytes: number;
108
111
  }
112
+ /** Bounded original input for public presentation, never added to model results. */
113
+ export interface ExecutionSource {
114
+ kind: 'SHELL' | 'CODE';
115
+ text: string;
116
+ cwd?: string;
117
+ truncated: boolean;
118
+ }
109
119
  export interface ToolExecutionResult {
110
120
  calls?: readonly import('../session/code-observation-contract.js').CodeToolCallRecord[];
111
121
  content: string;
@@ -124,6 +134,7 @@ export interface ToolExecutionResult {
124
134
  resultExtensions?: readonly import('./result-extension-contract.js').ToolResultExtension[];
125
135
  }
126
136
  export interface CodeCellEventData {
137
+ executionSource?: ExecutionSource;
127
138
  permissionStop?: import('./permission-contract.js').PermissionStop;
128
139
  calls?: readonly import('../session/code-observation-contract.js').CodeToolCallRecord[];
129
140
  cellId: string;
@@ -132,6 +143,8 @@ export interface CodeCellEventData {
132
143
  reason?: 'steer';
133
144
  cached?: boolean;
134
145
  truncated?: boolean;
146
+ /** Public output projection discarded older text; not inferred from source truncation. */
147
+ outputHeadTruncated?: boolean;
135
148
  observations?: readonly import('../session/code-observation-contract.js').CodeObservation[];
136
149
  error?: {
137
150
  code: string;
@@ -139,6 +152,7 @@ export interface CodeCellEventData {
139
152
  };
140
153
  }
141
154
  export interface ExecResult {
155
+ executionSource?: ExecutionSource;
142
156
  status: 'running' | 'completed' | 'cancelled';
143
157
  waitOutcome?: import('./wait.js').ToolWaitOutcome;
144
158
  reason?: 'steer';
@@ -24,6 +24,7 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
24
24
  const assertSessionLive = scope.assertLive;
25
25
  const assertExecutionLive = scope.assertExecutionLive;
26
26
  const emitCodeEvent = scope.emit;
27
+ const outputListeners = new Set();
27
28
  const retainSession = codeMode.retainSession?.bind(codeMode);
28
29
  let retained = 0;
29
30
  const assertDispatchLive = () => {
@@ -40,6 +41,10 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
40
41
  signal,
41
42
  codeMode: {
42
43
  ...codeMode,
44
+ subscribeToolOutput: (listener) => {
45
+ outputListeners.add(listener);
46
+ return () => outputListeners.delete(listener);
47
+ },
43
48
  ...(retainSession === undefined
44
49
  ? {}
45
50
  : {
@@ -67,7 +72,12 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
67
72
  parentCallId: call.callId,
68
73
  schedule: scope.schedule,
69
74
  assertLive: assertDispatchLive,
70
- emit: (event) => emitCodeEvent(event, assertDispatchLive)
75
+ emit: (event) => {
76
+ if (event.type === 'tool.output')
77
+ for (const listener of outputListeners)
78
+ listener(event.output);
79
+ return emitCodeEvent(event, assertDispatchLive);
80
+ }
71
81
  })
72
82
  },
73
83
  ...(scope.extensionToolNames.has(call.name) && recentImages !== undefined
@@ -118,7 +128,10 @@ export async function executeRoundTools(calls, scope) {
118
128
  : typeof output.eventSummary === 'string'
119
129
  ? output.eventSummary
120
130
  : '');
121
- const structuredData = call.name === 'exec' || call.name === 'exec_wait'
131
+ const structuredData = call.name === 'exec' ||
132
+ call.name === 'exec_wait' ||
133
+ call.name === 'code_exec' ||
134
+ call.name === 'code_exec_wait'
122
135
  ? structuredToolEventData(call.name, output.eventData ?? output.data)
123
136
  : output.eventData === undefined
124
137
  ? structuredToolEventData(call.name, output.data)
@@ -159,7 +172,7 @@ export async function executeRoundTools(calls, scope) {
159
172
  const eventData = failureData ??
160
173
  (partial?.data === undefined
161
174
  ? undefined
162
- : structuredToolEventData(call.name, partial.data));
175
+ : structuredToolEventData(call.name, partial.eventData ?? partial.data));
163
176
  await scope.emit({
164
177
  type: 'tool.failed',
165
178
  ...(failedToolResultExtensions(error)
@@ -2,7 +2,7 @@ import { AgentError } from '../../error.js';
2
2
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
3
3
  /** Share the model-facing body across direct calls and original Code Mode observations. */
4
4
  export function formatExecResult(result) {
5
- const { displayOutput, ...modelResult } = result;
5
+ const { displayOutput, executionSource, ...modelResult } = result;
6
6
  const visible = { ...modelResult };
7
7
  if (result.stderr === '')
8
8
  delete visible.stderr;
@@ -22,8 +22,10 @@ export function formatExecResult(result) {
22
22
  delete visible.cached;
23
23
  return {
24
24
  content: JSON.stringify(visible),
25
- data: displayOutput === undefined ? result : modelResult,
26
- ...(displayOutput === undefined ? {} : { eventData: { ...modelResult, displayOutput } }),
25
+ data: executionSource === undefined && displayOutput === undefined ? result : modelResult,
26
+ ...(displayOutput === undefined && executionSource === undefined
27
+ ? {}
28
+ : { eventData: { ...modelResult, displayOutput, executionSource } }),
27
29
  truncated: result.truncated
28
30
  };
29
31
  }
@@ -12,6 +12,8 @@ export declare function boundedModelToolOutput(toolName: string, value: string,
12
12
  };
13
13
  /** The opted-in allowlist fails closed and never mutates the invocation arguments. */
14
14
  export declare function publicToolEventInput(value: unknown, fields: readonly string[]): Readonly<Record<string, unknown>>;
15
+ /** Explicit observations are public, but their event projection must remain bounded. */
16
+ export declare function publicCodeObservations(values: unknown, data: CodeCellEventData): CodeCellEventData;
15
17
  /** Project only known, validated tool result shapes into public structured events. */
16
18
  export declare function structuredToolEventData(toolName: string, value: unknown): CodeCellEventData | ExecResult | ApplyPatchResult | ReadEventData | ViewImageResult | SleepResult | PublicToolMetadata | undefined;
17
19
  /** Partial patch facts are allowlisted; arbitrary error details never become public event data. */
@@ -68,7 +68,7 @@ export function publicToolEventInput(value, fields) {
68
68
  }
69
69
  }
70
70
  /** Explicit observations are public, but their event projection must remain bounded. */
71
- function publicCodeObservations(values, data) {
71
+ export function publicCodeObservations(values, data) {
72
72
  if (!Array.isArray(values))
73
73
  return data;
74
74
  const retained = new Set(data.observations ?? []);
@@ -76,6 +76,7 @@ function publicCodeObservations(values, data) {
76
76
  let displayedCount = 0;
77
77
  let remaining = 10_000;
78
78
  let truncated = false;
79
+ let headTruncated = data.outputHeadTruncated === true;
79
80
  const clip = (text) => {
80
81
  let start = Math.max(0, text.length - remaining);
81
82
  if (start > 0 && /[\uDC00-\uDFFF]/u.test(text[start]))
@@ -83,6 +84,7 @@ function publicCodeObservations(values, data) {
83
84
  const result = text.slice(start);
84
85
  remaining -= result.length;
85
86
  truncated ||= start > 0;
87
+ headTruncated ||= start > 0;
86
88
  return result;
87
89
  };
88
90
  for (let index = values.length - 1; index >= 0; index--) {
@@ -95,6 +97,7 @@ function publicCodeObservations(values, data) {
95
97
  (entry.format === 'text' || entry.format === 'json' || entry.format === 'image')) {
96
98
  if (displayedCount >= 32) {
97
99
  truncated = true;
100
+ headTruncated = true;
98
101
  continue;
99
102
  }
100
103
  displayedCount++;
@@ -122,6 +125,7 @@ function publicCodeObservations(values, data) {
122
125
  (entry.outcome === 'completed' || entry.outcome === 'failed')) {
123
126
  if (displayedCount >= 32) {
124
127
  truncated = true;
128
+ headTruncated = true;
125
129
  continue;
126
130
  }
127
131
  displayedCount++;
@@ -146,7 +150,12 @@ function publicCodeObservations(values, data) {
146
150
  }
147
151
  }
148
152
  }
149
- return { ...data, observations: selected, ...(truncated ? { truncated: true } : {}) };
153
+ return {
154
+ ...data,
155
+ observations: selected,
156
+ ...(truncated ? { truncated: true } : {}),
157
+ ...(headTruncated ? { outputHeadTruncated: true } : {})
158
+ };
150
159
  }
151
160
  /** Project only known, validated tool result shapes into public structured events. */
152
161
  export function structuredToolEventData(toolName, value) {
@@ -176,7 +185,20 @@ export function structuredToolEventData(toolName, value) {
176
185
  ['running', 'completed', 'failed', 'cancelled'].includes(String(value.status)))
177
186
  return publicCodeObservations(value.observations, {
178
187
  cellId: value.cellId,
188
+ ...(isRecord(value.executionSource) &&
189
+ value.executionSource.kind === 'CODE' &&
190
+ typeof value.executionSource.text === 'string'
191
+ ? {
192
+ executionSource: {
193
+ kind: 'CODE',
194
+ text: value.executionSource.text.slice(0, 10_000),
195
+ truncated: value.executionSource.truncated === true ||
196
+ value.executionSource.text.length > 10_000
197
+ }
198
+ }
199
+ : {}),
179
200
  status: value.status,
201
+ ...(value.outputHeadTruncated === true ? { outputHeadTruncated: true } : {}),
180
202
  cached: value.cached === true,
181
203
  ...(['snapshot', 'completion', 'timeout', 'yield', 'interrupted'].includes(String(value.waitOutcome))
182
204
  ? { waitOutcome: value.waitOutcome }
@@ -19,7 +19,7 @@ export interface ToolImageStore extends ResourceStore {
19
19
  }
20
20
  export type ToolRuntimeEvent = {
21
21
  type: 'tool.output';
22
- toolName: 'exec' | 'exec_wait';
22
+ toolName: 'exec' | 'exec_wait' | 'code_exec_wait';
23
23
  callId: string;
24
24
  parentCallId?: string;
25
25
  output: import('./contracts.js').ExecOutputSnapshot;
@@ -50,7 +50,8 @@ export async function executeCapturedTool(kind, call, context) {
50
50
  mode: context.mode,
51
51
  signal: context.signal,
52
52
  waitInterrupt: context.waitInterrupt,
53
- ...((name === 'exec' || name === 'exec_wait') && callId !== undefined
53
+ ...((name === 'exec' || name === 'exec_wait' || name === 'code_exec_wait') &&
54
+ callId !== undefined
54
55
  ? {
55
56
  onOutput: (output) => context.emit({
56
57
  type: 'tool.output',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.42",
3
+ "version": "0.11.44",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",