@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.
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 +75 -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 +41 -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 +24 -0
  71. package/dist/agent-core/tools/local/image-processing.js +135 -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,10 +1,12 @@
1
- import { restoreMessages } from '../runtime/context/restore.js';
1
+ import { readResourceImages, restoreMessages } from '../runtime/context/restore.js';
2
2
  import { createNativeSteer } from './native-steer.js';
3
3
  import { latestModelUsage, reasoningEffortBaseline } from '../runtime/context/usage.js';
4
4
  import { splitContextualPrompt } from '../runtime/context/contextual-blocks.js';
5
5
  import { contextualInstructionInsertIndex, latestContextualWorldStateGeneration, parsePersistedContextualWorldState, persistContextualWorldState, resetContextualWorldState, updateContextualWorldState, updatePromptWorldState } from '../runtime/context/world-state.js';
6
6
  import { eventForPersistence, publicModelUsage } from '../runtime/event-projection.js';
7
7
  import { ExecutionLoop } from '../runtime/execution.js';
8
+ import { publishModelResources } from '../runtime/resource-publication.js';
9
+ import { ResourceInputProjection } from './resource-input.js';
8
10
  import { closeModelAdapters, createExecutionModelResources, createModelTurnSession } from './model-runtime.js';
9
11
  import { prepareSessionModelRequest, projectSessionRequestMessages } from './request-preparation.js';
10
12
  import { createSubagentBackend } from './subagent-backend.js';
@@ -543,6 +545,7 @@ export function createSessionOpener(services) {
543
545
  replaceCompactMessages(replacementHistory, retainedModelContext) {
544
546
  messages.splice(0, messages.length, environmentContextUpdate(executionEnvironment), ...compactedHistoryMessages(replacementHistory), ...retainedModelContext);
545
547
  pendingAppends.length = 0;
548
+ resourceInput.replace(messages);
546
549
  selected.resetContinuation?.();
547
550
  },
548
551
  // Derived state follows the committed splice, never delays it past the writer window.
@@ -579,6 +582,8 @@ export function createSessionOpener(services) {
579
582
  .filter((entry) => !restoredIds.has(entry.recordId))
580
583
  .map((entry) => entry.message);
581
584
  messages.splice(0, messages.length, ...refreshed.messages, ...lateMessages);
585
+ resourceInput.replace(messages);
586
+ resourceInput.adoptGc(committed.projection.checkpoint?.inlineMedia?.state);
582
587
  restoreMcpDiscovery();
583
588
  recordedEnvironment = refreshed.environmentState;
584
589
  userMessages = [...refreshed.userMessages];
@@ -620,8 +625,18 @@ export function createSessionOpener(services) {
620
625
  smartPercent !== undefined &&
621
626
  smartPercent >= AGENT_EXECUTION_POLICY.smartCompactionPercent &&
622
627
  smartPercent < AGENT_EXECUTION_POLICY.automaticCompactionPercent;
623
- const images = content.images?.length
624
- ? await store.persistInputImages(sessionId, content.images)
628
+ const modelImages = await publishModelResources(host, content.images, {
629
+ sessionId,
630
+ executionId
631
+ }, store);
632
+ const actualContent = modelImages === content.images
633
+ ? content
634
+ : {
635
+ ...content,
636
+ ...(modelImages === undefined ? {} : { images: modelImages })
637
+ };
638
+ const images = modelImages?.length
639
+ ? await store.persistInputImages(sessionId, modelImages)
625
640
  : [];
626
641
  const record = await store.append(sessionId, {
627
642
  type,
@@ -649,7 +664,7 @@ export function createSessionOpener(services) {
649
664
  content: content.modelText ?? content.text,
650
665
  metadata: inputMetadata
651
666
  });
652
- committed(content, record.recordId, inputMetadata);
667
+ committed(actualContent, record.recordId, inputMetadata);
653
668
  for (const text of content.modelContext ?? content.context ?? [])
654
669
  messages.push({ role: 'user', content: text, contextKind: 'contextual' });
655
670
  });
@@ -757,10 +772,7 @@ export function createSessionOpener(services) {
757
772
  metadata,
758
773
  ...(actual.images?.length
759
774
  ? {
760
- images: actual.images.map(({ mimeType, dataBase64 }) => ({
761
- mimeType,
762
- dataBase64
763
- }))
775
+ images: actual.images.map((image) => ({ ...image }))
764
776
  }
765
777
  : {})
766
778
  });
@@ -775,6 +787,7 @@ export function createSessionOpener(services) {
775
787
  }, inputs.boundarySize);
776
788
  const hasRestoredUserImages = messages.some((message) => message.role === 'user' && Boolean(message.images?.length));
777
789
  let modelInputImages;
790
+ const inputImageBytes = new Map();
778
791
  const currentExecutionToolImages = [];
779
792
  let currentExecutionUserIndex;
780
793
  await emit({ type: 'execution.started', mode, permissionMode: permissionPolicy.mode });
@@ -791,6 +804,18 @@ export function createSessionOpener(services) {
791
804
  });
792
805
  selectedAdapter = sessionAdapter(input.modelId ?? executionDefaultModelId, executionModels);
793
806
  const selectedModel = executionModels.get(input.modelId ?? executionDefaultModelId);
807
+ const restoredMessageCount = messages.length;
808
+ const resourceInput = new ResourceInputProjection(selectedModel.useResourceUrl === true, store, sessionId, messages, {
809
+ records: contextRecords,
810
+ persist: async (state) => {
811
+ await withRecordProjection(() => store.append(sessionId, {
812
+ type: 'resource.window',
813
+ payload: state,
814
+ executionId,
815
+ turnId
816
+ }));
817
+ }
818
+ });
794
819
  const selected = modelResources.openTurn(selectedAdapter, services.hasCustomAdapters && selectedAdapter.createTurnSession === undefined);
795
820
  let lastServerUsage = latestModelUsage(contextRecords, selectedModel.id);
796
821
  let smartContextTokens = latestSmartContextTokens(contextRecords, selectedModel.id, selectedModel.contextWindowTokens);
@@ -898,9 +923,7 @@ export function createSessionOpener(services) {
898
923
  ?.filter((binding) => !executionSourceNames.has(binding.definition.name)) ?? [];
899
924
  const sources = (executionToolSources = new ExecutionToolSources(executionId, sessionId, combinedTools, builtinTools, mcpCatalog, supplementalBindings));
900
925
  const sourceBindings = sources.bindings;
901
- const coreExtensions = await services.extensions.prepare(sessionId, controller.signal, sourceBindings, sessionSource.type === 'user' &&
902
- (mode === 'run' || mode === 'plan') &&
903
- !internal.automaticInput);
926
+ const coreExtensions = await services.extensions.prepare(sessionId, controller.signal, sourceBindings, sessionSource.type === 'user' && !internal.automaticInput);
904
927
  executionContext.toolSources = sources;
905
928
  const operation = (run) => services.extensions.operations.run({
906
929
  sessionId,
@@ -928,9 +951,11 @@ export function createSessionOpener(services) {
928
951
  const transformModelRequest = async (candidate, bindings = captureTools(), modelId = selectedModel.id, purpose = mode === 'compact' ? 'compact' : 'conversation') => {
929
952
  let lowered = candidate;
930
953
  if (coreExtensions.hooks.hasInterceptors('model.request')) {
931
- const bridge = new ExtensionRequestBridge(modelId, candidate);
954
+ const bridge = new ExtensionRequestBridge(modelId, candidate, undefined, selected.continuationInputCount?.() ?? 0);
932
955
  const request = await operation((call) => coreExtensions.hooks.invoke('model.request', bridge.view, call, async (view) => view));
933
956
  lowered = bridge.lower(request);
957
+ if (bridge.rewroteCommittedInput)
958
+ selected.resetContinuation?.();
934
959
  }
935
960
  for (const definition of lowered.tools) {
936
961
  const binding = bindings.get(definition.name);
@@ -1068,6 +1093,7 @@ export function createSessionOpener(services) {
1068
1093
  : subagents.captureParentFrame(executionId);
1069
1094
  const roundSmartInput = smartInput;
1070
1095
  const toolContext = {
1096
+ imageStore: store,
1071
1097
  host: host,
1072
1098
  requestSmartCompact: () => {
1073
1099
  controller.signal.throwIfAborted();
@@ -1117,7 +1143,20 @@ export function createSessionOpener(services) {
1117
1143
  return sources.withFrame(context, () => parentFrame === undefined ? invoke() : parentFrame(invoke));
1118
1144
  }
1119
1145
  };
1120
- const recentImages = () => currentExecutionImages(modelInputImages, currentExecutionToolImages);
1146
+ const recentImages = async () => {
1147
+ const inputImages = await Promise.all((modelInputImages ?? []).map(async (image) => {
1148
+ if (image.dataBase64 !== undefined || image.resourceId === undefined)
1149
+ return image;
1150
+ const cached = inputImageBytes.get(image.resourceId);
1151
+ if (cached !== undefined)
1152
+ return cached;
1153
+ const source = await store.readResource(sessionId, image.resourceId);
1154
+ const restored = { ...image, ...source };
1155
+ inputImageBytes.set(image.resourceId, restored);
1156
+ return restored;
1157
+ }));
1158
+ return currentExecutionImages(inputImages, currentExecutionToolImages);
1159
+ };
1121
1160
  const toolScope = {
1122
1161
  context: toolContext,
1123
1162
  tools: roundTools,
@@ -1141,8 +1180,7 @@ export function createSessionOpener(services) {
1141
1180
  recentImages,
1142
1181
  captureCodeContext: () => ({
1143
1182
  ...toolContext,
1144
- waitInterrupt: undefined,
1145
- recentImages: recentImages()
1183
+ waitInterrupt: undefined
1146
1184
  }),
1147
1185
  codeModeContext: async (call) => {
1148
1186
  const stateSnapshot = call.name === 'code_exec' ? await store.readCodeModeState(sessionId) : [];
@@ -1214,6 +1252,9 @@ export function createSessionOpener(services) {
1214
1252
  sources.bind(captureRoundToolScope().toolScope);
1215
1253
  let preparingRequestMessages;
1216
1254
  let preparingRequestBaseLength = 0;
1255
+ let dispatchedInputCount = 0;
1256
+ const completedMediaRequests = new WeakSet();
1257
+ let dispatchedLocalCount = 0;
1217
1258
  executionContext.requestMessages = {
1218
1259
  read: () => preparingRequestMessages !== undefined
1219
1260
  ? normalizeMessagesForModel([...preparingRequestMessages, ...messages.slice(preparingRequestBaseLength)], { supportsImages: selectedModel.inputCapabilities.includes('IMAGE') })
@@ -1245,14 +1286,12 @@ export function createSessionOpener(services) {
1245
1286
  ...(input.clientMessageId ? { clientMessageId: input.clientMessageId } : {})
1246
1287
  }, call, (actual, recordId, metadata) => {
1247
1288
  entered = true;
1248
- modelInputImages = actual.images?.map(({ mimeType, dataBase64 }) => ({
1249
- mimeType,
1250
- dataBase64
1251
- }));
1289
+ modelInputImages = actual.images?.map((image) => ({ ...image }));
1252
1290
  messages.push({
1253
1291
  role: 'user',
1254
1292
  content: actual.modelText ?? actual.text,
1255
- metadata
1293
+ metadata,
1294
+ ...(modelInputImages?.length ? { images: modelInputImages } : {})
1256
1295
  });
1257
1296
  currentExecutionUserIndex = messages.length - 1;
1258
1297
  internal.inputCommitted?.(recordId);
@@ -1288,8 +1327,12 @@ export function createSessionOpener(services) {
1288
1327
  call.signal.throwIfAborted();
1289
1328
  if (content.images?.length && !selectedModel.inputCapabilities.includes('IMAGE'))
1290
1329
  throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Selected model does not support image input.');
1291
- const images = content.images?.length
1292
- ? await store.persistInputImages(sessionId, content.images)
1330
+ const modelImages = await publishModelResources(host, content.images, {
1331
+ sessionId,
1332
+ executionId
1333
+ }, store);
1334
+ const images = modelImages?.length
1335
+ ? await store.persistInputImages(sessionId, modelImages)
1293
1336
  : [];
1294
1337
  const record = await store.append(sessionId, {
1295
1338
  type: 'turn.user',
@@ -1401,7 +1444,12 @@ export function createSessionOpener(services) {
1401
1444
  turnId
1402
1445
  });
1403
1446
  let lastServerContextTokens = lastServerUsage?.contextInputTokens;
1404
- let compactBoundary = messages.length;
1447
+ let compactBoundary = restoredMessageCount;
1448
+ const advanceCompactBoundary = () => {
1449
+ // Keep a new image with its complete tool round until a model accepts it.
1450
+ if (!resourceInput.hasPendingImages(messages.slice(compactBoundary)))
1451
+ compactBoundary = messages.length;
1452
+ };
1405
1453
  const invalidateCompactUsage = () => {
1406
1454
  lastServerContextTokens = undefined;
1407
1455
  lastServerUsage = undefined;
@@ -1421,7 +1469,7 @@ export function createSessionOpener(services) {
1421
1469
  message.contextKind === undefined ||
1422
1470
  message.contextKind === 'summary'))
1423
1471
  return false;
1424
- const compactHistory = structuredClone(prefix);
1472
+ const compactHistory = structuredClone(prefix).map(({ images: _images, ...message }) => message);
1425
1473
  const prefixInputs = modelContext.compactInputs(prefix);
1426
1474
  const snapshotTools = structuredClone(projectTools(executionTools));
1427
1475
  const generation = modelContext.worldState.generation;
@@ -1503,6 +1551,7 @@ export function createSessionOpener(services) {
1503
1551
  const record = await store.compact(sessionId, {
1504
1552
  summary: actual,
1505
1553
  replacementHistory,
1554
+ inlineMediaState: resourceInput.checkpoint(retainedModelContext),
1506
1555
  retainedModelContext: retainedModelContext.map((message) => {
1507
1556
  const source = messageSources.get(message);
1508
1557
  const saved = source?.type === 'model.context' &&
@@ -1516,7 +1565,14 @@ export function createSessionOpener(services) {
1516
1565
  ...saved,
1517
1566
  ...(message.metadata ? { metadata: message.metadata } : {})
1518
1567
  }
1519
- : message;
1568
+ : {
1569
+ ...message,
1570
+ ...(message.images?.length
1571
+ ? {
1572
+ images: message.images.map(({ dataBase64: _bytes, ...image }) => image)
1573
+ }
1574
+ : {})
1575
+ };
1520
1576
  }),
1521
1577
  pendingInputs: [...pendingInputs.values()],
1522
1578
  snapshotMessageCount: prefix.length,
@@ -1530,6 +1586,7 @@ export function createSessionOpener(services) {
1530
1586
  turnId
1531
1587
  }, options);
1532
1588
  modelContext.replaceCompactMessages(replacementHistory, retainedModelContext);
1589
+ compactBoundary = messages.length - retainedModelContext.length;
1533
1590
  return record;
1534
1591
  });
1535
1592
  await modelContext.adoptCompactCheckpoint(compactRecord.recordId, replacementHistory, retainedModelContext, compactResources);
@@ -1619,7 +1676,7 @@ export function createSessionOpener(services) {
1619
1676
  controller.signal.throwIfAborted();
1620
1677
  if (!hadPending)
1621
1678
  throw error;
1622
- compactBoundary = messages.length;
1679
+ advanceCompactBoundary();
1623
1680
  compacted = await compactContext();
1624
1681
  }
1625
1682
  if (!compacted)
@@ -1680,7 +1737,7 @@ export function createSessionOpener(services) {
1680
1737
  updateMcpRegistry();
1681
1738
  executionTools = mcpRegistry.project(baseTools());
1682
1739
  await modelContext.syncMcpWorldState(messages.length);
1683
- compactBoundary = messages.length;
1740
+ advanceCompactBoundary();
1684
1741
  const blocking = compactRequests.drain((input) => startCompact({
1685
1742
  ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1686
1743
  async: input.async === true
@@ -1737,24 +1794,61 @@ export function createSessionOpener(services) {
1737
1794
  // Never sample the candidate assembled against the replaced context.
1738
1795
  return prepareRound(pendingOutputContinuation, true);
1739
1796
  }
1740
- return { request, toolScope };
1797
+ resourceInput.observe(candidateRequest.messages);
1798
+ const media = sessionSource.type === 'subagent'
1799
+ ? undefined
1800
+ : await resourceInput.gcCandidate(request, candidateRequest.images);
1801
+ if (media?.reason === 'HARD_LIMIT') {
1802
+ const appendBoundary = pendingAppends.length;
1803
+ const committed = await contextGc.run({
1804
+ inlineMedia: media,
1805
+ mediaView: (messages) => resourceInput.historyView(messages),
1806
+ store,
1807
+ sessionId,
1808
+ system: request.system,
1809
+ tools: toolScope.tools,
1810
+ modelId: selectedModel.id,
1811
+ contextWindowTokens: selectedModel.contextWindowTokens,
1812
+ usage: lastServerUsage ?? {},
1813
+ ...modelConfiguration.contextProjection,
1814
+ workspace: logicalWorkspace,
1815
+ platform: description.platform
1816
+ });
1817
+ if (committed) {
1818
+ await modelContext.adoptGcCheckpoint(committed, appendBoundary);
1819
+ return prepareRound(pendingOutputContinuation, true);
1820
+ }
1821
+ }
1822
+ return {
1823
+ request: await resourceInput.prepare(request, candidateRequest.images, candidateRequest.messages),
1824
+ toolScope
1825
+ };
1741
1826
  },
1742
1827
  startModel: async function* (request) {
1743
1828
  const snapshots = (sessionRuntime.requestSnapshots ??=
1744
1829
  new ExtensionRequestSnapshots());
1745
1830
  const snapshot = snapshots.record(selectedModel.id, request);
1831
+ dispatchedInputCount = request.messages.length;
1832
+ dispatchedLocalCount = messages.length;
1746
1833
  yield* streamModel(async function* (nativeSignal) {
1747
1834
  for await (const event of selected.start(request, nativeSignal)) {
1748
- if (snapshot && event.type === 'completed' && event.reason !== 'cancelled')
1749
- snapshots.complete(snapshot);
1835
+ if (event.type === 'completed' && event.reason !== 'cancelled') {
1836
+ completedMediaRequests.add(request.messages);
1837
+ if (snapshot)
1838
+ snapshots.complete(snapshot);
1839
+ }
1750
1840
  yield event;
1751
1841
  }
1752
1842
  }, controller.signal);
1753
1843
  },
1754
1844
  recoverySignal: () => controller.signal,
1755
1845
  compactContext,
1756
- afterModel: async (closedPrefixLength) => {
1757
- compactBoundary = closedPrefixLength;
1846
+ afterModel: async (closedPrefixLength, acceptedRequest) => {
1847
+ selected.commitContinuationInput?.(dispatchedInputCount + messages.length - dispatchedLocalCount);
1848
+ if (acceptedRequest && completedMediaRequests.has(acceptedRequest.messages)) {
1849
+ await resourceInput.complete(acceptedRequest);
1850
+ compactBoundary = closedPrefixLength;
1851
+ }
1758
1852
  if (lastServerContextTokens !== undefined &&
1759
1853
  (lastServerContextTokens * 100) / selectedModel.contextWindowTokens >=
1760
1854
  AGENT_EXECUTION_POLICY.backgroundCompactionPercent &&
@@ -1773,7 +1867,7 @@ export function createSessionOpener(services) {
1773
1867
  }
1774
1868
  },
1775
1869
  checkpoint: async (round, latestUsage) => {
1776
- compactBoundary = messages.length;
1870
+ advanceCompactBoundary();
1777
1871
  const blocking = compactRequests.drain((input) => startCompact({
1778
1872
  ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1779
1873
  async: input.async === true
@@ -1784,19 +1878,19 @@ export function createSessionOpener(services) {
1784
1878
  await compactCoordinator.checkpoint(false).catch(() => {
1785
1879
  controller.signal.throwIfAborted();
1786
1880
  });
1787
- if (!compactCoordinator.pending &&
1788
- sessionSource.type !== 'subagent' &&
1789
- latestUsage !== undefined) {
1881
+ if (!compactCoordinator.pending && sessionSource.type !== 'subagent') {
1790
1882
  // Only a committed GC checkpoint may replace active context and invalidate continuation.
1791
1883
  const appendBoundary = pendingAppends.length;
1792
1884
  const committed = await contextGc.run({
1885
+ inlineMedia: await resourceInput.gcCandidate(),
1886
+ mediaView: (messages) => resourceInput.historyView(messages),
1793
1887
  store,
1794
1888
  sessionId,
1795
1889
  system: round.request.system,
1796
1890
  tools: round.toolScope.tools,
1797
1891
  modelId: selectedModel.id,
1798
1892
  contextWindowTokens: selectedModel.contextWindowTokens,
1799
- usage: latestUsage,
1893
+ usage: latestUsage ?? {},
1800
1894
  ...modelConfiguration.contextProjection,
1801
1895
  workspace: logicalWorkspace,
1802
1896
  platform: description.platform,
@@ -2052,8 +2146,19 @@ export function createSessionOpener(services) {
2052
2146
  call.signal.throwIfAborted();
2053
2147
  const targetRuntime = sessionRuntimes.get(target);
2054
2148
  const requestMessages = targetRuntime?.execution?.requestMessages;
2055
- if (selection.view === 'request' && requestMessages)
2056
- return sessionMessagesPage(requestMessages.read(), selection);
2149
+ if (selection.view === 'request' && requestMessages) {
2150
+ const messages = requestMessages.read();
2151
+ const restored = selection.includeImages
2152
+ ? await Promise.all(messages.map(async (message) => message.images?.length
2153
+ ? {
2154
+ ...message,
2155
+ images: await readResourceImages(store, target, message.images)
2156
+ }
2157
+ : message))
2158
+ : messages;
2159
+ call.signal.throwIfAborted();
2160
+ return sessionMessagesPage(restored, selection);
2161
+ }
2057
2162
  const modelState = services.modelState();
2058
2163
  const requestModel = selection.view === 'request'
2059
2164
  ? modelState.models.get(targetModelId ?? modelState.defaultModelId)
@@ -72,6 +72,10 @@ export interface MarAgentSessionCatalog {
72
72
  readonly sequence: number;
73
73
  readonly imageIndex: number;
74
74
  }): Promise<MarAgentSessionImage | undefined>;
75
+ readResource(sessionId: string, resourceId: string): Promise<{
76
+ mimeType: string;
77
+ dataBase64: string;
78
+ } | undefined>;
75
79
  contextUsage(sessionId: string): Promise<MarAgentContextUsage | undefined>;
76
80
  readToolResultResource(sessionId: string, input: import('./tool-result-resources.js').ToolResultResourceInput): Promise<import('./tool-result-resources.js').ToolResultResourcePage>;
77
81
  fork(sessionId: string, input: {
@@ -103,6 +103,10 @@ export async function openMarAgentSessionCatalog(input) {
103
103
  dataBase64: image.dataBase64
104
104
  };
105
105
  },
106
+ readResource: async (sessionId, resourceId) => {
107
+ assertOpen();
108
+ return store.readResource(sessionId, resourceId);
109
+ },
106
110
  readToolResultResource: async (sessionId, input) => {
107
111
  assertOpen();
108
112
  return store.readToolResultResource(sessionId, input);
@@ -1,3 +1,4 @@
1
+ import { type InlineMediaGcCheckpoint } from './inline-media.js';
1
2
  import { type ContextGcWebToolName } from './context-gc-replacements.js';
2
3
  import { type ContextGcSubagentToolName } from './context-gc-subagent.js';
3
4
  import type { SessionRecord } from './store/index.js';
@@ -93,7 +94,7 @@ export type ContextGcReplacement = {
93
94
  readonly evidenceSequence: number;
94
95
  };
95
96
  export interface ContextGcMetrics {
96
- readonly stage: 'STAGE_A' | 'STAGE_A_AND_EXEC';
97
+ readonly stage: 'STAGE_A' | 'STAGE_A_AND_EXEC' | 'INLINE_MEDIA';
97
98
  readonly tokenEstimator?: 'UTF8_BYTES_V1';
98
99
  readonly beforeTokens: number;
99
100
  readonly afterTokens: number;
@@ -105,8 +106,8 @@ export interface ContextGcMetrics {
105
106
  readonly compactDeferralCreditCostUnits: number;
106
107
  readonly imminentCompactAvoidanceCreditCostUnits?: number;
107
108
  readonly appliedCompactCreditCostUnits?: number;
108
- readonly requiredPositiveNetRollouts: number;
109
- readonly requiredTargetRoiRollouts: number;
109
+ readonly requiredPositiveNetRollouts: number | null;
110
+ readonly requiredTargetRoiRollouts: number | null;
110
111
  readonly projectedRolloutsUntilCompactAfterGc: number;
111
112
  readonly projectedTokensBeforeGc?: number;
112
113
  readonly projectedTokensAfterGc?: number;
@@ -121,12 +122,13 @@ export interface ContextGcMetrics {
121
122
  readonly cachePenaltyKnown?: boolean;
122
123
  }
123
124
  export interface ContextGcCheckpointDraft {
125
+ readonly inlineMedia?: InlineMediaGcCheckpoint;
124
126
  readonly version: 1;
125
127
  readonly base: ContextGcBase;
126
128
  readonly boundarySequence: number;
127
129
  readonly replacements: readonly ContextGcReplacement[];
128
130
  readonly trigger: {
129
- readonly usageSequence: number;
131
+ readonly usageSequence: number | null;
130
132
  readonly contextInputTokens: number;
131
133
  readonly contextWindowTokens: number;
132
134
  readonly normalRolloutOrdinal: number;
@@ -1,5 +1,6 @@
1
1
  import { posix, win32 } from 'node:path';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
+ import { inlineMediaGcAnchored, parseInlineMediaGc } from './inline-media.js';
3
4
  import { codeCallEvidence, codePartialPatchCallEvidence, callsFromRecord, codeObservationEvidence, observationsFromRecord, retainedCodeInputs } from './context-gc-observations.js';
4
5
  import { parseReadContextOutput } from '../tools/local/read-context.js';
5
6
  import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcWebOutputReplacement, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from './context-gc-replacements.js';
@@ -120,10 +121,11 @@ export function parseContextGcCheckpoint(record) {
120
121
  !isContextGcBase(payload.base) ||
121
122
  !isPositiveInteger(payload.boundarySequence) ||
122
123
  !Array.isArray(payload.replacements) ||
123
- payload.replacements.length === 0 ||
124
+ (payload.replacements.length === 0 && payload.inlineMedia === undefined) ||
124
125
  !isContextGcTrigger(payload.trigger) ||
125
126
  !isContextGcEconomicsPolicy(payload.economics) ||
126
- !isContextGcMetrics(payload.metrics))
127
+ !isContextGcMetrics(payload.metrics) ||
128
+ (payload.inlineMedia !== undefined && !parseInlineMediaGc(payload.inlineMedia)))
127
129
  return undefined;
128
130
  const replacements = normalizeContextGcReplacements(payload.replacements);
129
131
  if (!replacements || replacements.length !== payload.replacements.length)
@@ -137,7 +139,10 @@ export function parseContextGcCheckpoint(record) {
137
139
  replacements,
138
140
  trigger: structuredClone(payload.trigger),
139
141
  economics: structuredClone(payload.economics),
140
- metrics: structuredClone(payload.metrics)
142
+ metrics: structuredClone(payload.metrics),
143
+ ...(payload.inlineMedia === undefined
144
+ ? {}
145
+ : { inlineMedia: parseInlineMediaGc(payload.inlineMedia) })
141
146
  };
142
147
  }
143
148
  export function validateContextGcCheckpoint(records, record, workspace = sessionWorkspace(records), committedReplacements = []) {
@@ -150,9 +155,14 @@ export function validateContextGcCheckpoint(records, record, workspace = session
150
155
  const baseSequence = contextGcBaseSequence(payload.base);
151
156
  // Later cell completion cannot retroactively authorize an earlier checkpoint.
152
157
  records = records.filter((item) => item.sequence <= payload.boundarySequence);
158
+ if (payload.inlineMedia && !inlineMediaGcAnchored(records, payload.inlineMedia))
159
+ return undefined;
153
160
  const recordsBySequence = new Map(records.map((item) => [item.sequence, item]));
154
161
  if (!triggerMatchesRecords(records, recordsBySequence, payload))
155
162
  return undefined;
163
+ if (payload.trigger.usageSequence === null &&
164
+ !isDeepStrictEqual(payload.replacements, committedReplacements))
165
+ return undefined;
156
166
  const committed = new Map(committedReplacements.map((replacement) => [contextGcReplacementKey(replacement), replacement]));
157
167
  const changed = payload.replacements.flatMap((replacement) => {
158
168
  const previous = committed.get(contextGcReplacementKey(replacement));
@@ -603,14 +613,25 @@ function isSuccessfulExecLifecycleEvidence(replacement, source, records, records
603
613
  targetResult.sequence <= evidence.result.sequence);
604
614
  }
605
615
  function triggerMatchesRecords(records, recordsBySequence, payload) {
606
- if (payload.trigger.usageSequence < contextGcBaseSequence(payload.base) ||
607
- payload.trigger.usageSequence > payload.boundarySequence)
616
+ if (payload.trigger.usageSequence !== null &&
617
+ (payload.trigger.usageSequence < contextGcBaseSequence(payload.base) ||
618
+ payload.trigger.usageSequence > payload.boundarySequence))
608
619
  return false;
609
- const usageRecord = recordsBySequence.get(payload.trigger.usageSequence);
610
- if (!usageRecord ||
611
- !isRecord(usageRecord.payload) ||
612
- usageRecord.payload.type !== 'usage.updated' ||
613
- modelContextUsage(usageRecord.payload) !== payload.trigger.contextInputTokens)
620
+ const withoutUsage = payload.trigger.usageSequence === null;
621
+ if (withoutUsage &&
622
+ (!payload.inlineMedia ||
623
+ payload.inlineMedia.reason === 'TEXT_GC' ||
624
+ payload.metrics.stage !== 'INLINE_MEDIA' ||
625
+ payload.trigger.contextInputTokens !== 0))
626
+ return false;
627
+ const usageRecord = withoutUsage
628
+ ? undefined
629
+ : recordsBySequence.get(payload.trigger.usageSequence);
630
+ if (!withoutUsage &&
631
+ (!usageRecord ||
632
+ !isRecord(usageRecord.payload) ||
633
+ usageRecord.payload.type !== 'usage.updated' ||
634
+ modelContextUsage(usageRecord.payload) !== payload.trigger.contextInputTokens))
614
635
  return false;
615
636
  const baseSequence = contextGcBaseSequence(payload.base);
616
637
  let modelId;
@@ -632,7 +653,7 @@ function triggerMatchesRecords(records, recordsBySequence, payload) {
632
653
  }
633
654
  const matchingConfiguration = modelId === payload.trigger.modelId &&
634
655
  contextWindowTokens === payload.trigger.contextWindowTokens;
635
- if (record.sequence === usageRecord.sequence)
656
+ if (record.sequence === usageRecord?.sequence || withoutUsage)
636
657
  triggerConfigurationMatches = matchingConfiguration;
637
658
  if (matchingConfiguration &&
638
659
  isRecord(record.payload) &&
@@ -647,7 +668,9 @@ function triggerMatchesRecords(records, recordsBySequence, payload) {
647
668
  normalRolloutOrdinal++;
648
669
  }
649
670
  return (triggerConfigurationMatches &&
650
- latestMatchingUsageSequence === payload.trigger.usageSequence &&
671
+ (withoutUsage
672
+ ? latestMatchingUsageSequence === undefined
673
+ : latestMatchingUsageSequence === payload.trigger.usageSequence) &&
651
674
  normalRolloutOrdinal === payload.trigger.normalRolloutOrdinal);
652
675
  }
653
676
  function modelContextUsage(value) {
@@ -822,7 +845,7 @@ function isContextGcBase(value) {
822
845
  }
823
846
  function isContextGcTrigger(value) {
824
847
  return (isRecord(value) &&
825
- isPositiveInteger(value.usageSequence) &&
848
+ (value.usageSequence === null || isPositiveInteger(value.usageSequence)) &&
826
849
  isNonnegativeInteger(value.contextInputTokens) &&
827
850
  isPositiveInteger(value.contextWindowTokens) &&
828
851
  isNonnegativeInteger(value.normalRolloutOrdinal) &&
@@ -851,7 +874,10 @@ function isContextGcEconomicsPolicy(value) {
851
874
  ].every((key) => isNonnegativeFinite(value[key])));
852
875
  }
853
876
  function isContextGcMetrics(value) {
854
- if (!isRecord(value) || (value.stage !== 'STAGE_A' && value.stage !== 'STAGE_A_AND_EXEC'))
877
+ if (!isRecord(value) ||
878
+ (value.stage !== 'STAGE_A' &&
879
+ value.stage !== 'STAGE_A_AND_EXEC' &&
880
+ value.stage !== 'INLINE_MEDIA'))
855
881
  return false;
856
882
  const nonnegative = [
857
883
  'beforeTokens',
@@ -860,8 +886,6 @@ function isContextGcMetrics(value) {
860
886
  'grossReductionRate',
861
887
  'cacheRebuildPenaltyCostUnits',
862
888
  'compactDeferralCreditCostUnits',
863
- 'requiredPositiveNetRollouts',
864
- 'requiredTargetRoiRollouts',
865
889
  'projectedRolloutsUntilCompactAfterGc',
866
890
  'contextGrowthSampleCount',
867
891
  'contextGrowthP75Tokens',
@@ -879,6 +903,7 @@ function isContextGcMetrics(value) {
879
903
  'projectedTokensAfterGc'
880
904
  ];
881
905
  return (nonnegative.every((key) => isNonnegativeFinite(value[key])) &&
906
+ ['requiredPositiveNetRollouts', 'requiredTargetRoiRollouts'].every((key) => isNonnegativeFinite(value[key]) || (value.stage === 'INLINE_MEDIA' && value[key] === null)) &&
882
907
  optionalNonnegative.every((key) => value[key] === undefined || isNonnegativeFinite(value[key])) &&
883
908
  Number.isFinite(value.expectedNetSavedCostUnits) &&
884
909
  Number.isFinite(value.cacheInvestmentRoi) &&
@@ -0,0 +1,45 @@
1
+ import type { ModelMessage, ModelRequest } from '../model/contracts.js';
2
+ import type { ModelImage } from '../model/image.js';
3
+ import type { SessionRecord } from './store/types.js';
4
+ export declare const INLINE_MEDIA_POLICY: Readonly<{
5
+ historicalInlineMediaTrigger: number;
6
+ historicalInlineMediaTarget: number;
7
+ requestInlineMediaHardLimit: number;
8
+ }>;
9
+ export interface InlineMediaCursor {
10
+ sourceId: string;
11
+ imageIndex: number;
12
+ }
13
+ /** Processing progress, not evidence that the provider inspected an image. */
14
+ export interface InlineMediaState {
15
+ version: 1;
16
+ consideredThrough?: InlineMediaCursor;
17
+ evictedThrough?: InlineMediaCursor;
18
+ }
19
+ export interface InlineMediaGcCheckpoint {
20
+ readonly reason: 'SOFT_LIMIT' | 'HARD_LIMIT' | 'TEXT_GC';
21
+ readonly state: InlineMediaState;
22
+ readonly beforeBytes: number;
23
+ readonly afterBytes: number;
24
+ }
25
+ export interface InlineMediaGcCandidate extends InlineMediaGcCheckpoint {
26
+ /** Apply the proposed eviction to the current visual-window projection. */
27
+ readonly project: (messages: readonly ModelMessage[]) => ModelMessage[];
28
+ }
29
+ export declare function parseInlineMediaGc(value: unknown): InlineMediaGcCheckpoint | undefined;
30
+ export declare function inlineMediaSource(message: ModelMessage): string | ModelMessage;
31
+ /** GC may advance only a durable, already-considered source prefix. */
32
+ export declare function inlineMediaGcAnchored(records: readonly SessionRecord[], media: InlineMediaGcCheckpoint): boolean;
33
+ export declare function inlineMediaBytes(image: ModelImage): number;
34
+ export declare function inlineMediaLimit(): never;
35
+ /** Final encoding guard; URL-only images do not consume the inline budget. */
36
+ export declare function assertInlineMediaBudget(request: Pick<ModelRequest, 'messages' | 'images'>): void;
37
+ export declare function parseInlineMediaState(value: unknown): InlineMediaState | undefined;
38
+ /** Recover ordering once at the lifecycle boundary, including GC-omitted sources. */
39
+ export declare function inlineMediaHistory(records: readonly SessionRecord[]): {
40
+ cursors: (InlineMediaCursor & {
41
+ rollout: number;
42
+ })[];
43
+ rollouts: number;
44
+ state?: InlineMediaState;
45
+ };