@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.
- package/dist/agent-core/extensions/compaction.d.ts +1 -0
- package/dist/agent-core/extensions/model-request.js +1 -1
- package/dist/agent-core/extensions/session-messages.d.ts +1 -1
- package/dist/agent-core/host/contracts.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.js +2 -0
- package/dist/agent-core/model/anthropic-messages.js +15 -23
- package/dist/agent-core/model/catalog-file.d.ts +14 -0
- package/dist/agent-core/model/configuration.d.ts +7 -0
- package/dist/agent-core/model/configuration.js +10 -0
- package/dist/agent-core/model/contracts.d.ts +8 -0
- package/dist/agent-core/model/openai-responses.d.ts +1 -1
- package/dist/agent-core/model/openai-responses.js +62 -29
- package/dist/agent-core/model/runtime-credential.d.ts +2 -0
- package/dist/agent-core/model/runtime-credential.js +15 -0
- package/dist/agent-core/model/sse.d.ts +1 -1
- package/dist/agent-core/model/sse.js +22 -4
- package/dist/agent-core/model/tool-input.d.ts +3 -0
- package/dist/agent-core/model/tool-input.js +18 -0
- package/dist/agent-core/model/usage-observer.d.ts +31 -0
- package/dist/agent-core/model/usage-observer.js +82 -0
- package/dist/agent-core/runtime/events.d.ts +2 -0
- package/dist/agent-core/sdk/agent.js +9 -1
- package/dist/agent-core/sdk/extension-models.d.ts +1 -1
- package/dist/agent-core/sdk/extension-models.js +9 -1
- package/dist/agent-core/sdk/session.js +31 -3
- package/dist/agent-core/session/catalog.d.ts +1 -0
- package/dist/agent-core/session/catalog.js +1 -0
- package/dist/agent-core/session/jsonl-store.d.ts +9 -0
- package/dist/agent-core/session/jsonl-store.js +24 -0
- package/dist/agent-core/session/tool-result-resources.d.ts +26 -0
- package/dist/agent-core/session/tool-result-resources.js +285 -0
- package/dist/agent-core/session/transaction-recovery.js +1 -1
- package/dist/agent-core/tools/code-mode/dispatch.js +5 -0
- package/dist/agent-core/tools/code-mode/index.js +8 -2
- package/dist/agent-core/tools/code-mode/observations.d.ts +1 -0
- package/dist/agent-core/tools/contracts.d.ts +2 -0
- package/dist/agent-core/tools/execution.js +11 -2
- package/dist/agent-core/tools/file-change-diff.d.ts +10 -0
- package/dist/agent-core/tools/file-change-diff.js +139 -0
- package/dist/agent-core/tools/local/apply-patch.js +19 -8
- package/dist/agent-core/tools/result-extensions.d.ts +46 -0
- package/dist/agent-core/tools/result-extensions.js +134 -0
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/agent-core/tools/runtime.js +6 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/shared/auto-title.d.ts +2 -0
- package/dist/shared/auto-title.js +6 -0
- package/dist/shared/model-text.d.ts +2 -0
- package/dist/shared/model-text.js +6 -0
- 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
|
|
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
|
|
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, {
|
|
@@ -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
|
|
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
|
|
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
|
+
}
|