@lensagent/core 0.1.0 → 0.1.2
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/README.md +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +18 -8
- package/proto/executor.proto +69 -55
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { type ArtifactOutput } from '../core/domain.js';
|
|
1
|
+
import { type ArtifactOutput, type Json } from '../core/domain.js';
|
|
2
2
|
import type { Document, Store } from '../store/store.js';
|
|
3
3
|
export declare class ArtifactStore {
|
|
4
|
+
#private;
|
|
4
5
|
readonly store: Store;
|
|
5
6
|
readonly mode: 'filesystem' | 'content_addressed';
|
|
6
7
|
readonly root: string;
|
|
7
8
|
constructor(root: string, directory: string, store: Store, mode: 'filesystem' | 'content_addressed');
|
|
8
9
|
save(projectId: string, artifacts: readonly ArtifactOutput[]): Promise<readonly string[]>;
|
|
9
10
|
read(artifact: Document): Promise<Buffer>;
|
|
11
|
+
readRange(artifact: Document, offset: number, limit: number, signal?: AbortSignal): Promise<Json>;
|
|
12
|
+
search(artifact: Document, query: string, offset: number, limit: number, signal?: AbortSignal): Promise<Json>;
|
|
10
13
|
remove(relativePaths: readonly string[]): Promise<{
|
|
11
14
|
readonly deleted: number;
|
|
12
15
|
readonly failed: number;
|
|
13
16
|
}>;
|
|
14
17
|
resolve(relativePath: string): string;
|
|
15
18
|
}
|
|
19
|
+
/** UTF-8 byte offsets; advance a non-boundary start, never split a code point. */
|
|
20
|
+
export declare function utf8Page(bytes: Buffer, offset: number, limit: number, total: number): {
|
|
21
|
+
content: string;
|
|
22
|
+
offset: number;
|
|
23
|
+
end_offset: number;
|
|
24
|
+
total_bytes: number;
|
|
25
|
+
has_more: boolean;
|
|
26
|
+
next_offset: number | null;
|
|
27
|
+
};
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
1
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir,
|
|
5
|
+
import { mkdir, stat, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
6
|
import { basename, dirname, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import {
|
|
7
|
+
import { createGunzip, gzipSync } from 'node:zlib';
|
|
5
8
|
import { fail } from '../core/domain.js';
|
|
9
|
+
const maximumArtifactBytes = 64 * 1024 * 1024;
|
|
6
10
|
export class ArtifactStore {
|
|
7
11
|
store;
|
|
8
12
|
mode;
|
|
9
13
|
root;
|
|
14
|
+
#fileTail = Promise.resolve();
|
|
10
15
|
constructor(root, directory, store, mode) {
|
|
11
16
|
this.store = store;
|
|
12
17
|
this.mode = mode;
|
|
@@ -15,7 +20,12 @@ export class ArtifactStore {
|
|
|
15
20
|
// Bytes are gzipped on disk; `sha256`/`size_bytes` describe the original, uncompressed content
|
|
16
21
|
// (so identical logical content still dedups by hash regardless of compression, and API
|
|
17
22
|
// consumers see the size they'll actually get back from read()) rather than the stored form.
|
|
18
|
-
|
|
23
|
+
save(projectId, artifacts) {
|
|
24
|
+
return this.#mutate(() => this.#save(projectId, artifacts));
|
|
25
|
+
}
|
|
26
|
+
async #save(projectId, artifacts) {
|
|
27
|
+
if (artifacts.some((artifact) => artifact.bytes.byteLength > maximumArtifactBytes))
|
|
28
|
+
fail('ARTIFACT_WRITE_LIMIT', 413, 'Artifact exceeds the 64 MiB verification limit.');
|
|
19
29
|
const ids = [];
|
|
20
30
|
for (const artifact of artifacts) {
|
|
21
31
|
const sha256 = createHash('sha256').update(artifact.bytes).digest('hex');
|
|
@@ -38,15 +48,106 @@ export class ArtifactStore {
|
|
|
38
48
|
}
|
|
39
49
|
return ids;
|
|
40
50
|
}
|
|
51
|
+
/** Verify the entire object before releasing any requested bytes. Memory is bounded by the selected range. */
|
|
52
|
+
async #scan(artifact, consume, signal) {
|
|
53
|
+
const maximum = maximumArtifactBytes;
|
|
54
|
+
const size = Number(artifact.size_bytes);
|
|
55
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
56
|
+
fail('ARTIFACT_METADATA_INVALID', 503, 'Invalid Artifact size.');
|
|
57
|
+
if (size > maximum)
|
|
58
|
+
fail('ARTIFACT_READ_LIMIT', 413, 'Artifact exceeds the 64 MiB verification limit.');
|
|
59
|
+
const path = this.resolve(string(artifact.relative_path));
|
|
60
|
+
const hash = createHash('sha256');
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
const boundedSignal = AbortSignal.any([AbortSignal.timeout(15000), ...(signal === undefined ? [] : [signal])]);
|
|
63
|
+
try {
|
|
64
|
+
if ((await stat(path)).size > maximum + 65536)
|
|
65
|
+
fail('ARTIFACT_READ_LIMIT', 413, 'Compressed Artifact exceeds the read limit.');
|
|
66
|
+
await pipeline(createReadStream(path), createGunzip(), new Writable({
|
|
67
|
+
write(chunk, _encoding, done) {
|
|
68
|
+
try {
|
|
69
|
+
const offset = bytes;
|
|
70
|
+
bytes += chunk.length;
|
|
71
|
+
if (bytes > size || bytes > maximum)
|
|
72
|
+
fail('ARTIFACT_INTEGRITY_FAILED', 422, 'Artifact decompressed size exceeds its declaration.');
|
|
73
|
+
hash.update(chunk);
|
|
74
|
+
consume(chunk, offset);
|
|
75
|
+
done();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
done(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}), { signal: boundedSignal });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (boundedSignal.aborted)
|
|
85
|
+
throw boundedSignal.reason;
|
|
86
|
+
const code = error.code;
|
|
87
|
+
if (code === 'ENOENT')
|
|
88
|
+
fail('ARTIFACT_MISSING', 404, 'Artifact bytes are no longer available.');
|
|
89
|
+
if (code?.startsWith('Z_'))
|
|
90
|
+
fail('ARTIFACT_INTEGRITY_FAILED', 422, 'Artifact compression is corrupt.');
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (bytes !== size || hash.digest('hex') !== artifact.sha256)
|
|
94
|
+
fail('ARTIFACT_INTEGRITY_FAILED', 422, 'Artifact size or SHA-256 does not match.');
|
|
95
|
+
}
|
|
41
96
|
async read(artifact) {
|
|
42
|
-
|
|
97
|
+
const chunks = [];
|
|
98
|
+
await this.#scan(artifact, (chunk) => { chunks.push(chunk); });
|
|
99
|
+
return Buffer.concat(chunks);
|
|
100
|
+
}
|
|
101
|
+
async readRange(artifact, offset, limit, signal) {
|
|
102
|
+
checkRange(offset, limit, 3000, 4);
|
|
103
|
+
const chunks = [];
|
|
104
|
+
await this.#scan(artifact, (chunk, start) => {
|
|
105
|
+
const from = Math.max(offset - start, 0);
|
|
106
|
+
const to = Math.min(offset + limit + 4 - start, chunk.length);
|
|
107
|
+
if (to > from)
|
|
108
|
+
chunks.push(chunk.subarray(from, to));
|
|
109
|
+
}, signal);
|
|
110
|
+
return { artifact_id: string(artifact.id), sha256: string(artifact.sha256), integrity: 'verified', capture: artifact.capture ?? 'unknown', stored: 'full', view: 'range',
|
|
111
|
+
...utf8Page(Buffer.concat(chunks), offset, limit, Number(artifact.size_bytes)) };
|
|
112
|
+
}
|
|
113
|
+
async search(artifact, query, offset, limit, signal) {
|
|
114
|
+
checkRange(offset, limit, 10, 1);
|
|
115
|
+
const needle = Buffer.from(query);
|
|
116
|
+
if (needle.length === 0 || needle.length > 256)
|
|
117
|
+
fail('INVALID_INPUT', 400, 'Search query must be 1-256 UTF-8 bytes.');
|
|
118
|
+
let tail = Buffer.alloc(0);
|
|
119
|
+
const matches = [];
|
|
120
|
+
await this.#scan(artifact, (chunk, start) => {
|
|
121
|
+
const data = Buffer.concat([tail, chunk]);
|
|
122
|
+
const base = start - tail.length;
|
|
123
|
+
let index = data.indexOf(needle, Math.max(0, offset - base));
|
|
124
|
+
while (index >= 0) {
|
|
125
|
+
if (matches.length <= limit)
|
|
126
|
+
matches.push({ offset: base + index, text: query });
|
|
127
|
+
else
|
|
128
|
+
break;
|
|
129
|
+
index = data.indexOf(needle, index + 1);
|
|
130
|
+
}
|
|
131
|
+
tail = data.subarray(Math.max(0, data.length - needle.length + 1));
|
|
132
|
+
}, signal);
|
|
133
|
+
return { artifact_id: string(artifact.id), sha256: string(artifact.sha256), integrity: 'verified', capture: artifact.capture ?? 'unknown', stored: 'full', view: 'search', query, matches: matches.slice(0, limit),
|
|
134
|
+
next_offset: matches.length > limit ? (matches[limit - 1]?.offset ?? offset) + 1 : null, has_more: matches.length > limit };
|
|
43
135
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
136
|
+
remove(relativePaths) {
|
|
137
|
+
return this.#mutate(async () => {
|
|
138
|
+
// A formerly orphaned content-addressed object may have acquired a new owner since purge.
|
|
139
|
+
const paths = relativePaths.filter(path => !this.store.artifactPathReferenced(path));
|
|
140
|
+
const results = await Promise.allSettled(paths.map((entry) => unlink(this.resolve(entry))));
|
|
141
|
+
return {
|
|
142
|
+
deleted: results.filter((entry) => entry.status === 'fulfilled' || fileMissing(entry)).length,
|
|
143
|
+
failed: results.filter((entry) => entry.status === 'rejected' && !fileMissing(entry)).length
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
#mutate(action) {
|
|
148
|
+
const result = this.#fileTail.then(action);
|
|
149
|
+
this.#fileTail = result.catch(() => undefined);
|
|
150
|
+
return result;
|
|
50
151
|
}
|
|
51
152
|
resolve(relativePath) {
|
|
52
153
|
const path = resolve(this.root, relativePath);
|
|
@@ -70,3 +171,29 @@ function string(value) {
|
|
|
70
171
|
fail('DATABASE_CORRUPT', 503, 'Artifact metadata is invalid.');
|
|
71
172
|
return value;
|
|
72
173
|
}
|
|
174
|
+
/** UTF-8 byte offsets; advance a non-boundary start, never split a code point. */
|
|
175
|
+
export function utf8Page(bytes, offset, limit, total) {
|
|
176
|
+
if (offset > total)
|
|
177
|
+
fail('INVALID_INPUT', 400, 'Offset is past the end of the object.');
|
|
178
|
+
let start = 0;
|
|
179
|
+
while (start < bytes.length && ((bytes[start] ?? 0) & 0xc0) === 0x80)
|
|
180
|
+
start++;
|
|
181
|
+
let end = Math.min(bytes.length, limit);
|
|
182
|
+
while (end > start && end < bytes.length && ((bytes[end] ?? 0) & 0xc0) === 0x80)
|
|
183
|
+
end--;
|
|
184
|
+
const next = offset + end;
|
|
185
|
+
let content;
|
|
186
|
+
try {
|
|
187
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes.subarray(start, end));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
fail('ARTIFACT_NOT_TEXT', 422, 'Requested range is not valid UTF-8 text; use the binary download API.');
|
|
191
|
+
}
|
|
192
|
+
return { content,
|
|
193
|
+
offset: offset + start, end_offset: next, total_bytes: total, has_more: next < total, next_offset: next < total ? next : null };
|
|
194
|
+
}
|
|
195
|
+
function checkRange(offset, limit, maximum, minimum) {
|
|
196
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < minimum || limit > maximum) {
|
|
197
|
+
fail('INVALID_INPUT', 400, 'Invalid read/search range.');
|
|
198
|
+
}
|
|
199
|
+
}
|
package/dist/runtime/cache.d.ts
CHANGED
|
@@ -1,20 +1,63 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface
|
|
1
|
+
import type { Json, ModelMessage } from '../core/domain.js';
|
|
2
|
+
export interface PrefixEntry {
|
|
3
3
|
readonly ordinal: number;
|
|
4
|
-
readonly container: 'message' | 'tool';
|
|
5
|
-
readonly stability: 'stable' | 'dynamic' | 'unknown';
|
|
6
|
-
readonly tokens: number;
|
|
7
|
-
readonly digest: string;
|
|
8
4
|
readonly prefixDigest: string;
|
|
9
|
-
readonly
|
|
10
|
-
readonly
|
|
5
|
+
readonly prefixTokens: number;
|
|
6
|
+
readonly blocks: number;
|
|
11
7
|
}
|
|
12
|
-
export interface
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly hintsApplied: false;
|
|
18
|
-
readonly entries: readonly CachePlanEntry[];
|
|
8
|
+
export interface CacheCandidate {
|
|
9
|
+
readonly ordinal: number;
|
|
10
|
+
readonly kind: 'tools' | 'system' | 'summary' | 'user' | 'tool_group' | 'tail' | 'user_mark' | 'lookback_anchor';
|
|
11
|
+
readonly status: 'applied' | 'rejected';
|
|
12
|
+
readonly reason: string | null;
|
|
19
13
|
}
|
|
20
|
-
|
|
14
|
+
/** Select semantic cuts, never an interior of a tool protocol group. */
|
|
15
|
+
export declare function selectCacheBreakpoints(messages: readonly ModelMessage[], entries: readonly PrefixEntry[], options: {
|
|
16
|
+
readonly mode: string;
|
|
17
|
+
readonly maximum: number;
|
|
18
|
+
readonly minimum: number;
|
|
19
|
+
readonly positions: readonly string[];
|
|
20
|
+
readonly supported: readonly boolean[];
|
|
21
|
+
readonly toolsTokens: number;
|
|
22
|
+
readonly hasTools: boolean;
|
|
23
|
+
readonly ttl: '5m' | '1h';
|
|
24
|
+
readonly previous?: Json | undefined;
|
|
25
|
+
}): {
|
|
26
|
+
breakpoints: number[];
|
|
27
|
+
candidates: CacheCandidate[];
|
|
28
|
+
ttls: Record<string, "5m" | "1h">;
|
|
29
|
+
};
|
|
30
|
+
export declare function userCacheMark(message: ModelMessage): boolean;
|
|
31
|
+
export declare function cacheMark(message: ModelMessage, mode: string): {
|
|
32
|
+
error: string | null;
|
|
33
|
+
ttl?: '5m' | '1h';
|
|
34
|
+
};
|
|
35
|
+
/** Local reusable-prefix estimates are not server hit counts. */
|
|
36
|
+
export declare function compareCachePrefix(previous: Json | undefined, current: {
|
|
37
|
+
readonly entries: readonly {
|
|
38
|
+
readonly prefixDigest: string;
|
|
39
|
+
readonly prefixTokens: number;
|
|
40
|
+
}[];
|
|
41
|
+
readonly model?: string;
|
|
42
|
+
readonly systemDigest?: string;
|
|
43
|
+
readonly toolsDigest?: string;
|
|
44
|
+
readonly routeDigest?: string;
|
|
45
|
+
readonly toolsTokens?: number;
|
|
46
|
+
readonly historyKinds?: readonly string[];
|
|
47
|
+
readonly snippedIds?: readonly string[];
|
|
48
|
+
readonly historyIds?: readonly string[];
|
|
49
|
+
readonly requestOptionsDigest?: string;
|
|
50
|
+
}): {
|
|
51
|
+
comparison: string;
|
|
52
|
+
unchangedPrefixMessages: number;
|
|
53
|
+
unchangedPrefixTokens: number;
|
|
54
|
+
reasons: string[];
|
|
55
|
+
firstChangedMessage?: never;
|
|
56
|
+
} | {
|
|
57
|
+
comparison: string;
|
|
58
|
+
unchangedPrefixMessages: number;
|
|
59
|
+
unchangedPrefixTokens: number;
|
|
60
|
+
firstChangedMessage: number | null;
|
|
61
|
+
reasons: string[];
|
|
62
|
+
};
|
|
63
|
+
export declare function record(value: Json | undefined): Readonly<Record<string, Json>> | undefined;
|
package/dist/runtime/cache.js
CHANGED
|
@@ -1,44 +1,125 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export function
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import { assertContextBoundaries } from '../core/context-boundaries.js';
|
|
2
|
+
/** Select semantic cuts, never an interior of a tool protocol group. */
|
|
3
|
+
export function selectCacheBreakpoints(messages, entries, options) {
|
|
4
|
+
const legal = new Set(assertContextBoundaries(messages).legalCuts);
|
|
5
|
+
const candidates = [];
|
|
6
|
+
const add = (ordinal, kind, priority) => { if (ordinal >= 0)
|
|
7
|
+
candidates.push({ ordinal, kind, priority }); };
|
|
8
|
+
if (options.hasTools)
|
|
9
|
+
candidates.push({ ordinal: -1, kind: 'tools', priority: 50 });
|
|
10
|
+
add(messages.findLastIndex((m) => m.role === 'system'), 'system', 70);
|
|
11
|
+
add(messages.findLastIndex((m) => m.metadata?.purpose === 'compaction'), 'summary', 60);
|
|
12
|
+
add(messages.findLastIndex((m) => m.role === 'user' && m.metadata?.purpose !== 'todo_reminder'), 'user', 65);
|
|
13
|
+
add(messages.findLastIndex((m, i) => m.role === 'tool' && legal.has(i + 1)), 'tool_group', 80);
|
|
14
|
+
add(messages.length - 1, 'tail', 100);
|
|
15
|
+
for (const [ordinal, message] of messages.entries()) {
|
|
16
|
+
if (userCacheMark(message))
|
|
17
|
+
add(ordinal, 'user_mark', 95);
|
|
18
|
+
}
|
|
19
|
+
const previous = record(options.previous);
|
|
20
|
+
const oldEntries = previous?.entries;
|
|
21
|
+
const oldPoints = previous?.breakpoints;
|
|
22
|
+
const last = entries.at(-1);
|
|
23
|
+
if (options.mode === 'anthropic' && Array.isArray(oldEntries) && Array.isArray(oldPoints) && last !== undefined) {
|
|
24
|
+
for (const value of oldPoints) {
|
|
25
|
+
if (typeof value !== 'number')
|
|
26
|
+
continue;
|
|
27
|
+
const old = record(oldEntries[value]);
|
|
28
|
+
const current = entries[value];
|
|
29
|
+
// A moving tail cannot find this old write through Anthropic's 20-block window.
|
|
30
|
+
if (current !== undefined && old?.prefixDigest === current.prefixDigest && last.blocks - current.blocks > 20)
|
|
31
|
+
add(value, 'lookback_anchor', 90);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const selected = new Set();
|
|
35
|
+
const reports = [];
|
|
36
|
+
for (const candidate of candidates.sort((a, b) => b.priority - a.priority || b.ordinal - a.ordinal)) {
|
|
37
|
+
const message = messages[candidate.ordinal];
|
|
38
|
+
const entry = entries[candidate.ordinal];
|
|
39
|
+
const explicit = options.mode === 'anthropic' || options.mode === 'openai-explicit';
|
|
40
|
+
const mark = message === undefined ? undefined : cacheMark(message, options.mode);
|
|
41
|
+
let reason = !explicit ? 'cache_mode' : candidate.kind === 'user_mark' && mark?.error !== null ? mark?.error ?? 'invalid_user_mark' : !legal.has(candidate.ordinal + 1) ? 'inside_tool_group'
|
|
42
|
+
: candidate.ordinal === -1 ? (options.mode !== 'anthropic' || !options.positions.includes('tool') ? 'unsupported_position' : options.toolsTokens < options.minimum ? 'below_minimum' : selected.size >= options.maximum ? 'breakpoint_budget' : null)
|
|
43
|
+
: !options.supported[candidate.ordinal] ? 'unsupported_block'
|
|
44
|
+
: !options.positions.includes(message?.role === 'system' ? 'system' : 'message') ? 'unsupported_position'
|
|
45
|
+
: (entry?.prefixTokens ?? 0) < options.minimum ? 'below_minimum'
|
|
46
|
+
: selected.has(candidate.ordinal) ? 'duplicate_position'
|
|
47
|
+
: selected.size >= options.maximum ? 'breakpoint_budget' : null;
|
|
48
|
+
if (reason === null && options.mode === 'anthropic') {
|
|
49
|
+
const marked = [...selected, candidate.ordinal].sort((a, b) => a - b).flatMap((ordinal) => {
|
|
50
|
+
const item = messages[ordinal];
|
|
51
|
+
const ttl = item === undefined ? undefined : cacheMark(item, options.mode).ttl;
|
|
52
|
+
return ttl === undefined ? [] : [ttl];
|
|
53
|
+
});
|
|
54
|
+
const short = marked.indexOf('5m');
|
|
55
|
+
if (short >= 0 && marked.slice(short + 1).includes('1h'))
|
|
56
|
+
reason = 'ttl_order';
|
|
57
|
+
}
|
|
58
|
+
reports.push({ ordinal: candidate.ordinal, kind: candidate.kind, status: reason === null ? 'applied' : 'rejected', reason });
|
|
59
|
+
if (reason === null)
|
|
60
|
+
selected.add(candidate.ordinal);
|
|
61
|
+
}
|
|
62
|
+
const breakpoints = [...selected].sort((a, b) => a - b);
|
|
63
|
+
const ttls = {};
|
|
64
|
+
for (const ordinal of breakpoints) {
|
|
65
|
+
const explicitTtl = (index) => messages[index] === undefined ? undefined : cacheMark(messages[index], options.mode).ttl;
|
|
66
|
+
ttls[ordinal] = breakpoints.some((index) => index <= ordinal && explicitTtl(index) === '5m') ? '5m'
|
|
67
|
+
: breakpoints.some((index) => index >= ordinal && explicitTtl(index) === '1h') ? '1h' : options.ttl;
|
|
68
|
+
}
|
|
69
|
+
return { breakpoints, candidates: reports, ttls };
|
|
70
|
+
}
|
|
71
|
+
export function userCacheMark(message) {
|
|
72
|
+
const options = record(message.metadata?.providerOptions);
|
|
73
|
+
const anthropic = record(options?.anthropic);
|
|
74
|
+
const openai = record(options?.openai);
|
|
75
|
+
return message.metadata?.cache_prefix === true || anthropic?.cacheControl !== undefined || anthropic?.cache_control !== undefined || openai?.promptCacheBreakpoint !== undefined;
|
|
76
|
+
}
|
|
77
|
+
export function cacheMark(message, mode) {
|
|
78
|
+
const options = record(message.metadata?.providerOptions);
|
|
79
|
+
const anthropic = record(options?.anthropic);
|
|
80
|
+
const openai = record(options?.openai);
|
|
81
|
+
const control = anthropic?.cacheControl ?? anthropic?.cache_control;
|
|
82
|
+
const point = openai?.promptCacheBreakpoint;
|
|
83
|
+
if (control !== undefined) {
|
|
84
|
+
if (mode !== 'anthropic')
|
|
85
|
+
return { error: 'user_mark_provider_mismatch' };
|
|
86
|
+
const value = record(control);
|
|
87
|
+
if (value?.type !== 'ephemeral' || (value.ttl !== undefined && value.ttl !== '5m' && value.ttl !== '1h'))
|
|
88
|
+
return { error: 'invalid_user_mark' };
|
|
89
|
+
return { error: null, ...(value.ttl === undefined ? {} : { ttl: value.ttl }) };
|
|
90
|
+
}
|
|
91
|
+
if (point !== undefined && (mode !== 'openai-explicit' || record(point)?.mode !== 'explicit'))
|
|
92
|
+
return { error: 'invalid_user_mark' };
|
|
93
|
+
return { error: null };
|
|
94
|
+
}
|
|
95
|
+
/** Local reusable-prefix estimates are not server hit counts. */
|
|
96
|
+
export function compareCachePrefix(previous, current) {
|
|
97
|
+
const plan = record(record(previous)?.cache_plan);
|
|
98
|
+
const entries = plan?.entries;
|
|
99
|
+
if (!Array.isArray(entries))
|
|
100
|
+
return { comparison: 'unavailable', unchangedPrefixMessages: 0, unchangedPrefixTokens: 0, reasons: ['no_previous_request'] };
|
|
101
|
+
let count = 0;
|
|
102
|
+
while (count < current.entries.length && count < entries.length && record(entries[count])?.prefixDigest === current.entries[count]?.prefixDigest)
|
|
103
|
+
count += 1;
|
|
104
|
+
const reasons = [];
|
|
105
|
+
for (const [field, reason] of [['model', 'model_switch'], ['routeDigest', 'route_change'], ['systemDigest', 'system_change'], ['toolsDigest', 'tool_schema_change'], ['requestOptionsDigest', 'request_options_change']]) {
|
|
106
|
+
if (current[field] !== undefined && plan?.[field] !== current[field])
|
|
107
|
+
reasons.push(reason);
|
|
108
|
+
}
|
|
109
|
+
const oldSnips = Array.isArray(plan?.snippedIds) ? plan.snippedIds : [];
|
|
110
|
+
if (current.snippedIds?.some((id) => !oldSnips.includes(id)))
|
|
111
|
+
reasons.push('snip');
|
|
112
|
+
const oldIds = Array.isArray(plan?.historyIds) ? plan.historyIds : [];
|
|
113
|
+
if (current.historyKinds?.some((kind, i) => kind === 'compaction' && !oldIds.includes(current.historyIds?.[i] ?? '')))
|
|
114
|
+
reasons.push('compact');
|
|
115
|
+
if (current.historyKinds?.some((kind, i) => kind === 'todo_reminder' && !oldIds.includes(current.historyIds?.[i] ?? '')))
|
|
116
|
+
reasons.push('todo_reminder');
|
|
117
|
+
if (reasons.length === 0)
|
|
118
|
+
reasons.push(count === entries.length && count === current.entries.length ? 'unchanged' : count === entries.length ? 'append' : 'history_edit');
|
|
119
|
+
return { comparison: 'available', unchangedPrefixMessages: count,
|
|
120
|
+
unchangedPrefixTokens: current.entries[count - 1]?.prefixTokens ?? (plan?.model === current.model && plan?.routeDigest === current.routeDigest && plan?.toolsDigest === current.toolsDigest ? current.toolsTokens ?? 0 : 0),
|
|
121
|
+
firstChangedMessage: count < Math.max(entries.length, current.entries.length) ? count : null, reasons };
|
|
122
|
+
}
|
|
123
|
+
export function record(value) {
|
|
124
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
44
125
|
}
|
package/dist/runtime/calls.d.ts
CHANGED