@myagentroam/agent 0.11.21 → 0.11.22
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/error.d.ts +1 -1
- package/dist/agent-core/error.js +20 -14
- package/dist/agent-core/extensions/compaction.d.ts +6 -0
- package/dist/agent-core/extensions/compaction.js +16 -2
- package/dist/agent-core/extensions/model-request.d.ts +5 -9
- package/dist/agent-core/extensions/model-request.js +14 -3
- package/dist/agent-core/extensions/prompt-input.js +3 -1
- package/dist/agent-core/host/contracts.d.ts +8 -0
- package/dist/agent-core/host/local-agent-host.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.js +3 -0
- package/dist/agent-core/model/anthropic-messages.js +15 -26
- package/dist/agent-core/model/catalog-file.d.ts +2 -0
- package/dist/agent-core/model/configuration.d.ts +3 -1
- package/dist/agent-core/model/configuration.js +1 -0
- package/dist/agent-core/model/contracts.d.ts +5 -8
- package/dist/agent-core/model/image.d.ts +17 -0
- package/dist/agent-core/model/image.js +53 -0
- package/dist/agent-core/model/openai-responses.d.ts +7 -2
- package/dist/agent-core/model/openai-responses.js +81 -27
- package/dist/agent-core/runtime/compaction/retained-context.js +2 -9
- package/dist/agent-core/runtime/context/gc-runtime.d.ts +3 -1
- package/dist/agent-core/runtime/context/gc-runtime.js +17 -6
- package/dist/agent-core/runtime/context/gc-tokens.js +13 -0
- package/dist/agent-core/runtime/context/gc.d.ts +2 -1
- package/dist/agent-core/runtime/context/gc.js +75 -12
- package/dist/agent-core/runtime/context/projection.js +4 -0
- package/dist/agent-core/runtime/context/request-budget.d.ts +2 -4
- package/dist/agent-core/runtime/context/restore.d.ts +2 -0
- package/dist/agent-core/runtime/context/restore.js +19 -3
- package/dist/agent-core/runtime/contracts.d.ts +2 -3
- package/dist/agent-core/runtime/execution.d.ts +1 -1
- package/dist/agent-core/runtime/execution.js +10 -4
- package/dist/agent-core/runtime/resource-publication.d.ts +8 -0
- package/dist/agent-core/runtime/resource-publication.js +60 -0
- package/dist/agent-core/sdk/agent.js +16 -0
- package/dist/agent-core/sdk/model-runtime.js +2 -0
- package/dist/agent-core/sdk/resource-input.d.ts +31 -0
- package/dist/agent-core/sdk/resource-input.js +342 -0
- package/dist/agent-core/sdk/session.js +144 -39
- package/dist/agent-core/session/catalog.d.ts +4 -0
- package/dist/agent-core/session/catalog.js +4 -0
- package/dist/agent-core/session/context-gc-checkpoint.d.ts +6 -4
- package/dist/agent-core/session/context-gc-checkpoint.js +41 -16
- package/dist/agent-core/session/inline-media.d.ts +45 -0
- package/dist/agent-core/session/inline-media.js +163 -0
- package/dist/agent-core/session/store/contracts.d.ts +11 -0
- package/dist/agent-core/session/store/history.d.ts +1 -1
- package/dist/agent-core/session/store/history.js +26 -2
- package/dist/agent-core/session/store/images.d.ts +4 -6
- package/dist/agent-core/session/store/images.js +18 -111
- package/dist/agent-core/session/store/jsonl-store.d.ts +9 -0
- package/dist/agent-core/session/store/jsonl-store.js +100 -28
- package/dist/agent-core/session/store/resources.d.ts +26 -0
- package/dist/agent-core/session/store/resources.js +196 -0
- package/dist/agent-core/session/store/types.d.ts +7 -6
- package/dist/agent-core/session/transaction-recovery.js +1 -1
- package/dist/agent-core/tools/code-mode/cells.js +3 -4
- package/dist/agent-core/tools/code-mode/index.js +32 -8
- package/dist/agent-core/tools/code-mode/observations.d.ts +5 -16
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +23 -26
- package/dist/agent-core/tools/contracts.d.ts +15 -5
- package/dist/agent-core/tools/execution-limits.d.ts +3 -1
- package/dist/agent-core/tools/execution-limits.js +3 -1
- package/dist/agent-core/tools/execution.d.ts +2 -1
- package/dist/agent-core/tools/execution.js +31 -4
- package/dist/agent-core/tools/input-diagnostics/source-preview.d.ts +10 -0
- package/dist/agent-core/tools/input-diagnostics/source-preview.js +64 -0
- package/dist/agent-core/tools/local/apply-patch-parser.js +2 -53
- package/dist/agent-core/tools/local/apply-patch.js +1 -1
- package/dist/agent-core/tools/local/image-processing.d.ts +24 -0
- package/dist/agent-core/tools/local/image-processing.js +135 -0
- package/dist/agent-core/tools/local/read.js +1 -1
- package/dist/agent-core/tools/local/view-image.d.ts +4 -0
- package/dist/agent-core/tools/local/view-image.js +154 -25
- package/dist/agent-core/tools/output-projection.d.ts +2 -4
- package/dist/agent-core/tools/output-projection.js +81 -10
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/agent-core/tools/runtime.js +20 -1
- package/dist/cli/main.js +2 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { inlineMediaHistory } from '../inline-media.js';
|
|
2
3
|
import { lstat, mkdir, open, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { basename, join } from 'node:path';
|
|
4
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
6
|
import { MarAgentError } from '../../error.js';
|
|
7
|
+
import { isModelImageUrl } from '../../model/image.js';
|
|
6
8
|
import { cloneCoreJson } from '../../extensions/data.js';
|
|
7
9
|
import { acquireFileLock } from '../../storage/file-lock.js';
|
|
8
10
|
import { todoItemsFromSnapshot } from '../../tools/agent/todo.js';
|
|
@@ -14,7 +16,8 @@ import { extensionCheckpointFromPayload, indexExtensionCheckpoint, restoreExtens
|
|
|
14
16
|
import { decodeSessionListCursor, encodeSessionListCursor, mergeSessionDerivedCache, normalizeSessionSource, persistSessionIndex, readSessionIndex, sessionListFilter, sessionMatchesFilter, sessionTreeIds } from './directory-index.js';
|
|
15
17
|
import { deriveExecutionState, executionModelFromRecord, isCompactCheckpoint, isNativeUserTurn, sameResumeIndex, userMessagePreviewFromRecord } from './execution-state.js';
|
|
16
18
|
import { forkedPayload, renameIfExists, restoreRenamedFiles, retainedSessionTitle, stageSessionSegments, stageUnreferencedImages } from './history.js';
|
|
17
|
-
import { MAX_INPUT_IMAGES, copyGeneratedImages,
|
|
19
|
+
import { MAX_INPUT_IMAGES, copyGeneratedImages, generatedImageFileNames, inputImageReference, inputImageReferences, readInputImageBytes } from './images.js';
|
|
20
|
+
import { persistResource, setResourceUrl, readResource, copyResources, resourceFiles, completeResources, recoverResources } from './resources.js';
|
|
18
21
|
import { MAX_SEGMENT_BYTES, appendRecordBytes, readRecordPageFromTail, readRecords, readRecordsFromHead, readRecordsFromTail, repairSegmentTail, segmentNumberFromPath } from './records.js';
|
|
19
22
|
const MAX_CONTEXTUAL_WORLD_STATE_BYTES = 8 * 1024 * 1024;
|
|
20
23
|
const CONTEXTUAL_WORLD_STATE_FILE = 'context-world-state.json';
|
|
@@ -446,8 +449,59 @@ export class JsonlSessionStore {
|
|
|
446
449
|
return [];
|
|
447
450
|
if (images.length > MAX_INPUT_IMAGES)
|
|
448
451
|
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Too many image inputs were provided.');
|
|
449
|
-
const
|
|
450
|
-
|
|
452
|
+
const resources = [];
|
|
453
|
+
let total = 0;
|
|
454
|
+
for (const image of images) {
|
|
455
|
+
if ((image.url !== undefined && !isModelImageUrl(image.url)) ||
|
|
456
|
+
(image.urlExpiresAt !== undefined &&
|
|
457
|
+
(!Number.isSafeInteger(image.urlExpiresAt) ||
|
|
458
|
+
image.urlExpiresAt <= 0 ||
|
|
459
|
+
image.url === undefined)))
|
|
460
|
+
throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Image model URL is invalid.');
|
|
461
|
+
let resource = await this.persistResource(sessionId, image);
|
|
462
|
+
if (image.url !== undefined && image.urlExpiresAt !== undefined)
|
|
463
|
+
resource = await this.setResourceUrl(sessionId, resource.resourceId, {
|
|
464
|
+
url: image.url,
|
|
465
|
+
urlExpiresAt: image.urlExpiresAt
|
|
466
|
+
});
|
|
467
|
+
total += resource.byteSize;
|
|
468
|
+
if (resource.byteSize > 5 * 1024 * 1024 || total > 10 * 1024 * 1024)
|
|
469
|
+
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Image input exceeds the supported size limit.');
|
|
470
|
+
resources.push({
|
|
471
|
+
...resource,
|
|
472
|
+
resourceInputVersion: 1,
|
|
473
|
+
name: image.name,
|
|
474
|
+
fileName: `${resource.resourceId}.bin`
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return resources;
|
|
478
|
+
}
|
|
479
|
+
async persistResource(sessionId, image) {
|
|
480
|
+
this.#assertOpen();
|
|
481
|
+
return this.#serializeRecordOperation(sessionId, () => persistResource(join(this.#sessionDirectory(sessionId), 'resources'), image));
|
|
482
|
+
}
|
|
483
|
+
async setResourceUrl(sessionId, resourceId, published) {
|
|
484
|
+
this.#assertOpen();
|
|
485
|
+
return this.#serializeRecordOperation(sessionId, () => setResourceUrl(join(this.#sessionDirectory(sessionId), 'resources'), resourceId, published));
|
|
486
|
+
}
|
|
487
|
+
async readResource(sessionId, resourceId) {
|
|
488
|
+
this.#assertOpen();
|
|
489
|
+
const directory = join(this.#sessionDirectory(sessionId), 'resources');
|
|
490
|
+
if (!/^[a-f0-9]{64}$/u.test(resourceId))
|
|
491
|
+
return undefined;
|
|
492
|
+
return this.#serializeRecordOperation(sessionId, async () => {
|
|
493
|
+
try {
|
|
494
|
+
await lstat(join(directory, `.${resourceId}.pending`));
|
|
495
|
+
if (this.#shared.writerLeases.get(sessionId)?.appendFailure !== undefined ||
|
|
496
|
+
!resourceFiles(await this.#readRecords(sessionId)).has(`${resourceId}.bin`))
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
if (error.code !== 'ENOENT')
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
return readResource(directory, resourceId);
|
|
504
|
+
});
|
|
451
505
|
}
|
|
452
506
|
async writeToolResultResource(sessionId, text) {
|
|
453
507
|
this.#assertOpen();
|
|
@@ -467,14 +521,21 @@ export class JsonlSessionStore {
|
|
|
467
521
|
}
|
|
468
522
|
async readInputImages(sessionId, value) {
|
|
469
523
|
this.#assertOpen();
|
|
470
|
-
const references = inputImageReferences(value);
|
|
524
|
+
const references = inputImageReferences(Array.isArray(value) ? value.filter((item) => item?.resourceInputVersion === 1) : undefined);
|
|
471
525
|
if (references.length === 0)
|
|
472
526
|
return [];
|
|
473
|
-
const directory = join(this.#sessionDirectory(sessionId), 'input_images');
|
|
474
527
|
const images = [];
|
|
475
528
|
for (const reference of references) {
|
|
476
|
-
|
|
477
|
-
|
|
529
|
+
if (reference.resourceId === undefined)
|
|
530
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session resource identity is missing.');
|
|
531
|
+
images.push({
|
|
532
|
+
resourceInputVersion: 1,
|
|
533
|
+
mimeType: reference.mimeType,
|
|
534
|
+
byteSize: reference.byteSize,
|
|
535
|
+
resourceId: reference.resourceId,
|
|
536
|
+
...(reference.url === undefined ? {} : { url: reference.url }),
|
|
537
|
+
...(reference.urlExpiresAt === undefined ? {} : { urlExpiresAt: reference.urlExpiresAt })
|
|
538
|
+
});
|
|
478
539
|
}
|
|
479
540
|
return images;
|
|
480
541
|
}
|
|
@@ -497,10 +558,14 @@ export class JsonlSessionStore {
|
|
|
497
558
|
return undefined;
|
|
498
559
|
if (!Array.isArray(record.payload.images) || input.imageIndex >= record.payload.images.length)
|
|
499
560
|
return undefined;
|
|
561
|
+
if (record.payload.images[input.imageIndex]?.resourceInputVersion !== 1)
|
|
562
|
+
return undefined;
|
|
500
563
|
const reference = inputImageReference(record.payload.images[input.imageIndex]);
|
|
501
564
|
if (reference === undefined)
|
|
502
565
|
return undefined;
|
|
503
|
-
|
|
566
|
+
if (reference.resourceId === undefined)
|
|
567
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session resource identity is missing.');
|
|
568
|
+
const data = await readInputImageBytes(join(this.#sessionDirectory(sessionId), 'resources'), reference);
|
|
504
569
|
return {
|
|
505
570
|
name: reference.name,
|
|
506
571
|
mimeType: reference.mimeType,
|
|
@@ -564,6 +629,9 @@ export class JsonlSessionStore {
|
|
|
564
629
|
}
|
|
565
630
|
];
|
|
566
631
|
const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
|
|
632
|
+
const inlineState = inlineMediaHistory(retained).state;
|
|
633
|
+
const stateAnchor = inlineState &&
|
|
634
|
+
retained.findLast((record) => record.type === 'resource.window' || record.type === 'compact.completed');
|
|
567
635
|
const forkIdentities = new Map();
|
|
568
636
|
for (const record of retained) {
|
|
569
637
|
if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
|
|
@@ -575,7 +643,14 @@ export class JsonlSessionStore {
|
|
|
575
643
|
...identity,
|
|
576
644
|
sessionId: target.id,
|
|
577
645
|
type: record.type,
|
|
578
|
-
payload: forkedPayload(record.type, record
|
|
646
|
+
payload: forkedPayload(record.type, record === stateAnchor
|
|
647
|
+
? record.type === 'resource.window'
|
|
648
|
+
? inlineState
|
|
649
|
+
: {
|
|
650
|
+
...record.payload,
|
|
651
|
+
inlineMediaState: inlineState
|
|
652
|
+
}
|
|
653
|
+
: record.payload, sourceSessionId, target.id, input.title, forkIdentities),
|
|
579
654
|
...(record.executionId === undefined ? {} : { executionId: record.executionId }),
|
|
580
655
|
...(record.turnId === undefined ? {} : { turnId: record.turnId }),
|
|
581
656
|
timestamp: record.timestamp
|
|
@@ -592,7 +667,7 @@ export class JsonlSessionStore {
|
|
|
592
667
|
this.#applyRecordToResumeIndex(target, record);
|
|
593
668
|
await copyToolResultResources(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), copied);
|
|
594
669
|
await copyGeneratedImages(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), generatedImageFileNames(retained));
|
|
595
|
-
await
|
|
670
|
+
await copyResources(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), retained);
|
|
596
671
|
target.lastActivityAt = target.createdAt;
|
|
597
672
|
this.#shared.pendingSessions.delete(target.id);
|
|
598
673
|
this.#shared.index = {
|
|
@@ -766,7 +841,7 @@ export class JsonlSessionStore {
|
|
|
766
841
|
const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.${operation.kind}-backup`;
|
|
767
842
|
let contextualWorldStateStaged = false;
|
|
768
843
|
let generatedTrash = [];
|
|
769
|
-
let
|
|
844
|
+
let resourceTrash = [];
|
|
770
845
|
const originalMeta = structuredClone(meta);
|
|
771
846
|
const originalIndexRevision = this.#shared.index.revision;
|
|
772
847
|
const originalSegment = this.#shared.segments.get(sessionId);
|
|
@@ -792,10 +867,7 @@ export class JsonlSessionStore {
|
|
|
792
867
|
await beginSessionStorageTransaction(this.#sessionDirectory(sessionId), transaction);
|
|
793
868
|
contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
|
|
794
869
|
generatedTrash = await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'generated_images', operation.kind === 'rollback' ? new Set(generatedImageFileNames(retained)) : new Set(), transactionId);
|
|
795
|
-
|
|
796
|
-
? new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName))
|
|
797
|
-
: new Set(), transactionId);
|
|
798
|
-
inputTrash.push(...(await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'tool_results', operation.kind === 'rollback' ? toolResultResourceFiles(retained) : new Set(), transactionId)));
|
|
870
|
+
resourceTrash.push(...(await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'resources', operation.kind === 'rollback' ? resourceFiles(retained) : new Set(), transactionId)), ...(await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'tool_results', operation.kind === 'rollback' ? toolResultResourceFiles(retained) : new Set(), transactionId)));
|
|
799
871
|
for (const item of backups)
|
|
800
872
|
await rename(item.path, item.backup);
|
|
801
873
|
for (const item of staged.files) {
|
|
@@ -850,7 +922,7 @@ export class JsonlSessionStore {
|
|
|
850
922
|
await restoreRenamedFiles([
|
|
851
923
|
{ from: contextualWorldStateBackup, to: contextualWorldStatePath }
|
|
852
924
|
]);
|
|
853
|
-
await restoreRenamedFiles([...generatedTrash, ...
|
|
925
|
+
await restoreRenamedFiles([...generatedTrash, ...resourceTrash]);
|
|
854
926
|
restoreSessionMeta(meta, originalMeta);
|
|
855
927
|
this.#shared.index.revision = originalIndexRevision;
|
|
856
928
|
if (originalSegment === undefined)
|
|
@@ -990,14 +1062,7 @@ export class JsonlSessionStore {
|
|
|
990
1062
|
}
|
|
991
1063
|
updateSessionCurrentState(this.#shared.writerLeases.get(sessionId), record.type, serialized);
|
|
992
1064
|
await completeToolResultResources(this.#sessionDirectory(sessionId), record);
|
|
993
|
-
|
|
994
|
-
isObject(record.payload) &&
|
|
995
|
-
Array.isArray(record.payload.images))
|
|
996
|
-
for (const image of record.payload.images)
|
|
997
|
-
if (isObject(image) &&
|
|
998
|
-
typeof image.fileName === 'string' &&
|
|
999
|
-
validInputImageFileName(image.fileName))
|
|
1000
|
-
await rm(join(this.#sessionDirectory(sessionId), 'input_images', `.${image.fileName}.pending`), { force: true }).catch(() => undefined);
|
|
1065
|
+
await completeResources(join(this.#sessionDirectory(sessionId), 'resources'), record);
|
|
1001
1066
|
return record;
|
|
1002
1067
|
}
|
|
1003
1068
|
async read(sessionId) {
|
|
@@ -1150,17 +1215,21 @@ export class JsonlSessionStore {
|
|
|
1150
1215
|
!sameContextGcBase(contextGcBaseFromRecords(records, boundarySequence), draft.base))
|
|
1151
1216
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC planning boundary is invalid.');
|
|
1152
1217
|
const previous = latestValidContextGcCheckpoint(records, boundarySequence);
|
|
1153
|
-
if (!
|
|
1218
|
+
if ((!draft.inlineMedia || draft.inlineMedia.reason === 'TEXT_GC') &&
|
|
1219
|
+
!contextGcCheckpointIntervalEligible(draft.trigger, previous?.payload.trigger))
|
|
1154
1220
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
|
|
1155
1221
|
const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
|
|
1156
|
-
if (!plannedReplacements || plannedReplacements.length === 0)
|
|
1222
|
+
if (!plannedReplacements || (plannedReplacements.length === 0 && !draft.inlineMedia))
|
|
1157
1223
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
|
|
1158
1224
|
const replacements = normalizeContextGcReplacements([
|
|
1159
1225
|
...(previous?.payload.replacements ?? []),
|
|
1160
1226
|
...plannedReplacements
|
|
1161
1227
|
]);
|
|
1162
1228
|
if (!replacements ||
|
|
1163
|
-
(previous &&
|
|
1229
|
+
(previous &&
|
|
1230
|
+
isDeepStrictEqual(replacements, previous.payload.replacements) &&
|
|
1231
|
+
(!draft.inlineMedia ||
|
|
1232
|
+
isDeepStrictEqual(draft.inlineMedia, previous.payload.inlineMedia))))
|
|
1164
1233
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC replacement map is invalid.');
|
|
1165
1234
|
const payload = {
|
|
1166
1235
|
...structuredClone(draft),
|
|
@@ -1180,8 +1249,10 @@ export class JsonlSessionStore {
|
|
|
1180
1249
|
if (Buffer.byteLength(JSON.stringify(payload)) >= MAX_SEGMENT_BYTES ||
|
|
1181
1250
|
validateContextGcCheckpoint(records, candidate, meta.workspace, previous?.payload.replacements) === undefined)
|
|
1182
1251
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint validation failed.');
|
|
1252
|
+
const turnId = records.findLast((record) => record.type === 'execution.header')?.turnId;
|
|
1183
1253
|
return this.#appendRecord(sessionId, {
|
|
1184
1254
|
type: 'context.gc.completed',
|
|
1255
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
1185
1256
|
payload
|
|
1186
1257
|
});
|
|
1187
1258
|
});
|
|
@@ -1238,6 +1309,7 @@ export class JsonlSessionStore {
|
|
|
1238
1309
|
...(input.retainedModelContext?.length
|
|
1239
1310
|
? { retainedModelContext: input.retainedModelContext }
|
|
1240
1311
|
: {}),
|
|
1312
|
+
...(input.inlineMediaState ? { inlineMediaState: input.inlineMediaState } : {}),
|
|
1241
1313
|
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
1242
1314
|
boundarySequence: meta.nextSequence - 1,
|
|
1243
1315
|
automatic: input.automatic === true,
|
|
@@ -1891,7 +1963,7 @@ export class JsonlSessionStore {
|
|
|
1891
1963
|
const latestPath = paths.at(-1);
|
|
1892
1964
|
await repairSegmentTail(latestPath);
|
|
1893
1965
|
const records = await readRecords(latestPath);
|
|
1894
|
-
await
|
|
1966
|
+
await recoverResources(join(this.#sessionDirectory(sessionId), 'resources'), () => this.#readRecords(sessionId));
|
|
1895
1967
|
await cleanupUnreferencedToolResources(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
|
|
1896
1968
|
const last = records.at(-1);
|
|
1897
1969
|
const match = /events-(\d{6})\.jsonl$/.exec(latestPath);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ModelImage } from '../../model/image.js';
|
|
2
|
+
import type { SessionRecord } from './types.js';
|
|
3
|
+
export interface SessionResource {
|
|
4
|
+
readonly resourceInputVersion?: 1;
|
|
5
|
+
readonly resourceId: string;
|
|
6
|
+
readonly mimeType: string;
|
|
7
|
+
readonly byteSize: number;
|
|
8
|
+
readonly sha256: string;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly urlExpiresAt?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function resourceMetadata(directory: string, resourceId: string): Promise<SessionResource | undefined>;
|
|
13
|
+
export declare function persistResource(directory: string, image: ModelImage): Promise<SessionResource>;
|
|
14
|
+
export declare function setResourceUrl(directory: string, resourceId: string, published: {
|
|
15
|
+
url: string;
|
|
16
|
+
urlExpiresAt: number;
|
|
17
|
+
}): Promise<SessionResource>;
|
|
18
|
+
export declare function readResource(directory: string, resourceId: string): Promise<{
|
|
19
|
+
mimeType: string;
|
|
20
|
+
dataBase64: string;
|
|
21
|
+
} | undefined>;
|
|
22
|
+
/** Resource identities are SDK-owned metadata, never interpreted as file paths. */
|
|
23
|
+
export declare function resourceFiles(records: readonly SessionRecord[]): Set<string>;
|
|
24
|
+
export declare function copyResources(source: string, target: string, records: readonly SessionRecord[]): Promise<void>;
|
|
25
|
+
export declare function completeResources(directory: string, record: SessionRecord): Promise<void>;
|
|
26
|
+
export declare function recoverResources(directory: string, readHistory: () => Promise<SessionRecord[]>): Promise<void>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { MarAgentError } from '../../error.js';
|
|
5
|
+
import { isModelImageUrl } from '../../model/image.js';
|
|
6
|
+
import { readInputImageBytes } from './images.js';
|
|
7
|
+
const idPattern = /^[a-f0-9]{64}$/u;
|
|
8
|
+
const mimeTypes = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
9
|
+
function resourceIdentity(mimeType, sha256) {
|
|
10
|
+
return createHash('sha256').update(mimeType).update('\0').update(sha256).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
export async function resourceMetadata(directory, resourceId) {
|
|
13
|
+
if (!idPattern.test(resourceId))
|
|
14
|
+
return undefined;
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
text = await readFile(join(directory, `${resourceId}.json`), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT')
|
|
21
|
+
return undefined;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Invalid JSON is reported below without exposing metadata contents.
|
|
30
|
+
}
|
|
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
|
+
}
|
|
47
|
+
export async function persistResource(directory, image) {
|
|
48
|
+
if (image.resourceId !== undefined && image.dataBase64 === undefined) {
|
|
49
|
+
const existing = await resourceMetadata(directory, image.resourceId);
|
|
50
|
+
if (existing !== undefined && existing.mimeType === image.mimeType)
|
|
51
|
+
return existing;
|
|
52
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
|
|
53
|
+
}
|
|
54
|
+
const data = Buffer.from(image.dataBase64 ?? '', 'base64');
|
|
55
|
+
if (data.toString('base64') !== image.dataBase64)
|
|
56
|
+
throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Image resource data is not canonical base64.');
|
|
57
|
+
if (!mimeTypes.has(image.mimeType) || data.length === 0 || data.length > 20 * 1024 * 1024)
|
|
58
|
+
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Image resource exceeds supported limits.');
|
|
59
|
+
const sha256 = createHash('sha256').update(data).digest('hex');
|
|
60
|
+
const resourceId = resourceIdentity(image.mimeType, sha256);
|
|
61
|
+
if (image.resourceId !== undefined && image.resourceId !== resourceId)
|
|
62
|
+
throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Image resource identity does not match its bytes.');
|
|
63
|
+
const existing = await resourceMetadata(directory, resourceId);
|
|
64
|
+
if (existing !== undefined)
|
|
65
|
+
return existing;
|
|
66
|
+
const resource = {
|
|
67
|
+
resourceInputVersion: 1,
|
|
68
|
+
resourceId,
|
|
69
|
+
mimeType: image.mimeType,
|
|
70
|
+
byteSize: data.length,
|
|
71
|
+
sha256
|
|
72
|
+
};
|
|
73
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
74
|
+
await writeFile(join(directory, `.${resourceId}.pending`), '', { mode: 0o600, flush: true });
|
|
75
|
+
await atomicWrite(join(directory, `${resourceId}.bin`), data);
|
|
76
|
+
await atomicWrite(join(directory, `${resourceId}.json`), JSON.stringify(resource));
|
|
77
|
+
return resource;
|
|
78
|
+
}
|
|
79
|
+
export async function setResourceUrl(directory, resourceId, published) {
|
|
80
|
+
const resource = await resourceMetadata(directory, resourceId);
|
|
81
|
+
if (!resource)
|
|
82
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
|
|
83
|
+
if (resource.url !== undefined)
|
|
84
|
+
return resource;
|
|
85
|
+
if (!isModelImageUrl(published.url) ||
|
|
86
|
+
!Number.isSafeInteger(published.urlExpiresAt) ||
|
|
87
|
+
published.urlExpiresAt <= 0)
|
|
88
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_PUBLICATION_FAILED', 'Image URL publication failed.');
|
|
89
|
+
const result = { ...resource, url: published.url, urlExpiresAt: published.urlExpiresAt };
|
|
90
|
+
await atomicWrite(join(directory, `${resourceId}.json`), JSON.stringify(result));
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
export async function readResource(directory, resourceId) {
|
|
94
|
+
const resource = await resourceMetadata(directory, resourceId);
|
|
95
|
+
if (!resource)
|
|
96
|
+
return undefined;
|
|
97
|
+
const bytes = await readInputImageBytes(directory, {
|
|
98
|
+
...resource,
|
|
99
|
+
name: 'image',
|
|
100
|
+
fileName: `${resourceId}.bin`
|
|
101
|
+
});
|
|
102
|
+
return { mimeType: resource.mimeType, dataBase64: bytes.toString('base64') };
|
|
103
|
+
}
|
|
104
|
+
/** Resource identities are SDK-owned metadata, never interpreted as file paths. */
|
|
105
|
+
export function resourceFiles(records) {
|
|
106
|
+
const ids = new Set();
|
|
107
|
+
const visit = (value) => {
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
for (const item of value)
|
|
110
|
+
visit(item);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!value || typeof value !== 'object')
|
|
114
|
+
return;
|
|
115
|
+
const item = value;
|
|
116
|
+
if (item.resourceInputVersion === 1 &&
|
|
117
|
+
typeof item.resourceId === 'string' &&
|
|
118
|
+
idPattern.test(item.resourceId))
|
|
119
|
+
ids.add(item.resourceId);
|
|
120
|
+
for (const key of ['images', 'input', 'pendingInputs', 'payload', 'retainedModelContext'])
|
|
121
|
+
if (item[key] !== undefined)
|
|
122
|
+
visit(item[key]);
|
|
123
|
+
};
|
|
124
|
+
for (const record of records)
|
|
125
|
+
visit(record.payload);
|
|
126
|
+
return new Set([...ids].flatMap((id) => [`${id}.bin`, `${id}.json`]));
|
|
127
|
+
}
|
|
128
|
+
export async function copyResources(source, target, records) {
|
|
129
|
+
const files = resourceFiles(records);
|
|
130
|
+
if (!files.size)
|
|
131
|
+
return;
|
|
132
|
+
await mkdir(join(target, 'resources'), { recursive: true, mode: 0o700 });
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
if (!file.endsWith('.bin'))
|
|
135
|
+
continue;
|
|
136
|
+
const resourceId = file.slice(0, -4);
|
|
137
|
+
const resource = await resourceMetadata(join(source, 'resources'), resourceId);
|
|
138
|
+
if (resource === undefined)
|
|
139
|
+
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session image resource is missing.');
|
|
140
|
+
const bytes = await readInputImageBytes(join(source, 'resources'), {
|
|
141
|
+
...resource,
|
|
142
|
+
name: 'image',
|
|
143
|
+
fileName: file
|
|
144
|
+
});
|
|
145
|
+
await writeFile(join(target, 'resources', file), bytes, {
|
|
146
|
+
mode: 0o600,
|
|
147
|
+
flag: 'wx',
|
|
148
|
+
flush: true
|
|
149
|
+
});
|
|
150
|
+
await writeFile(join(target, 'resources', `${resourceId}.json`), JSON.stringify(resource), {
|
|
151
|
+
mode: 0o600,
|
|
152
|
+
flag: 'wx',
|
|
153
|
+
flush: true
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function completeResources(directory, record) {
|
|
158
|
+
for (const file of resourceFiles([record]))
|
|
159
|
+
if (file.endsWith('.bin'))
|
|
160
|
+
await rm(join(directory, `.${file.slice(0, -4)}.pending`), { force: true });
|
|
161
|
+
}
|
|
162
|
+
export async function recoverResources(directory, readHistory) {
|
|
163
|
+
let names;
|
|
164
|
+
try {
|
|
165
|
+
names = await readdir(directory);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === 'ENOENT')
|
|
169
|
+
return;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
const pending = names.filter((name) => /^\.[a-f0-9]{64}\.pending$/u.test(name));
|
|
173
|
+
if (pending.length) {
|
|
174
|
+
const retained = resourceFiles(await readHistory());
|
|
175
|
+
for (const name of pending) {
|
|
176
|
+
const id = name.slice(1, -8);
|
|
177
|
+
if (!retained.has(`${id}.bin`))
|
|
178
|
+
for (const suffix of ['bin', 'json'])
|
|
179
|
+
await rm(join(directory, `${id}.${suffix}`), { force: true });
|
|
180
|
+
await rm(join(directory, name), { force: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const name of names)
|
|
184
|
+
if (/^\.[a-f0-9]{64}\.(?:bin|json)\.[a-f0-9-]{36}\.tmp$/u.test(name))
|
|
185
|
+
await rm(join(directory, name), { force: true });
|
|
186
|
+
}
|
|
187
|
+
async function atomicWrite(path, data) {
|
|
188
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
189
|
+
try {
|
|
190
|
+
await writeFile(temporary, data, { mode: 0o600, flag: 'wx', flush: true });
|
|
191
|
+
await rename(temporary, path);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await rm(temporary, { force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModelInputMetadata } from '../../model/contracts.js';
|
|
2
2
|
import type { TodoSnapshot } from '../../tools/agent/todo.js';
|
|
3
3
|
import type { SessionSource } from '../types.js';
|
|
4
|
+
import type { ModelImage } from '../../model/image.js';
|
|
4
5
|
export interface SessionRecord {
|
|
5
6
|
schemaVersion: 1;
|
|
6
7
|
recordId: string;
|
|
@@ -25,21 +26,21 @@ export interface CompactHistoryMessage extends CompactUserInput {
|
|
|
25
26
|
role: 'user';
|
|
26
27
|
kind: 'user_input' | 'extension_input' | 'summary';
|
|
27
28
|
}
|
|
28
|
-
export interface SessionInputImage {
|
|
29
|
+
export interface SessionInputImage extends ModelImage {
|
|
29
30
|
readonly name: string;
|
|
30
|
-
readonly mimeType: string;
|
|
31
|
-
readonly dataBase64: string;
|
|
32
31
|
}
|
|
33
32
|
export interface SessionInputImageMetadata {
|
|
33
|
+
readonly resourceInputVersion?: 1;
|
|
34
34
|
readonly name: string;
|
|
35
35
|
readonly mimeType: string;
|
|
36
36
|
readonly byteSize: number;
|
|
37
37
|
readonly fileName: string;
|
|
38
38
|
readonly sha256: string;
|
|
39
|
+
readonly resourceId?: string;
|
|
40
|
+
readonly url?: string;
|
|
41
|
+
readonly urlExpiresAt?: number;
|
|
39
42
|
}
|
|
40
|
-
export interface RestoredSessionInputImage {
|
|
41
|
-
readonly mimeType: string;
|
|
42
|
-
readonly dataBase64: string;
|
|
43
|
+
export interface RestoredSessionInputImage extends ModelImage {
|
|
43
44
|
}
|
|
44
45
|
export interface SessionInputImageData extends SessionInputImageMetadata {
|
|
45
46
|
readonly dataBase64: string;
|
|
@@ -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 ['
|
|
95
|
+
for (const folder of ['generated_images', 'tool_results', 'resources']) {
|
|
96
96
|
const path = join(directory, folder);
|
|
97
97
|
let names;
|
|
98
98
|
try {
|
|
@@ -10,10 +10,9 @@ import { boundObservations, selectObservationImages, newObservationId, visibleOb
|
|
|
10
10
|
export class CodeCellExecutionError extends MarAgentError {
|
|
11
11
|
result;
|
|
12
12
|
constructor(error, result) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
: diagnostic.message, {
|
|
13
|
+
// Preserve the original chain; the public boundary summarizes its causes only once.
|
|
14
|
+
const diagnostic = describeMarAgentError(error, 'MAR_AGENT_CODE_FAILED', undefined, false);
|
|
15
|
+
super(diagnostic.code, diagnostic.message, {
|
|
17
16
|
cause: error,
|
|
18
17
|
...(error instanceof MarAgentError ? { retryable: error.retryable } : {})
|
|
19
18
|
});
|
|
@@ -16,6 +16,7 @@ import { BoundedTextOutput, renderCapturedText } from './text-output.js';
|
|
|
16
16
|
import { TOOL_EXECUTION_LIMITS as limits } from '../execution-limits.js';
|
|
17
17
|
import { createToolCallScheduler } from '../scheduler.js';
|
|
18
18
|
import { detectImageMime } from '../local/view-image.js';
|
|
19
|
+
import { isModelImageUrl } from '../../model/image.js';
|
|
19
20
|
import { structuredToolFailureData } from '../output-projection.js';
|
|
20
21
|
import { failedToolResultExtensions } from '../result-extensions.js';
|
|
21
22
|
export const codeModeToolDefinition = {
|
|
@@ -33,7 +34,7 @@ export const codeModeToolDefinition = {
|
|
|
33
34
|
|
|
34
35
|
Tool calls:
|
|
35
36
|
- tools.NAME(input) returns a Promise. Pass a JavaScript value matching the current catalog inputSchema. For parameterless tools prefer tools.NAME({}); tools.NAME() is accepted only for explicitly closed empty-object schemas.
|
|
36
|
-
- Results use the MAR envelope: { content?: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string}>, truncated?: boolean }. Redundant JSON content is omitted when it exactly renders data; independent text remains in content. Prefer data for programmatic results: result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. content, when present, is rendered text, not raw file bytes. Use only fields documented by that tool; do not invent success or ok flags.
|
|
37
|
+
- Results use the MAR envelope: { content?: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string,url?:string,urlExpiresAt?:number}>, truncated?: boolean }. Redundant JSON content is omitted when it exactly renders data; independent text remains in content. Prefer data for programmatic results: result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. content, when present, is rendered text, not raw file bytes. Use only fields documented by that tool; do not invent success or ok flags.
|
|
37
38
|
- Tool errors reject with code/message and may be caught, except nested Auto deny, which skips only that call and resolves with data.error/data.message. A failed cell preserves already emitted output alongside its status and error; completed tool effects are not rolled back. Inspect it before retrying completed work. Await every call; unawaited calls fail the cell. Use Promise.all for independent calls and sequential await for dependencies. Runtime preserves safe/serial barriers and Host checks.
|
|
38
39
|
- skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
|
|
39
40
|
|
|
@@ -183,6 +184,7 @@ function defaultEngineFactory() {
|
|
|
183
184
|
runtimeDirectories: [
|
|
184
185
|
...new Set([
|
|
185
186
|
fileURLToPath(new URL('.', import.meta.url)),
|
|
187
|
+
fileURLToPath(new URL('../input-diagnostics/', import.meta.url)),
|
|
186
188
|
dependencyRoot(core),
|
|
187
189
|
dependencyRoot(variant),
|
|
188
190
|
dependencyRoot(ffi)
|
|
@@ -565,10 +567,7 @@ export class CodeModeTool {
|
|
|
565
567
|
ok: false,
|
|
566
568
|
error: {
|
|
567
569
|
code: diagnostic.code,
|
|
568
|
-
|
|
569
|
-
message: diagnostic.message.startsWith(`${diagnostic.code}: `)
|
|
570
|
-
? diagnostic.message.slice(diagnostic.code.length + 2)
|
|
571
|
-
: diagnostic.message
|
|
570
|
+
message: diagnostic.message
|
|
572
571
|
}
|
|
573
572
|
});
|
|
574
573
|
}
|
|
@@ -706,9 +705,27 @@ function parseImage(value) {
|
|
|
706
705
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
707
706
|
throw invalid();
|
|
708
707
|
const image = value;
|
|
709
|
-
if (Object.keys(image).some((key) =>
|
|
708
|
+
if (Object.keys(image).some((key) => !['mimeType', 'dataBase64', 'url', 'urlExpiresAt', 'resourceId', 'byteSize'].includes(key)) ||
|
|
710
709
|
typeof image.mimeType !== 'string' ||
|
|
711
|
-
|
|
710
|
+
(image.resourceId !== undefined &&
|
|
711
|
+
(typeof image.resourceId !== 'string' || !/^[a-f0-9]{64}$/u.test(image.resourceId))) ||
|
|
712
|
+
(image.byteSize !== undefined &&
|
|
713
|
+
(!Number.isSafeInteger(image.byteSize) || image.byteSize <= 0)) ||
|
|
714
|
+
(image.url !== undefined && !isModelImageUrl(image.url)) ||
|
|
715
|
+
(image.urlExpiresAt !== undefined &&
|
|
716
|
+
(typeof image.urlExpiresAt !== 'number' ||
|
|
717
|
+
!Number.isSafeInteger(image.urlExpiresAt) ||
|
|
718
|
+
image.urlExpiresAt <= 0)))
|
|
719
|
+
throw invalid();
|
|
720
|
+
if (image.dataBase64 === undefined && isModelImageUrl(image.url))
|
|
721
|
+
return {
|
|
722
|
+
mimeType: image.mimeType,
|
|
723
|
+
url: image.url,
|
|
724
|
+
...(image.urlExpiresAt === undefined ? {} : { urlExpiresAt: image.urlExpiresAt }),
|
|
725
|
+
...(typeof image.resourceId === 'string' ? { resourceId: image.resourceId } : {}),
|
|
726
|
+
...(typeof image.byteSize === 'number' ? { byteSize: image.byteSize } : {})
|
|
727
|
+
};
|
|
728
|
+
if (typeof image.dataBase64 !== 'string' ||
|
|
712
729
|
image.dataBase64.length === 0 ||
|
|
713
730
|
image.dataBase64.length > Math.ceil(limits.viewImageMaxBytes / 3) * 4 ||
|
|
714
731
|
image.dataBase64.length % 4 !== 0)
|
|
@@ -718,5 +735,12 @@ function parseImage(value) {
|
|
|
718
735
|
bytes.toString('base64') !== image.dataBase64 ||
|
|
719
736
|
detectImageMime(bytes) !== image.mimeType)
|
|
720
737
|
throw invalid();
|
|
721
|
-
return {
|
|
738
|
+
return {
|
|
739
|
+
mimeType: image.mimeType,
|
|
740
|
+
dataBase64: image.dataBase64,
|
|
741
|
+
...(typeof image.resourceId === 'string' ? { resourceId: image.resourceId } : {}),
|
|
742
|
+
...(typeof image.byteSize === 'number' ? { byteSize: image.byteSize } : {}),
|
|
743
|
+
...(image.url === undefined ? {} : { url: image.url }),
|
|
744
|
+
...(image.urlExpiresAt === undefined ? {} : { urlExpiresAt: image.urlExpiresAt })
|
|
745
|
+
};
|
|
722
746
|
}
|