@xlaunch/llm 0.2.0-beta.1

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
3
+ * adapters from drifting. See
4
+ * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
5
+ *
6
+ * App-attribution vocabulary for provider requests.
7
+ * @module @xlaunch/llm/attribution
8
+ */
9
+ import { createRequire } from 'node:module';
10
+ // The package's own manifest is the single source of the version so the
11
+ // User-Agent cannot drift from what is published (`./package.json` is an
12
+ // export of this package; the relative path resolves from both `src/` and
13
+ // the bundled `lib/`).
14
+ const { version } = createRequire(import.meta.url)('../package.json');
15
+ /**
16
+ * The harness's own identity: the default every adapter sends. Deployments
17
+ * that need a white-label identity pass their own {@link AppIdentity} to
18
+ * {@link attributionHeaders} — omission falls back to this default; nothing
19
+ * can suppress attribution entirely.
20
+ */
21
+ export const APP_IDENTITY = {
22
+ product: 'xlaunch',
23
+ version,
24
+ url: 'https://github.com/Northlatch-Labs-LLC/xlaunch-agent',
25
+ };
26
+ /**
27
+ * The standard `User-Agent` value: `product/version (+url)`. The
28
+ * parenthesized `+url` comment is the conventional self-identification form
29
+ * (RFC 9110 §10.1.5 product + comment syntax).
30
+ * @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
31
+ * @returns the ready-to-send header value.
32
+ */
33
+ export function userAgent(identity = APP_IDENTITY) {
34
+ return `${identity.product}/${identity.version} (+${identity.url})`;
35
+ }
36
+ /**
37
+ * Build the attribution headers an adapter must send on every provider
38
+ * request. Header names are lowercase (HTTP field names are case-insensitive
39
+ * on the wire).
40
+ * @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
41
+ * @returns headers to merge into the provider request (currently just `user-agent`).
42
+ */
43
+ export function attributionHeaders(identity = APP_IDENTITY) {
44
+ return { 'user-agent': userAgent(identity) };
45
+ }
46
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * xlaunch-llm's owned branded ids: tool-call correlation and provider request
3
+ * diagnostics.
4
+ *
5
+ * The `Branded<B>` primitive and stateless constructor live in
6
+ * `@xlaunch/brand` so every owner of a cross-boundary id can brand it
7
+ * without depending on xlaunch-llm; see that package's README for the
8
+ * nominal-typing policy.
9
+ *
10
+ * @module @xlaunch/llm/brand
11
+ */
12
+ import { type Branded } from '@xlaunch/brand';
13
+ /** Stable identity carried by one message across inbox, log, and model-request boundaries. */
14
+ export type MessageId = Branded<'MessageId'>;
15
+ /**
16
+ * Brand a message identifier.
17
+ * @param id - the opaque message identifier.
18
+ * @returns the same string with the message-id brand.
19
+ */
20
+ export declare function MessageId(id: string): MessageId;
21
+ /**
22
+ * Correlates a model-issued tool call with its result. Provider-issued for
23
+ * real adapters; synthesized by mocks/assembler fallbacks.
24
+ */
25
+ export type ToolCallId = Branded<'ToolCallId'>;
26
+ /**
27
+ * Brand a string as a {@link ToolCallId}.
28
+ * @param id - the provider-issued or synthesized call id.
29
+ * @returns the same string with the tool-call-id brand.
30
+ */
31
+ export declare function ToolCallId(id: string): ToolCallId;
32
+ /** Provider-issued request identifier retained for diagnostics across package boundaries. */
33
+ export type ProviderRequestId = Branded<'ProviderRequestId'>;
34
+ /**
35
+ * Brand a provider-issued request identifier.
36
+ * @param id - the opaque provider-issued string.
37
+ * @returns the same string, branded; no validation is performed.
38
+ */
39
+ export declare function ProviderRequestId(id: string): ProviderRequestId;
40
+ /** Identity of one model streaming attempt, unique within one Agent lifecycle. */
41
+ export type LlmAttemptId = Branded<'LlmAttemptId'>;
42
+ /**
43
+ * Brand one loop-owned streaming attempt identifier.
44
+ * @param id - the opaque Agent-lifecycle-local identifier.
45
+ * @returns the same string with the attempt-id brand.
46
+ */
47
+ export declare function LlmAttemptId(id: string): LlmAttemptId;
48
+ /** Adapter-owned identifier for one model's selectable reasoning effort. */
49
+ export type ReasoningEffortId = Branded<'ReasoningEffortId'>;
50
+ /**
51
+ * Brand an adapter-owned reasoning-effort identifier.
52
+ * @param id - the opaque identifier exposed by one model capability.
53
+ * @returns the same string, branded; no validation is performed.
54
+ */
55
+ export declare function ReasoningEffortId(id: string): ReasoningEffortId;
56
+ //# sourceMappingURL=brand.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * xlaunch-llm's owned branded ids: tool-call correlation and provider request
3
+ * diagnostics.
4
+ *
5
+ * The `Branded<B>` primitive and stateless constructor live in
6
+ * `@xlaunch/brand` so every owner of a cross-boundary id can brand it
7
+ * without depending on xlaunch-llm; see that package's README for the
8
+ * nominal-typing policy.
9
+ *
10
+ * @module @xlaunch/llm/brand
11
+ */
12
+ import { brandString } from '@xlaunch/brand';
13
+ /**
14
+ * Brand a message identifier.
15
+ * @param id - the opaque message identifier.
16
+ * @returns the same string with the message-id brand.
17
+ */
18
+ export function MessageId(id) {
19
+ return brandString(id);
20
+ }
21
+ /**
22
+ * Brand a string as a {@link ToolCallId}.
23
+ * @param id - the provider-issued or synthesized call id.
24
+ * @returns the same string with the tool-call-id brand.
25
+ */
26
+ export function ToolCallId(id) {
27
+ return brandString(id);
28
+ }
29
+ /**
30
+ * Brand a provider-issued request identifier.
31
+ * @param id - the opaque provider-issued string.
32
+ * @returns the same string, branded; no validation is performed.
33
+ */
34
+ export function ProviderRequestId(id) {
35
+ return brandString(id);
36
+ }
37
+ /**
38
+ * Brand one loop-owned streaming attempt identifier.
39
+ * @param id - the opaque Agent-lifecycle-local identifier.
40
+ * @returns the same string with the attempt-id brand.
41
+ */
42
+ export function LlmAttemptId(id) {
43
+ return brandString(id);
44
+ }
45
+ /**
46
+ * Brand an adapter-owned reasoning-effort identifier.
47
+ * @param id - the opaque identifier exposed by one model capability.
48
+ * @returns the same string, branded; no validation is performed.
49
+ */
50
+ export function ReasoningEffortId(id) {
51
+ return brandString(id);
52
+ }
53
+ //# sourceMappingURL=brand.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Conversation call configuration and freeze utilities. Provider routing,
3
+ * model, reasoning effort, and sampling values are request-header state that
4
+ * can affect cache reuse; request waterfalls replace them and the loop logs
5
+ * changed snapshots instead of allowing silent per-call drift.
6
+ * @module xlaunch-llm/call-config
7
+ */
8
+ import type { GenerateOptions } from './types.ts';
9
+ import type { ReasoningEffortId } from './brand.ts';
10
+ /**
11
+ * Provider, model, reasoning effort, and sampling scalars of one conversation's
12
+ * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field;
13
+ * the loop builds requests from the logged header rather than accepting these
14
+ * per call.
15
+ */
16
+ export interface LlmCallConfig {
17
+ provider: string;
18
+ model: string;
19
+ reasoningEffort?: ReasoningEffortId;
20
+ temperature?: number;
21
+ maxTokens?: number;
22
+ stop?: string[];
23
+ }
24
+ /**
25
+ * Effective config fields supplied by exact-model adapter resolution rather
26
+ * than by the caller's request proposal.
27
+ */
28
+ export interface LlmCallConfigAdapterDefaults {
29
+ reasoningEffort?: true;
30
+ maxTokens?: true;
31
+ }
32
+ /**
33
+ * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
34
+ * runs to decide whether a proposed configuration is a real change (worth a
35
+ * logged header snapshot) or the held one restated.
36
+ * @param a - one configuration.
37
+ * @param b - the other.
38
+ * @returns whether every field (including the `stop` list, element-wise) matches.
39
+ */
40
+ export declare function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean;
41
+ /**
42
+ * Mark one exact request object as assembled by xlaunch-agent-loop.
43
+ * @param request - loop-owned request envelope before LLM dispatch.
44
+ * @returns the same request object marked as created by the process-local agent loop.
45
+ */
46
+ export declare function markAgentLoopRequest<T extends GenerateOptions>(request: T): T;
47
+ /**
48
+ * Test whether the exact request object was assembled by xlaunch-agent-loop.
49
+ * @param request - request envelope observed at the LLM waterfall.
50
+ * @returns whether {@link markAgentLoopRequest} recorded this object.
51
+ */
52
+ export declare function isAgentLoopRequest(request: GenerateOptions): boolean;
53
+ //# sourceMappingURL=call-config.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Conversation call configuration and freeze utilities. Provider routing,
3
+ * model, reasoning effort, and sampling values are request-header state that
4
+ * can affect cache reuse; request waterfalls replace them and the loop logs
5
+ * changed snapshots instead of allowing silent per-call drift.
6
+ * @module xlaunch-llm/call-config
7
+ */
8
+ /** Process-local identities of request objects assembled by xlaunch-agent-loop. */
9
+ const AGENT_LOOP_REQUESTS = new WeakSet();
10
+ /**
11
+ * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
12
+ * runs to decide whether a proposed configuration is a real change (worth a
13
+ * logged header snapshot) or the held one restated.
14
+ * @param a - one configuration.
15
+ * @param b - the other.
16
+ * @returns whether every field (including the `stop` list, element-wise) matches.
17
+ */
18
+ export function callConfigEquals(a, b) {
19
+ if (a.provider !== b.provider
20
+ || a.model !== b.model
21
+ || a.reasoningEffort !== b.reasoningEffort
22
+ || a.temperature !== b.temperature
23
+ || a.maxTokens !== b.maxTokens)
24
+ return false;
25
+ if (a.stop === undefined || b.stop === undefined)
26
+ return a.stop === b.stop;
27
+ return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
28
+ }
29
+ /**
30
+ * Mark one exact request object as assembled by xlaunch-agent-loop.
31
+ * @param request - loop-owned request envelope before LLM dispatch.
32
+ * @returns the same request object marked as created by the process-local agent loop.
33
+ */
34
+ export function markAgentLoopRequest(request) {
35
+ AGENT_LOOP_REQUESTS.add(request);
36
+ return request;
37
+ }
38
+ /**
39
+ * Test whether the exact request object was assembled by xlaunch-agent-loop.
40
+ * @param request - request envelope observed at the LLM waterfall.
41
+ * @returns whether {@link markAgentLoopRequest} recorded this object.
42
+ */
43
+ export function isAgentLoopRequest(request) {
44
+ return AGENT_LOOP_REQUESTS.has(request);
45
+ }
46
+ //# sourceMappingURL=call-config.js.map
@@ -0,0 +1,130 @@
1
+ /** Content-block structure helpers. @module @xlaunch/llm/content */
2
+ import type { ContentBlock } from './types.ts';
3
+ import type { Message } from './message.ts';
4
+ import type { AttachmentStore, FileAttachmentRef, ImageAttachmentRef, RequestImageAttachment } from '@xlaunch/attachment';
5
+ /** Execution-world path that model tools can use to read one normalized attachment. */
6
+ export interface ImageAttachmentAccess {
7
+ /** Absolute path to immutable normalized bytes; callers must treat it as read-only. */
8
+ readonlyPath: string;
9
+ }
10
+ /**
11
+ * Resolve current execution-world access for one durable image reference.
12
+ * @param ref - durable normalized attachment reference.
13
+ * @returns a read-only execution-world path, or undefined when unavailable.
14
+ */
15
+ export type ImageAttachmentAccessResolver = (ref: ImageAttachmentRef) => ImageAttachmentAccess | undefined;
16
+ /**
17
+ * Bridge one attachment provider's host object location into the mounted
18
+ * tool execution world. The consumer supplies the current filesystem
19
+ * provider's mapping without making attachment or LLM definitions depend on it.
20
+ * @param attachments - provider that owns the normalized attachment object.
21
+ * @param mapHostPath - map one absolute host path into the current tool execution world.
22
+ * @param ref - durable normalized attachment reference.
23
+ * @returns a read-only execution-world path, or undefined when either provider exposes no mapping.
24
+ * @throws an attachment error when the durable reference is invalid.
25
+ */
26
+ export declare function resolveImageAttachmentAccess(attachments: AttachmentStore, mapHostPath: (hostPath: string) => string | undefined, ref: ImageAttachmentRef): ImageAttachmentAccess | undefined;
27
+ /**
28
+ * Stable text shown to a model that cannot accept one durable image reference.
29
+ * @param ref - durable normalized attachment omitted from the request.
30
+ * @returns deterministic text-only placeholder.
31
+ */
32
+ export declare function textOnlyImageText(ref: ImageAttachmentRef): string;
33
+ /**
34
+ * Stable model-facing handle for one exact request image. Identity comes from
35
+ * the occurrence's own durable reference: request versions are prepared per
36
+ * attachment id, so one shared version may serve occurrences whose display
37
+ * names differ.
38
+ * @param ref - the occurrence's durable normalized attachment.
39
+ * @param version - exact request-image dimensions shown beside the text.
40
+ * @param access - optional path resolved for the current tool execution world.
41
+ * @returns attachment handle and request-image dimensions.
42
+ */
43
+ export declare function requestImageHandleText(ref: ImageAttachmentRef, version: Pick<RequestImageAttachment, 'width' | 'height'>, access?: ImageAttachmentAccess): string;
44
+ /**
45
+ * Stable per-image placeholder for a request-limit omission.
46
+ * @param ref - durable normalized attachment omitted from this request.
47
+ * @param access - optional provider-resolved path for model tools.
48
+ * @returns identity, normalized metadata, and the available recovery path.
49
+ */
50
+ export declare function offloadedImageText(ref: ImageAttachmentRef, access?: ImageAttachmentAccess): string;
51
+ /**
52
+ * True when typed model content contains an image block, walking nested
53
+ * tool-result content. This is the one recursive image walk shared by every
54
+ * image policy (capability gating, text-only serialization, compaction
55
+ * survey), so a consumer cannot silently diverge on nesting depth.
56
+ * @param content - typed model content blocks.
57
+ * @returns whether any nested block is an image.
58
+ */
59
+ export declare function contentHasImage(content: readonly ContentBlock[]): boolean;
60
+ /**
61
+ * True when typed model content contains a file block, walking nested
62
+ * tool-result content on the same recursion every file policy shares.
63
+ * @param content - typed model content blocks.
64
+ * @returns whether any nested block is a file.
65
+ */
66
+ export declare function contentHasFile(content: readonly ContentBlock[]): boolean;
67
+ /**
68
+ * Stable model-facing handle for one durable file reference: the address of
69
+ * the verbatim stored copy and the instruction to read it on demand. This is
70
+ * the only representation a provider ever receives for a file.
71
+ * @param ref - durable verbatim file reference.
72
+ * @param readonlyPath - execution-world path of the stored copy, when resolvable.
73
+ * @returns deterministic handle text naming the file, its size, and its address.
74
+ */
75
+ export declare function fileHandleText(ref: FileAttachmentRef, readonlyPath: string | undefined): string;
76
+ /**
77
+ * Project durable file history into deterministic handle text for every model
78
+ * route. Unlike images, no provider receives file blocks natively, so this
79
+ * projection is unconditional in request assembly.
80
+ * @param messages - complete request history.
81
+ * @param resolvePath - resolve one reference's current execution-world read path.
82
+ * @returns the original list without files, otherwise shallow message copies with handle text.
83
+ */
84
+ export declare function projectFilesToText(messages: readonly Message[], resolvePath: (ref: FileAttachmentRef) => string | undefined): readonly Message[];
85
+ /** Byte accounting and quantized removal policy for one request representation. */
86
+ export interface RequestImageOffloadPolicy {
87
+ /** Image count accepted by the route; omission leaves count unbounded. */
88
+ maxImages?: number;
89
+ /** Accumulated image bytes accepted by the route; omission leaves bytes unbounded. */
90
+ maxBytes?: number;
91
+ /** Number of excess images removed as one deterministic step. */
92
+ countQuantum?: number;
93
+ /** Number of excess bytes removed as one deterministic step. */
94
+ byteQuantum?: number;
95
+ /** Whether byte accounting uses raw file bytes or inline base64 length. */
96
+ representation: 'raw' | 'base64';
97
+ /** Resolve the encoded request-version length; omission uses normalized attachment bytes. */
98
+ byteLength?: (ref: ImageAttachmentRef) => number;
99
+ /** Build the model-visible replacement for each omitted attachment. */
100
+ placeholder: (ref: ImageAttachmentRef) => string;
101
+ }
102
+ /**
103
+ * Project durable image history into deterministic text for an exact text-only model.
104
+ * @param messages - complete request history.
105
+ * @returns the original list without images, otherwise shallow message copies with stable placeholders.
106
+ */
107
+ export declare function projectImagesForTextModel(messages: readonly Message[]): readonly Message[];
108
+ /**
109
+ * Number of oldest image occurrences one request projection removes, in whole
110
+ * count and byte quanta, once a route budget is exceeded. The result depends
111
+ * only on the represented lengths, so provider request pricing reproduces the
112
+ * exact serialization decision without building the projected messages.
113
+ * @param lengths - represented byte length of every occurrence, in request order.
114
+ * @param policy - count/byte budgets and removal quanta; unbounded when absent.
115
+ * @returns how many leading occurrences the projection replaces with placeholders.
116
+ */
117
+ export declare function offloadedImagePrefixCount(lengths: readonly number[], policy: Pick<RequestImageOffloadPolicy, 'maxImages' | 'maxBytes' | 'countQuantum' | 'byteQuantum'>): number;
118
+ /**
119
+ * Return a deterministic transient projection whose oldest images are replaced
120
+ * in whole count and byte quanta after a route budget is exceeded. The target
121
+ * depends only on complete durable history: at 129 one-megabyte images under
122
+ * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
123
+ * 64 MiB remain; that removed prefix stays fixed until total history exceeds
124
+ * 192 MiB.
125
+ * @param messages - complete request history, oldest first.
126
+ * @param policy - route representation, budgets, and removal quanta.
127
+ * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
128
+ */
129
+ export declare function offloadRequestImagesWithPolicy(messages: readonly Message[], policy: RequestImageOffloadPolicy): readonly Message[];
130
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1,284 @@
1
+ /** Content-block structure helpers. @module @xlaunch/llm/content */
2
+ import { assertNever } from '@xlaunch/util-values';
3
+ /**
4
+ * Bridge one attachment provider's host object location into the mounted
5
+ * tool execution world. The consumer supplies the current filesystem
6
+ * provider's mapping without making attachment or LLM definitions depend on it.
7
+ * @param attachments - provider that owns the normalized attachment object.
8
+ * @param mapHostPath - map one absolute host path into the current tool execution world.
9
+ * @param ref - durable normalized attachment reference.
10
+ * @returns a read-only execution-world path, or undefined when either provider exposes no mapping.
11
+ * @throws an attachment error when the durable reference is invalid.
12
+ */
13
+ export function resolveImageAttachmentAccess(attachments, mapHostPath, ref) {
14
+ const hostPath = attachments.imageHostPath(ref);
15
+ if (hostPath === undefined)
16
+ return undefined;
17
+ const readonlyPath = mapHostPath(hostPath);
18
+ return readonlyPath === undefined ? undefined : { readonlyPath };
19
+ }
20
+ function quoted(value) {
21
+ return JSON.stringify(value);
22
+ }
23
+ function imageIdentity(ref) {
24
+ return ref.name === undefined
25
+ ? String(ref.attachmentId)
26
+ : `${quoted(ref.name)} (${ref.attachmentId})`;
27
+ }
28
+ function extension(mediaType) {
29
+ switch (mediaType) {
30
+ case 'image/png': return '.png';
31
+ case 'image/jpeg': return '.jpg';
32
+ case 'image/webp': return '.webp';
33
+ case 'image/gif': return '.gif';
34
+ default: return assertNever(mediaType, 'image extension');
35
+ }
36
+ }
37
+ function normalizedAccessText(ref, access) {
38
+ return ` Normalized copy (read-only; may be resized or re-encoded): ${quoted(access.readonlyPath)} (${ref.width}x${ref.height}px, ${ref.mediaType}).`
39
+ + ' Source dimensions, format, and byte size may differ.'
40
+ + ` Copy to a writable path ending in ${extension(ref.mediaType)} before editing.`;
41
+ }
42
+ /**
43
+ * Stable text shown to a model that cannot accept one durable image reference.
44
+ * @param ref - durable normalized attachment omitted from the request.
45
+ * @returns deterministic text-only placeholder.
46
+ */
47
+ export function textOnlyImageText(ref) {
48
+ const digest = String(ref.attachmentId).slice('sha256:'.length, 'sha256:'.length + 8);
49
+ return `[image omitted because this model accepts text only; attachment sha256:${digest}]`;
50
+ }
51
+ /**
52
+ * Stable model-facing handle for one exact request image. Identity comes from
53
+ * the occurrence's own durable reference: request versions are prepared per
54
+ * attachment id, so one shared version may serve occurrences whose display
55
+ * names differ.
56
+ * @param ref - the occurrence's durable normalized attachment.
57
+ * @param version - exact request-image dimensions shown beside the text.
58
+ * @param access - optional path resolved for the current tool execution world.
59
+ * @returns attachment handle and request-image dimensions.
60
+ */
61
+ export function requestImageHandleText(ref, version, access) {
62
+ const preview = `Image ${imageIdentity(ref)}; request preview ${version.width}x${version.height}px.`;
63
+ return access === undefined
64
+ ? `${preview} It may be resized or re-encoded; source dimensions, format, and byte size may differ.`
65
+ : preview + normalizedAccessText(ref, access);
66
+ }
67
+ /**
68
+ * Stable per-image placeholder for a request-limit omission.
69
+ * @param ref - durable normalized attachment omitted from this request.
70
+ * @param access - optional provider-resolved path for model tools.
71
+ * @returns identity, normalized metadata, and the available recovery path.
72
+ */
73
+ export function offloadedImageText(ref, access) {
74
+ const identity = `image omitted to fit request image limits; ${imageIdentity(ref)}.`;
75
+ if (access === undefined) {
76
+ return `[${identity} No local normalized image path is available; ask the user to attach it again if needed.]`;
77
+ }
78
+ return `[${identity}${normalizedAccessText(ref, access)}]`;
79
+ }
80
+ /**
81
+ * True when typed model content contains an image block, walking nested
82
+ * tool-result content. This is the one recursive image walk shared by every
83
+ * image policy (capability gating, text-only serialization, compaction
84
+ * survey), so a consumer cannot silently diverge on nesting depth.
85
+ * @param content - typed model content blocks.
86
+ * @returns whether any nested block is an image.
87
+ */
88
+ export function contentHasImage(content) {
89
+ return content.some(block => block.type === 'image'
90
+ || (block.type === 'tool-result' && contentHasImage(block.content)));
91
+ }
92
+ /**
93
+ * True when typed model content contains a file block, walking nested
94
+ * tool-result content on the same recursion every file policy shares.
95
+ * @param content - typed model content blocks.
96
+ * @returns whether any nested block is a file.
97
+ */
98
+ export function contentHasFile(content) {
99
+ return content.some(block => block.type === 'file'
100
+ || (block.type === 'tool-result' && contentHasFile(block.content)));
101
+ }
102
+ /**
103
+ * Stable model-facing handle for one durable file reference: the address of
104
+ * the verbatim stored copy and the instruction to read it on demand. This is
105
+ * the only representation a provider ever receives for a file.
106
+ * @param ref - durable verbatim file reference.
107
+ * @param readonlyPath - execution-world path of the stored copy, when resolvable.
108
+ * @returns deterministic handle text naming the file, its size, and its address.
109
+ */
110
+ export function fileHandleText(ref, readonlyPath) {
111
+ const digest = String(ref.attachmentId).slice('sha256:'.length, 'sha256:'.length + 8);
112
+ const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
113
+ if (readonlyPath === undefined) {
114
+ return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
115
+ }
116
+ return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
117
+ }
118
+ /** Replace every file occurrence, including nested tool results, with handle text. */
119
+ function replaceFilesWithHandles(blocks, resolvePath) {
120
+ let next;
121
+ for (const [index, block] of blocks.entries()) {
122
+ if (block.type === 'file') {
123
+ next ??= blocks.slice(0, index);
124
+ next.push({ type: 'text', text: fileHandleText(block.attachment, resolvePath(block.attachment)) });
125
+ continue;
126
+ }
127
+ if (block.type === 'tool-result') {
128
+ const content = replaceFilesWithHandles(block.content, resolvePath);
129
+ if (content !== block.content) {
130
+ next ??= blocks.slice(0, index);
131
+ next.push({ ...block, content });
132
+ continue;
133
+ }
134
+ }
135
+ next?.push(block);
136
+ }
137
+ return next ?? blocks;
138
+ }
139
+ /**
140
+ * Project durable file history into deterministic handle text for every model
141
+ * route. Unlike images, no provider receives file blocks natively, so this
142
+ * projection is unconditional in request assembly.
143
+ * @param messages - complete request history.
144
+ * @param resolvePath - resolve one reference's current execution-world read path.
145
+ * @returns the original list without files, otherwise shallow message copies with handle text.
146
+ */
147
+ export function projectFilesToText(messages, resolvePath) {
148
+ if (!messages.some(message => contentHasFile(message.content)))
149
+ return messages;
150
+ return messages.map((message) => {
151
+ const content = replaceFilesWithHandles(message.content, resolvePath);
152
+ return content === message.content ? message : { ...message, content };
153
+ });
154
+ }
155
+ /** Base64 length of raw image bytes, including padding. */
156
+ function base64Length(bytes) {
157
+ return Math.ceil(bytes / 3) * 4;
158
+ }
159
+ /** Collect represented image lengths in request and nested-block order. */
160
+ function collectImageLengths(blocks, lengths, policy) {
161
+ for (const block of blocks) {
162
+ if (block.type === 'image') {
163
+ const bytes = policy.byteLength === undefined
164
+ ? block.attachment.bytes
165
+ : policy.byteLength(block.attachment);
166
+ lengths.push(policy.representation === 'base64' ? base64Length(bytes) : bytes);
167
+ }
168
+ else if (block.type === 'tool-result') {
169
+ collectImageLengths(block.content, lengths, policy);
170
+ }
171
+ }
172
+ }
173
+ /** Replace the first `remaining.count` image occurrences without mutating durable messages. */
174
+ function replaceOldestImages(blocks, remaining, placeholder) {
175
+ let next;
176
+ for (const [index, block] of blocks.entries()) {
177
+ if (block.type === 'image' && remaining.count > 0) {
178
+ remaining.count -= 1;
179
+ next ??= blocks.slice(0, index);
180
+ next.push({ type: 'text', text: placeholder(block.attachment) });
181
+ continue;
182
+ }
183
+ if (block.type === 'tool-result') {
184
+ const content = replaceOldestImages(block.content, remaining, placeholder);
185
+ if (content !== block.content) {
186
+ next ??= blocks.slice(0, index);
187
+ next.push({ ...block, content });
188
+ continue;
189
+ }
190
+ }
191
+ next?.push(block);
192
+ }
193
+ return next ?? blocks;
194
+ }
195
+ /** Replace every image occurrence, including nested tool results, for a text-only model. */
196
+ function replaceImagesForTextModel(blocks) {
197
+ let next;
198
+ for (const [index, block] of blocks.entries()) {
199
+ if (block.type === 'image') {
200
+ next ??= blocks.slice(0, index);
201
+ next.push({ type: 'text', text: textOnlyImageText(block.attachment) });
202
+ continue;
203
+ }
204
+ if (block.type === 'tool-result') {
205
+ const content = replaceImagesForTextModel(block.content);
206
+ if (content !== block.content) {
207
+ next ??= blocks.slice(0, index);
208
+ next.push({ ...block, content });
209
+ continue;
210
+ }
211
+ }
212
+ next?.push(block);
213
+ }
214
+ return next ?? blocks;
215
+ }
216
+ /**
217
+ * Project durable image history into deterministic text for an exact text-only model.
218
+ * @param messages - complete request history.
219
+ * @returns the original list without images, otherwise shallow message copies with stable placeholders.
220
+ */
221
+ export function projectImagesForTextModel(messages) {
222
+ if (!messages.some(message => contentHasImage(message.content)))
223
+ return messages;
224
+ return messages.map((message) => {
225
+ const content = replaceImagesForTextModel(message.content);
226
+ return content === message.content ? message : { ...message, content };
227
+ });
228
+ }
229
+ /**
230
+ * Number of oldest image occurrences one request projection removes, in whole
231
+ * count and byte quanta, once a route budget is exceeded. The result depends
232
+ * only on the represented lengths, so provider request pricing reproduces the
233
+ * exact serialization decision without building the projected messages.
234
+ * @param lengths - represented byte length of every occurrence, in request order.
235
+ * @param policy - count/byte budgets and removal quanta; unbounded when absent.
236
+ * @returns how many leading occurrences the projection replaces with placeholders.
237
+ */
238
+ export function offloadedImagePrefixCount(lengths, policy) {
239
+ const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
240
+ const excessCount = policy.maxImages === undefined ? 0 : Math.max(0, lengths.length - policy.maxImages);
241
+ const excessBytes = policy.maxBytes === undefined ? 0 : Math.max(0, total - policy.maxBytes);
242
+ if (excessCount === 0 && excessBytes === 0)
243
+ return 0;
244
+ const countQuantum = policy.countQuantum ?? 1;
245
+ const byteQuantum = policy.byteQuantum ?? 1;
246
+ const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
247
+ const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
248
+ let count = 0;
249
+ let removedBytes = 0;
250
+ for (const imageBytes of lengths) {
251
+ const byteTargetMet = removeBytes === 0
252
+ || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes);
253
+ if (count >= removeCount && byteTargetMet)
254
+ break;
255
+ removedBytes += imageBytes;
256
+ count += 1;
257
+ }
258
+ return count;
259
+ }
260
+ /**
261
+ * Return a deterministic transient projection whose oldest images are replaced
262
+ * in whole count and byte quanta after a route budget is exceeded. The target
263
+ * depends only on complete durable history: at 129 one-megabyte images under
264
+ * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
265
+ * 64 MiB remain; that removed prefix stays fixed until total history exceeds
266
+ * 192 MiB.
267
+ * @param messages - complete request history, oldest first.
268
+ * @param policy - route representation, budgets, and removal quanta.
269
+ * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
270
+ */
271
+ export function offloadRequestImagesWithPolicy(messages, policy) {
272
+ const lengths = [];
273
+ for (const message of messages)
274
+ collectImageLengths(message.content, lengths, policy);
275
+ const count = offloadedImagePrefixCount(lengths, policy);
276
+ if (count === 0)
277
+ return messages;
278
+ const remaining = { count };
279
+ return messages.map((message) => {
280
+ const content = replaceOldestImages(message.content, remaining, policy.placeholder);
281
+ return content === message.content ? message : { ...message, content };
282
+ });
283
+ }
284
+ //# sourceMappingURL=content.js.map