@myagentroam/agent 0.9.142 → 0.9.144

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.
Files changed (31) hide show
  1. package/dist/agent-core/model/anthropic-messages.js +24 -13
  2. package/dist/agent-core/model/contracts.d.ts +4 -0
  3. package/dist/agent-core/model/openai-responses.js +16 -9
  4. package/dist/agent-core/runtime/async-queue.js +7 -5
  5. package/dist/agent-core/runtime/context/gc-tokens.js +4 -2
  6. package/dist/agent-core/runtime/context/gc.js +147 -14
  7. package/dist/agent-core/runtime/context/projection.js +32 -1
  8. package/dist/agent-core/runtime/events.d.ts +1 -0
  9. package/dist/agent-core/runtime/execution.js +4 -0
  10. package/dist/agent-core/sdk/session.js +1 -1
  11. package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -1
  12. package/dist/agent-core/session/context-gc-checkpoint.js +123 -26
  13. package/dist/agent-core/session/context-gc-observations.d.ts +38 -0
  14. package/dist/agent-core/session/context-gc-observations.js +148 -0
  15. package/dist/agent-core/session/context-gc-replacements.d.ts +1 -1
  16. package/dist/agent-core/session/context-gc-replacements.js +6 -4
  17. package/dist/agent-core/tools/callers.js +1 -1
  18. package/dist/agent-core/tools/code-mode/cells.d.ts +3 -0
  19. package/dist/agent-core/tools/code-mode/cells.js +62 -6
  20. package/dist/agent-core/tools/code-mode/dispatch.js +7 -4
  21. package/dist/agent-core/tools/code-mode/engine.d.ts +12 -0
  22. package/dist/agent-core/tools/code-mode/index.js +183 -14
  23. package/dist/agent-core/tools/code-mode/observations.d.ts +103 -0
  24. package/dist/agent-core/tools/code-mode/observations.js +195 -0
  25. package/dist/agent-core/tools/code-mode/quickjs-engine.js +121 -20
  26. package/dist/agent-core/tools/code-mode/worker.js +8 -1
  27. package/dist/agent-core/tools/contracts.d.ts +4 -0
  28. package/dist/agent-core/tools/execution.d.ts +2 -0
  29. package/dist/agent-core/tools/execution.js +10 -1
  30. package/dist/agent-core/tools/output-projection.js +35 -1
  31. package/package.json +1 -1
@@ -3,12 +3,15 @@ import { MarAgentError, describeMarAgentError } from '../../error.js';
3
3
  import { truncateModelText } from '../../runtime/token-budget.js';
4
4
  import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
5
5
  import { BoundedTextOutput, renderCapturedText } from './text-output.js';
6
+ import { boundObservations, selectObservationImages, newObservationId, visibleObservation } from './observations.js';
6
7
  /** Keeps a failed tool call observable without discarding earlier explicit output. */
7
8
  export class CodeCellExecutionError extends MarAgentError {
8
9
  result;
9
10
  constructor(error, result) {
10
11
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_CODE_FAILED');
11
- super(diagnostic.code, diagnostic.message.replace(`${diagnostic.code}: `, ''), {
12
+ super(diagnostic.code, diagnostic.message.startsWith(`${diagnostic.code}: `)
13
+ ? diagnostic.message.slice(diagnostic.code.length + 2)
14
+ : diagnostic.message, {
12
15
  cause: error,
13
16
  ...(error instanceof MarAgentError ? { retryable: error.retryable } : {})
14
17
  });
@@ -88,6 +91,9 @@ export class CodeCells {
88
91
  status: 'running',
89
92
  text: new BoundedTextOutput(limits.codeMaxOutputBytes),
90
93
  images: [],
94
+ observations: [],
95
+ calls: new Map(),
96
+ imageOffset: 0,
91
97
  yielded: false,
92
98
  truncated: false,
93
99
  listeners: new Set(),
@@ -104,6 +110,8 @@ export class CodeCells {
104
110
  .then(async () => {
105
111
  releaseSession = await context.codeMode?.retainSession?.();
106
112
  return run(source, { ...context, signal: controller.signal }, {
113
+ call: (value) => cell.calls.set(value.callId, value),
114
+ observe: (value) => cell.observations.push(value),
107
115
  text: (text, originalBytes) => cell.text.append(text, originalBytes),
108
116
  image: (image) => cell.images.push(image),
109
117
  yield: () => {
@@ -199,6 +207,31 @@ export class CodeCells {
199
207
  if (!supportsImages && (cell.images.length || cell.terminal?.images?.length))
200
208
  throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'The current model cannot receive this cell image output.');
201
209
  if (cell.terminal) {
210
+ if (cell.terminal.observations) {
211
+ const bounded = boundObservations(cell.terminal.observations, input.maxOutputTokens);
212
+ const visible = selectObservationImages(bounded.observations, cell.terminal.images ?? []);
213
+ const original = cell.terminal.data;
214
+ const data = {
215
+ ...original,
216
+ output: truncateModelText(original.output ?? '', input.maxOutputTokens).content,
217
+ observations: visible.observations,
218
+ cached: true,
219
+ truncated: cell.terminal.truncated === true || bounded.truncated
220
+ };
221
+ return {
222
+ ...cell.terminal,
223
+ observations: visible.observations,
224
+ images: visible.images,
225
+ content: JSON.stringify({
226
+ ...data,
227
+ output: undefined,
228
+ calls: undefined,
229
+ observations: visible.observations.map(visibleObservation)
230
+ }),
231
+ data,
232
+ truncated: data.truncated
233
+ };
234
+ }
202
235
  const snapshot = cell.terminal.data;
203
236
  const bounded = truncateModelText(snapshot.output, input.maxOutputTokens);
204
237
  const data = {
@@ -219,7 +252,21 @@ export class CodeCells {
219
252
  const text = renderCapturedText(captured, images.length);
220
253
  cell.yielded = false;
221
254
  const bounded = truncateModelText(text, input.maxOutputTokens);
222
- cell.truncated ||= captured.droppedBytes > 0 || bounded.truncated;
255
+ const pending = cell.observations.splice(0);
256
+ if (!pending.length && captured.text)
257
+ pending.push({
258
+ type: 'value',
259
+ id: newObservationId(),
260
+ format: 'text',
261
+ value: captured.text,
262
+ truncated: captured.droppedBytes > 0
263
+ });
264
+ const structured = boundObservations(pending, input.maxOutputTokens);
265
+ const visible = selectObservationImages(structured.observations, images, cell.imageOffset);
266
+ const remapped = visible.observations;
267
+ const visibleImages = visible.images;
268
+ cell.imageOffset += images.length;
269
+ cell.truncated ||= captured.droppedBytes > 0 || bounded.truncated || structured.truncated;
223
270
  const error = cell.error === undefined
224
271
  ? undefined
225
272
  : describeMarAgentError(cell.error, 'MAR_AGENT_CODE_FAILED');
@@ -227,6 +274,8 @@ export class CodeCells {
227
274
  cellId: cell.cellId,
228
275
  status: cell.status,
229
276
  output: bounded.content,
277
+ observations: remapped,
278
+ calls: [...cell.calls.values()],
230
279
  ...cell.result?.data,
231
280
  outputBytes: captured.originalBytes,
232
281
  outputDroppedBytes: captured.droppedBytes,
@@ -235,10 +284,17 @@ export class CodeCells {
235
284
  cached: false
236
285
  };
237
286
  const result = {
238
- content: JSON.stringify(data),
287
+ content: JSON.stringify({
288
+ ...data,
289
+ output: undefined,
290
+ calls: undefined,
291
+ observations: remapped.map(visibleObservation)
292
+ }),
293
+ calls: [...cell.calls.values()],
294
+ observations: remapped,
239
295
  data,
240
- truncated: cell.truncated,
241
- ...(images.length ? { images } : {})
296
+ truncated: cell.truncated || structured.truncated,
297
+ ...(visibleImages.length ? { images: visibleImages } : {})
242
298
  };
243
299
  if (cell.status !== 'running')
244
300
  cell.terminal = result;
@@ -246,7 +302,7 @@ export class CodeCells {
246
302
  if (initial && cell.status !== 'running') {
247
303
  if (cell.error !== undefined)
248
304
  throw new CodeCellExecutionError(cell.error, result);
249
- return { ...result, content: bounded.content || 'Code Exec completed without emitted text.' };
305
+ return result;
250
306
  }
251
307
  return result;
252
308
  }
@@ -1,9 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { MarAgentError, describeMarAgentError } from '../../error.js';
3
3
  import { AGENT_EXECUTION_POLICY } from '../../runtime/execution-policy.js';
4
- import { truncateModelText } from '../../runtime/token-budget.js';
5
4
  import { allowsToolCaller } from '../callers.js';
6
- import { structuredToolEventData } from '../output-projection.js';
5
+ import { boundedModelToolOutput, structuredToolEventData } from '../output-projection.js';
7
6
  import { executeRegisteredTool } from '../runtime.js';
8
7
  /** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
9
8
  export function createCodeToolDispatch(input) {
@@ -24,7 +23,7 @@ export function createCodeToolDispatch(input) {
24
23
  signal,
25
24
  ...(expected ? { expectedMcpTool: expected } : {})
26
25
  });
27
- const bounded = truncateModelText(result.content, AGENT_EXECUTION_POLICY.modelToolOutputTokens);
26
+ const bounded = boundedModelToolOutput(call.name, result.content, result.data);
28
27
  const data = structuredToolEventData(call.name, result.data);
29
28
  const summary = result.content.slice(0, AGENT_EXECUTION_POLICY.toolEventSummaryCharacters);
30
29
  await input.emit({
@@ -37,7 +36,11 @@ export function createCodeToolDispatch(input) {
37
36
  summary.length < result.content.length,
38
37
  ...(result.artifacts ? { artifacts: result.artifacts } : {})
39
38
  });
40
- return { ...result, content: bounded.content };
39
+ return {
40
+ ...result,
41
+ content: bounded.content,
42
+ truncated: result.truncated === true || bounded.truncated
43
+ };
41
44
  }
42
45
  catch (error) {
43
46
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_HOST_OPERATION_FAILED');
@@ -32,6 +32,18 @@ export interface CodeModeEngine {
32
32
  onToolCall(call: ToolCall): Promise<ProgrammaticToolResult>;
33
33
  onText(value: string, originalBytes?: number): void;
34
34
  onImage(value: unknown): void;
35
+ onObserve(value: {
36
+ type: 'tool_result';
37
+ operationId: string;
38
+ } | {
39
+ type: 'value';
40
+ format: 'text' | 'json';
41
+ value: string;
42
+ originalBytes: number;
43
+ } | {
44
+ type: 'image';
45
+ value: unknown;
46
+ }): void;
35
47
  onYield(): void;
36
48
  }): Promise<{
37
49
  readonly stateMutations: readonly CodeModeStateMutation[];
@@ -8,6 +8,7 @@ import { MarAgentError, describeMarAgentError } from '../../error.js';
8
8
  import { directLocalProcessExecutor, signalProcessTree } from '../../host/local-process-executor.js';
9
9
  import { allowsDirectTool, allowsToolCaller } from '../callers.js';
10
10
  import { CodeCells } from './cells.js';
11
+ import { newObservationId, renderObservations } from './observations.js';
11
12
  import { parseCodeModeStateMutations } from './state.js';
12
13
  import { BoundedTextOutput, renderCapturedText } from './text-output.js';
13
14
  import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
@@ -20,7 +21,7 @@ export const codeModeToolDefinition = {
20
21
  - Use bounded combinations, parallel independent calls, and filtering of large intermediate results to reduce model round trips.
21
22
  - Evaluates source in a fresh restricted ECMAScript runtime with top-level await and Promise.all. It is not Node.js or a browser. JavaScript globals do not survive between cells; bounded JSON state is available through state.
22
23
  - Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences. Responses uses the source directly; Anthropic uses the code field of its tool input object. In either case, do not quote or fence the source again.
23
- - Use only standard ECMAScript built-ins and the explicitly documented tools, out, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
24
+ - Use only standard ECMAScript built-ins and the explicitly documented tools, observe, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
24
25
 
25
26
  Tool calls:
26
27
  - tools.NAME(input) returns a Promise. Use the existing tool input schema; freeform tools such as apply_patch accept a raw string, not a JSON wrapper.
@@ -29,25 +30,26 @@ Tool calls:
29
30
  - skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
30
31
 
31
32
  Built-in namespaces:
32
- - out.text(value): void emits text. Strings and scalar values are stringified; JSON-compatible objects/arrays are serialized as compact JSON. Structured image objects (image/* mimeType and string dataBase64, or native MCP type:image with string data) are recursively summarized without their Base64 payload; originals remain unchanged. Ordinary strings remain unchanged. This does not emit pixels: use out.image(result.images[0]) for images. Prefer result.data or result.content over the whole image result. Excess ordinary text is truncated with omission metadata, without stopping later code or tools. Cyclic or unsupported object values fail. Only explicitly emitted output reaches the model.
33
- - out.image(value: {mimeType:string,dataBase64:string}): void emits one returned image; for example out.image(result.images[0]) when that image exists and the model supports images. It does not accept a path, URL, or result.content.
33
+ - observe.result(result): void registers an unmodified complete tools.NAME result or caught tool error as a trusted tool_result, using the original text and all images without repeating program data. Non-original, copied or modified values are rejected. This selects evidence for the next model response, not a log and not a new model call. Prefer observe.result for inspecting tool results.
34
+ - observe.text(value): void registers bounded text/JSON values. observe.image(value): void registers one image value using the existing capability, MIME, byte and count limits. Both accept original tool results for compatibility and use the same complete tool_result observation path; they do not select part of an original result. Extracted single images, copies and computed summaries are value observations, never trusted tool results. Observations are returned in registration order. Only explicitly observed evidence reaches the model; registration does not prove inspection. Sources and types are runtime-assigned. No out or callable observe(value) alias exists.
34
35
  - state.get(key), state.has(key), state.set(key, value), state.delete(key) access bounded session JSON state. Mutations commit atomically only when the cell succeeds; failed or cancelled cells discard them.
35
36
  - catalog.tools() returns a read-only snapshot of the tools callable in this cell, including name, description, inputSchema and inputMode. It does not discover or authorize additional tools.
36
37
  - await cell.sleep(ms) performs a cancellable Host-side delay up to 30000 ms and remains subject to the cell deadline.
37
38
  - await cell.yield_control() returns accumulated output to the model immediately while this cell continues running.
38
39
  - Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}, "maxOutputTokens": 10000}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and is a wait window (0..${limits.codeMaxYieldMs}), not a cell timeout. Explicit values are preserved and do not change nested tools' wait windows. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
39
40
  - Registered cells are Session warm resources: ending or cancelling the model Execution cancels its wait, not the cell. Reuse retained cellId; do not start duplicate work. Host cleanup or explicit termination stops the cell.
40
- - tools, out, state, catalog and cell are frozen bindings. Do not shadow their names.
41
+ - tools, observe, state, catalog and cell are frozen bindings. Do not shadow their names.
41
42
  - Input max 1 MiB; cumulative text capture max 1 MiB with truncation; 128 calls, 120 seconds. Tool-specific limits apply before explicit text capture. Protocol, image, memory and time limits remain hard failures. Failures do not roll back completed tool work; do not replay successful operations blindly.
42
43
 
43
44
  Examples (copy the JavaScript source, not the fences; use only tools available in the current directory):
44
- Independent reads, emitting only source text:
45
+ Independent reads, observing the original tool results:
45
46
  \`\`\`js
46
47
  const [a, b] = await Promise.all([
47
48
  tools.read({ reads: [{ path: "a.txt" }] }),
48
49
  tools.read({ reads: [{ path: "b.txt" }] })
49
50
  ]);
50
- out.text({ a: a.data.results[0].text, b: b.data.results[0].text });
51
+ observe.result(a);
52
+ observe.result(b);
51
53
  \`\`\`
52
54
  Inspect before a dependent edit; successful await needs no invented success flag:
53
55
  \`\`\`js
@@ -55,7 +57,7 @@ const inspected = await tools.read({ reads: [{ path: "source.txt" }] });
55
57
  const sourceText = inspected.data.results[0].text;
56
58
  if (!sourceText?.includes("original=1")) throw new Error("Unexpected source");
57
59
  const patched = await tools.apply_patch("*** Begin Patch\\n*** Update File: source.txt\\n@@\\n original=1\\n+updated=2\\n*** End Patch");
58
- out.text({ files: patched.data.files.length });
60
+ observe.result(patched);
59
61
  \`\`\``,
60
62
  inputMode: 'freeform',
61
63
  inputSchema: {
@@ -267,6 +269,34 @@ export class CodeModeTool {
267
269
  });
268
270
  const text = new BoundedTextOutput(limits.codeMaxOutputBytes);
269
271
  const images = [];
272
+ const observations = [];
273
+ let observationBytes = 0;
274
+ const appendObservation = (value, originalBytes) => {
275
+ const capture = new BoundedTextOutput(Math.max(0, limits.codeMaxOutputBytes - observationBytes), '');
276
+ const body = value.type === 'tool_result' ? value.result.content : value.value;
277
+ capture.append(body);
278
+ const bounded = capture.snapshot();
279
+ observationBytes += bounded.retainedBytes;
280
+ text.append(body, originalBytes ?? Buffer.byteLength(body));
281
+ output?.text(body, originalBytes ?? Buffer.byteLength(body));
282
+ const observation = value.type === 'tool_result'
283
+ ? {
284
+ ...value,
285
+ result: {
286
+ ...value.result,
287
+ content: bounded.text,
288
+ truncated: value.result.truncated === true || bounded.droppedBytes > 0
289
+ }
290
+ }
291
+ : {
292
+ ...value,
293
+ value: bounded.text,
294
+ truncated: value.truncated === true || bounded.droppedBytes > 0
295
+ };
296
+ observations.push(observation);
297
+ output?.observe(observation);
298
+ };
299
+ const toolResults = new Map();
270
300
  let transportBytes = 0;
271
301
  let calls = 0;
272
302
  let complete = false;
@@ -316,13 +346,94 @@ export class CodeModeTool {
316
346
  const receive = (frame) => {
317
347
  if (complete)
318
348
  throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode sent data after completion.');
319
- if (frame.kind === 'text' && typeof frame.value === 'string') {
349
+ if (frame.kind === 'observe' && frame.observation && typeof frame.observation === 'object') {
350
+ const value = frame.observation;
351
+ if (observations.length >= 4096)
352
+ throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Too many Code Mode observations.');
353
+ const addImage = (raw) => {
354
+ if (!context.codeMode?.supportsImages)
355
+ throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'The current model cannot receive image observations.');
356
+ if (images.length >= 5)
357
+ throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode emitted too many images.');
358
+ const image = parseImage(raw);
359
+ const index = images.length;
360
+ images.push(image);
361
+ output?.image(image);
362
+ return index;
363
+ };
364
+ let observation;
365
+ if (value.type === 'tool_result' && typeof value.operationId === 'string') {
366
+ const original = toolResults.get(value.operationId);
367
+ if (!original)
368
+ throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Unknown observation source.');
369
+ const bounded = {
370
+ content: original.result.content,
371
+ truncated: original.result.truncated === true
372
+ };
373
+ const indices = original.result.images?.map(addImage);
374
+ const data = [
375
+ 'apply_patch',
376
+ 'todo_read',
377
+ 'todo_write',
378
+ 'agent_start',
379
+ 'agent_wait',
380
+ 'agent_message',
381
+ 'agent_cancel'
382
+ ].includes(original.source.toolName)
383
+ ? original.result.data
384
+ : undefined;
385
+ observation = {
386
+ type: 'tool_result',
387
+ id: newObservationId(),
388
+ source: original.source,
389
+ outcome: original.outcome,
390
+ result: {
391
+ content: bounded.content,
392
+ truncated: original.result.truncated === true || bounded.truncated,
393
+ ...(data === undefined ? {} : { data }),
394
+ ...(indices?.length ? { imageIndices: indices } : {})
395
+ }
396
+ };
397
+ }
398
+ else if (value.type === 'image') {
399
+ observation = {
400
+ type: 'value',
401
+ id: newObservationId(),
402
+ format: 'image',
403
+ value: '',
404
+ imageIndices: [addImage(value.value)]
405
+ };
406
+ }
407
+ else if (value.type === 'value' &&
408
+ typeof value.value === 'string' &&
409
+ ['text', 'json'].includes(String(value.format))) {
410
+ observation = {
411
+ type: 'value',
412
+ id: newObservationId(),
413
+ format: value.format,
414
+ value: value.value,
415
+ truncated: typeof value.originalBytes === 'number' &&
416
+ value.originalBytes > Buffer.byteLength(value.value)
417
+ };
418
+ }
419
+ else
420
+ throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Invalid observation frame.');
421
+ appendObservation(observation, value.type === 'value' && typeof value.originalBytes === 'number'
422
+ ? value.originalBytes
423
+ : undefined);
424
+ }
425
+ else if (frame.kind === 'text' && typeof frame.value === 'string') {
320
426
  const originalBytes = frame.originalBytes ?? Buffer.byteLength(frame.value);
321
427
  if (!Number.isSafeInteger(originalBytes) ||
322
428
  originalBytes < Buffer.byteLength(frame.value))
323
429
  throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode sent invalid text byte counts.');
324
- text.append(frame.value, originalBytes);
325
- output?.text(frame.value, originalBytes);
430
+ appendObservation({
431
+ type: 'value',
432
+ id: newObservationId(),
433
+ format: 'text',
434
+ value: frame.value,
435
+ truncated: originalBytes > Buffer.byteLength(frame.value)
436
+ }, originalBytes);
326
437
  }
327
438
  else if (frame.kind === 'image') {
328
439
  if (!context.codeMode?.supportsImages)
@@ -332,6 +443,13 @@ export class CodeModeTool {
332
443
  throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode emitted too many images.');
333
444
  images.push(image);
334
445
  output?.image(image);
446
+ appendObservation({
447
+ type: 'value',
448
+ id: newObservationId(),
449
+ format: 'image',
450
+ value: '',
451
+ imageIndices: [images.length - 1]
452
+ });
335
453
  }
336
454
  else if (frame.kind === 'yield') {
337
455
  output?.yield();
@@ -343,6 +461,14 @@ export class CodeModeTool {
343
461
  if (seen.has(id) || ++calls > limits.codeMaxCalls)
344
462
  throw new MarAgentError('MAR_AGENT_CODE_PROTOCOL_ERROR', 'Code Mode call ID or call count is invalid.');
345
463
  seen.add(id);
464
+ const source = {
465
+ callId: randomUUID(),
466
+ toolName: frame.name,
467
+ input: frame.arguments
468
+ };
469
+ const operationId = typeof frame.operationId === 'string' ? frame.operationId : String(id);
470
+ const recordCall = (value) => output?.call(value);
471
+ recordCall({ ...source, status: 'running' });
346
472
  const job = (async () => {
347
473
  try {
348
474
  if (frame.name === 'code_exec' || !available.has(frame.name))
@@ -350,7 +476,27 @@ export class CodeModeTool {
350
476
  const output = await invoke({
351
477
  name: frame.name,
352
478
  arguments: frame.arguments,
353
- callId: randomUUID()
479
+ callId: source.callId
480
+ });
481
+ toolResults.set(operationId, { source, outcome: 'completed', result: output });
482
+ recordCall({
483
+ ...source,
484
+ status: 'completed',
485
+ result: {
486
+ content: '',
487
+ truncated: output.truncated === true,
488
+ data: [
489
+ 'apply_patch',
490
+ 'todo_read',
491
+ 'todo_write',
492
+ 'agent_start',
493
+ 'agent_wait',
494
+ 'agent_message',
495
+ 'agent_cancel'
496
+ ].includes(source.toolName)
497
+ ? output.data
498
+ : undefined
499
+ }
354
500
  });
355
501
  const { content, ...structured } = output;
356
502
  const projected = output.data !== undefined && content === JSON.stringify(output.data)
@@ -359,13 +505,34 @@ export class CodeModeTool {
359
505
  await write({ id, ok: true, output: projected });
360
506
  }
361
507
  catch (error) {
508
+ // Serialization/transport failures cannot undo an already completed tool call.
509
+ if (toolResults.get(operationId)?.outcome === 'completed')
510
+ throw error;
362
511
  if (context.signal.aborted)
363
512
  return;
364
513
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_HOST_OPERATION_FAILED');
514
+ recordCall({
515
+ ...source,
516
+ status: 'failed',
517
+ error: { code: diagnostic.code, message: diagnostic.message }
518
+ });
519
+ toolResults.set(operationId, {
520
+ source,
521
+ outcome: 'failed',
522
+ result: {
523
+ content: JSON.stringify({ error: diagnostic.code, message: diagnostic.message })
524
+ }
525
+ });
365
526
  await write({
366
527
  id,
367
528
  ok: false,
368
- error: { code: diagnostic.code, message: diagnostic.message }
529
+ error: {
530
+ code: diagnostic.code,
531
+ // The guest carries the code separately; the outer error adds its own prefix.
532
+ message: diagnostic.message.startsWith(`${diagnostic.code}: `)
533
+ ? diagnostic.message.slice(diagnostic.code.length + 2)
534
+ : diagnostic.message
535
+ }
369
536
  });
370
537
  }
371
538
  })().catch(fail);
@@ -470,8 +637,10 @@ export class CodeModeTool {
470
637
  await context.codeMode.commitState?.(stateMutations);
471
638
  const captured = text.snapshot();
472
639
  return {
473
- content: renderCapturedText(captured, images.length) ||
474
- 'Code Mode completed without emitted text.',
640
+ content: observations.length
641
+ ? renderObservations(observations)
642
+ : renderCapturedText(captured, images.length) || '[]',
643
+ observations,
475
644
  data: {
476
645
  toolCalls: calls,
477
646
  outputBytes: captured.originalBytes,
@@ -0,0 +1,103 @@
1
+ /** Execution metadata is independent of explicit model observations. */
2
+ export interface CodeToolCallRecord {
3
+ callId: string;
4
+ toolName: string;
5
+ input: unknown;
6
+ status: 'running' | 'completed' | 'failed';
7
+ result?: {
8
+ content: string;
9
+ data?: unknown;
10
+ truncated?: boolean;
11
+ };
12
+ error?: {
13
+ code: string;
14
+ message: string;
15
+ };
16
+ }
17
+ export declare function parseCodeCalls(value: unknown): CodeToolCallRecord[] | undefined;
18
+ export type CodeObservation = {
19
+ type: 'tool_result';
20
+ id: string;
21
+ source: {
22
+ callId: string;
23
+ toolName: string;
24
+ input: unknown;
25
+ };
26
+ outcome: 'completed' | 'failed';
27
+ result: {
28
+ content: string;
29
+ data?: unknown;
30
+ truncated?: boolean;
31
+ imageIndices?: number[];
32
+ };
33
+ } | {
34
+ type: 'value';
35
+ id: string;
36
+ format: 'text' | 'json' | 'image';
37
+ value: string;
38
+ truncated?: boolean;
39
+ imageIndices?: number[];
40
+ };
41
+ export declare function observationContent(value: CodeObservation): string;
42
+ export declare function observationImages(value: CodeObservation): readonly number[];
43
+ export declare function selectObservationImages(values: readonly CodeObservation[], images: readonly {
44
+ mimeType: string;
45
+ dataBase64: string;
46
+ }[], offset?: number): {
47
+ observations: ({
48
+ result: {
49
+ imageIndices: number[];
50
+ content: string;
51
+ data?: unknown;
52
+ truncated?: boolean;
53
+ };
54
+ type: "tool_result";
55
+ id: string;
56
+ source: {
57
+ callId: string;
58
+ toolName: string;
59
+ input: unknown;
60
+ };
61
+ outcome: "completed" | "failed";
62
+ } | {
63
+ imageIndices: number[];
64
+ type: "value";
65
+ id: string;
66
+ format: "text" | "json" | "image";
67
+ value: string;
68
+ truncated?: boolean;
69
+ })[];
70
+ images: {
71
+ mimeType: string;
72
+ dataBase64: string;
73
+ }[];
74
+ };
75
+ /** Only the visible projection crosses into model text; program data and inputs stay private. */
76
+ export declare function visibleObservation(value: CodeObservation): Record<string, unknown>;
77
+ export declare function renderObservations(values: readonly CodeObservation[]): string;
78
+ export declare function renderObservationResult(values: readonly CodeObservation[], original: string): string;
79
+ /** Truncate each body before encoding, never a JSON array or its trusted identity. */
80
+ export declare function boundObservations(values: readonly CodeObservation[], maximumTokens: number): {
81
+ observations: CodeObservation[];
82
+ content: string;
83
+ truncated: boolean;
84
+ };
85
+ export declare function newObservationId(): string;
86
+ export declare function observationModelParts(message: {
87
+ content?: string;
88
+ observations?: readonly CodeObservation[];
89
+ images?: readonly {
90
+ mimeType: string;
91
+ dataBase64: string;
92
+ }[];
93
+ }): Array<{
94
+ type: 'text';
95
+ text: string;
96
+ } | {
97
+ type: 'image';
98
+ image: {
99
+ mimeType: string;
100
+ dataBase64: string;
101
+ };
102
+ }> | undefined;
103
+ export declare function parseObservations(value: unknown): CodeObservation[] | undefined;