@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,9 +1,11 @@
1
1
  import { observeModelRequestUsage } from './usage-observer.js';
2
2
  import { observationModelParts } from '../tools/code-mode/observations.js';
3
+ import { modelImageKey, modelImageUrl } from './image.js';
3
4
  import { stableToolSchema } from '../tools/definition-projection.js';
4
5
  import { isDeepStrictEqual } from 'node:util';
5
6
  import { createHash, randomUUID } from 'node:crypto';
6
7
  import { describeMarAgentError, MarAgentError } from '../error.js';
8
+ import { assertInlineMediaBudget } from '../session/inline-media.js';
7
9
  import { marAgentReasoningEffortSchema, resolveModelDefaultReasoningEffort, usesResponsesConfigurationUpdates } from './configuration.js';
8
10
  import WebSocket from 'ws';
9
11
  import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observeModelStreamEvent, modelStreamEventObservedAt } from './sse.js';
@@ -95,7 +97,7 @@ export class OpenAiResponsesAdapter {
95
97
  const attemptTransport = this.configuration.responsesTransport?.transport === 'WEBSOCKET' ? 'WEBSOCKET' : 'HTTP';
96
98
  const requestPrefixDiagnostic = request.onAttemptDiagnostic
97
99
  ? {
98
- requestInputItems: prepared.fullBody.input.length
100
+ requestInputItems: prepared.inputItemCount
99
101
  }
100
102
  : {};
101
103
  const prelude = [];
@@ -667,7 +669,6 @@ class OpenAiResponsesTurnSession {
667
669
  #active = false;
668
670
  #closed = false;
669
671
  #turnState;
670
- #fullBody;
671
672
  #completedResponseId;
672
673
  #connectionDiagnostic;
673
674
  #webSocketDiagnostic;
@@ -686,11 +687,15 @@ class OpenAiResponsesTurnSession {
686
687
  const outputItems = [];
687
688
  let completed = false;
688
689
  let sawUnrepresentedOutput = false;
690
+ let outputMessageCount = 0;
689
691
  try {
690
692
  const prepared = this.prepareRequest(request);
691
- this.#fullBody = prepared.fullBody;
692
693
  this.#completedResponseId = undefined;
693
694
  for await (const event of this.adapter.startTurn(request, signal, this, prepared)) {
695
+ if (event.type === 'response.item' ||
696
+ event.type === 'text.completed' ||
697
+ event.type === 'client_tool.call')
698
+ outputMessageCount++;
694
699
  if (event.type === 'response.started' && event.responseId !== 'unknown')
695
700
  responseId = event.responseId;
696
701
  else if (event.type === 'response.item')
@@ -713,7 +718,13 @@ class OpenAiResponsesTurnSession {
713
718
  completed &&
714
719
  responseId !== undefined &&
715
720
  (outputItems.length > 0 || !sawUnrepresentedOutput)) {
716
- this.state.lastRequest = prepared.fullBody;
721
+ this.state.lastRequest =
722
+ this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET'
723
+ ? { ...prepared.body, input: [] }
724
+ : prepared.fullBody;
725
+ this.state.lastMessageCount = request.messages.length + outputMessageCount;
726
+ this.state.lastInputItemCount = prepared.inputItemCount;
727
+ this.state.lastSystem = JSON.stringify([request.system, request.systemBlocks]);
717
728
  this.state.lastResponse = { responseId, outputItems };
718
729
  }
719
730
  else
@@ -725,7 +736,6 @@ class OpenAiResponsesTurnSession {
725
736
  }
726
737
  finally {
727
738
  this.#completedResponseId = undefined;
728
- this.#fullBody = undefined;
729
739
  this.#active = false;
730
740
  }
731
741
  }
@@ -738,11 +748,19 @@ class OpenAiResponsesTurnSession {
738
748
  resetContinuation() {
739
749
  clearResponsesContinuation(this.state);
740
750
  }
751
+ continuationInputCount() {
752
+ return this.state.lastMessageCount ?? 0;
753
+ }
754
+ commitContinuationInput(messageCount) {
755
+ if (this.state.lastResponse !== undefined)
756
+ this.state.lastMessageCount = messageCount;
757
+ }
741
758
  captureCompletedResponseId(responseId) {
742
759
  if (responseId !== undefined)
743
760
  this.#completedResponseId = responseId;
744
761
  }
745
762
  prepareRequest(request) {
763
+ assertInlineMediaBudget(request);
746
764
  if (this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET' &&
747
765
  this.state.socket !== undefined &&
748
766
  this.state.socket.readyState !== WebSocket.OPEN) {
@@ -750,16 +768,44 @@ class OpenAiResponsesTurnSession {
750
768
  clearResponsesContinuation(this.state);
751
769
  }
752
770
  // The continuation baseline must not share mutable schemas or items with callers.
753
- const fullBody = JSON.parse(JSON.stringify(responsesRequestBody(this.adapter.configuration, request, request.messages)));
771
+ const snapshot = (body) => JSON.parse(JSON.stringify(body));
772
+ let full;
773
+ const fullBody = () => (full ??= snapshot(responsesRequestBody(this.adapter.configuration, request, request.messages)));
754
774
  if (responsesContinuationEnabled(this.adapter.configuration)) {
755
- const incremental = incrementalResponsesInput(this.state, fullBody);
775
+ const websocket = this.adapter.configuration.responsesTransport?.transport === 'WEBSOCKET';
776
+ const suffixMessages = websocket && this.state.lastMessageCount !== undefined
777
+ ? request.messages.slice(this.state.lastMessageCount)
778
+ : undefined;
779
+ const suffixBody = websocket &&
780
+ this.state.lastResponse !== undefined &&
781
+ suffixMessages !== undefined &&
782
+ this.state.lastRequest !== undefined &&
783
+ this.state.lastSystem === JSON.stringify([request.system, request.systemBlocks]) &&
784
+ // Top-level images attach to the last real user, which must be in this suffix.
785
+ (!request.images?.length || suffixMessages.some(isRealUserMessage))
786
+ ? snapshot(responsesRequestBody(this.adapter.configuration, { ...request, system: '', systemBlocks: [] }, suffixMessages, this.state.lastResponse.outputItems))
787
+ : undefined;
788
+ const incremental = websocket
789
+ ? suffixBody !== undefined &&
790
+ responsesContinuationPropertiesMatch(this.state.lastRequest, suffixBody)
791
+ ? suffixBody.input
792
+ : undefined
793
+ : incrementalResponsesInput(this.state, fullBody());
756
794
  if (incremental !== undefined) {
757
795
  const responseId = this.state.lastResponse.responseId;
796
+ const inputItemCount = websocket
797
+ ? (this.state.lastInputItemCount ?? 0) +
798
+ this.state.lastResponse.outputItems.length +
799
+ incremental.length
800
+ : fullBody().input.length;
758
801
  delete this.state.lastResponse;
759
802
  return {
760
- fullBody,
803
+ get fullBody() {
804
+ return fullBody();
805
+ },
806
+ inputItemCount,
761
807
  body: {
762
- ...fullBody,
808
+ ...(suffixBody ?? fullBody()),
763
809
  input: incremental,
764
810
  previous_response_id: responseId
765
811
  },
@@ -768,16 +814,12 @@ class OpenAiResponsesTurnSession {
768
814
  }
769
815
  }
770
816
  return {
771
- fullBody,
772
- body: fullBody,
817
+ fullBody: fullBody(),
818
+ inputItemCount: fullBody().input.length,
819
+ body: fullBody(),
773
820
  continuation: false
774
821
  };
775
822
  }
776
- fullRequestBody() {
777
- if (this.#fullBody === undefined)
778
- throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Full Responses request is unavailable.');
779
- return this.#fullBody;
780
- }
781
823
  currentTurnState() {
782
824
  return this.#turnState;
783
825
  }
@@ -906,7 +948,7 @@ function localContinuationReset() {
906
948
  function isLocalContinuationReset(error) {
907
949
  return error instanceof MarAgentError && error.code === 'MAR_AGENT_RESPONSES_CONTINUATION_RESET';
908
950
  }
909
- function responsesRequestBody(configuration, request, messages) {
951
+ function responsesRequestBody(configuration, request, messages, previousOutputItems = []) {
910
952
  const systemBlocks = resolveModelTextBlocks(request.system, request.systemBlocks ?? []);
911
953
  if ((request.systemBlocks !== undefined && systemBlocks.some((block) => block.cache)) ||
912
954
  messages.some((message) => message.textBlocks !== undefined &&
@@ -940,7 +982,7 @@ function responsesRequestBody(configuration, request, messages) {
940
982
  configuration.hostedWebSearch)
941
983
  tools.push({ type: 'web_search' });
942
984
  const toolChoice = request.toolChoice ?? 'auto';
943
- const input = openAiInput(messages, request.images, configuration.id, true, usesResponsesConfigurationUpdates(configuration), configuration.responsesOptionalParameters?.nativeTools !== false, request.tools);
985
+ const input = openAiInput(messages, request.images, configuration.id, true, usesResponsesConfigurationUpdates(configuration), configuration.responsesOptionalParameters?.nativeTools !== false, request.tools, previousOutputItems);
944
986
  const systemInput = responsesSystemInput(request, systemBlocks);
945
987
  if (systemInput !== undefined)
946
988
  input.unshift(systemInput);
@@ -1021,11 +1063,14 @@ function incrementalResponsesInput(state, current) {
1021
1063
  return current.input.slice(baseline.length);
1022
1064
  }
1023
1065
  function responsesContinuationPropertiesMatch(previous, current) {
1024
- return (JSON.stringify({ ...previous, input: undefined }) ===
1025
- JSON.stringify({ ...current, input: undefined }));
1066
+ return (JSON.stringify({ ...previous, input: undefined, previous_response_id: undefined }) ===
1067
+ JSON.stringify({ ...current, input: undefined, previous_response_id: undefined }));
1026
1068
  }
1027
1069
  function clearResponsesContinuation(state) {
1028
1070
  delete state.lastRequest;
1071
+ delete state.lastMessageCount;
1072
+ delete state.lastInputItemCount;
1073
+ delete state.lastSystem;
1029
1074
  delete state.lastResponse;
1030
1075
  }
1031
1076
  function hashResponsesValue(value) {
@@ -1846,7 +1891,7 @@ function responseCitations(response) {
1846
1891
  function responseEventId(event) {
1847
1892
  return text(record(event.response).id);
1848
1893
  }
1849
- export function openAiInput(messages, images, modelId, requireEncryptedReasoning, configurationUpdates = true, nativeTools = true, tools = []) {
1894
+ export function openAiInput(messages, images, modelId, requireEncryptedReasoning, configurationUpdates = true, nativeTools = true, tools = [], previousOutputItems = []) {
1850
1895
  const compatibleMessages = messages.filter((message) => message.role !== 'provider' ||
1851
1896
  (message.provider === 'OPENAI_RESPONSES' &&
1852
1897
  (message.providerModelId === undefined || message.providerModelId === modelId) &&
@@ -1861,6 +1906,15 @@ export function openAiInput(messages, images, modelId, requireEncryptedReasoning
1861
1906
  const functionCallIds = new Set();
1862
1907
  const customCallIds = new Set();
1863
1908
  const providerMessageTexts = new Set();
1909
+ // Incremental input omits the calls already retained by the WebSocket response.
1910
+ for (const item of previousOutputItems) {
1911
+ if (typeof item.call_id !== 'string')
1912
+ continue;
1913
+ if (item.type === 'function_call')
1914
+ functionCallIds.add(item.call_id);
1915
+ if (nativeTools && item.type === 'custom_tool_call')
1916
+ customCallIds.add(item.call_id);
1917
+ }
1864
1918
  for (const message of compatibleMessages) {
1865
1919
  if (message.role !== 'provider' || !message.item)
1866
1920
  continue;
@@ -1970,7 +2024,7 @@ function openAiInputItem(message, images, customCallIds = new Set(), functionCal
1970
2024
  ? { type: 'input_text', text: part.text }
1971
2025
  : {
1972
2026
  type: 'input_image',
1973
- image_url: `data:${part.image.mimeType};base64,${part.image.dataBase64}`
2027
+ image_url: modelImageUrl(part.image)
1974
2028
  }) ??
1975
2029
  (message.textBlocks !== undefined
1976
2030
  ? [
@@ -1980,7 +2034,7 @@ function openAiInputItem(message, images, customCallIds = new Set(), functionCal
1980
2034
  })),
1981
2035
  ...(message.images ?? []).map((image) => ({
1982
2036
  type: 'input_image',
1983
- image_url: `data:${image.mimeType};base64,${image.dataBase64}`
2037
+ image_url: modelImageUrl(image)
1984
2038
  }))
1985
2039
  ]
1986
2040
  : message.images?.length
@@ -1988,7 +2042,7 @@ function openAiInputItem(message, images, customCallIds = new Set(), functionCal
1988
2042
  { type: 'input_text', text: message.content },
1989
2043
  ...message.images.map((image) => ({
1990
2044
  type: 'input_image',
1991
- image_url: `data:${image.mimeType};base64,${image.dataBase64}`
2045
+ image_url: modelImageUrl(image)
1992
2046
  }))
1993
2047
  ]
1994
2048
  : message.content)
@@ -2005,7 +2059,7 @@ function openAiInputItem(message, images, customCallIds = new Set(), functionCal
2005
2059
  })),
2006
2060
  ...userImages.map((image) => ({
2007
2061
  type: 'input_image',
2008
- image_url: `data:${image.mimeType};base64,${image.dataBase64}`
2062
+ image_url: modelImageUrl(image)
2009
2063
  }))
2010
2064
  ]
2011
2065
  : userImages.length > 0
@@ -2013,7 +2067,7 @@ function openAiInputItem(message, images, customCallIds = new Set(), functionCal
2013
2067
  { type: 'input_text', text: message.content },
2014
2068
  ...userImages.map((image) => ({
2015
2069
  type: 'input_image',
2016
- image_url: `data:${image.mimeType};base64,${image.dataBase64}`
2070
+ image_url: modelImageUrl(image)
2017
2071
  }))
2018
2072
  ]
2019
2073
  : message.content
@@ -2060,7 +2114,7 @@ function hasOpenFunctionObject(value) {
2060
2114
  function uniqueModelImages(...groups) {
2061
2115
  const seen = new Set();
2062
2116
  return groups.flatMap((group) => (group ?? []).flatMap((image) => {
2063
- const key = `${image.mimeType}\u0000${image.dataBase64}`;
2117
+ const key = modelImageKey(image);
2064
2118
  if (seen.has(key))
2065
2119
  return [];
2066
2120
  seen.add(key);
@@ -1,5 +1,6 @@
1
1
  import { parseModelInputMetadata } from '../../session/store/execution-state.js';
2
2
  import { restoreModelContext } from '../context/projection.js';
3
+ import { parseModelImages } from '../../model/image.js';
3
4
  /** Use a public native identity, never private context text, as the visible boundary. */
4
5
  export function compactSnapshotAnchor(messages, sources) {
5
6
  for (let index = messages.length - 1; index >= 0; index--) {
@@ -39,15 +40,7 @@ export function restoreCompactTail(value) {
39
40
  : {}),
40
41
  ...(metadata ? { metadata } : {}),
41
42
  ...(projected?.textBlocks ? { textBlocks: projected.textBlocks } : {}),
42
- ...(Array.isArray(input.images) &&
43
- input.images.every((image) => image && typeof image.mimeType === 'string' && typeof image.dataBase64 === 'string')
44
- ? {
45
- images: input.images.map((image) => ({
46
- mimeType: image.mimeType,
47
- dataBase64: image.dataBase64
48
- }))
49
- }
50
- : {})
43
+ ...(parseModelImages(input.images)?.length ? { images: parseModelImages(input.images) } : {})
51
44
  };
52
45
  }
53
46
  return restoreModelContext(input);
@@ -1,11 +1,13 @@
1
- import type { ClientToolDefinition, ModelUsage } from '../../model/contracts.js';
1
+ import type { ClientToolDefinition, ModelMessage, ModelUsage } from '../../model/contracts.js';
2
2
  import { type ContextGcPlatform } from '../../session/context-gc-checkpoint.js';
3
3
  import type { SessionStore, SessionRecord } from '../../session/store/contracts.js';
4
4
  import { type ContextProjection } from './projection.js';
5
5
  export interface ContextGcRuntimeInput {
6
+ readonly inlineMedia?: import('../../session/inline-media.js').InlineMediaGcCandidate | undefined;
6
7
  readonly store: SessionStore;
7
8
  readonly sessionId: string;
8
9
  readonly records?: readonly SessionRecord[];
10
+ readonly mediaView?: (messages: readonly ModelMessage[]) => readonly ModelMessage[];
9
11
  readonly system: string;
10
12
  readonly tools: readonly ClientToolDefinition[];
11
13
  readonly modelId: string;
@@ -29,7 +29,10 @@ export class ContextGcRuntime {
29
29
  this.reset();
30
30
  this.#scope = scope;
31
31
  }
32
- const usage = contextGcEligibleUsage(input.usage, input.contextWindowTokens);
32
+ const mediaRequired = input.inlineMedia !== undefined && input.inlineMedia.reason !== 'TEXT_GC';
33
+ const usage = mediaRequired
34
+ ? (input.usage.contextInputTokens ?? input.usage.inputTokens ?? 0)
35
+ : contextGcEligibleUsage(input.usage, input.contextWindowTokens);
33
36
  if (usage === undefined) {
34
37
  this.#lastUsage = undefined;
35
38
  this.#failures = 0;
@@ -45,11 +48,12 @@ export class ContextGcRuntime {
45
48
  this.#rolloutsSinceCheckpoint++;
46
49
  const maximumInterval = usage / input.contextWindowTokens >= 0.8 ? 2 : 8;
47
50
  this.#remaining = Math.min(this.#remaining, maximumInterval - 1);
48
- if (this.#remaining > 0) {
51
+ if (this.#remaining > 0 && !mediaRequired) {
49
52
  this.#remaining--;
50
53
  return false;
51
54
  }
52
- if (this.#checkpoint?.trigger.modelId === input.modelId &&
55
+ if (!mediaRequired &&
56
+ this.#checkpoint?.trigger.modelId === input.modelId &&
53
57
  this.#checkpoint.trigger.contextWindowTokens === input.contextWindowTokens &&
54
58
  !this.#checkpoint.protectedCandidatesPending &&
55
59
  this.#rolloutsSinceCheckpoint < CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
@@ -90,7 +94,10 @@ export async function tryCommitContextGc(input) {
90
94
  return (await attemptContextGc(input)).result;
91
95
  }
92
96
  async function attemptContextGc(input) {
93
- const contextUsage = contextGcEligibleUsage(input.usage, input.contextWindowTokens);
97
+ const mediaRequired = input.inlineMedia !== undefined && input.inlineMedia.reason !== 'TEXT_GC';
98
+ const contextUsage = mediaRequired
99
+ ? (input.usage.contextInputTokens ?? input.usage.inputTokens ?? 0)
100
+ : contextGcEligibleUsage(input.usage, input.contextWindowTokens);
94
101
  if (contextUsage === undefined)
95
102
  return { result: false };
96
103
  let deferred = { result: false };
@@ -108,7 +115,8 @@ async function attemptContextGc(input) {
108
115
  protectedCandidatesPending: previous.payload.metrics.protectedCandidateTokens > 0
109
116
  };
110
117
  }
111
- if (previous?.payload.trigger.modelId === input.modelId &&
118
+ if (!mediaRequired &&
119
+ previous?.payload.trigger.modelId === input.modelId &&
112
120
  previous.payload.trigger.contextWindowTokens === input.contextWindowTokens) {
113
121
  const rolloutGrowth = records.filter((record) => record.sequence > previous.record.sequence &&
114
122
  record.type === 'model.attempt' &&
@@ -124,10 +132,12 @@ async function attemptContextGc(input) {
124
132
  return deferred;
125
133
  }
126
134
  const projection = buildContextProjection(records, checkpointCache);
135
+ const messages = projection.entries.map((entry) => entry.message);
127
136
  const inspection = inspectContextGc({
137
+ ...(input.inlineMedia ? { inlineMedia: input.inlineMedia } : {}),
128
138
  records,
129
139
  checkpointCache,
130
- messages: projection.entries.map((entry) => entry.message),
140
+ messages: input.mediaView?.(messages) ?? messages,
131
141
  messageSources: projection.entries.map((entry) => entry.record),
132
142
  system: input.system,
133
143
  tools: input.tools,
@@ -197,6 +207,7 @@ async function matchingCheckpointWasPersisted(store, sessionId, draft, records,
197
207
  !isDeepStrictEqual(latest.payload.base, draft.base) ||
198
208
  !isDeepStrictEqual(latest.payload.trigger, draft.trigger) ||
199
209
  !isDeepStrictEqual(latest.payload.economics, draft.economics) ||
210
+ !isDeepStrictEqual(latest.payload.inlineMedia, draft.inlineMedia) ||
200
211
  !isDeepStrictEqual(latest.payload.metrics, draft.metrics))
201
212
  return false;
202
213
  const persisted = new Map(latest.payload.replacements.map((replacement) => [
@@ -10,6 +10,19 @@ export function contextGcTokenEntries(input, messages) {
10
10
  }));
11
11
  }
12
12
  function serializedEntries(input, messages) {
13
+ // Media bytes have a separate budget. Keep identity for cache boundaries without
14
+ // loading resources or counting base64 payloads as text tokens.
15
+ messages = messages.map((message) => message.images?.length
16
+ ? {
17
+ ...message,
18
+ images: message.images.map((image) => ({
19
+ mimeType: image.mimeType,
20
+ url: image.resourceId
21
+ ? `https://mar.invalid/resource/${image.resourceId}`
22
+ : (image.url ?? 'https://mar.invalid/inline-image')
23
+ }))
24
+ }
25
+ : message);
13
26
  const responses = input.modelProtocol === 'OPENAI_RESPONSES' ||
14
27
  (input.modelProtocol === undefined &&
15
28
  (input.messages ?? messages).some((message) => message.provider === 'OPENAI_RESPONSES'));
@@ -24,7 +24,7 @@ export interface ContextGcEconomicsResult {
24
24
  readonly passes: boolean;
25
25
  }
26
26
  export interface ContextGcPlan {
27
- readonly stage: 'STAGE_A' | 'STAGE_A_AND_EXEC';
27
+ readonly stage: ContextGcMetrics['stage'];
28
28
  readonly draft: ContextGcCheckpointDraft;
29
29
  readonly metrics: ContextGcMetrics;
30
30
  }
@@ -38,6 +38,7 @@ export interface ContextGcInspection {
38
38
  readonly retryAfterRollouts?: number;
39
39
  }
40
40
  export interface ContextGcPlannerInput {
41
+ readonly inlineMedia?: import('../../session/inline-media.js').InlineMediaGcCandidate;
41
42
  readonly records: readonly SessionRecord[];
42
43
  readonly messages: readonly ModelMessage[];
43
44
  readonly messageSources: readonly SessionRecord[];
@@ -118,6 +118,55 @@ export function planContextGc(input) {
118
118
  return inspectContextGc(input).plan;
119
119
  }
120
120
  export function inspectContextGc(input) {
121
+ const text = inspectTextGc(input);
122
+ const media = input.inlineMedia;
123
+ if (!media || (media.reason === 'TEXT_GC' && !text.plan))
124
+ return text;
125
+ const usageRecord = latestUsageRecord(input.records, input.modelId) ?? input.records.at(-1);
126
+ const contextUsageTokens = modelContextUsage(input.usage) ?? 0;
127
+ if (!usageRecord || contextUsageTokens === undefined)
128
+ return text;
129
+ input = {
130
+ ...input,
131
+ tokenCache: new Map(),
132
+ checkpointCache: input.checkpointCache ?? new ContextGcCheckpointCache()
133
+ };
134
+ const state = {
135
+ stage: text.plan?.stage ?? 'INLINE_MEDIA',
136
+ contextUsageTokens,
137
+ usageRatio: contextUsageTokens / input.contextWindowTokens,
138
+ usageRecord,
139
+ normalRolloutOrdinal: countSuccessfulNormalRollouts(input.records, input.modelId, input.contextWindowTokens),
140
+ protectedCandidateTokens: text.plan?.metrics.protectedCandidateTokens ?? 0
141
+ };
142
+ if (text.plan) {
143
+ const combined = evaluateCandidateSet(input, [], [
144
+ {
145
+ replacements: text.plan.draft.replacements,
146
+ observedRollouts: text.plan.metrics.minimumObservedRollouts,
147
+ evidenceSequence: text.plan.draft.boundarySequence
148
+ }
149
+ ], state, media.project);
150
+ if (combined?.economics.passes) {
151
+ const plan = finalizePlan(input, combined, true);
152
+ if (plan)
153
+ return { plan };
154
+ }
155
+ if (media.reason === 'TEXT_GC')
156
+ return text;
157
+ }
158
+ // Byte pressure has its own admission signal; it never relaxes text GC eligibility.
159
+ const onlyMedia = evaluateCandidateSet(input, input.checkpointCache.read(input.records)?.payload.replacements ?? [], [
160
+ {
161
+ replacements: [],
162
+ observedRollouts: media.reason === 'HARD_LIMIT' ? 0 : 2,
163
+ evidenceSequence: input.records.at(-1).sequence
164
+ }
165
+ ], { ...state, stage: 'INLINE_MEDIA', mediaOnly: true }, media.project);
166
+ const plan = onlyMedia && finalizePlan(input, onlyMedia, true);
167
+ return plan ? { plan } : text;
168
+ }
169
+ function inspectTextGc(input) {
121
170
  const contextUsageTokens = contextGcEligibleUsage(input.usage, input.contextWindowTokens);
122
171
  if (contextUsageTokens === undefined)
123
172
  return {};
@@ -396,14 +445,19 @@ export function contextGcEarliestReplacementSequence(replacements) {
396
445
  : [])
397
446
  ]));
398
447
  }
399
- function finalizePlan(input, evaluated) {
448
+ function finalizePlan(input, evaluated, includeMedia = false) {
400
449
  if (!evaluated)
401
450
  return undefined;
451
+ let inlineMedia;
452
+ if (includeMedia && input.inlineMedia) {
453
+ const { project: _project, ...checkpoint } = input.inlineMedia;
454
+ inlineMedia = checkpoint;
455
+ }
402
456
  const base = contextGcBaseFromRecords(input.records, evaluated.boundarySequence);
403
457
  if (!base)
404
458
  return undefined;
405
459
  const replacements = normalizeContextGcReplacements(evaluated.replacements);
406
- if (!replacements || replacements.length === 0)
460
+ if (!replacements || (replacements.length === 0 && !inlineMedia))
407
461
  return undefined;
408
462
  const metrics = {
409
463
  stage: evaluated.stage,
@@ -418,8 +472,12 @@ function finalizePlan(input, evaluated) {
418
472
  compactDeferralCreditCostUnits: evaluated.compactDeferralCreditCostUnits,
419
473
  imminentCompactAvoidanceCreditCostUnits: evaluated.imminentCompactAvoidanceCreditCostUnits,
420
474
  appliedCompactCreditCostUnits: evaluated.appliedCompactCreditCostUnits,
421
- requiredPositiveNetRollouts: evaluated.economics.requiredPositiveNetRollouts,
422
- requiredTargetRoiRollouts: evaluated.economics.requiredTargetRoiRollouts,
475
+ requiredPositiveNetRollouts: Number.isFinite(evaluated.economics.requiredPositiveNetRollouts)
476
+ ? evaluated.economics.requiredPositiveNetRollouts
477
+ : null,
478
+ requiredTargetRoiRollouts: Number.isFinite(evaluated.economics.requiredTargetRoiRollouts)
479
+ ? evaluated.economics.requiredTargetRoiRollouts
480
+ : null,
423
481
  projectedRolloutsUntilCompactAfterGc: evaluated.projectedRolloutsUntilCompactAfterGc,
424
482
  projectedTokensBeforeGc: evaluated.projectedTokensBeforeGc,
425
483
  projectedTokensAfterGc: evaluated.projectedTokensAfterGc,
@@ -436,12 +494,16 @@ function finalizePlan(input, evaluated) {
436
494
  cachePenaltyKnown: input.usage.cacheReadTokens !== undefined
437
495
  };
438
496
  const draft = {
497
+ ...(inlineMedia ? { inlineMedia } : {}),
439
498
  version: 1,
440
499
  base,
441
500
  boundarySequence: evaluated.boundarySequence,
442
501
  replacements,
443
502
  trigger: {
444
- usageSequence: evaluated.usageRecord.sequence,
503
+ usageSequence: isRecord(evaluated.usageRecord.payload) &&
504
+ evaluated.usageRecord.payload.type === 'usage.updated'
505
+ ? evaluated.usageRecord.sequence
506
+ : null,
445
507
  contextInputTokens: evaluated.contextUsageTokens,
446
508
  contextWindowTokens: input.contextWindowTokens,
447
509
  normalRolloutOrdinal: evaluated.normalRolloutOrdinal,
@@ -453,7 +515,7 @@ function finalizePlan(input, evaluated) {
453
515
  };
454
516
  return { stage: evaluated.stage, draft, metrics };
455
517
  }
456
- function evaluateCandidateSet(input, existing, candidates, state) {
518
+ function evaluateCandidateSet(input, existing, candidates, state, mediaProjection) {
457
519
  if (candidates.length === 0)
458
520
  return undefined;
459
521
  const replacements = normalizeContextGcReplacements([
@@ -463,16 +525,17 @@ function evaluateCandidateSet(input, existing, candidates, state) {
463
525
  if (!replacements)
464
526
  return undefined;
465
527
  const projected = applyContextGcReplacementsToMessages(input.messages, input.messageSources, replacements);
466
- const reclaimedTokens = contextGcReclaimedTokens(input, input.messages, projected.messages);
528
+ const projectedMessages = mediaProjection?.(projected.messages) ?? projected.messages;
529
+ const reclaimedTokens = contextGcReclaimedTokens(input, input.messages, projectedMessages);
467
530
  const minimumGrossReductionRate = contextGcMinimumGrossReductionRate(state.usageRatio);
468
531
  const minimumReclaimedTokens = contextGcMinimumReclaimedTokens(state.contextUsageTokens, input.contextWindowTokens);
469
- if (reclaimedTokens < minimumReclaimedTokens)
532
+ if (!state.mediaOnly && reclaimedTokens < minimumReclaimedTokens)
470
533
  return undefined;
471
534
  const beforeTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input, input.messages);
472
- const afterTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input, projected.messages);
473
- if (reclaimedTokens === 0)
535
+ const afterTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input, projectedMessages);
536
+ if (!state.mediaOnly && reclaimedTokens === 0)
474
537
  return undefined;
475
- const cache = contextGcCacheImpact(input, input.messages, projected.messages, input.usage.cacheReadTokens ?? 0, reclaimedTokens);
538
+ const cache = contextGcCacheImpact(input, input.messages, projectedMessages, input.usage.cacheReadTokens ?? 0, reclaimedTokens);
476
539
  const growthSamples = collectContextGrowthSamples(input.records, input.modelId, input.contextWindowTokens, input.checkpointCache);
477
540
  const contextGrowthP75Tokens = percentile75(growthSamples);
478
541
  const compactThresholdTokens = Math.floor(input.contextWindowTokens * COMPACT_THRESHOLD_RATIO);
@@ -481,7 +544,7 @@ function evaluateCandidateSet(input, existing, candidates, state) {
481
544
  const pendingTokens = Math.max(0, input.usage.outputTokens ?? 0) + contextGcMessageTokens(input, pendingMessages);
482
545
  const projectedTokensBeforeGc = state.contextUsageTokens + pendingTokens;
483
546
  const projectedTokensAfterGc = Math.max(0, projectedTokensBeforeGc - reclaimedTokens);
484
- if (projectedTokensAfterGc >= compactThresholdTokens)
547
+ if (!state.mediaOnly && projectedTokensAfterGc >= compactThresholdTokens)
485
548
  return undefined;
486
549
  const projectedRolloutsUntilCompactAfterGc = contextGrowthP75Tokens <= 0
487
550
  ? 0
@@ -1,6 +1,7 @@
1
1
  import { restoreNativeModelContext } from '../../model/native-context.js';
2
2
  import { appliedInputRecord } from '../../session/deferred-input.js';
3
3
  import { inputMetadataFromRecord } from '../../session/store/execution-state.js';
4
+ import { parseModelImages } from '../../model/image.js';
4
5
  import { contextGcTargetKey, latestValidContextGcCheckpoint } from '../../session/context-gc-checkpoint.js';
5
6
  import { TOOL_EXECUTION_LIMITS } from '../../tools/execution-limits.js';
6
7
  import { replaceReadContextItems } from '../../tools/local/read-context.js';
@@ -187,6 +188,9 @@ export function restoreModelContext(payload, providerModelId) {
187
188
  role: 'tool',
188
189
  content: payload.content,
189
190
  callId: payload.callId,
191
+ ...(parseModelImages(payload.images)?.length
192
+ ? { images: parseModelImages(payload.images) }
193
+ : {}),
190
194
  ...(typeof payload.name === 'string' ? { name: payload.name } : {}),
191
195
  ...(typeof payload.isError === 'boolean' ? { isError: payload.isError } : {}),
192
196
  ...(Array.isArray(payload.textBlocks) &&
@@ -1,6 +1,4 @@
1
1
  import type { ClientToolDefinition, ModelMessage } from '../../model/contracts.js';
2
- export declare function estimatedRequestTokens(system: string, messages: readonly ModelMessage[], tools: readonly ClientToolDefinition[], images?: readonly {
3
- mimeType: string;
4
- dataBase64: string;
5
- }[]): number;
2
+ import type { ModelImage } from '../../model/image.js';
3
+ export declare function estimatedRequestTokens(system: string, messages: readonly ModelMessage[], tools: readonly ClientToolDefinition[], images?: readonly ModelImage[]): number;
6
4
  export declare function stableJson(value: unknown): string;
@@ -1,8 +1,10 @@
1
1
  import type { SessionStore } from '../../session/store/contracts.js';
2
2
  import type { ModelMessage } from '../../model/contracts.js';
3
+ import type { ModelImage } from '../../model/image.js';
3
4
  import type { CompactUserInput } from '../../session/store/contracts.js';
4
5
  import type { EnvironmentContextState } from './contracts.js';
5
6
  import { type ContextProjection } from './projection.js';
7
+ export declare function readResourceImages(store: Pick<SessionStore, 'readResource'>, sessionId: string, images: readonly ModelImage[]): Promise<ModelImage[]>;
6
8
  export declare function restoreMessages(store: SessionStore, sessionId: string, records: readonly import('../../session/store/index.js').SessionRecord[], options: {
7
9
  includeImages: boolean;
8
10
  strictImages?: boolean;
@@ -5,6 +5,16 @@ import { compactedHistoryMessages, continuationMessage } from '../compaction/his
5
5
  import { restoreCompactTail } from '../compaction/retained-context.js';
6
6
  import { environmentContextUpdate, restoreEnvironmentState } from './environment.js';
7
7
  import { buildContextProjection } from './projection.js';
8
+ export async function readResourceImages(store, sessionId, images) {
9
+ return Promise.all(images.map(async (image) => {
10
+ const resource = image.resourceId === undefined
11
+ ? undefined
12
+ : await store.readResource(sessionId, image.resourceId);
13
+ if (!resource)
14
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input images cannot be fully restored.');
15
+ return { ...image, dataBase64: resource.dataBase64 };
16
+ }));
17
+ }
8
18
  export async function restoreMessages(store, sessionId, records, options) {
9
19
  const messages = [];
10
20
  const outcomes = new Map();
@@ -41,8 +51,9 @@ export async function restoreMessages(store, sessionId, records, options) {
41
51
  try {
42
52
  images = await store.readInputImages(sessionId, payload.images);
43
53
  if (options.strictImages &&
44
- payload.images !== undefined &&
45
- (!Array.isArray(payload.images) || images.length !== payload.images.length))
54
+ Array.isArray(payload.images) &&
55
+ images.length !==
56
+ payload.images.filter((image) => image?.resourceInputVersion === 1).length)
46
57
  throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input images cannot be fully restored.');
47
58
  }
48
59
  catch (error) {
@@ -145,8 +156,13 @@ export async function restoreMessages(store, sessionId, records, options) {
145
156
  }
146
157
  }
147
158
  }
148
- for (const message of messages.slice(messageStart))
159
+ for (const message of messages.slice(messageStart)) {
160
+ if (!options.includeImages)
161
+ delete message.images;
162
+ else if (options.strictImages && message.images?.length)
163
+ message.images = await readResourceImages(store, sessionId, message.images);
149
164
  options.messageSources?.set(message, record);
165
+ }
150
166
  }
151
167
  const completedToolCalls = new Set(messages.filter((message) => message.role === 'tool').map((message) => message.callId));
152
168
  return {