@myagentroam/agent 0.11.21 → 0.11.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/agent-core/error.d.ts +1 -1
  2. package/dist/agent-core/error.js +20 -14
  3. package/dist/agent-core/extensions/compaction.d.ts +6 -0
  4. package/dist/agent-core/extensions/compaction.js +16 -2
  5. package/dist/agent-core/extensions/model-request.d.ts +5 -9
  6. package/dist/agent-core/extensions/model-request.js +14 -3
  7. package/dist/agent-core/extensions/prompt-input.js +3 -1
  8. package/dist/agent-core/host/contracts.d.ts +8 -0
  9. package/dist/agent-core/host/local-agent-host.d.ts +2 -0
  10. package/dist/agent-core/host/local-agent-host.js +3 -0
  11. package/dist/agent-core/model/anthropic-messages.js +15 -26
  12. package/dist/agent-core/model/catalog-file.d.ts +2 -0
  13. package/dist/agent-core/model/configuration.d.ts +3 -1
  14. package/dist/agent-core/model/configuration.js +1 -0
  15. package/dist/agent-core/model/contracts.d.ts +5 -8
  16. package/dist/agent-core/model/image.d.ts +17 -0
  17. package/dist/agent-core/model/image.js +53 -0
  18. package/dist/agent-core/model/openai-responses.d.ts +7 -2
  19. package/dist/agent-core/model/openai-responses.js +81 -27
  20. package/dist/agent-core/runtime/compaction/retained-context.js +2 -9
  21. package/dist/agent-core/runtime/context/gc-runtime.d.ts +3 -1
  22. package/dist/agent-core/runtime/context/gc-runtime.js +17 -6
  23. package/dist/agent-core/runtime/context/gc-tokens.js +13 -0
  24. package/dist/agent-core/runtime/context/gc.d.ts +2 -1
  25. package/dist/agent-core/runtime/context/gc.js +78 -12
  26. package/dist/agent-core/runtime/context/projection.js +4 -0
  27. package/dist/agent-core/runtime/context/request-budget.d.ts +2 -4
  28. package/dist/agent-core/runtime/context/restore.d.ts +2 -0
  29. package/dist/agent-core/runtime/context/restore.js +19 -3
  30. package/dist/agent-core/runtime/contracts.d.ts +2 -3
  31. package/dist/agent-core/runtime/execution.d.ts +1 -1
  32. package/dist/agent-core/runtime/execution.js +10 -4
  33. package/dist/agent-core/runtime/resource-publication.d.ts +8 -0
  34. package/dist/agent-core/runtime/resource-publication.js +60 -0
  35. package/dist/agent-core/sdk/agent.js +16 -0
  36. package/dist/agent-core/sdk/model-runtime.js +2 -0
  37. package/dist/agent-core/sdk/resource-input.d.ts +31 -0
  38. package/dist/agent-core/sdk/resource-input.js +342 -0
  39. package/dist/agent-core/sdk/session.js +144 -39
  40. package/dist/agent-core/session/catalog.d.ts +4 -0
  41. package/dist/agent-core/session/catalog.js +4 -0
  42. package/dist/agent-core/session/context-gc-checkpoint.d.ts +6 -4
  43. package/dist/agent-core/session/context-gc-checkpoint.js +43 -16
  44. package/dist/agent-core/session/inline-media.d.ts +45 -0
  45. package/dist/agent-core/session/inline-media.js +163 -0
  46. package/dist/agent-core/session/store/contracts.d.ts +11 -0
  47. package/dist/agent-core/session/store/history.d.ts +1 -1
  48. package/dist/agent-core/session/store/history.js +26 -2
  49. package/dist/agent-core/session/store/images.d.ts +4 -6
  50. package/dist/agent-core/session/store/images.js +18 -111
  51. package/dist/agent-core/session/store/jsonl-store.d.ts +9 -0
  52. package/dist/agent-core/session/store/jsonl-store.js +100 -28
  53. package/dist/agent-core/session/store/resources.d.ts +26 -0
  54. package/dist/agent-core/session/store/resources.js +196 -0
  55. package/dist/agent-core/session/store/types.d.ts +7 -6
  56. package/dist/agent-core/session/transaction-recovery.js +1 -1
  57. package/dist/agent-core/tools/code-mode/cells.js +3 -4
  58. package/dist/agent-core/tools/code-mode/index.js +32 -8
  59. package/dist/agent-core/tools/code-mode/observations.d.ts +5 -16
  60. package/dist/agent-core/tools/code-mode/quickjs-engine.js +23 -26
  61. package/dist/agent-core/tools/contracts.d.ts +15 -5
  62. package/dist/agent-core/tools/execution-limits.d.ts +3 -1
  63. package/dist/agent-core/tools/execution-limits.js +3 -1
  64. package/dist/agent-core/tools/execution.d.ts +2 -1
  65. package/dist/agent-core/tools/execution.js +31 -4
  66. package/dist/agent-core/tools/input-diagnostics/source-preview.d.ts +10 -0
  67. package/dist/agent-core/tools/input-diagnostics/source-preview.js +64 -0
  68. package/dist/agent-core/tools/local/apply-patch-parser.js +2 -53
  69. package/dist/agent-core/tools/local/apply-patch.js +1 -1
  70. package/dist/agent-core/tools/local/image-processing.d.ts +25 -0
  71. package/dist/agent-core/tools/local/image-processing.js +146 -0
  72. package/dist/agent-core/tools/local/read.js +1 -1
  73. package/dist/agent-core/tools/local/view-image.d.ts +4 -0
  74. package/dist/agent-core/tools/local/view-image.js +154 -25
  75. package/dist/agent-core/tools/output-projection.d.ts +2 -4
  76. package/dist/agent-core/tools/output-projection.js +81 -10
  77. package/dist/agent-core/tools/runtime-context.d.ts +1 -0
  78. package/dist/agent-core/tools/runtime.js +20 -1
  79. package/dist/cli/main.js +2 -1
  80. package/dist/index.d.ts +1 -1
  81. 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, copyInputImages, generatedImageFileNames, inputImageReference, inputImageReferences, inputImageReferencesFromRecords, readInputImageBytes, recoverPendingInputImages, validInputImageFileName, writeInputImages } from './images.js';
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 directory = join(this.#sessionDirectory(sessionId), 'input_images');
450
- return writeInputImages(directory, images);
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
- const data = await readInputImageBytes(directory, reference);
477
- images.push({ mimeType: reference.mimeType, dataBase64: data.toString('base64') });
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
- const data = await readInputImageBytes(join(this.#sessionDirectory(sessionId), 'input_images'), reference);
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.payload, sourceSessionId, target.id, input.title, forkIdentities),
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 copyInputImages(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), inputImageReferencesFromRecords(retained));
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 inputTrash = [];
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
- inputTrash = await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'input_images', operation.kind === 'rollback'
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, ...inputTrash]);
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
- if (record.type === 'turn.user' &&
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 (!contextGcCheckpointIntervalEligible(draft.trigger, previous?.payload.trigger))
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 && isDeepStrictEqual(replacements, previous.payload.replacements)))
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 recoverPendingInputImages(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
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 ['input_images', 'generated_images', 'tool_results']) {
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
- const diagnostic = describeMarAgentError(error, 'MAR_AGENT_CODE_FAILED');
14
- super(diagnostic.code, diagnostic.message.startsWith(`${diagnostic.code}: `)
15
- ? diagnostic.message.slice(diagnostic.code.length + 2)
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
- // The guest carries the code separately; the outer error adds its own prefix.
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) => key !== 'mimeType' && key !== 'dataBase64') ||
708
+ if (Object.keys(image).some((key) => !['mimeType', 'dataBase64', 'url', 'urlExpiresAt', 'resourceId', 'byteSize'].includes(key)) ||
710
709
  typeof image.mimeType !== 'string' ||
711
- typeof image.dataBase64 !== 'string' ||
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 { mimeType: image.mimeType, dataBase64: image.dataBase64 };
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
  }