@myagentroam/agent 0.11.24 → 0.11.25
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/sdk/resource-input.d.ts +1 -0
- package/dist/agent-core/sdk/resource-input.js +108 -47
- package/dist/agent-core/sdk/session.js +3 -0
- package/dist/agent-core/session/store/images.js +7 -52
- package/dist/agent-core/session/store/jsonl-store.js +20 -27
- package/dist/agent-core/session/store/resources.js +34 -33
- package/dist/agent-core/session/tool-result-resources.js +6 -35
- package/package.json +1 -1
|
@@ -27,5 +27,6 @@ export declare class ResourceInputProjection {
|
|
|
27
27
|
/** Propose a cursor; only the unified GC checkpoint may adopt it. */
|
|
28
28
|
gcCandidate(request?: ModelRequest, mirroredImages?: readonly ModelImage[]): Promise<InlineMediaGcCandidate | undefined>;
|
|
29
29
|
adoptGc(state: InlineMediaState | undefined): void;
|
|
30
|
+
releaseBytes(): void;
|
|
30
31
|
}
|
|
31
32
|
export {};
|
|
@@ -3,9 +3,11 @@ import { MarAgentError } from '../error.js';
|
|
|
3
3
|
import { isModelImageUrl } from '../model/image.js';
|
|
4
4
|
import { observationImages, renderObservationResult, selectObservationImages } from '../tools/code-mode/observations.js';
|
|
5
5
|
import { INLINE_MEDIA_POLICY as policy, assertInlineMediaBudget, inlineMediaBytes, inlineMediaHistory, inlineMediaLimit, inlineMediaSource } from '../session/inline-media.js';
|
|
6
|
-
function releasedImageNotice(message, indices) {
|
|
6
|
+
function releasedImageNotice(message, indices, unavailable) {
|
|
7
7
|
return indices
|
|
8
8
|
.map((index) => {
|
|
9
|
+
if (unavailable?.has(index))
|
|
10
|
+
return 'Image unavailable; omitted from context.';
|
|
9
11
|
const resourceId = message.images?.[index]?.resourceId;
|
|
10
12
|
const hint = resourceId
|
|
11
13
|
? ` Reopen with view_image(${JSON.stringify({ resourceId })}) if needed.`
|
|
@@ -14,7 +16,7 @@ function releasedImageNotice(message, indices) {
|
|
|
14
16
|
})
|
|
15
17
|
.join('\n');
|
|
16
18
|
}
|
|
17
|
-
function projectRetainedImages(message, images, removed) {
|
|
19
|
+
function projectRetainedImages(message, images, removed, unavailable) {
|
|
18
20
|
if (!removed.size)
|
|
19
21
|
return { ...message, images };
|
|
20
22
|
if (message.observations) {
|
|
@@ -22,7 +24,7 @@ function projectRetainedImages(message, images, removed) {
|
|
|
22
24
|
const removedIndices = observationImages(observation).filter((index) => removed.has(index));
|
|
23
25
|
if (!removedIndices.length)
|
|
24
26
|
return observation;
|
|
25
|
-
const notice = releasedImageNotice(message, removedIndices);
|
|
27
|
+
const notice = releasedImageNotice(message, removedIndices, unavailable);
|
|
26
28
|
return observation.type === 'tool_result'
|
|
27
29
|
? {
|
|
28
30
|
...observation,
|
|
@@ -37,7 +39,7 @@ function projectRetainedImages(message, images, removed) {
|
|
|
37
39
|
content: renderObservationResult(selected.observations, message.content)
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
|
-
const notice = releasedImageNotice(message, [...removed]);
|
|
42
|
+
const notice = releasedImageNotice(message, [...removed], unavailable);
|
|
41
43
|
return {
|
|
42
44
|
...message,
|
|
43
45
|
images: images.filter(Boolean),
|
|
@@ -54,6 +56,8 @@ export class ResourceInputProjection {
|
|
|
54
56
|
#entries = [];
|
|
55
57
|
#sources = new Map();
|
|
56
58
|
#candidates = new WeakMap();
|
|
59
|
+
#resourceReads = new Map();
|
|
60
|
+
#cachedBytes = 0;
|
|
57
61
|
#considered = -1;
|
|
58
62
|
#rollout = 0;
|
|
59
63
|
#evicted = -1;
|
|
@@ -98,12 +102,15 @@ export class ResourceInputProjection {
|
|
|
98
102
|
for (const message of messages)
|
|
99
103
|
for (const [index, image] of (message.images ?? []).entries()) {
|
|
100
104
|
const entry = this.#entries[this.#entry(inlineMediaSource(message), index)];
|
|
105
|
+
if (entry.image && !isDeepStrictEqual(entry.image, image))
|
|
106
|
+
delete entry.unavailable;
|
|
101
107
|
entry.image = image;
|
|
102
108
|
entry.active = true;
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
/** Called only at an explicit context replacement, never as a history diff detector. */
|
|
106
112
|
replace(messages) {
|
|
113
|
+
this.releaseBytes();
|
|
107
114
|
for (const entry of this.#entries) {
|
|
108
115
|
entry.active = false;
|
|
109
116
|
delete entry.image;
|
|
@@ -133,16 +140,23 @@ export class ResourceInputProjection {
|
|
|
133
140
|
if (!images?.length)
|
|
134
141
|
return message;
|
|
135
142
|
const removed = new Set();
|
|
143
|
+
const unavailable = new Set();
|
|
136
144
|
const retained = [];
|
|
137
145
|
images.forEach((image, index) => {
|
|
138
|
-
|
|
146
|
+
const position = positions?.[index];
|
|
147
|
+
const entry = position === undefined ? undefined : this.#entries[position];
|
|
148
|
+
if (entry?.unavailable && isDeepStrictEqual(entry.image, image)) {
|
|
149
|
+
removed.add(index);
|
|
150
|
+
unavailable.add(index);
|
|
151
|
+
}
|
|
152
|
+
else if (!this.useResourceUrl &&
|
|
139
153
|
positions?.[index] !== undefined &&
|
|
140
154
|
positions[index] <= evicted)
|
|
141
155
|
removed.add(index);
|
|
142
156
|
else
|
|
143
157
|
retained[index] = image;
|
|
144
158
|
});
|
|
145
|
-
return projectRetainedImages({ ...message, images }, retained, removed);
|
|
159
|
+
return projectRetainedImages({ ...message, images }, retained, removed, unavailable);
|
|
146
160
|
});
|
|
147
161
|
}
|
|
148
162
|
checkpoint(messages) {
|
|
@@ -167,50 +181,44 @@ export class ResourceInputProjection {
|
|
|
167
181
|
this.observe(canonicalMessages);
|
|
168
182
|
const through = this.#entries.length - 1;
|
|
169
183
|
const cached = new Map();
|
|
170
|
-
const project = (image) => {
|
|
171
|
-
|
|
184
|
+
const project = (image, entry) => {
|
|
185
|
+
const key = entry && isDeepStrictEqual(entry.image, image) ? entry : image;
|
|
186
|
+
let value = cached.get(key);
|
|
172
187
|
if (!value)
|
|
173
|
-
cached.set(
|
|
188
|
+
cached.set(key, (value = this.#project(image, entry)));
|
|
174
189
|
return value;
|
|
175
190
|
};
|
|
176
|
-
const sized = async (image) => image.dataBase64 !== undefined || image.byteSize !== undefined ? image : project(image);
|
|
177
191
|
const top = (request.images ?? []).filter((image) => !mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)));
|
|
178
192
|
const evicted = this.#evicted;
|
|
179
|
-
if (!this.useResourceUrl) {
|
|
180
|
-
let total = 0;
|
|
181
|
-
for (const message of request.messages)
|
|
182
|
-
for (const [index, image] of (message.images ?? []).entries()) {
|
|
183
|
-
const position = this.#sources.get(inlineMediaSource(message))?.[index];
|
|
184
|
-
const original = position === undefined ? undefined : this.#entries[position]?.image;
|
|
185
|
-
if (position !== undefined && position <= evicted && isDeepStrictEqual(original, image))
|
|
186
|
-
continue;
|
|
187
|
-
total += inlineMediaBytes(await sized(image));
|
|
188
|
-
}
|
|
189
|
-
for (const image of top)
|
|
190
|
-
total += inlineMediaBytes(await sized(image));
|
|
191
|
-
if (total > policy.requestInlineMediaHardLimit)
|
|
192
|
-
inlineMediaLimit();
|
|
193
|
-
}
|
|
194
193
|
const messages = await Promise.all(request.messages.map(async (message) => {
|
|
195
194
|
if (!message.images?.length)
|
|
196
195
|
return message;
|
|
197
196
|
const removed = new Set();
|
|
197
|
+
const unavailable = new Set();
|
|
198
198
|
const images = [];
|
|
199
199
|
await Promise.all(message.images.map(async (image, index) => {
|
|
200
200
|
const position = this.#sources.get(inlineMediaSource(message))?.[index];
|
|
201
|
-
const
|
|
201
|
+
const entry = position === undefined ? undefined : this.#entries[position];
|
|
202
|
+
const original = entry?.image;
|
|
202
203
|
if (!this.useResourceUrl &&
|
|
203
204
|
position !== undefined &&
|
|
204
205
|
position <= evicted &&
|
|
205
206
|
isDeepStrictEqual(original, image))
|
|
206
207
|
removed.add(index);
|
|
207
|
-
else
|
|
208
|
-
|
|
208
|
+
else {
|
|
209
|
+
const projected = await project(image, entry);
|
|
210
|
+
if (projected)
|
|
211
|
+
images[index] = projected;
|
|
212
|
+
else {
|
|
213
|
+
removed.add(index);
|
|
214
|
+
unavailable.add(index);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
209
217
|
}));
|
|
210
|
-
return projectRetainedImages(message, images, removed);
|
|
218
|
+
return projectRetainedImages(message, images, removed, unavailable);
|
|
211
219
|
}));
|
|
212
220
|
const { images: _topImages, ...retained } = request;
|
|
213
|
-
const images = await Promise.all(top.map(project));
|
|
221
|
+
const images = (await Promise.all(top.map((image) => project(image)))).filter((image) => image !== undefined);
|
|
214
222
|
const result = { ...retained, messages, ...(images.length ? { images } : {}) };
|
|
215
223
|
if (!this.useResourceUrl)
|
|
216
224
|
assertInlineMediaBudget(result);
|
|
@@ -241,16 +249,25 @@ export class ResourceInputProjection {
|
|
|
241
249
|
async gcCandidate(request, mirroredImages = []) {
|
|
242
250
|
if (this.useResourceUrl)
|
|
243
251
|
return undefined;
|
|
244
|
-
const size = async (image) =>
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
const size = async (image, entry) => {
|
|
253
|
+
if (entry?.unavailable && isDeepStrictEqual(entry.image, image))
|
|
254
|
+
return 0;
|
|
255
|
+
const sized = image.byteSize !== undefined || image.dataBase64 !== undefined
|
|
256
|
+
? image
|
|
257
|
+
: await this.#project(image, entry);
|
|
258
|
+
return sized ? inlineMediaBytes(sized) : 0;
|
|
259
|
+
};
|
|
260
|
+
const actualSize = async (image, entry) => {
|
|
261
|
+
const projected = await this.#project(image, entry);
|
|
262
|
+
return projected ? inlineMediaBytes(projected) : 0;
|
|
263
|
+
};
|
|
247
264
|
const history = await Promise.all(this.#entries.map(async (entry, index) => ({
|
|
248
265
|
index,
|
|
249
266
|
bytes: entry.active && entry.image && index > this.#evicted && index <= this.#considered
|
|
250
|
-
? await size(entry.image)
|
|
267
|
+
? await size(entry.image, entry)
|
|
251
268
|
: 0
|
|
252
269
|
})));
|
|
253
|
-
|
|
270
|
+
let beforeBytes = history.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
254
271
|
let freshBytes = 0;
|
|
255
272
|
let requestBytes = 0;
|
|
256
273
|
const requestHistory = new Map();
|
|
@@ -262,20 +279,23 @@ export class ResourceInputProjection {
|
|
|
262
279
|
const original = entry && isDeepStrictEqual(entry.image, image);
|
|
263
280
|
if (original && position <= this.#evicted)
|
|
264
281
|
continue;
|
|
265
|
-
const bytes = await
|
|
282
|
+
const bytes = await actualSize(image, entry);
|
|
266
283
|
requestBytes += bytes;
|
|
267
|
-
if (original && position <= this.#considered)
|
|
284
|
+
if (original && position <= this.#considered) {
|
|
268
285
|
requestHistory.set(position, (requestHistory.get(position) ?? 0) + bytes);
|
|
286
|
+
history[position].bytes = bytes;
|
|
287
|
+
}
|
|
269
288
|
else
|
|
270
289
|
freshBytes += bytes;
|
|
271
290
|
}
|
|
272
291
|
for (const image of request.images ?? []) {
|
|
273
292
|
if (mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)))
|
|
274
293
|
continue;
|
|
275
|
-
const bytes = await
|
|
294
|
+
const bytes = await actualSize(image);
|
|
276
295
|
freshBytes += bytes;
|
|
277
296
|
requestBytes += bytes;
|
|
278
297
|
}
|
|
298
|
+
beforeBytes = history.reduce((sum, item) => sum + item.bytes, 0);
|
|
279
299
|
if (freshBytes > policy.requestInlineMediaHardLimit)
|
|
280
300
|
inlineMediaLimit();
|
|
281
301
|
}
|
|
@@ -319,23 +339,64 @@ export class ResourceInputProjection {
|
|
|
319
339
|
};
|
|
320
340
|
}
|
|
321
341
|
adoptGc(state) {
|
|
322
|
-
if (state)
|
|
342
|
+
if (state) {
|
|
323
343
|
this.#evicted = this.#position(state.evictedThrough);
|
|
344
|
+
this.releaseBytes();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
releaseBytes() {
|
|
348
|
+
this.#resourceReads.clear();
|
|
349
|
+
this.#cachedBytes = 0;
|
|
350
|
+
}
|
|
351
|
+
#readBytes(resourceId) {
|
|
352
|
+
const cached = this.#resourceReads.get(resourceId);
|
|
353
|
+
if (cached)
|
|
354
|
+
return cached;
|
|
355
|
+
const pending = this.store.readResource(this.sessionId, resourceId).then((resource) => {
|
|
356
|
+
if (this.#resourceReads.get(resourceId) === pending) {
|
|
357
|
+
const bytes = resource?.dataBase64 ? inlineMediaBytes(resource) : 0;
|
|
358
|
+
if (!bytes || this.#cachedBytes + bytes > policy.requestInlineMediaHardLimit)
|
|
359
|
+
this.#resourceReads.delete(resourceId);
|
|
360
|
+
else
|
|
361
|
+
this.#cachedBytes += bytes;
|
|
362
|
+
}
|
|
363
|
+
return resource;
|
|
364
|
+
}, (error) => {
|
|
365
|
+
if (this.#resourceReads.get(resourceId) === pending)
|
|
366
|
+
this.#resourceReads.delete(resourceId);
|
|
367
|
+
throw error;
|
|
368
|
+
});
|
|
369
|
+
this.#resourceReads.set(resourceId, pending);
|
|
370
|
+
return pending;
|
|
324
371
|
}
|
|
325
|
-
async #project(image) {
|
|
372
|
+
async #project(image, entry) {
|
|
373
|
+
const original = entry && isDeepStrictEqual(entry.image, image);
|
|
374
|
+
if (original && entry.unavailable)
|
|
375
|
+
return undefined;
|
|
326
376
|
if (this.useResourceUrl) {
|
|
327
377
|
if (!isModelImageUrl(image.url))
|
|
328
378
|
throw new MarAgentError('MAR_AGENT_RESOURCE_URL_UNAVAILABLE', 'Resource URL is unavailable.');
|
|
329
379
|
const { dataBase64: _bytes, ...reference } = image;
|
|
330
380
|
return reference;
|
|
331
381
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
let bytes;
|
|
383
|
+
try {
|
|
384
|
+
bytes =
|
|
385
|
+
image.dataBase64 !== undefined
|
|
386
|
+
? image
|
|
387
|
+
: image.resourceId === undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: await this.#readBytes(image.resourceId);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_IMAGE_UNAVAILABLE')
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
if (!bytes?.dataBase64) {
|
|
396
|
+
if (original)
|
|
397
|
+
entry.unavailable = true;
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
339
400
|
const { url: _url, urlExpiresAt: _expires, ...reference } = image;
|
|
340
401
|
return { ...reference, dataBase64: bytes.dataBase64 };
|
|
341
402
|
}
|
|
@@ -389,6 +389,7 @@ export function createSessionOpener(services) {
|
|
|
389
389
|
let finalAnswer = '';
|
|
390
390
|
let executionLoop;
|
|
391
391
|
let executionToolSources;
|
|
392
|
+
let releaseImageBytes;
|
|
392
393
|
const contextGc = new ContextGcRuntime();
|
|
393
394
|
let selectedAdapter;
|
|
394
395
|
const modelResources = createExecutionModelResources(activeExecutionAdapters);
|
|
@@ -816,6 +817,7 @@ export function createSessionOpener(services) {
|
|
|
816
817
|
}));
|
|
817
818
|
}
|
|
818
819
|
});
|
|
820
|
+
releaseImageBytes = () => resourceInput.releaseBytes();
|
|
819
821
|
const selected = modelResources.openTurn(selectedAdapter, services.hasCustomAdapters && selectedAdapter.createTurnSession === undefined);
|
|
820
822
|
let lastServerUsage = latestModelUsage(contextRecords, selectedModel.id);
|
|
821
823
|
let smartContextTokens = latestSmartContextTokens(contextRecords, selectedModel.id, selectedModel.contextWindowTokens);
|
|
@@ -1981,6 +1983,7 @@ export function createSessionOpener(services) {
|
|
|
1981
1983
|
};
|
|
1982
1984
|
}
|
|
1983
1985
|
finally {
|
|
1986
|
+
releaseImageBytes?.();
|
|
1984
1987
|
// Revoke this Execution's request and input bindings, not Session warm resources.
|
|
1985
1988
|
delete executionContext.requestMessages;
|
|
1986
1989
|
compactRequests.close(new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { copyFile, lstat, mkdir, open } from 'node:fs/promises';
|
|
1
|
+
import { copyFile, mkdir, readFile } from 'node:fs/promises';
|
|
4
2
|
import { basename, join } from 'node:path';
|
|
5
3
|
import { MarAgentError } from '../../error.js';
|
|
6
4
|
import { isModelImageUrl } from '../../model/image.js';
|
|
@@ -93,57 +91,14 @@ export function validInputImageFileName(fileName) {
|
|
|
93
91
|
return (basename(fileName) === fileName && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}\.bin$/u.test(fileName));
|
|
94
92
|
}
|
|
95
93
|
export async function readInputImageBytes(directory, reference) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
let handle;
|
|
99
|
-
try {
|
|
100
|
-
const before = await lstat(path);
|
|
101
|
-
if (!before.isFile() || before.size !== reference.byteSize)
|
|
102
|
-
throw corruptInputImageReference();
|
|
103
|
-
const flags = process.platform === 'win32'
|
|
104
|
-
? constants.O_RDONLY
|
|
105
|
-
: constants.O_RDONLY | constants.O_NOFOLLOW;
|
|
106
|
-
handle = await open(path, flags);
|
|
107
|
-
const opened = await handle.stat();
|
|
108
|
-
if (!opened.isFile() ||
|
|
109
|
-
opened.size !== reference.byteSize ||
|
|
110
|
-
opened.dev !== before.dev ||
|
|
111
|
-
opened.ino !== before.ino)
|
|
112
|
-
continue;
|
|
113
|
-
const data = await readExactBytes(handle, reference.byteSize);
|
|
114
|
-
const after = await handle.stat();
|
|
115
|
-
if (after.size !== opened.size ||
|
|
116
|
-
after.mtimeMs !== opened.mtimeMs ||
|
|
117
|
-
after.ctimeMs !== opened.ctimeMs)
|
|
118
|
-
continue;
|
|
119
|
-
if (createHash('sha256').update(data).digest('hex') !== reference.sha256)
|
|
120
|
-
throw corruptInputImageReference();
|
|
121
|
-
return data;
|
|
122
|
-
}
|
|
123
|
-
catch (cause) {
|
|
124
|
-
if (cause instanceof MarAgentError)
|
|
125
|
-
throw cause;
|
|
126
|
-
throw corruptInputImageReference(cause);
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
await handle?.close();
|
|
130
|
-
}
|
|
94
|
+
try {
|
|
95
|
+
return await readFile(join(directory, reference.fileName));
|
|
131
96
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let offset = 0;
|
|
137
|
-
while (offset < byteSize) {
|
|
138
|
-
const { bytesRead } = await handle.read(data, offset, byteSize - offset, offset);
|
|
139
|
-
if (bytesRead === 0)
|
|
140
|
-
throw corruptInputImageReference();
|
|
141
|
-
offset += bytesRead;
|
|
97
|
+
catch (cause) {
|
|
98
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session input image is unavailable.', {
|
|
99
|
+
cause
|
|
100
|
+
});
|
|
142
101
|
}
|
|
143
|
-
const extra = Buffer.allocUnsafe(1);
|
|
144
|
-
if ((await handle.read(extra, 0, 1, byteSize)).bytesRead !== 0)
|
|
145
|
-
throw corruptInputImageReference();
|
|
146
|
-
return data;
|
|
147
102
|
}
|
|
148
103
|
function corruptInputImageReference(cause) {
|
|
149
104
|
return new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input image is corrupt.', {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { inlineMediaHistory } from '../inline-media.js';
|
|
3
|
-
import { lstat, mkdir,
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
6
6
|
import { MarAgentError } from '../../error.js';
|
|
@@ -394,27 +394,15 @@ export class JsonlSessionStore {
|
|
|
394
394
|
async readContextualWorldState(sessionId) {
|
|
395
395
|
const path = join(this.#sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
396
396
|
try {
|
|
397
|
-
const
|
|
398
|
-
|
|
397
|
+
const envelope = JSON.parse(await readFile(path, 'utf8'));
|
|
398
|
+
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
399
|
+
if (envelope.version !== 1 ||
|
|
400
|
+
!Number.isSafeInteger(envelope.historySequence) ||
|
|
401
|
+
envelope.historySequence < 1 ||
|
|
402
|
+
meta === undefined ||
|
|
403
|
+
envelope.historySequence >= meta.nextSequence)
|
|
399
404
|
return undefined;
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
const info = await file.stat();
|
|
403
|
-
if (!info.isFile() || info.size > MAX_CONTEXTUAL_WORLD_STATE_BYTES)
|
|
404
|
-
return undefined;
|
|
405
|
-
const envelope = JSON.parse(await file.readFile('utf8'));
|
|
406
|
-
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
407
|
-
if (envelope.version !== 1 ||
|
|
408
|
-
!Number.isSafeInteger(envelope.historySequence) ||
|
|
409
|
-
envelope.historySequence < 1 ||
|
|
410
|
-
meta === undefined ||
|
|
411
|
-
envelope.historySequence >= meta.nextSequence)
|
|
412
|
-
return undefined;
|
|
413
|
-
return envelope.state;
|
|
414
|
-
}
|
|
415
|
-
finally {
|
|
416
|
-
await file.close();
|
|
417
|
-
}
|
|
405
|
+
return envelope.state;
|
|
418
406
|
}
|
|
419
407
|
catch {
|
|
420
408
|
return undefined;
|
|
@@ -490,16 +478,21 @@ export class JsonlSessionStore {
|
|
|
490
478
|
if (!/^[a-f0-9]{64}$/u.test(resourceId))
|
|
491
479
|
return undefined;
|
|
492
480
|
return this.#serializeRecordOperation(sessionId, async () => {
|
|
481
|
+
let pending = false;
|
|
493
482
|
try {
|
|
494
483
|
await lstat(join(directory, `.${resourceId}.pending`));
|
|
495
|
-
|
|
496
|
-
!resourceFiles(await this.#readRecords(sessionId)).has(`${resourceId}.bin`))
|
|
497
|
-
return undefined;
|
|
484
|
+
pending = true;
|
|
498
485
|
}
|
|
499
|
-
catch (
|
|
500
|
-
if (
|
|
501
|
-
throw
|
|
486
|
+
catch (cause) {
|
|
487
|
+
if (cause.code !== 'ENOENT')
|
|
488
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is unavailable.', {
|
|
489
|
+
cause
|
|
490
|
+
});
|
|
502
491
|
}
|
|
492
|
+
if (pending &&
|
|
493
|
+
(this.#shared.writerLeases.get(sessionId)?.appendFailure !== undefined ||
|
|
494
|
+
!resourceFiles(await this.#readRecords(sessionId)).has(`${resourceId}.bin`)))
|
|
495
|
+
return undefined;
|
|
503
496
|
return readResource(directory, resourceId);
|
|
504
497
|
});
|
|
505
498
|
}
|
|
@@ -12,44 +12,39 @@ function resourceIdentity(mimeType, sha256) {
|
|
|
12
12
|
export async function resourceMetadata(directory, resourceId) {
|
|
13
13
|
if (!idPattern.test(resourceId))
|
|
14
14
|
return undefined;
|
|
15
|
-
let text;
|
|
16
15
|
try {
|
|
17
|
-
|
|
16
|
+
const value = JSON.parse(await readFile(join(directory, `${resourceId}.json`), 'utf8'));
|
|
17
|
+
const resource = value;
|
|
18
|
+
if (!value ||
|
|
19
|
+
typeof value !== 'object' ||
|
|
20
|
+
Array.isArray(value) ||
|
|
21
|
+
typeof resource.resourceId !== 'string' ||
|
|
22
|
+
typeof resource.mimeType !== 'string' ||
|
|
23
|
+
typeof resource.byteSize !== 'number' ||
|
|
24
|
+
typeof resource.sha256 !== 'string' ||
|
|
25
|
+
(resource.url !== undefined &&
|
|
26
|
+
(typeof resource.url !== 'string' ||
|
|
27
|
+
!isModelImageUrl(resource.url) ||
|
|
28
|
+
!Number.isSafeInteger(resource.urlExpiresAt) ||
|
|
29
|
+
resource.urlExpiresAt <= 0)) ||
|
|
30
|
+
(resource.url === undefined && resource.urlExpiresAt !== undefined))
|
|
31
|
+
throw new Error('Invalid Session image resource metadata.');
|
|
32
|
+
return value;
|
|
18
33
|
}
|
|
19
|
-
catch (
|
|
20
|
-
if (
|
|
34
|
+
catch (cause) {
|
|
35
|
+
if (cause.code === 'ENOENT')
|
|
21
36
|
return undefined;
|
|
22
|
-
throw
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
value = JSON.parse(text);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Invalid JSON is reported below without exposing metadata contents.
|
|
37
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is unavailable.', {
|
|
38
|
+
cause
|
|
39
|
+
});
|
|
30
40
|
}
|
|
31
|
-
if (!value ||
|
|
32
|
-
value.resourceId !== resourceId ||
|
|
33
|
-
!mimeTypes.has(value.mimeType) ||
|
|
34
|
-
!Number.isSafeInteger(value.byteSize) ||
|
|
35
|
-
value.byteSize <= 0 ||
|
|
36
|
-
value.byteSize > 20 * 1024 * 1024 ||
|
|
37
|
-
!idPattern.test(value.sha256) ||
|
|
38
|
-
resourceIdentity(value.mimeType, value.sha256) !== resourceId ||
|
|
39
|
-
(value.url !== undefined &&
|
|
40
|
-
(!isModelImageUrl(value.url) ||
|
|
41
|
-
!Number.isSafeInteger(value.urlExpiresAt) ||
|
|
42
|
-
value.urlExpiresAt <= 0)) ||
|
|
43
|
-
(value.url === undefined && value.urlExpiresAt !== undefined))
|
|
44
|
-
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is corrupt.');
|
|
45
|
-
return value;
|
|
46
41
|
}
|
|
47
42
|
export async function persistResource(directory, image) {
|
|
48
43
|
if (image.resourceId !== undefined && image.dataBase64 === undefined) {
|
|
49
44
|
const existing = await resourceMetadata(directory, image.resourceId);
|
|
50
45
|
if (existing !== undefined && existing.mimeType === image.mimeType)
|
|
51
46
|
return existing;
|
|
52
|
-
throw new MarAgentError('
|
|
47
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
|
|
53
48
|
}
|
|
54
49
|
const data = Buffer.from(image.dataBase64 ?? '', 'base64');
|
|
55
50
|
if (data.toString('base64') !== image.dataBase64)
|
|
@@ -60,10 +55,16 @@ export async function persistResource(directory, image) {
|
|
|
60
55
|
const resourceId = resourceIdentity(image.mimeType, sha256);
|
|
61
56
|
if (image.resourceId !== undefined && image.resourceId !== resourceId)
|
|
62
57
|
throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Image resource identity does not match its bytes.');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
let existing;
|
|
59
|
+
try {
|
|
60
|
+
existing = await resourceMetadata(directory, resourceId);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_IMAGE_UNAVAILABLE')
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
66
|
const resource = {
|
|
67
|
+
...existing,
|
|
67
68
|
resourceInputVersion: 1,
|
|
68
69
|
resourceId,
|
|
69
70
|
mimeType: image.mimeType,
|
|
@@ -79,7 +80,7 @@ export async function persistResource(directory, image) {
|
|
|
79
80
|
export async function setResourceUrl(directory, resourceId, published) {
|
|
80
81
|
const resource = await resourceMetadata(directory, resourceId);
|
|
81
82
|
if (!resource)
|
|
82
|
-
throw new MarAgentError('
|
|
83
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
|
|
83
84
|
if (resource.url !== undefined)
|
|
84
85
|
return resource;
|
|
85
86
|
if (!isModelImageUrl(published.url) ||
|
|
@@ -136,7 +137,7 @@ export async function copyResources(source, target, records) {
|
|
|
136
137
|
const resourceId = file.slice(0, -4);
|
|
137
138
|
const resource = await resourceMetadata(join(source, 'resources'), resourceId);
|
|
138
139
|
if (resource === undefined)
|
|
139
|
-
throw new MarAgentError('
|
|
140
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_UNAVAILABLE', 'Session image resource is missing.');
|
|
140
141
|
const bytes = await readInputImageBytes(join(source, 'resources'), {
|
|
141
142
|
...resource,
|
|
142
143
|
name: 'image',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import { lstat, mkdir, open, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { MarAgentError } from '../error.js';
|
|
6
5
|
import { parseToolResultExtensions } from '../tools/result-extensions.js';
|
|
@@ -79,12 +78,8 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
|
|
|
79
78
|
catch (error) {
|
|
80
79
|
if (error.code !== 'EEXIST')
|
|
81
80
|
throw error;
|
|
82
|
-
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
83
81
|
try {
|
|
84
|
-
const
|
|
85
|
-
if (!info.isFile() || info.size > 1024)
|
|
86
|
-
throw resourceError('CORRUPT', 'Invalid tool resource locator.');
|
|
87
|
-
const locator = JSON.parse(await handle.readFile('utf8'));
|
|
82
|
+
const locator = JSON.parse(await readFile(path, 'utf8'));
|
|
88
83
|
const existing = locator.sequence === record.sequence && locator.recordId === record.recordId
|
|
89
84
|
? record
|
|
90
85
|
: Number.isSafeInteger(locator.sequence)
|
|
@@ -98,9 +93,6 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
|
|
|
98
93
|
catch (cause) {
|
|
99
94
|
throw normalizedResourceError(cause);
|
|
100
95
|
}
|
|
101
|
-
finally {
|
|
102
|
-
await handle.close();
|
|
103
|
-
}
|
|
104
96
|
}
|
|
105
97
|
}
|
|
106
98
|
export function toolResultResourceFiles(records) {
|
|
@@ -184,16 +176,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
|
|
|
184
176
|
let locator;
|
|
185
177
|
try {
|
|
186
178
|
const file = join(directory, 'tool_results', `${input.resourceRef}.json`);
|
|
187
|
-
|
|
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
|
-
}
|
|
179
|
+
locator = JSON.parse(await readFile(file, 'utf8'));
|
|
197
180
|
}
|
|
198
181
|
catch (error) {
|
|
199
182
|
throw normalizedResourceError(error);
|
|
@@ -208,6 +191,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
|
|
|
208
191
|
: undefined;
|
|
209
192
|
if (!resource)
|
|
210
193
|
throw resourceError('NOT_FOUND', 'Tool resource is not part of this session history.');
|
|
194
|
+
const bytes = await readResourceBytes(directory, resource);
|
|
211
195
|
let offset = 0;
|
|
212
196
|
if (input.cursor !== undefined) {
|
|
213
197
|
try {
|
|
@@ -219,7 +203,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
|
|
|
219
203
|
cursor.sha256 !== resource.sha256 ||
|
|
220
204
|
!Number.isSafeInteger(cursor.offset) ||
|
|
221
205
|
cursor.offset < 0 ||
|
|
222
|
-
cursor.offset >=
|
|
206
|
+
cursor.offset >= bytes.length)
|
|
223
207
|
throw new Error('cursor');
|
|
224
208
|
offset = cursor.offset;
|
|
225
209
|
}
|
|
@@ -227,7 +211,6 @@ export async function readToolResultResource(directory, sessionId, input, readRe
|
|
|
227
211
|
throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor.');
|
|
228
212
|
}
|
|
229
213
|
}
|
|
230
|
-
const bytes = await readResourceBytes(directory, resource);
|
|
231
214
|
if (offset > 0 && (bytes[offset] & 0xc0) === 0x80)
|
|
232
215
|
throw resourceError('INPUT_INVALID', 'Invalid tool resource cursor boundary.');
|
|
233
216
|
let end = Math.min(bytes.length, offset + (input.maxBytes ?? 256 * 1024));
|
|
@@ -257,19 +240,7 @@ export async function readToolResultResource(directory, sessionId, input, readRe
|
|
|
257
240
|
}
|
|
258
241
|
async function readResourceBytes(directory, resource) {
|
|
259
242
|
try {
|
|
260
|
-
|
|
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
|
-
}
|
|
243
|
+
return await readFile(join(directory, 'tool_results', `${resource.resourceRef}.diff`));
|
|
273
244
|
}
|
|
274
245
|
catch (error) {
|
|
275
246
|
throw normalizedResourceError(error);
|