@myagentroam/agent 0.10.7 → 0.10.9

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 (52) hide show
  1. package/dist/agent-core/extensions/compaction.d.ts +1 -0
  2. package/dist/agent-core/extensions/model-request.js +1 -1
  3. package/dist/agent-core/extensions/session-messages.d.ts +1 -1
  4. package/dist/agent-core/host/contracts.d.ts +2 -0
  5. package/dist/agent-core/host/local-agent-host.d.ts +2 -0
  6. package/dist/agent-core/host/local-agent-host.js +2 -0
  7. package/dist/agent-core/model/anthropic-messages.js +15 -23
  8. package/dist/agent-core/model/catalog-file.d.ts +14 -0
  9. package/dist/agent-core/model/configuration.d.ts +7 -0
  10. package/dist/agent-core/model/configuration.js +10 -0
  11. package/dist/agent-core/model/contracts.d.ts +8 -0
  12. package/dist/agent-core/model/openai-responses.d.ts +1 -1
  13. package/dist/agent-core/model/openai-responses.js +62 -29
  14. package/dist/agent-core/model/runtime-credential.d.ts +2 -0
  15. package/dist/agent-core/model/runtime-credential.js +15 -0
  16. package/dist/agent-core/model/sse.d.ts +1 -1
  17. package/dist/agent-core/model/sse.js +22 -4
  18. package/dist/agent-core/model/tool-input.d.ts +3 -0
  19. package/dist/agent-core/model/tool-input.js +18 -0
  20. package/dist/agent-core/model/usage-observer.d.ts +31 -0
  21. package/dist/agent-core/model/usage-observer.js +82 -0
  22. package/dist/agent-core/runtime/events.d.ts +2 -0
  23. package/dist/agent-core/sdk/agent.js +9 -1
  24. package/dist/agent-core/sdk/extension-models.d.ts +1 -1
  25. package/dist/agent-core/sdk/extension-models.js +9 -1
  26. package/dist/agent-core/sdk/session.js +31 -3
  27. package/dist/agent-core/session/catalog.d.ts +1 -0
  28. package/dist/agent-core/session/catalog.js +1 -0
  29. package/dist/agent-core/session/jsonl-store.d.ts +9 -0
  30. package/dist/agent-core/session/jsonl-store.js +24 -0
  31. package/dist/agent-core/session/tool-result-resources.d.ts +26 -0
  32. package/dist/agent-core/session/tool-result-resources.js +285 -0
  33. package/dist/agent-core/session/transaction-recovery.js +1 -1
  34. package/dist/agent-core/tools/code-mode/dispatch.js +5 -0
  35. package/dist/agent-core/tools/code-mode/index.js +8 -2
  36. package/dist/agent-core/tools/code-mode/observations.d.ts +1 -0
  37. package/dist/agent-core/tools/contracts.d.ts +2 -0
  38. package/dist/agent-core/tools/execution.js +11 -2
  39. package/dist/agent-core/tools/file-change-diff.d.ts +10 -0
  40. package/dist/agent-core/tools/file-change-diff.js +139 -0
  41. package/dist/agent-core/tools/local/apply-patch.js +19 -8
  42. package/dist/agent-core/tools/result-extensions.d.ts +46 -0
  43. package/dist/agent-core/tools/result-extensions.js +134 -0
  44. package/dist/agent-core/tools/runtime-context.d.ts +1 -0
  45. package/dist/agent-core/tools/runtime.js +6 -3
  46. package/dist/index.d.ts +4 -0
  47. package/dist/index.js +1 -0
  48. package/dist/shared/auto-title.d.ts +2 -0
  49. package/dist/shared/auto-title.js +6 -0
  50. package/dist/shared/model-text.d.ts +2 -0
  51. package/dist/shared/model-text.js +6 -0
  52. package/package.json +1 -1
@@ -0,0 +1,285 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { MarAgentError } from '../error.js';
6
+ import { parseToolResultExtensions } from '../tools/result-extensions.js';
7
+ const REF = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
8
+ const MAX_BYTES = 16 * 1024 * 1024;
9
+ export async function writeToolResultResource(directory, text) {
10
+ const bytes = Buffer.from(text);
11
+ if (bytes.length > MAX_BYTES)
12
+ throw resourceError('LIMIT', 'Tool result resource exceeds its limit.');
13
+ const resourceRef = randomUUID();
14
+ const folder = join(directory, 'tool_results');
15
+ await mkdir(folder, { recursive: true, mode: 0o700 });
16
+ const temporary = join(folder, `.${resourceRef}.tmp`);
17
+ await writeFile(join(folder, `.${resourceRef}.pending`), '', {
18
+ flag: 'wx',
19
+ mode: 0o600,
20
+ flush: true
21
+ });
22
+ try {
23
+ await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600, flush: true });
24
+ await rename(temporary, join(folder, `${resourceRef}.diff`));
25
+ }
26
+ finally {
27
+ await rm(temporary, { force: true });
28
+ }
29
+ return {
30
+ status: 'available',
31
+ resourceRef,
32
+ format: 'unified-diff',
33
+ byteSize: bytes.length,
34
+ sha256: createHash('sha256').update(bytes).digest('hex')
35
+ };
36
+ }
37
+ export function recordToolResources(record) {
38
+ if (!['tool.completed', 'tool.failed'].includes(record.type))
39
+ return [];
40
+ const payload = record.payload;
41
+ const sources = [
42
+ payload?.resultExtensions,
43
+ ...(Array.isArray(payload?.data?.calls)
44
+ ? payload.data.calls.map((call) => call.resultExtensions)
45
+ : [])
46
+ ];
47
+ return sources.flatMap((source) => (parseToolResultExtensions(source) ?? []).flatMap((extension) => extension.entries.flatMap((entry) => (entry.diff.status === 'available' ? [entry.diff] : []))));
48
+ }
49
+ /** A locator is only a hint: reads must verify its exact committed record identity. */
50
+ export async function bindToolResultResources(directory, record, readRecord, degrade = false) {
51
+ for (const resource of recordToolResources(record)) {
52
+ try {
53
+ await bindToolResultResource(directory, record, resource, readRecord);
54
+ }
55
+ catch (error) {
56
+ if (!degrade)
57
+ throw error;
58
+ // Shared descriptors are downgraded before persistence and live publication, never the tool outcome.
59
+ const payload = record.payload;
60
+ for (const extensions of [
61
+ payload.resultExtensions,
62
+ ...(payload.data?.calls ?? []).map((call) => call.resultExtensions)
63
+ ]) {
64
+ for (const extension of extensions ?? [])
65
+ for (const entry of extension.entries) {
66
+ if (entry.diff.status === 'available' &&
67
+ entry.diff.resourceRef === resource.resourceRef)
68
+ entry.diff = { status: 'unavailable', reason: 'storage-failed' };
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ async function bindToolResultResource(directory, record, resource, readRecord) {
75
+ const path = join(directory, 'tool_results', `${resource.resourceRef}.json`);
76
+ try {
77
+ await writeFile(path, JSON.stringify({ sequence: record.sequence, recordId: record.recordId }), { flag: 'wx', mode: 0o600, flush: true });
78
+ }
79
+ catch (error) {
80
+ if (error.code !== 'EEXIST')
81
+ throw error;
82
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
83
+ try {
84
+ const info = await handle.stat();
85
+ if (!info.isFile() || info.size > 1024)
86
+ throw resourceError('CORRUPT', 'Invalid tool resource locator.');
87
+ const locator = JSON.parse(await handle.readFile('utf8'));
88
+ const existing = locator.sequence === record.sequence && locator.recordId === record.recordId
89
+ ? record
90
+ : Number.isSafeInteger(locator.sequence)
91
+ ? await readRecord?.(locator.sequence)
92
+ : undefined;
93
+ if (!existing ||
94
+ existing.recordId !== locator.recordId ||
95
+ !recordToolResources(existing).some((entry) => entry.resourceRef === resource.resourceRef && entry.sha256 === resource.sha256))
96
+ throw resourceError('CORRUPT', 'Tool resource locator conflicts with committed history.');
97
+ }
98
+ catch (cause) {
99
+ throw normalizedResourceError(cause);
100
+ }
101
+ finally {
102
+ await handle.close();
103
+ }
104
+ }
105
+ }
106
+ export function toolResultResourceFiles(records) {
107
+ return new Set(records.flatMap((record) => recordToolResources(record).flatMap((resource) => [
108
+ `${resource.resourceRef}.diff`,
109
+ `${resource.resourceRef}.json`
110
+ ])));
111
+ }
112
+ export async function copyToolResultResources(source, target, records) {
113
+ const copied = new Set();
114
+ for (const record of records) {
115
+ for (const resource of recordToolResources(record)) {
116
+ if (copied.has(resource.resourceRef))
117
+ continue;
118
+ copied.add(resource.resourceRef);
119
+ try {
120
+ const bytes = await readResourceBytes(source, resource);
121
+ const directory = join(target, 'tool_results');
122
+ await mkdir(directory, { recursive: true, mode: 0o700 });
123
+ await writeFile(join(directory, `${resource.resourceRef}.diff`), bytes, {
124
+ flag: 'wx',
125
+ mode: 0o600,
126
+ flush: true
127
+ });
128
+ }
129
+ catch (error) {
130
+ if (error instanceof MarAgentError && error.code === 'MAR_AGENT_TOOL_RESOURCE_NOT_FOUND')
131
+ continue;
132
+ throw error;
133
+ }
134
+ }
135
+ if (recordToolResources(record).length) {
136
+ await mkdir(join(target, 'tool_results'), { recursive: true, mode: 0o700 });
137
+ await bindToolResultResources(target, record, async (sequence) => records.find((item) => item.sequence === sequence));
138
+ }
139
+ }
140
+ }
141
+ export async function completeToolResultResources(directory, record) {
142
+ await Promise.all(recordToolResources(record).map((resource) => rm(join(directory, 'tool_results', `.${resource.resourceRef}.pending`), {
143
+ force: true
144
+ }).catch(() => undefined)));
145
+ }
146
+ export async function cleanupUnreferencedToolResources(directory, readRecords) {
147
+ const folder = join(directory, 'tool_results');
148
+ let names;
149
+ try {
150
+ names = await readdir(folder);
151
+ }
152
+ catch (error) {
153
+ if (error.code === 'ENOENT')
154
+ return;
155
+ throw error;
156
+ }
157
+ const pending = names.filter((name) => name.startsWith('.') && name.endsWith('.pending') && REF.test(name.slice(1, -8)));
158
+ if (!pending.length)
159
+ return;
160
+ const retained = toolResultResourceFiles(await readRecords());
161
+ for (const marker of pending) {
162
+ const ref = marker.slice(1, -8);
163
+ for (const name of [`${ref}.diff`, `${ref}.json`, `.${ref}.tmp`]) {
164
+ if (retained.has(name))
165
+ continue;
166
+ const path = join(folder, name);
167
+ try {
168
+ if ((await lstat(path)).isFile())
169
+ await rm(path);
170
+ }
171
+ catch (error) {
172
+ if (error.code !== 'ENOENT')
173
+ throw error;
174
+ }
175
+ }
176
+ await rm(join(folder, marker), { force: true });
177
+ }
178
+ }
179
+ export async function readToolResultResource(directory, sessionId, input, readRecord) {
180
+ if (!REF.test(input.resourceRef) ||
181
+ (input.maxBytes !== undefined &&
182
+ (!Number.isSafeInteger(input.maxBytes) || input.maxBytes < 4 || input.maxBytes > 256 * 1024)))
183
+ throw resourceError('INPUT_INVALID', 'Invalid tool resource request.');
184
+ let locator;
185
+ try {
186
+ const file = join(directory, 'tool_results', `${input.resourceRef}.json`);
187
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
188
+ try {
189
+ const info = await handle.stat();
190
+ if (!info.isFile() || info.size > 1024)
191
+ throw resourceError('CORRUPT', 'Invalid tool resource locator.');
192
+ locator = JSON.parse(await handle.readFile('utf8'));
193
+ }
194
+ finally {
195
+ await handle.close();
196
+ }
197
+ }
198
+ catch (error) {
199
+ throw normalizedResourceError(error);
200
+ }
201
+ if (!Number.isSafeInteger(locator.sequence) ||
202
+ locator.sequence < 1 ||
203
+ typeof locator.recordId !== 'string')
204
+ throw resourceError('CORRUPT', 'Invalid tool resource locator.');
205
+ const record = await readRecord(locator.sequence);
206
+ const resource = record?.recordId === locator.recordId
207
+ ? recordToolResources(record).find((entry) => entry.resourceRef === input.resourceRef)
208
+ : undefined;
209
+ if (!resource)
210
+ throw resourceError('NOT_FOUND', 'Tool resource is not part of this session history.');
211
+ let offset = 0;
212
+ if (input.cursor !== undefined) {
213
+ try {
214
+ if (input.cursor.length > 1024)
215
+ throw new Error('cursor');
216
+ const cursor = JSON.parse(Buffer.from(input.cursor, 'base64url').toString('utf8'));
217
+ if (cursor.sessionId !== sessionId ||
218
+ cursor.ref !== resource.resourceRef ||
219
+ cursor.sha256 !== resource.sha256 ||
220
+ !Number.isSafeInteger(cursor.offset) ||
221
+ cursor.offset < 0 ||
222
+ cursor.offset >= resource.byteSize)
223
+ throw new Error('cursor');
224
+ offset = cursor.offset;
225
+ }
226
+ catch {
227
+ throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor.');
228
+ }
229
+ }
230
+ const bytes = await readResourceBytes(directory, resource);
231
+ if (offset > 0 && (bytes[offset] & 0xc0) === 0x80)
232
+ throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor boundary.');
233
+ let end = Math.min(bytes.length, offset + (input.maxBytes ?? 256 * 1024));
234
+ while (end < bytes.length && (bytes[end] & 0xc0) === 0x80)
235
+ end--;
236
+ if ((await readRecord(locator.sequence))?.recordId !== locator.recordId)
237
+ throw resourceError('NOT_FOUND', 'Tool resource history has changed.');
238
+ return {
239
+ type: 'file_changes',
240
+ version: 1,
241
+ format: resource.format,
242
+ resourceRef: resource.resourceRef,
243
+ byteSize: resource.byteSize,
244
+ sha256: resource.sha256,
245
+ content: bytes.subarray(offset, end).toString('utf8'),
246
+ ...(end < bytes.length
247
+ ? {
248
+ nextCursor: Buffer.from(JSON.stringify({
249
+ sessionId,
250
+ ref: resource.resourceRef,
251
+ sha256: resource.sha256,
252
+ offset: end
253
+ })).toString('base64url')
254
+ }
255
+ : {})
256
+ };
257
+ }
258
+ async function readResourceBytes(directory, resource) {
259
+ try {
260
+ const handle = await open(join(directory, 'tool_results', `${resource.resourceRef}.diff`), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
261
+ try {
262
+ const stat = await handle.stat();
263
+ if (!stat.isFile() || stat.size !== resource.byteSize || stat.size > MAX_BYTES)
264
+ throw resourceError('CORRUPT', 'Tool resource size does not match its history.');
265
+ const bytes = await handle.readFile();
266
+ if (createHash('sha256').update(bytes).digest('hex') !== resource.sha256)
267
+ throw resourceError('CORRUPT', 'Tool resource checksum does not match its history.');
268
+ return bytes;
269
+ }
270
+ finally {
271
+ await handle.close();
272
+ }
273
+ }
274
+ catch (error) {
275
+ throw normalizedResourceError(error);
276
+ }
277
+ }
278
+ function resourceError(suffix, message) {
279
+ return new MarAgentError(`MAR_AGENT_TOOL_RESOURCE_${suffix}`, message);
280
+ }
281
+ function normalizedResourceError(error) {
282
+ if (error instanceof MarAgentError)
283
+ return error;
284
+ return resourceError(error?.code === 'ENOENT' ? 'NOT_FOUND' : 'CORRUPT', 'Tool result resource is unavailable.');
285
+ }
@@ -92,7 +92,7 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
92
92
  await rename(backup, sidecar);
93
93
  }
94
94
  }
95
- for (const folder of ['input_images', 'generated_images']) {
95
+ for (const folder of ['input_images', 'generated_images', 'tool_results']) {
96
96
  const path = join(directory, folder);
97
97
  let names;
98
98
  try {
@@ -4,6 +4,7 @@ import { AGENT_EXECUTION_POLICY } from '../../runtime/execution-policy.js';
4
4
  import { allowsToolCaller } from '../callers.js';
5
5
  import { boundedModelToolOutput, structuredToolEventData, structuredToolFailureData } from '../output-projection.js';
6
6
  import { executeRegisteredTool } from '../runtime.js';
7
+ import { failedToolResultExtensions } from '../result-extensions.js';
7
8
  /** Capture only the cell's immutable dispatch identity, never the model loop or its AbortSignal. */
8
9
  export function createCodeToolDispatch(input) {
9
10
  const definitions = new Map(input.tools.map((tool) => [tool.name, tool]));
@@ -33,6 +34,7 @@ export function createCodeToolDispatch(input) {
33
34
  input.assertLive();
34
35
  await input.emit({
35
36
  type: 'tool.completed',
37
+ ...(result.resultExtensions ? { resultExtensions: result.resultExtensions } : {}),
36
38
  ...identity,
37
39
  summary,
38
40
  ...(data === undefined ? {} : { data }),
@@ -59,6 +61,9 @@ export function createCodeToolDispatch(input) {
59
61
  const data = structuredToolFailureData(call.name, error);
60
62
  await input.emit({
61
63
  type: 'tool.failed',
64
+ ...(failedToolResultExtensions(error)
65
+ ? { resultExtensions: failedToolResultExtensions(error) }
66
+ : {}),
62
67
  ...identity,
63
68
  code: diagnostic.code,
64
69
  message: diagnostic.message,
@@ -16,6 +16,7 @@ import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
16
16
  import { createToolCallScheduler } from '../scheduler.js';
17
17
  import { detectImageMime } from '../local/view-image.js';
18
18
  import { structuredToolFailureData } from '../output-projection.js';
19
+ import { failedToolResultExtensions } from '../result-extensions.js';
19
20
  export const codeModeToolDefinition = {
20
21
  name: 'code_exec',
21
22
  description: `Run JavaScript to orchestrate or compose tool calls.
@@ -25,7 +26,7 @@ export const codeModeToolDefinition = {
25
26
  - Observe only evidence needed for the next decision or verification: use observe.result for original results, observe.text for derived values, and observe.image for image values. Preserve source and failure details needed for reasoning; keep other intermediate data in the cell.
26
27
  - Single-tool cells are valid. Keep loops and retries bounded, and do not blindly replay operations with side effects.
27
28
  - 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.
28
- - 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.
29
+ - Accepts JavaScript source text. When exposed as a freeform tool, send the source directly; when exposed with a JSON input schema, put the source in its code field. Do not add another JSON wrapper, quoted string, or markdown code fence inside the source.
29
30
  - 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.
30
31
 
31
32
  Tool calls:
@@ -474,15 +475,17 @@ export class CodeModeTool {
474
475
  try {
475
476
  if (frame.name === 'code_exec' || !available.has(frame.name))
476
477
  throw new MarAgentError('MAR_AGENT_TOOL_NOT_AVAILABLE', 'The requested nested tool is unavailable.');
477
- const output = await invoke({
478
+ const rawOutput = await invoke({
478
479
  name: frame.name,
479
480
  arguments: frame.arguments,
480
481
  callId: source.callId
481
482
  });
483
+ const { resultExtensions, ...output } = rawOutput;
482
484
  toolResults.set(operationId, { source, outcome: 'completed', result: output });
483
485
  recordCall({
484
486
  ...source,
485
487
  status: 'completed',
488
+ ...(resultExtensions ? { resultExtensions } : {}),
486
489
  result: {
487
490
  content: '',
488
491
  truncated: output.truncated === true,
@@ -516,6 +519,9 @@ export class CodeModeTool {
516
519
  recordCall({
517
520
  ...source,
518
521
  status: 'failed',
522
+ ...(failedToolResultExtensions(error)
523
+ ? { resultExtensions: failedToolResultExtensions(error) }
524
+ : {}),
519
525
  error: { code: diagnostic.code, message: diagnostic.message }
520
526
  });
521
527
  toolResults.set(operationId, {
@@ -4,6 +4,7 @@ export interface CodeToolCallRecord {
4
4
  toolName: string;
5
5
  input: unknown;
6
6
  status: 'running' | 'completed' | 'failed';
7
+ resultExtensions?: readonly import('../result-extensions.js').ToolResultExtension[];
7
8
  result?: {
8
9
  content: string;
9
10
  data?: unknown;
@@ -95,6 +95,8 @@ export interface ToolExecutionResult {
95
95
  dataBase64: string;
96
96
  }[];
97
97
  artifacts?: readonly MarAgentToolArtifact[];
98
+ /** Host-facing evidence, excluded from model and programmatic guest results. */
99
+ resultExtensions?: readonly import('./result-extensions.js').ToolResultExtension[];
98
100
  }
99
101
  export interface CodeCellEventData {
100
102
  calls?: readonly import('./code-mode/observations.js').CodeToolCallRecord[];
@@ -5,6 +5,7 @@ import { CodeCellExecutionError } from './code-mode/cells.js';
5
5
  import { createCodeToolDispatch } from './code-mode/dispatch.js';
6
6
  import { boundedModelToolOutput, structuredToolArtifacts, structuredToolEventData, structuredToolFailureData } from './output-projection.js';
7
7
  import { executeRegisteredTool } from './runtime.js';
8
+ import { failedToolResultExtensions } from './result-extensions.js';
8
9
  import { scheduleToolCallBatch } from './scheduler.js';
9
10
  /** Shared raw context construction for physical tools and native calls in a real model round. */
10
11
  export async function createRoundToolRuntimeContext(scope, call, signal) {
@@ -91,6 +92,7 @@ export async function executeRoundTools(calls, scope) {
91
92
  const artifacts = structuredToolArtifacts(output.artifacts);
92
93
  await scope.emit({
93
94
  type: 'tool.completed',
95
+ ...(output.resultExtensions ? { resultExtensions: output.resultExtensions } : {}),
94
96
  toolName: call.name,
95
97
  callId: call.callId,
96
98
  ...(parentCallId === undefined ? {} : { parentCallId }),
@@ -105,7 +107,9 @@ export async function executeRoundTools(calls, scope) {
105
107
  call,
106
108
  output,
107
109
  content: modelOutput.content,
108
- ...(output.calls ? { calls: output.calls } : {}),
110
+ ...(output.calls
111
+ ? { calls: output.calls.map(({ resultExtensions: _evidence, ...call }) => call) }
112
+ : {}),
109
113
  ...(output.observations ? { observations: output.observations } : {}),
110
114
  ...(output.images?.length ? { images: output.images } : {}),
111
115
  failed: false
@@ -121,6 +125,9 @@ export async function executeRoundTools(calls, scope) {
121
125
  : structuredToolEventData(call.name, partial.data));
122
126
  await scope.emit({
123
127
  type: 'tool.failed',
128
+ ...(failedToolResultExtensions(error)
129
+ ? { resultExtensions: failedToolResultExtensions(error) }
130
+ : {}),
124
131
  toolName: call.name,
125
132
  callId: call.callId,
126
133
  ...(parentCallId === undefined ? {} : { parentCallId }),
@@ -140,7 +147,9 @@ export async function executeRoundTools(calls, scope) {
140
147
  : boundedModelToolOutput(call.name, partial.content, partial.data).content,
141
148
  ...(partial?.images?.length ? { images: partial.images } : {}),
142
149
  failed: true,
143
- ...(partial?.calls ? { calls: partial.calls } : {}),
150
+ ...(partial?.calls
151
+ ? { calls: partial.calls.map(({ resultExtensions: _evidence, ...call }) => call) }
152
+ : {}),
144
153
  ...(partial?.observations ? { observations: partial.observations } : {}),
145
154
  error
146
155
  };
@@ -0,0 +1,10 @@
1
+ /** Bounded line comparison for immutable, per-operation file evidence. */
2
+ export interface FileDiff {
3
+ text: string;
4
+ additions: number;
5
+ deletions: number;
6
+ }
7
+ export declare const MAX_FILE_DIFF_BYTES: number;
8
+ export declare class FileDiffLimitError extends Error {
9
+ }
10
+ export declare function createFileDiff(path: string, before?: Buffer, after?: Buffer): FileDiff | undefined;
@@ -0,0 +1,139 @@
1
+ const MAX_WORK = 2_000_000;
2
+ export const MAX_FILE_DIFF_BYTES = 16 * 1024 * 1024;
3
+ export class FileDiffLimitError extends Error {
4
+ }
5
+ export function createFileDiff(path, before, after) {
6
+ let oldText;
7
+ let newText;
8
+ try {
9
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
10
+ oldText = decoder.decode(before ?? Buffer.alloc(0));
11
+ newText = decoder.decode(after ?? Buffer.alloc(0));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ if (oldText.includes('\0') || newText.includes('\0'))
17
+ return undefined;
18
+ const lines = (value) => value.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
19
+ const a = lines(oldText);
20
+ const b = lines(newText);
21
+ if (a.length + b.length > 200_000)
22
+ throw new FileDiffLimitError();
23
+ const edits = compare(a, b);
24
+ if (!edits)
25
+ throw new FileDiffLimitError();
26
+ const output = [
27
+ `--- ${before === undefined ? '/dev/null' : JSON.stringify(`a/${path}`)}`,
28
+ `+++ ${after === undefined ? '/dev/null' : JSON.stringify(`b/${path}`)}`
29
+ ];
30
+ const positions = [];
31
+ const ranges = [];
32
+ let old = 1;
33
+ let next = 1;
34
+ let additions = 0;
35
+ let deletions = 0;
36
+ edits.forEach((edit, index) => {
37
+ positions.push({ old, next });
38
+ if (edit.kind !== '+')
39
+ old++;
40
+ if (edit.kind !== '-')
41
+ next++;
42
+ if (edit.kind === '+')
43
+ additions++;
44
+ if (edit.kind === '-')
45
+ deletions++;
46
+ if (edit.kind === ' ')
47
+ return;
48
+ const start = Math.max(0, index - 3);
49
+ const end = Math.min(edits.length, index + 4);
50
+ const last = ranges.at(-1);
51
+ if (last && start <= last.end)
52
+ last.end = end;
53
+ else
54
+ ranges.push({ start, end });
55
+ });
56
+ for (const range of ranges) {
57
+ const block = edits.slice(range.start, range.end);
58
+ const oldCount = block.filter((edit) => edit.kind !== '+').length;
59
+ const newCount = block.filter((edit) => edit.kind !== '-').length;
60
+ const position = positions[range.start];
61
+ output.push(`@@ -${position.old - (oldCount === 0 ? 1 : 0)},${oldCount} +${position.next - (newCount === 0 ? 1 : 0)},${newCount} @@`);
62
+ for (const edit of block) {
63
+ output.push(`${edit.kind}${edit.text.endsWith('\n') ? edit.text.slice(0, -1) : edit.text}`);
64
+ if (!edit.text.endsWith('\n'))
65
+ output.push('\');
66
+ }
67
+ }
68
+ const text = `${output.join('\n')}\n`;
69
+ if (Buffer.byteLength(text) > MAX_FILE_DIFF_BYTES)
70
+ throw new FileDiffLimitError();
71
+ return { text, additions, deletions };
72
+ }
73
+ function compare(a, b) {
74
+ let prefix = 0;
75
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix])
76
+ prefix++;
77
+ let suffix = 0;
78
+ while (suffix < a.length - prefix &&
79
+ suffix < b.length - prefix &&
80
+ a[a.length - suffix - 1] === b[b.length - suffix - 1])
81
+ suffix++;
82
+ const left = a.slice(prefix, a.length - suffix);
83
+ const right = b.slice(prefix, b.length - suffix);
84
+ if (!left.length || !right.length)
85
+ return [
86
+ ...a.slice(0, prefix).map((text) => ({ kind: ' ', text })),
87
+ ...left.map((text) => ({ kind: '-', text })),
88
+ ...right.map((text) => ({ kind: '+', text })),
89
+ ...a.slice(a.length - suffix).map((text) => ({ kind: ' ', text }))
90
+ ];
91
+ const trace = [];
92
+ const frontier = new Map([[1, 0]]);
93
+ let work = 0;
94
+ for (let distance = 0; distance <= left.length + right.length; distance++) {
95
+ trace.push(new Map(frontier));
96
+ for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
97
+ if (++work > MAX_WORK)
98
+ return undefined;
99
+ const down = diagonal === -distance ||
100
+ (diagonal !== distance &&
101
+ (frontier.get(diagonal - 1) ?? -1) < (frontier.get(diagonal + 1) ?? -1));
102
+ let x = down ? (frontier.get(diagonal + 1) ?? 0) : (frontier.get(diagonal - 1) ?? 0) + 1;
103
+ let y = x - diagonal;
104
+ while (x < left.length && y < right.length && left[x] === right[y]) {
105
+ if (++work > MAX_WORK)
106
+ return undefined;
107
+ x++;
108
+ y++;
109
+ }
110
+ frontier.set(diagonal, x);
111
+ if (x < left.length || y < right.length)
112
+ continue;
113
+ const reversed = [];
114
+ for (let d = distance; d >= 0; d--) {
115
+ const previous = trace[d];
116
+ const k = x - y;
117
+ const previousK = k === -d || (k !== d && (previous.get(k - 1) ?? -1) < (previous.get(k + 1) ?? -1))
118
+ ? k + 1
119
+ : k - 1;
120
+ const previousX = previous.get(previousK) ?? 0;
121
+ const previousY = previousX - previousK;
122
+ while (x > previousX && y > previousY)
123
+ (reversed.push({ kind: ' ', text: left[--x] }), y--);
124
+ if (d === 0)
125
+ break;
126
+ if (x === previousX)
127
+ reversed.push({ kind: '+', text: right[--y] });
128
+ else
129
+ reversed.push({ kind: '-', text: left[--x] });
130
+ }
131
+ return [
132
+ ...a.slice(0, prefix).map((text) => ({ kind: ' ', text })),
133
+ ...reversed.reverse(),
134
+ ...a.slice(a.length - suffix).map((text) => ({ kind: ' ', text }))
135
+ ];
136
+ }
137
+ }
138
+ return undefined;
139
+ }