@myagentroam/agent 0.11.55 → 0.11.57

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.
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { AgentError } from '../error.js';
5
5
  import { parseToolResultExtensions } from '../tools/result-extensions.js';
6
6
  const REF = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
@@ -73,13 +73,8 @@ export async function bindToolResultResources(directory, record, readRecord, deg
73
73
  async function bindToolResultResource(directory, record, resource, readRecord) {
74
74
  const path = join(directory, 'tool_results', `${resource.resourceRef}.json`);
75
75
  try {
76
- await writeFile(path, JSON.stringify({ sequence: record.sequence, recordId: record.recordId }), { flag: 'wx', mode: 0o600, flush: true });
77
- }
78
- catch (error) {
79
- if (error.code !== 'EEXIST')
80
- throw error;
81
- try {
82
- const locator = JSON.parse(await readFile(path, 'utf8'));
76
+ const locator = await readToolResourceLocator(path, resource.resourceRef);
77
+ if (locator !== undefined) {
83
78
  const existing = locator.sequence === record.sequence && locator.recordId === record.recordId
84
79
  ? record
85
80
  : Number.isSafeInteger(locator.sequence)
@@ -89,46 +84,103 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
89
84
  existing.recordId !== locator.recordId ||
90
85
  !recordToolResources(existing).some((entry) => entry.resourceRef === resource.resourceRef && entry.sha256 === resource.sha256))
91
86
  throw resourceError('CORRUPT', 'Tool resource locator conflicts with committed history.');
87
+ return;
92
88
  }
93
- catch (cause) {
89
+ }
90
+ catch (cause) {
91
+ if (cause.code !== 'ENOENT')
94
92
  throw normalizedResourceError(cause);
95
- }
96
93
  }
94
+ await writeFile(path, JSON.stringify({ sequence: record.sequence, recordId: record.recordId }), {
95
+ flag: 'wx',
96
+ mode: 0o600,
97
+ flush: true
98
+ });
97
99
  }
98
100
  export function toolResultResourceFiles(records) {
99
- return new Set(records.flatMap((record) => recordToolResources(record).flatMap((resource) => [
101
+ const files = new Set(records.flatMap((record) => recordToolResources(record).flatMap((resource) => [
100
102
  `${resource.resourceRef}.diff`,
101
103
  `${resource.resourceRef}.json`
102
104
  ])));
105
+ if (files.size > 0)
106
+ files.add('index.json');
107
+ return files;
103
108
  }
104
- export async function copyToolResultResources(source, target, records) {
105
- const copied = new Set();
109
+ export async function prepareToolResultResources(source, records) {
110
+ const resources = new Map();
106
111
  for (const record of records) {
107
112
  for (const resource of recordToolResources(record)) {
108
- if (copied.has(resource.resourceRef))
109
- continue;
110
- copied.add(resource.resourceRef);
113
+ const previous = resources.get(resource.resourceRef);
114
+ if (previous && previous.resource.sha256 !== resource.sha256)
115
+ throw resourceError('CORRUPT', 'Tool resource locator conflicts with committed history.');
116
+ if (!previous)
117
+ resources.set(resource.resourceRef, { record, resource });
118
+ }
119
+ }
120
+ const bodies = new Map();
121
+ if (resources.size === 0)
122
+ return bodies;
123
+ const bySequence = new Map(records.map((record) => [record.sequence, record]));
124
+ const pending = resources.values();
125
+ let failure;
126
+ const read = async () => {
127
+ while (!failure) {
128
+ const next = pending.next();
129
+ if (next.done)
130
+ return;
131
+ const { record, resource } = next.value;
111
132
  try {
112
- const bytes = await readResourceBytes(source, resource);
113
- const directory = join(target, 'tool_results');
114
- await mkdir(directory, { recursive: true, mode: 0o700 });
115
- await writeFile(join(directory, `${resource.resourceRef}.diff`), bytes, {
116
- flag: 'wx',
117
- mode: 0o600,
118
- flush: true
119
- });
133
+ const bytes = await readResourceBytes(source, resource, async (sequence) => bySequence.get(sequence));
134
+ bodies.set(resource.resourceRef, { owner: record.recordId, bytes });
120
135
  }
121
136
  catch (error) {
122
- if (error instanceof AgentError && error.code === 'MAR_AGENT_TOOL_RESOURCE_NOT_FOUND')
123
- continue;
124
- throw error;
137
+ if (!(error instanceof AgentError && error.code === 'MAR_AGENT_TOOL_RESOURCE_NOT_FOUND'))
138
+ failure ??= { error };
125
139
  }
126
140
  }
127
- if (recordToolResources(record).length) {
128
- await mkdir(join(target, 'tool_results'), { recursive: true, mode: 0o700 });
129
- await bindToolResultResources(target, record, async (sequence) => records.find((item) => item.sequence === sequence));
130
- }
141
+ };
142
+ await Promise.all(Array.from({ length: Math.min(8, resources.size) }, read));
143
+ if (failure)
144
+ throw failure.error;
145
+ return bodies;
146
+ }
147
+ export async function writeToolResultResourceIndex(target, locators) {
148
+ if (locators.size === 0)
149
+ return;
150
+ const directory = join(target, 'tool_results');
151
+ await mkdir(directory, { recursive: true, mode: 0o700 });
152
+ await writeFile(join(directory, 'index.json'), JSON.stringify({ version: 1, locators: Object.fromEntries(locators) }), {
153
+ flag: 'wx',
154
+ mode: 0o600,
155
+ flush: true
156
+ });
157
+ }
158
+ async function readToolResourceLocator(path, resourceRef) {
159
+ let text;
160
+ try {
161
+ text = await readFile(path, 'utf8');
131
162
  }
163
+ catch (error) {
164
+ if (error.code !== 'ENOENT')
165
+ throw error;
166
+ text = await readFile(join(dirname(path), 'index.json'), 'utf8');
167
+ }
168
+ const value = JSON.parse(text);
169
+ const locator = value?.version === 1 ? value.locators?.[resourceRef] : value;
170
+ if (value?.version === 1 && locator === undefined)
171
+ return undefined;
172
+ if (!Number.isSafeInteger(locator?.sequence) ||
173
+ locator.sequence < 1 ||
174
+ typeof locator.recordId !== 'string')
175
+ throw resourceError('CORRUPT', 'Invalid tool resource locator.');
176
+ if (locator.bodySequences !== undefined &&
177
+ (!Array.isArray(locator.bodySequences) ||
178
+ locator.bodySequences.length === 0 ||
179
+ !locator.bodySequences.every((sequence) => Number.isSafeInteger(sequence) &&
180
+ sequence > 0 &&
181
+ sequence < locator.sequence)))
182
+ throw resourceError('CORRUPT', 'Invalid tool resource body locator.');
183
+ return locator;
132
184
  }
133
185
  export async function completeToolResultResources(directory, record) {
134
186
  await Promise.all(recordToolResources(record).map((resource) => rm(join(directory, 'tool_results', `.${resource.resourceRef}.pending`), {
@@ -177,22 +229,20 @@ export async function readToolResultResource(directory, sessionId, input, readRe
177
229
  let locator;
178
230
  try {
179
231
  const file = join(directory, 'tool_results', `${input.resourceRef}.json`);
180
- locator = JSON.parse(await readFile(file, 'utf8'));
232
+ locator = await readToolResourceLocator(file, input.resourceRef);
181
233
  }
182
234
  catch (error) {
183
235
  throw normalizedResourceError(error);
184
236
  }
185
- if (!Number.isSafeInteger(locator.sequence) ||
186
- locator.sequence < 1 ||
187
- typeof locator.recordId !== 'string')
188
- throw resourceError('CORRUPT', 'Invalid tool resource locator.');
237
+ if (locator === undefined)
238
+ throw resourceError('NOT_FOUND', 'Tool resource is unavailable.');
189
239
  const record = await readRecord(locator.sequence);
190
240
  const resource = record?.recordId === locator.recordId
191
241
  ? recordToolResources(record).find((entry) => entry.resourceRef === input.resourceRef)
192
242
  : undefined;
193
243
  if (!resource)
194
244
  throw resourceError('NOT_FOUND', 'Tool resource is not part of this session history.');
195
- const bytes = await readResourceBytes(directory, resource);
245
+ const bytes = await readResourceBytes(directory, resource, readRecord, locator);
196
246
  let offset = 0;
197
247
  if (input.cursor !== undefined) {
198
248
  try {
@@ -237,10 +287,31 @@ export async function readToolResultResource(directory, sessionId, input, readRe
237
287
  : {})
238
288
  };
239
289
  }
240
- async function readResourceBytes(directory, resource) {
290
+ async function readResourceBytes(directory, resource, readRecord, knownLocator) {
241
291
  try {
242
292
  return await readFile(join(directory, 'tool_results', `${resource.resourceRef}.diff`));
243
293
  }
294
+ catch (error) {
295
+ if (error.code !== 'ENOENT')
296
+ throw normalizedResourceError(error);
297
+ }
298
+ try {
299
+ const locator = knownLocator ??
300
+ (await readToolResourceLocator(join(directory, 'tool_results', `${resource.resourceRef}.json`), resource.resourceRef));
301
+ if (!locator?.bodySequences || !readRecord)
302
+ throw resourceError('NOT_FOUND', 'Tool resource is unavailable.');
303
+ const chunks = [];
304
+ for (const sequence of locator.bodySequences) {
305
+ const record = await readRecord(sequence);
306
+ const payload = record?.payload;
307
+ if (record?.type !== 'tool.resource' ||
308
+ payload?.resourceRef !== resource.resourceRef ||
309
+ typeof payload.dataBase64 !== 'string')
310
+ throw resourceError('CORRUPT', 'Invalid tool resource body.');
311
+ chunks.push(Buffer.from(payload.dataBase64, 'base64'));
312
+ }
313
+ return Buffer.concat(chunks);
314
+ }
244
315
  catch (error) {
245
316
  throw normalizedResourceError(error);
246
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.55",
3
+ "version": "0.11.57",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",