@myagentroam/agent 0.11.20 → 0.11.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-core/error.d.ts +1 -1
- package/dist/agent-core/error.js +20 -14
- package/dist/agent-core/extensions/compaction.d.ts +6 -0
- package/dist/agent-core/extensions/compaction.js +16 -2
- package/dist/agent-core/extensions/model-request.d.ts +5 -9
- package/dist/agent-core/extensions/model-request.js +14 -3
- package/dist/agent-core/extensions/prompt-input.js +3 -1
- package/dist/agent-core/host/contracts.d.ts +8 -0
- package/dist/agent-core/host/local-agent-host.d.ts +2 -0
- package/dist/agent-core/host/local-agent-host.js +3 -0
- package/dist/agent-core/model/anthropic-messages.js +15 -26
- package/dist/agent-core/model/catalog-file.d.ts +2 -0
- package/dist/agent-core/model/configuration.d.ts +3 -1
- package/dist/agent-core/model/configuration.js +1 -0
- package/dist/agent-core/model/contracts.d.ts +5 -8
- package/dist/agent-core/model/image.d.ts +17 -0
- package/dist/agent-core/model/image.js +53 -0
- package/dist/agent-core/model/openai-responses.d.ts +7 -2
- package/dist/agent-core/model/openai-responses.js +81 -27
- package/dist/agent-core/runtime/compaction/retained-context.js +2 -9
- package/dist/agent-core/runtime/context/gc-runtime.d.ts +3 -1
- package/dist/agent-core/runtime/context/gc-runtime.js +17 -6
- package/dist/agent-core/runtime/context/gc-tokens.js +13 -0
- package/dist/agent-core/runtime/context/gc.d.ts +2 -1
- package/dist/agent-core/runtime/context/gc.js +75 -12
- package/dist/agent-core/runtime/context/projection.js +4 -0
- package/dist/agent-core/runtime/context/request-budget.d.ts +2 -4
- package/dist/agent-core/runtime/context/restore.d.ts +2 -0
- package/dist/agent-core/runtime/context/restore.js +19 -3
- package/dist/agent-core/runtime/contracts.d.ts +2 -3
- package/dist/agent-core/runtime/execution.d.ts +1 -1
- package/dist/agent-core/runtime/execution.js +10 -4
- package/dist/agent-core/runtime/resource-publication.d.ts +8 -0
- package/dist/agent-core/runtime/resource-publication.js +60 -0
- package/dist/agent-core/sdk/agent.js +16 -0
- package/dist/agent-core/sdk/model-runtime.js +2 -0
- package/dist/agent-core/sdk/resource-input.d.ts +31 -0
- package/dist/agent-core/sdk/resource-input.js +342 -0
- package/dist/agent-core/sdk/session.js +144 -39
- package/dist/agent-core/session/catalog.d.ts +4 -0
- package/dist/agent-core/session/catalog.js +4 -0
- package/dist/agent-core/session/context-gc-checkpoint.d.ts +6 -4
- package/dist/agent-core/session/context-gc-checkpoint.js +41 -16
- package/dist/agent-core/session/inline-media.d.ts +45 -0
- package/dist/agent-core/session/inline-media.js +163 -0
- package/dist/agent-core/session/store/contracts.d.ts +11 -0
- package/dist/agent-core/session/store/history.d.ts +1 -1
- package/dist/agent-core/session/store/history.js +26 -2
- package/dist/agent-core/session/store/images.d.ts +4 -6
- package/dist/agent-core/session/store/images.js +18 -111
- package/dist/agent-core/session/store/jsonl-store.d.ts +9 -0
- package/dist/agent-core/session/store/jsonl-store.js +100 -28
- package/dist/agent-core/session/store/resources.d.ts +26 -0
- package/dist/agent-core/session/store/resources.js +196 -0
- package/dist/agent-core/session/store/types.d.ts +7 -6
- package/dist/agent-core/session/transaction-recovery.js +1 -1
- package/dist/agent-core/tools/code-mode/cells.js +3 -4
- package/dist/agent-core/tools/code-mode/index.js +32 -8
- package/dist/agent-core/tools/code-mode/observations.d.ts +5 -16
- package/dist/agent-core/tools/code-mode/quickjs-engine.js +23 -26
- package/dist/agent-core/tools/contracts.d.ts +15 -5
- package/dist/agent-core/tools/execution-limits.d.ts +3 -1
- package/dist/agent-core/tools/execution-limits.js +3 -1
- package/dist/agent-core/tools/execution.d.ts +2 -1
- package/dist/agent-core/tools/execution.js +31 -4
- package/dist/agent-core/tools/input-diagnostics/source-preview.d.ts +10 -0
- package/dist/agent-core/tools/input-diagnostics/source-preview.js +64 -0
- package/dist/agent-core/tools/local/apply-patch-parser.js +2 -53
- package/dist/agent-core/tools/local/apply-patch.js +1 -1
- package/dist/agent-core/tools/local/image-processing.d.ts +24 -0
- package/dist/agent-core/tools/local/image-processing.js +135 -0
- package/dist/agent-core/tools/local/read.js +1 -1
- package/dist/agent-core/tools/local/view-image.d.ts +4 -0
- package/dist/agent-core/tools/local/view-image.js +154 -25
- package/dist/agent-core/tools/output-projection.d.ts +2 -4
- package/dist/agent-core/tools/output-projection.js +81 -10
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/agent-core/tools/runtime.js +20 -1
- package/dist/cli/main.js +2 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -1
|
@@ -1,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
|
-
|
|
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 ('
|
|
330
|
-
input.toolImages.push(...result.
|
|
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' &&
|
|
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
|
+
}
|