@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,11 +1,10 @@
1
1
  import type { MarAgentReasoningEffort } from '../model/configuration.js';
2
+ import type { ModelImage } from '../model/image.js';
2
3
  /** @deprecated MAR Agent always executes tools directly through the Host. */
3
4
  export type AccessMode = 'full-access';
4
5
  export type ExecutionMode = 'run' | 'plan' | 'compact';
5
- export interface RunInputImage {
6
+ export interface RunInputImage extends ModelImage {
6
7
  name: string;
7
- mimeType: string;
8
- dataBase64: string;
9
8
  }
10
9
  export interface MarAgentImageMetadata {
11
10
  name: string;
@@ -27,7 +27,7 @@ export interface ExecutionLoopInput {
27
27
  readonly compactionFallback?: (() => boolean) | undefined;
28
28
  readonly observeUsage: (usage: Omit<AuthoritativeModelUsage, 'historyMessages'> | undefined) => void;
29
29
  readonly observeContextTokens: (tokens: number) => void;
30
- readonly afterModel?: (closedPrefixLength: number) => Promise<void>;
30
+ readonly afterModel?: (closedPrefixLength: number, acceptedRequest?: ModelRequest) => Promise<void>;
31
31
  readonly checkpoint: (round: PreparedExecutionRound, usage: ModelUsage | undefined) => Promise<void>;
32
32
  readonly mailbox: {
33
33
  hasPending(): boolean;
@@ -323,11 +323,14 @@ export class ExecutionLoop {
323
323
  }
324
324
  recoveringFromContextOverflow = false;
325
325
  input.observeUsage(roundServerUsage);
326
- await input.afterModel?.(pendingTools.length ? roundMessageCount : messages.length);
326
+ const emptyCompletion = stop === 'completed' && !sawTool && !this.finalAnswer.trim();
327
+ await input.afterModel?.(pendingTools.length ? roundMessageCount : messages.length, emptyCompletion || stop === 'cancelled' ? undefined : round.request);
327
328
  const toolResults = await executeRoundTools(pendingTools, round.toolScope);
328
329
  for (const result of toolResults) {
329
- if ('images' in result && result.images?.length)
330
- input.toolImages.push(...result.images.map((image) => ({
330
+ if ('sourceImages' in result && result.sourceImages?.length)
331
+ input.toolImages.push(...result.sourceImages
332
+ .filter((image) => image.dataBase64 !== undefined)
333
+ .map((image) => ({
331
334
  mimeType: image.mimeType,
332
335
  data: Buffer.from(image.dataBase64, 'base64')
333
336
  })));
@@ -354,6 +357,9 @@ export class ExecutionLoop {
354
357
  content: message.content,
355
358
  ...(message.calls ? { calls: message.calls } : {}),
356
359
  ...(message.observations ? { observations: message.observations } : {}),
360
+ ...(message.images?.length
361
+ ? { images: message.images.map(({ dataBase64: _bytes, ...image }) => image) }
362
+ : {}),
357
363
  ...(message.toolSearchTools === undefined
358
364
  ? {}
359
365
  : { toolSearchTools: message.toolSearchTools })
@@ -401,7 +407,7 @@ export class ExecutionLoop {
401
407
  const compactSummary = mode === 'compact' ? this.finalAnswer.trim() : '';
402
408
  if (mode === 'compact' && !compactSummary)
403
409
  throw new MarAgentError('MAR_AGENT_COMPACT_FAILED', 'Compaction returned an empty handoff.');
404
- if (mode !== 'compact' && !this.finalAnswer.trim()) {
410
+ if (mode !== 'compact' && emptyCompletion) {
405
411
  const error = new MarAgentError(input.sourceType === 'subagent'
406
412
  ? 'MAR_AGENT_SUBAGENT_FAILED'
407
413
  : 'MAR_AGENT_MODEL_OUTPUT_INCOMPLETE', input.sourceType === 'subagent'
@@ -0,0 +1,8 @@
1
+ import type { MarAgentHost } from '../host/contracts.js';
2
+ import type { ModelImage } from '../model/image.js';
3
+ import type { SessionStore } from '../session/store/contracts.js';
4
+ /** Persist immutable bytes first; publication never changes an existing resource URL. */
5
+ export declare function publishModelResources<T extends ModelImage>(host: MarAgentHost, images: readonly T[] | undefined, context: {
6
+ sessionId: string;
7
+ executionId: string;
8
+ }, store?: SessionStore): Promise<readonly T[] | undefined>;
@@ -0,0 +1,60 @@
1
+ import { isModelImageUrl } from '../model/image.js';
2
+ import { MarAgentError } from '../error.js';
3
+ const pending = new WeakMap();
4
+ /** Persist immutable bytes first; publication never changes an existing resource URL. */
5
+ export async function publishModelResources(host, images, context, store) {
6
+ if (!images?.length)
7
+ return images;
8
+ if (store === undefined) {
9
+ if (host.publishModelResource === undefined)
10
+ return images;
11
+ throw new MarAgentError('MAR_AGENT_IMAGE_PUBLICATION_FAILED', 'Image publication requires a SessionStore.');
12
+ }
13
+ let publications = pending.get(store);
14
+ if (publications === undefined)
15
+ pending.set(store, (publications = new Map()));
16
+ return Promise.all(images.map(async (image) => {
17
+ if (image.dataBase64 === undefined &&
18
+ image.resourceId === undefined &&
19
+ isModelImageUrl(image.url))
20
+ return image;
21
+ let resource = await store.persistResource(context.sessionId, image);
22
+ if (resource.url === undefined && image.url !== undefined && image.urlExpiresAt !== undefined)
23
+ resource = await store.setResourceUrl(context.sessionId, resource.resourceId, {
24
+ url: image.url,
25
+ urlExpiresAt: image.urlExpiresAt
26
+ });
27
+ const key = `${context.sessionId}\0${resource.resourceId}`;
28
+ let publication = publications.get(key);
29
+ if (publication === undefined) {
30
+ publication = (async () => {
31
+ if (resource.url === undefined && host.publishModelResource !== undefined) {
32
+ const published = await host.publishModelResource(resource, {
33
+ ...context,
34
+ homeDirectory: (await host.describe()).homeDirectory
35
+ });
36
+ if (published === undefined ||
37
+ !isModelImageUrl(published.url) ||
38
+ !Number.isSafeInteger(published.urlExpiresAt) ||
39
+ published.urlExpiresAt <= 0)
40
+ throw new MarAgentError('MAR_AGENT_IMAGE_PUBLICATION_FAILED', 'Image publication did not return a valid fixed URL.');
41
+ resource = await store.setResourceUrl(context.sessionId, resource.resourceId, published);
42
+ }
43
+ const { sha256: _hash, ...reference } = resource;
44
+ return resource.url === undefined && image.dataBase64 !== undefined
45
+ ? { ...reference, dataBase64: image.dataBase64 }
46
+ : reference;
47
+ })();
48
+ publications.set(key, publication);
49
+ }
50
+ try {
51
+ const published = await publication;
52
+ const { dataBase64: _bytes, ...metadata } = image;
53
+ return { ...metadata, ...published, resourceInputVersion: 1 };
54
+ }
55
+ finally {
56
+ if (publications.get(key) === publication)
57
+ publications.delete(key);
58
+ }
59
+ }));
60
+ }
@@ -20,6 +20,7 @@ export async function createMarAgentWithAdapter(options) {
20
20
  let { models, defaultModelId } = resolvedModels;
21
21
  let modelGeneration = 0;
22
22
  const { credentialProviders } = resolvedModels;
23
+ let retainedExtensionProviders;
23
24
  const description = negotiateHost(await options.host.describe());
24
25
  if (description.mode !== options.mode)
25
26
  throw new MarAgentError('MAR_AGENT_HOST_INCOMPATIBLE', 'Host mode does not match Agent mode.');
@@ -318,6 +319,19 @@ export async function createMarAgentWithAdapter(options) {
318
319
  }
319
320
  for (const commit of reconfigurationCommits.values())
320
321
  commit();
322
+ if (extensions.hasReferences() || activeSessions.size > 0) {
323
+ if (!retainedExtensionProviders ||
324
+ retainedExtensionProviders.revision !== extensions.revision) {
325
+ const providers = new Set();
326
+ for (const configuration of models.values())
327
+ addModelCredentialProviders(providers, configuration);
328
+ // A pending composition retains its original providers, not every intermediate catalog.
329
+ retainedExtensionProviders = { revision: extensions.revision, providers };
330
+ }
331
+ }
332
+ else {
333
+ retainedExtensionProviders = undefined;
334
+ }
321
335
  models = next.models;
322
336
  defaultModelId = next.defaultModelId;
323
337
  modelGeneration++;
@@ -327,6 +341,8 @@ export async function createMarAgentWithAdapter(options) {
327
341
  runtime.adapterGeneration = modelGeneration;
328
342
  }
329
343
  const liveProviders = new Set(next.credentialProviders);
344
+ for (const provider of retainedExtensionProviders?.providers ?? [])
345
+ liveProviders.add(provider);
330
346
  for (const adapter of activeExecutionAdapters)
331
347
  addModelCredentialProviders(liveProviders, adapter.configuration);
332
348
  for (const runtime of sessionRuntimes.values())
@@ -48,6 +48,8 @@ export function createModelTurnSession(adapter) {
48
48
  }
49
49
  },
50
50
  resetContinuation: () => turn.resetContinuation?.(),
51
+ continuationInputCount: () => turn.continuationInputCount?.() ?? 0,
52
+ commitContinuationInput: (count) => turn.commitContinuationInput?.(count),
51
53
  close: () => turn.close()
52
54
  };
53
55
  }
@@ -0,0 +1,31 @@
1
+ import type { ModelMessage, ModelRequest } from '../model/contracts.js';
2
+ import { type ModelImage } from '../model/image.js';
3
+ import type { SessionStore } from '../session/store/contracts.js';
4
+ import type { SessionRecord } from '../session/store/types.js';
5
+ import { type InlineMediaGcCandidate, type InlineMediaState } from '../session/inline-media.js';
6
+ interface Options {
7
+ records?: readonly SessionRecord[];
8
+ persist?: (state: InlineMediaState) => Promise<void>;
9
+ }
10
+ /** SDK-owned ordered visual window; durable image references are never rewritten. */
11
+ export declare class ResourceInputProjection {
12
+ #private;
13
+ private readonly useResourceUrl;
14
+ private readonly store;
15
+ private readonly sessionId;
16
+ private readonly options;
17
+ constructor(useResourceUrl: boolean, store: Pick<SessionStore, 'readResource'>, sessionId: string, history: readonly ModelMessage[], options?: Options);
18
+ observe(messages: readonly ModelMessage[]): void;
19
+ /** Called only at an explicit context replacement, never as a history diff detector. */
20
+ replace(messages: readonly ModelMessage[]): void;
21
+ hasPendingImages(messages: readonly ModelMessage[]): boolean;
22
+ /** Reference-only view shared by GC; no byte loading or new eviction decisions. */
23
+ historyView(messages: readonly ModelMessage[]): ModelMessage[];
24
+ checkpoint(messages?: readonly ModelMessage[]): InlineMediaState;
25
+ prepare(request: ModelRequest, mirroredImages?: readonly ModelImage[], canonicalMessages?: readonly ModelMessage[]): Promise<ModelRequest>;
26
+ complete(request: ModelRequest): Promise<void>;
27
+ /** Propose a cursor; only the unified GC checkpoint may adopt it. */
28
+ gcCandidate(request?: ModelRequest, mirroredImages?: readonly ModelImage[]): Promise<InlineMediaGcCandidate | undefined>;
29
+ adoptGc(state: InlineMediaState | undefined): void;
30
+ }
31
+ export {};
@@ -0,0 +1,342 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { MarAgentError } from '../error.js';
3
+ import { isModelImageUrl } from '../model/image.js';
4
+ import { observationImages, renderObservationResult, selectObservationImages } from '../tools/code-mode/observations.js';
5
+ import { INLINE_MEDIA_POLICY as policy, assertInlineMediaBudget, inlineMediaBytes, inlineMediaHistory, inlineMediaLimit, inlineMediaSource } from '../session/inline-media.js';
6
+ function releasedImageNotice(message, indices) {
7
+ return indices
8
+ .map((index) => {
9
+ const resourceId = message.images?.[index]?.resourceId;
10
+ const hint = resourceId
11
+ ? ` Reopen with view_image(${JSON.stringify({ resourceId })}) if needed.`
12
+ : '';
13
+ return `<mar_context_gc kind="image">Image released.${hint}</mar_context_gc>`;
14
+ })
15
+ .join('\n');
16
+ }
17
+ function projectRetainedImages(message, images, removed) {
18
+ if (!removed.size)
19
+ return { ...message, images };
20
+ if (message.observations) {
21
+ const observations = message.observations.map((observation) => {
22
+ const removedIndices = observationImages(observation).filter((index) => removed.has(index));
23
+ if (!removedIndices.length)
24
+ return observation;
25
+ const notice = releasedImageNotice(message, removedIndices);
26
+ return observation.type === 'tool_result'
27
+ ? {
28
+ ...observation,
29
+ result: { ...observation.result, content: `${observation.result.content}\n${notice}` }
30
+ }
31
+ : { ...observation, value: `${observation.value}\n${notice}` };
32
+ });
33
+ const selected = selectObservationImages(observations, images);
34
+ return {
35
+ ...message,
36
+ ...selected,
37
+ content: renderObservationResult(selected.observations, message.content)
38
+ };
39
+ }
40
+ const notice = releasedImageNotice(message, [...removed]);
41
+ return {
42
+ ...message,
43
+ images: images.filter(Boolean),
44
+ content: `${message.content}\n${notice}`,
45
+ ...(message.textBlocks ? { textBlocks: [...message.textBlocks, { text: notice }] } : {})
46
+ };
47
+ }
48
+ /** SDK-owned ordered visual window; durable image references are never rewritten. */
49
+ export class ResourceInputProjection {
50
+ useResourceUrl;
51
+ store;
52
+ sessionId;
53
+ options;
54
+ #entries = [];
55
+ #sources = new Map();
56
+ #candidates = new WeakMap();
57
+ #considered = -1;
58
+ #rollout = 0;
59
+ #evicted = -1;
60
+ constructor(useResourceUrl, store, sessionId, history, options = {}) {
61
+ this.useResourceUrl = useResourceUrl;
62
+ this.store = store;
63
+ this.sessionId = sessionId;
64
+ this.options = options;
65
+ const restored = inlineMediaHistory(options.records ?? []);
66
+ this.#rollout = restored.rollouts;
67
+ for (const cursor of restored.cursors)
68
+ this.#entries[this.#entry(cursor.sourceId, cursor.imageIndex)].rollout = cursor.rollout;
69
+ this.observe(history);
70
+ if (restored.state) {
71
+ this.#considered = this.#position(restored.state.consideredThrough);
72
+ this.#evicted = this.#position(restored.state.evictedThrough);
73
+ }
74
+ else if (options.records === undefined)
75
+ this.#considered = this.#entries.length - 1;
76
+ }
77
+ #entry(source, imageIndex) {
78
+ let positions = this.#sources.get(source);
79
+ if (!positions)
80
+ this.#sources.set(source, (positions = []));
81
+ let position = positions[imageIndex];
82
+ if (position === undefined) {
83
+ position = this.#entries.length;
84
+ positions[imageIndex] = position;
85
+ this.#entries.push({ source, imageIndex, active: false, rollout: this.#rollout });
86
+ }
87
+ return position;
88
+ }
89
+ #position(cursor) {
90
+ if (!cursor)
91
+ return -1;
92
+ const position = this.#sources.get(cursor.sourceId)?.[cursor.imageIndex];
93
+ if (position === undefined)
94
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Inline media boundary is missing.');
95
+ return position;
96
+ }
97
+ observe(messages) {
98
+ for (const message of messages)
99
+ for (const [index, image] of (message.images ?? []).entries()) {
100
+ const entry = this.#entries[this.#entry(inlineMediaSource(message), index)];
101
+ entry.image = image;
102
+ entry.active = true;
103
+ }
104
+ }
105
+ /** Called only at an explicit context replacement, never as a history diff detector. */
106
+ replace(messages) {
107
+ for (const entry of this.#entries) {
108
+ entry.active = false;
109
+ delete entry.image;
110
+ }
111
+ this.observe(messages);
112
+ }
113
+ hasPendingImages(messages) {
114
+ return messages.some((message) => (message.images ?? []).some((_, index) => {
115
+ const position = this.#sources.get(inlineMediaSource(message))?.[index];
116
+ return position === undefined || position > this.#considered;
117
+ }));
118
+ }
119
+ /** Reference-only view shared by GC; no byte loading or new eviction decisions. */
120
+ historyView(messages) {
121
+ return this.#historyView(messages, this.#evicted);
122
+ }
123
+ #historyView(messages, evicted, fromEvicted = -1) {
124
+ return messages.map((message) => {
125
+ const positions = this.#sources
126
+ .get(inlineMediaSource(message))
127
+ ?.filter((position) => position > fromEvicted);
128
+ const images = message.images ??
129
+ positions?.flatMap((position) => {
130
+ const entry = this.#entries[position];
131
+ return entry.active && entry.image ? [entry.image] : [];
132
+ });
133
+ if (!images?.length)
134
+ return message;
135
+ const removed = new Set();
136
+ const retained = [];
137
+ images.forEach((image, index) => {
138
+ if (!this.useResourceUrl &&
139
+ positions?.[index] !== undefined &&
140
+ positions[index] <= evicted)
141
+ removed.add(index);
142
+ else
143
+ retained[index] = image;
144
+ });
145
+ return projectRetainedImages({ ...message, images }, retained, removed);
146
+ });
147
+ }
148
+ checkpoint(messages) {
149
+ const retained = messages && new Set(messages.map(inlineMediaSource));
150
+ const cursor = (through) => {
151
+ for (let index = through; index >= 0; index--) {
152
+ const entry = this.#entries[index];
153
+ if (typeof entry.source === 'string' && (!retained || retained.has(entry.source)))
154
+ return { sourceId: entry.source, imageIndex: entry.imageIndex };
155
+ }
156
+ return undefined;
157
+ };
158
+ const consideredThrough = cursor(this.#considered);
159
+ const evictedThrough = cursor(this.#evicted);
160
+ return {
161
+ version: 1,
162
+ ...(consideredThrough ? { consideredThrough } : {}),
163
+ ...(evictedThrough ? { evictedThrough } : {})
164
+ };
165
+ }
166
+ async prepare(request, mirroredImages = [], canonicalMessages = request.messages) {
167
+ this.observe(canonicalMessages);
168
+ const through = this.#entries.length - 1;
169
+ const cached = new Map();
170
+ const project = (image) => {
171
+ let value = cached.get(image);
172
+ if (!value)
173
+ cached.set(image, (value = this.#project(image)));
174
+ return value;
175
+ };
176
+ const sized = async (image) => image.dataBase64 !== undefined || image.byteSize !== undefined ? image : project(image);
177
+ const top = (request.images ?? []).filter((image) => !mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)));
178
+ const evicted = this.#evicted;
179
+ if (!this.useResourceUrl) {
180
+ let total = 0;
181
+ for (const message of request.messages)
182
+ for (const [index, image] of (message.images ?? []).entries()) {
183
+ const position = this.#sources.get(inlineMediaSource(message))?.[index];
184
+ const original = position === undefined ? undefined : this.#entries[position]?.image;
185
+ if (position !== undefined && position <= evicted && isDeepStrictEqual(original, image))
186
+ continue;
187
+ total += inlineMediaBytes(await sized(image));
188
+ }
189
+ for (const image of top)
190
+ total += inlineMediaBytes(await sized(image));
191
+ if (total > policy.requestInlineMediaHardLimit)
192
+ inlineMediaLimit();
193
+ }
194
+ const messages = await Promise.all(request.messages.map(async (message) => {
195
+ if (!message.images?.length)
196
+ return message;
197
+ const removed = new Set();
198
+ const images = [];
199
+ await Promise.all(message.images.map(async (image, index) => {
200
+ const position = this.#sources.get(inlineMediaSource(message))?.[index];
201
+ const original = position === undefined ? undefined : this.#entries[position]?.image;
202
+ if (!this.useResourceUrl &&
203
+ position !== undefined &&
204
+ position <= evicted &&
205
+ isDeepStrictEqual(original, image))
206
+ removed.add(index);
207
+ else
208
+ images[index] = await project(image);
209
+ }));
210
+ return projectRetainedImages(message, images, removed);
211
+ }));
212
+ const { images: _topImages, ...retained } = request;
213
+ const images = await Promise.all(top.map(project));
214
+ const result = { ...retained, messages, ...(images.length ? { images } : {}) };
215
+ if (!this.useResourceUrl)
216
+ assertInlineMediaBudget(result);
217
+ this.#candidates.set(result.messages, through);
218
+ return result;
219
+ }
220
+ async complete(request) {
221
+ const candidate = this.#candidates.get(request.messages);
222
+ if (candidate === undefined)
223
+ return;
224
+ this.#candidates.delete(request.messages);
225
+ this.#rollout++;
226
+ const considered = Math.max(this.#considered, candidate);
227
+ if (considered === this.#considered)
228
+ return;
229
+ const before = this.#considered;
230
+ this.#considered = considered;
231
+ try {
232
+ await this.options.persist?.(this.checkpoint());
233
+ }
234
+ catch (error) {
235
+ this.#considered = before;
236
+ this.#rollout--;
237
+ throw error;
238
+ }
239
+ }
240
+ /** Propose a cursor; only the unified GC checkpoint may adopt it. */
241
+ async gcCandidate(request, mirroredImages = []) {
242
+ if (this.useResourceUrl)
243
+ return undefined;
244
+ const size = async (image) => inlineMediaBytes(image.byteSize !== undefined || image.dataBase64 !== undefined
245
+ ? image
246
+ : await this.#project(image));
247
+ const history = await Promise.all(this.#entries.map(async (entry, index) => ({
248
+ index,
249
+ bytes: entry.active && entry.image && index > this.#evicted && index <= this.#considered
250
+ ? await size(entry.image)
251
+ : 0
252
+ })));
253
+ const beforeBytes = history.reduce((sum, entry) => sum + entry.bytes, 0);
254
+ let freshBytes = 0;
255
+ let requestBytes = 0;
256
+ const requestHistory = new Map();
257
+ if (request) {
258
+ for (const message of request.messages)
259
+ for (const [index, image] of (message.images ?? []).entries()) {
260
+ const position = this.#sources.get(inlineMediaSource(message))?.[index];
261
+ const entry = position === undefined ? undefined : this.#entries[position];
262
+ const original = entry && isDeepStrictEqual(entry.image, image);
263
+ if (original && position <= this.#evicted)
264
+ continue;
265
+ const bytes = await size(image);
266
+ requestBytes += bytes;
267
+ if (original && position <= this.#considered)
268
+ requestHistory.set(position, (requestHistory.get(position) ?? 0) + bytes);
269
+ else
270
+ freshBytes += bytes;
271
+ }
272
+ for (const image of request.images ?? []) {
273
+ if (mirroredImages.some((mirror) => isDeepStrictEqual(image, mirror)))
274
+ continue;
275
+ const bytes = await size(image);
276
+ freshBytes += bytes;
277
+ requestBytes += bytes;
278
+ }
279
+ if (freshBytes > policy.requestInlineMediaHardLimit)
280
+ inlineMediaLimit();
281
+ }
282
+ const hard = requestBytes > policy.requestInlineMediaHardLimit;
283
+ if (!hard && beforeBytes <= policy.historicalInlineMediaTarget)
284
+ return undefined;
285
+ const target = hard
286
+ ? Math.max(0, policy.historicalInlineMediaTarget - freshBytes)
287
+ : policy.historicalInlineMediaTarget;
288
+ let afterBytes = beforeBytes;
289
+ let evicted = this.#evicted;
290
+ for (const item of history) {
291
+ if (afterBytes <= target && (!hard || requestBytes <= policy.requestInlineMediaHardLimit))
292
+ break;
293
+ if (!item.bytes)
294
+ continue;
295
+ if (!hard && this.#rollout - this.#entries[item.index].rollout < 2)
296
+ break;
297
+ afterBytes -= item.bytes;
298
+ requestBytes -= requestHistory.get(item.index) ?? 0;
299
+ evicted = item.index;
300
+ }
301
+ if (afterBytes > target || (hard && requestBytes > policy.requestInlineMediaHardLimit))
302
+ return undefined;
303
+ const entry = this.#entries[evicted];
304
+ if (!entry || typeof entry.source !== 'string' || evicted <= this.#evicted)
305
+ return undefined;
306
+ return {
307
+ reason: hard
308
+ ? 'HARD_LIMIT'
309
+ : beforeBytes > policy.historicalInlineMediaTrigger
310
+ ? 'SOFT_LIMIT'
311
+ : 'TEXT_GC',
312
+ state: {
313
+ ...this.checkpoint(),
314
+ evictedThrough: { sourceId: entry.source, imageIndex: entry.imageIndex }
315
+ },
316
+ beforeBytes,
317
+ afterBytes,
318
+ project: (messages) => this.#historyView(messages, evicted, this.#evicted)
319
+ };
320
+ }
321
+ adoptGc(state) {
322
+ if (state)
323
+ this.#evicted = this.#position(state.evictedThrough);
324
+ }
325
+ async #project(image) {
326
+ if (this.useResourceUrl) {
327
+ if (!isModelImageUrl(image.url))
328
+ throw new MarAgentError('MAR_AGENT_RESOURCE_URL_UNAVAILABLE', 'Resource URL is unavailable.');
329
+ const { dataBase64: _bytes, ...reference } = image;
330
+ return reference;
331
+ }
332
+ const bytes = image.dataBase64 !== undefined
333
+ ? image
334
+ : image.resourceId === undefined
335
+ ? undefined
336
+ : await this.store.readResource(this.sessionId, image.resourceId);
337
+ if (!bytes?.dataBase64)
338
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Resource bytes are unavailable.');
339
+ const { url: _url, urlExpiresAt: _expires, ...reference } = image;
340
+ return { ...reference, dataBase64: bytes.dataBase64 };
341
+ }
342
+ }