@arnilo/prism 0.5.6 → 0.7.0
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/CHANGELOG.md +81 -5
- package/README.md +10 -10
- package/dist/agent-approval.js +7 -6
- package/dist/agent-definitions.js +1 -0
- package/dist/agent-loops.js +51 -12
- package/dist/agent-run-lifecycle.js +11 -0
- package/dist/agent-run-state.d.ts +6 -0
- package/dist/agent-run-state.js +29 -9
- package/dist/agent-session/session/assemble.js +33 -2
- package/dist/agent-session/session/persist.js +6 -2
- package/dist/agent-session/session/tool-round.js +1 -0
- package/dist/agent-session/session/types.d.ts +10 -0
- package/dist/agent-session/session.d.ts +15 -0
- package/dist/agent-session/session.js +59 -4
- package/dist/agent-tool-dispatch.js +5 -4
- package/dist/artifacts.d.ts +39 -1
- package/dist/artifacts.js +73 -0
- package/dist/attention-compiler.d.ts +121 -0
- package/dist/attention-compiler.js +479 -0
- package/dist/cli-init.js +20 -6
- package/dist/content.d.ts +3 -16
- package/dist/content.js +9 -99
- package/dist/context-budget.d.ts +32 -2
- package/dist/context-budget.js +51 -19
- package/dist/contracts-core/agent.d.ts +18 -0
- package/dist/contracts-core/agent.js +4 -1
- package/dist/contracts-core/attention.d.ts +66 -0
- package/dist/contracts-core/attention.js +2 -0
- package/dist/contracts-core/compaction.d.ts +59 -0
- package/dist/contracts-core/compaction.js +77 -1
- package/dist/contracts-core/provider.d.ts +4 -0
- package/dist/contracts-core.d.ts +1 -0
- package/dist/contracts-core.js +1 -0
- package/dist/contracts-protocol.d.ts +29 -0
- package/dist/contracts-run-state.d.ts +6 -0
- package/dist/host-composition.d.ts +78 -0
- package/dist/host-composition.js +248 -0
- package/dist/index.d.ts +11 -8
- package/dist/index.js +6 -5
- package/dist/input.d.ts +19 -1
- package/dist/input.js +52 -2
- package/dist/media-types.d.ts +34 -0
- package/dist/media-types.js +158 -0
- package/dist/pinned-fetch.d.ts +2 -2
- package/dist/pinned-fetch.js +11 -12
- package/dist/redaction.js +74 -1
- package/dist/secure-agent.d.ts +2 -0
- package/dist/secure-agent.js +6 -1
- package/dist/session-stores.d.ts +11 -0
- package/dist/session-stores.js +23 -8
- package/dist/tool-result-fold.d.ts +12 -0
- package/dist/tool-result-fold.js +13 -6
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +41 -0
- package/docs/acp-agent.md +42 -11
- package/docs/acp.md +3 -2
- package/docs/ag-ui.md +9 -5
- package/docs/agent-definitions.md +9 -1
- package/docs/agent-events.md +6 -1
- package/docs/agent-loops.md +1 -1
- package/docs/agent-session-runtime.md +9 -7
- package/docs/attention-compiler.md +272 -0
- package/docs/browser-automation.md +5 -2
- package/docs/cli-rpc.md +4 -2
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +5 -3
- package/docs/coding-tools.md +1 -1
- package/docs/coding-workspaces.md +22 -0
- package/docs/compaction-and-retry.md +36 -4
- package/docs/compaction-observational-memory.md +62 -9
- package/docs/context-and-skills.md +4 -2
- package/docs/contributing.md +37 -0
- package/docs/conversations.md +1 -1
- package/docs/core.md +2 -0
- package/docs/dev-inspector.md +4 -0
- package/docs/device-adapters.md +1 -0
- package/docs/document-reader.md +12 -2
- package/docs/documents.md +11 -3
- package/docs/enterprise-postgres-state.md +2 -2
- package/docs/evaluations.md +168 -4
- package/docs/execution-timeline.md +180 -0
- package/docs/graft.md +3 -1
- package/docs/history/0.7.0-primitive-review.md +254 -0
- package/docs/history/migration-0.0.md +2 -2
- package/docs/history/release-handoffs.md +70 -1
- package/docs/host-compositions.md +147 -0
- package/docs/host-security.md +2 -2
- package/docs/hosted-sandboxes.md +94 -0
- package/docs/index.md +73 -41
- package/docs/input-and-prompt-assembly.md +5 -4
- package/docs/knowledge-sync.md +84 -0
- package/docs/language-intelligence.md +2 -2
- package/docs/live-testing.md +4 -1
- package/docs/mcp-tools.md +2 -1
- package/docs/memory-fabric.md +416 -0
- package/docs/migrate-to-0.5.md +8 -3
- package/docs/migrate-to-0.6.md +90 -0
- package/docs/migrate-to-0.7.md +345 -0
- package/docs/migration.md +43 -1
- package/docs/model-registry.md +1 -1
- package/docs/model-routing.md +79 -4
- package/docs/multi-agent-patterns.md +20 -6
- package/docs/multimodal-content.md +1 -1
- package/docs/obscura.md +3 -1
- package/docs/observability.md +52 -1
- package/docs/operations.md +13 -1
- package/docs/options-index.md +298 -0
- package/docs/peer-dependencies.md +96 -0
- package/docs/performance.md +34 -2
- package/docs/ponytail.md +2 -0
- package/docs/postgres-persistence.md +3 -1
- package/docs/process-sessions.md +3 -1
- package/docs/prompt-registry.md +1 -1
- package/docs/provider-caching.md +4 -2
- package/docs/provider-conformance.md +2 -2
- package/docs/provider-packages.md +23 -23
- package/docs/provider-primitives.md +2 -1
- package/docs/providers/ai-sdk.md +5 -2
- package/docs/providers/bedrock.md +71 -7
- package/docs/providers/openai.md +1 -1
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +24 -8
- package/docs/realtime-voice.md +87 -0
- package/docs/release-and-install.md +78 -56
- package/docs/runs-and-usage.md +3 -2
- package/docs/server.md +6 -4
- package/docs/session-stores.md +3 -1
- package/docs/speech.md +2 -0
- package/docs/sqlite-persistence.md +2 -0
- package/docs/supervisors.md +33 -5
- package/docs/testing.md +38 -0
- package/docs/thinking-and-reasoning.md +3 -1
- package/docs/tools.md +7 -6
- package/docs/web-tools.md +2 -1
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +14 -4
- package/docs/work-connectors.md +3 -1
- package/docs/work-tools.md +14 -4
- package/docs/workflows.md +69 -1
- package/docs/working-and-semantic-memory.md +25 -14
- package/package.json +5 -5
- package/templates/README.md +2 -0
- package/templates/business-worker/README.md.tmpl +19 -0
- package/templates/business-worker/env.example.tmpl +1 -0
- package/templates/business-worker/gitignore.tmpl +11 -0
- package/templates/business-worker/manifest.json +11 -0
- package/templates/business-worker/package.json.tmpl +23 -0
- package/templates/business-worker/src/agent.ts.tmpl +92 -0
- package/templates/business-worker/src/index.ts.tmpl +13 -0
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
- package/templates/business-worker/tsconfig.json.tmpl +15 -0
- package/templates/personal-assistant/README.md.tmpl +18 -0
- package/templates/personal-assistant/env.example.tmpl +1 -0
- package/templates/personal-assistant/gitignore.tmpl +11 -0
- package/templates/personal-assistant/manifest.json +11 -0
- package/templates/personal-assistant/package.json.tmpl +23 -0
- package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
- package/templates/personal-assistant/src/index.ts.tmpl +13 -0
- package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
- package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
package/dist/content.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
|
+
import { assertSsrfAllowedUrl, isAllowedByCidr, isBlockedIp, MediaContentError, normalizeHostname, } from "./media-types.js";
|
|
3
4
|
import { pinnedFetch } from "./pinned-fetch.js";
|
|
4
5
|
import { assertPermission } from "./security.js";
|
|
6
|
+
// SSRF policy, host/address types, `MediaContentError`, and the URL gate live in the leaf
|
|
7
|
+
// module shared with `pinned-fetch.ts` (plan 070 Task 10); re-exported here so the
|
|
8
|
+
// `@arnilo/prism` surface and the error-class identity are unchanged. `normalizeHostname`
|
|
9
|
+
// is imported for internal use only — it is public through `pinned-fetch.ts`, not here.
|
|
10
|
+
export { assertSsrfAllowedUrl, MediaContentError } from "./media-types.js";
|
|
5
11
|
/** Known model input capability tags for `ModelCapabilities.input`. */
|
|
6
12
|
export const MODEL_INPUT_CAPABILITIES = ["text", "image", "audio", "file", "document", "video"];
|
|
7
13
|
/** Default per-item media byte ceiling (10 MB; aligns with coding-agent image bounds). */
|
|
@@ -26,14 +32,6 @@ export class UnsupportedModalityError extends Error {
|
|
|
26
32
|
this.model = model.model;
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
|
-
export class MediaContentError extends Error {
|
|
30
|
-
code;
|
|
31
|
-
constructor(code, message, options) {
|
|
32
|
-
super(message, options);
|
|
33
|
-
this.name = "MediaContentError";
|
|
34
|
-
this.code = code;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
35
|
export function contentBlockInputModality(block) {
|
|
38
36
|
switch (block.type) {
|
|
39
37
|
case "image":
|
|
@@ -90,41 +88,6 @@ export function assertMediaBlocksWithinBounds(blocks, bounds = {}) {
|
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
|
-
export function assertSsrfAllowedUrl(url, policy = {}) {
|
|
94
|
-
let parsed;
|
|
95
|
-
try {
|
|
96
|
-
parsed = new URL(url);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
throw new MediaContentError("ssrf_denied", "Media URL is not a valid absolute URL");
|
|
100
|
-
}
|
|
101
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
102
|
-
throw new MediaContentError("unsupported_url_scheme", `Media URL scheme ${parsed.protocol} is not allowed`);
|
|
103
|
-
}
|
|
104
|
-
if (parsed.username || parsed.password) {
|
|
105
|
-
throw new MediaContentError("ssrf_denied", "Media URL must not embed credentials");
|
|
106
|
-
}
|
|
107
|
-
const hostname = normalizeHostname(parsed.hostname);
|
|
108
|
-
if (policy.allowedHostnames?.length) {
|
|
109
|
-
if (!policy.allowedHostnames.some((allowed) => hostname === normalizeHostname(allowed))) {
|
|
110
|
-
throw new MediaContentError("ssrf_denied", `Media URL host ${hostname} is not allow-listed`);
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (policy.denyPrivateHosts === false)
|
|
115
|
-
return;
|
|
116
|
-
if (hostname === "localhost" ||
|
|
117
|
-
hostname.endsWith(".localhost") ||
|
|
118
|
-
hostname.endsWith(".local") ||
|
|
119
|
-
hostname === "metadata" ||
|
|
120
|
-
hostname === "metadata.google.internal" ||
|
|
121
|
-
hostname === "instance-data") {
|
|
122
|
-
throw new MediaContentError("ssrf_denied", `Media URL host ${hostname} is not allowed`);
|
|
123
|
-
}
|
|
124
|
-
if (isBlockedIp(hostname)) {
|
|
125
|
-
throw new MediaContentError("ssrf_denied", `Media URL host ${hostname} is not allowed`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
91
|
export function sniffMediaMimeType(bytes) {
|
|
129
92
|
if (startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]))
|
|
130
93
|
return "application/pdf";
|
|
@@ -364,7 +327,9 @@ async function resolvePublicAddress(hostname, resolver, signal, policy) {
|
|
|
364
327
|
if (addresses.length > 32)
|
|
365
328
|
throw new MediaContentError("fetch_failed", "Media hostname resolved to too many addresses");
|
|
366
329
|
if (policy?.denyPrivateHosts !== false && !policy?.allowedHostnames?.length) {
|
|
367
|
-
|
|
330
|
+
// An allow-listed CIDR covers resolved answers too, but only the private-IP block.
|
|
331
|
+
const blocked = addresses.some(({ address }) => isBlockedIp(normalizeHostname(address)) && !isAllowedByCidr(address, policy?.allowedCidrs));
|
|
332
|
+
if (blocked) {
|
|
368
333
|
throw new MediaContentError("ssrf_denied", `Media URL host ${hostname} resolved to a private address`);
|
|
369
334
|
}
|
|
370
335
|
}
|
|
@@ -450,61 +415,6 @@ function mediaTypesCompatible(declared, sniffed) {
|
|
|
450
415
|
return true;
|
|
451
416
|
return false;
|
|
452
417
|
}
|
|
453
|
-
function normalizeHostname(hostname) {
|
|
454
|
-
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
455
|
-
return normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
|
|
456
|
-
}
|
|
457
|
-
function isBlockedIp(hostname) {
|
|
458
|
-
const normalized = normalizeHostname(hostname);
|
|
459
|
-
const family = isIP(normalized);
|
|
460
|
-
if (family === 4)
|
|
461
|
-
return isBlockedIpv4(normalized);
|
|
462
|
-
if (family === 6)
|
|
463
|
-
return isBlockedIpv6(normalized);
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
function isBlockedIpv4(address) {
|
|
467
|
-
const [a, b] = address.split(".").map(Number);
|
|
468
|
-
return (a === 0 ||
|
|
469
|
-
a === 10 ||
|
|
470
|
-
a === 127 ||
|
|
471
|
-
(a === 100 && b >= 64 && b <= 127) ||
|
|
472
|
-
(a === 169 && b === 254) ||
|
|
473
|
-
(a === 172 && b >= 16 && b <= 31) ||
|
|
474
|
-
(a === 192 && (b === 0 || b === 168)) ||
|
|
475
|
-
(a === 198 && (b === 18 || b === 19 || b === 51)) ||
|
|
476
|
-
(a === 203 && b === 0) ||
|
|
477
|
-
a >= 224);
|
|
478
|
-
}
|
|
479
|
-
function isBlockedIpv6(address) {
|
|
480
|
-
const words = parseIpv6Words(address);
|
|
481
|
-
if (!words)
|
|
482
|
-
return true;
|
|
483
|
-
if (words.every((word) => word === 0) || (words.slice(0, 7).every((word) => word === 0) && words[7] === 1))
|
|
484
|
-
return true;
|
|
485
|
-
if ((words[0] & 0xfe00) === 0xfc00)
|
|
486
|
-
return true;
|
|
487
|
-
if ((words[0] & 0xffc0) === 0xfe80 || (words[0] & 0xffc0) === 0xfec0)
|
|
488
|
-
return true;
|
|
489
|
-
if ((words[0] & 0xff00) === 0xff00)
|
|
490
|
-
return true;
|
|
491
|
-
if (words[0] === 0x2001 && words[1] === 0x0db8)
|
|
492
|
-
return true;
|
|
493
|
-
const mapped = words.slice(0, 5).every((word) => word === 0) && (words[5] === 0 || words[5] === 0xffff);
|
|
494
|
-
return mapped && isBlockedIpv4(`${words[6] >> 8}.${words[6] & 0xff}.${words[7] >> 8}.${words[7] & 0xff}`);
|
|
495
|
-
}
|
|
496
|
-
function parseIpv6Words(address) {
|
|
497
|
-
const parts = address.split("::");
|
|
498
|
-
if (parts.length > 2)
|
|
499
|
-
return undefined;
|
|
500
|
-
const left = parts[0] ? parts[0].split(":") : [];
|
|
501
|
-
const right = parts[1] ? parts[1].split(":") : [];
|
|
502
|
-
const missing = 8 - left.length - right.length;
|
|
503
|
-
if (missing < 0 || (parts.length === 1 && missing !== 0))
|
|
504
|
-
return undefined;
|
|
505
|
-
const words = [...left, ...Array.from({ length: missing }, () => "0"), ...right].map((part) => Number.parseInt(part, 16));
|
|
506
|
-
return words.length === 8 && words.every((word) => Number.isInteger(word) && word >= 0 && word <= 0xffff) ? words : undefined;
|
|
507
|
-
}
|
|
508
418
|
function startsWith(bytes, prefix) {
|
|
509
419
|
if (bytes.length < prefix.length)
|
|
510
420
|
return false;
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { ContextBlock, InputAssemblyLayout, Message, ProviderRequest, Skill, ToolDefinition } from "./contracts.js";
|
|
2
|
-
import { type LoadedSkillSet, type SkillsDisclosure } from "./skill-disclosure.js";
|
|
2
|
+
import { type LoadedSkillSet, type SkillRenderContext, type SkillsDisclosure } from "./skill-disclosure.js";
|
|
3
|
+
/**
|
|
4
|
+
* Host-supplied token estimator. Budget-only: it never reaches billing, provider
|
|
5
|
+
* usage, or the wire — it decides what the assembler evicts and nothing else.
|
|
6
|
+
*/
|
|
7
|
+
export type TokenEstimator = (text: string) => number;
|
|
3
8
|
/** Assembler-time input budget. At least one max required when present. */
|
|
4
9
|
export interface ContextBudget {
|
|
5
10
|
readonly maxInputTokens?: number;
|
|
6
11
|
readonly maxInputBytes?: number;
|
|
7
12
|
readonly reportOmissions?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Overrides the built-in UTF-16/4 heuristic for eviction accounting. Must return a
|
|
15
|
+
* non-negative finite token count (a NaN/negative/absent return fails the assembly
|
|
16
|
+
* closed with a `TypeError`). Byte caps are estimator-independent and always enforced.
|
|
17
|
+
*/
|
|
18
|
+
readonly tokenEstimator?: TokenEstimator;
|
|
8
19
|
}
|
|
9
20
|
export type ContextBudgetOmissionKind = "skills" | "skill_body" | "context" | "history" | "tool_results" | "summaries" | "attachments" | "tools";
|
|
10
21
|
export interface ContextBudgetOmission {
|
|
@@ -43,7 +54,7 @@ export declare function isContextBudgetError(error: unknown): error is ContextBu
|
|
|
43
54
|
/** UTF-16 code units / 4. Estimate only — not billing. */
|
|
44
55
|
export declare function estimateTextTokens(text: string): number;
|
|
45
56
|
export declare function estimateTextBytes(text: string): number;
|
|
46
|
-
export declare function estimateMessageTokens(message: Message): number;
|
|
57
|
+
export declare function estimateMessageTokens(message: Message, estimateTokens?: TokenEstimator): number;
|
|
47
58
|
export declare function estimateMessageBytes(message: Message): number;
|
|
48
59
|
export declare function estimateAssemblyTokens(messages: readonly Message[]): number;
|
|
49
60
|
export declare function resolveContextBudget(budget: ContextBudget): Required<Pick<ContextBudget, "reportOmissions">> & ContextBudget;
|
|
@@ -65,3 +76,22 @@ export declare function applyContextBudget(options: {
|
|
|
65
76
|
readonly demotedSkillBodies: readonly string[];
|
|
66
77
|
readonly report: ContextBudgetReport;
|
|
67
78
|
};
|
|
79
|
+
/** Everything the assembler will send, in the order it will send it. */
|
|
80
|
+
export interface MeasureInputCostOptions {
|
|
81
|
+
readonly groups: ContextBudgetMessageGroups;
|
|
82
|
+
readonly context?: readonly ContextBlock[];
|
|
83
|
+
readonly skills?: readonly Skill[];
|
|
84
|
+
readonly tools?: readonly ToolDefinition[];
|
|
85
|
+
readonly skillContext?: SkillRenderContext;
|
|
86
|
+
readonly demotedBodies?: ReadonlySet<string>;
|
|
87
|
+
readonly estimateTokens?: TokenEstimator;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One O(n) cost measurement of the whole request (groups, context, skills, tool declarations).
|
|
91
|
+
* Shared with the attention-compiler gate, which measures once per turn and then subtracts a
|
|
92
|
+
* per-mutation delta instead of re-measuring — the same trick `applyContextBudget` uses.
|
|
93
|
+
*/
|
|
94
|
+
export declare function measureInputCost(options: MeasureInputCostOptions): {
|
|
95
|
+
tokens: number;
|
|
96
|
+
bytes: number;
|
|
97
|
+
};
|
package/dist/context-budget.js
CHANGED
|
@@ -22,8 +22,8 @@ export function estimateTextTokens(text) {
|
|
|
22
22
|
export function estimateTextBytes(text) {
|
|
23
23
|
return Buffer.byteLength(text, "utf8");
|
|
24
24
|
}
|
|
25
|
-
export function estimateMessageTokens(message) {
|
|
26
|
-
return
|
|
25
|
+
export function estimateMessageTokens(message, estimateTokens = estimateTextTokens) {
|
|
26
|
+
return estimateTokens(messageText(message));
|
|
27
27
|
}
|
|
28
28
|
export function estimateMessageBytes(message) {
|
|
29
29
|
return estimateTextBytes(messageText(message));
|
|
@@ -41,6 +41,9 @@ export function resolveContextBudget(budget) {
|
|
|
41
41
|
assertPositiveCap(budget.maxInputTokens, "maxInputTokens", HARD_MAX_CONTEXT_BUDGET_TOKENS);
|
|
42
42
|
if (hasBytes)
|
|
43
43
|
assertPositiveCap(budget.maxInputBytes, "maxInputBytes", HARD_MAX_CONTEXT_BUDGET_BYTES);
|
|
44
|
+
if (budget.tokenEstimator !== undefined && typeof budget.tokenEstimator !== "function") {
|
|
45
|
+
throw new TypeError("contextBudget.tokenEstimator must be a function");
|
|
46
|
+
}
|
|
44
47
|
return { ...budget, reportOmissions: budget.reportOmissions === true };
|
|
45
48
|
}
|
|
46
49
|
export function getContextBudgetReport(request) {
|
|
@@ -49,6 +52,7 @@ export function getContextBudgetReport(request) {
|
|
|
49
52
|
}
|
|
50
53
|
export function applyContextBudget(options) {
|
|
51
54
|
const budget = resolveContextBudget(options.budget);
|
|
55
|
+
const estimateTokens = resolveTokenEstimator(budget);
|
|
52
56
|
const layout = options.layout ?? "cache_aware";
|
|
53
57
|
const groups = {
|
|
54
58
|
instructions: [...options.groups.instructions],
|
|
@@ -67,9 +71,9 @@ export function applyContextBudget(options) {
|
|
|
67
71
|
const historyCursor = { index: 0 };
|
|
68
72
|
// Measure once, then subtract each dropped item's own estimate (dropNext computes it
|
|
69
73
|
// with the same estimators) — avoids an O(n²) re-scan of the full keep-set per drop.
|
|
70
|
-
const kept =
|
|
74
|
+
const kept = measureInputCost({ groups, context, skills, tools, skillContext, demotedBodies, estimateTokens });
|
|
71
75
|
while (overBudget(kept, budget)) {
|
|
72
|
-
const drop = dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor);
|
|
76
|
+
const drop = dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor, estimateTokens);
|
|
73
77
|
if (!drop) {
|
|
74
78
|
throw new ContextBudgetError();
|
|
75
79
|
}
|
|
@@ -97,7 +101,7 @@ export function applyContextBudget(options) {
|
|
|
97
101
|
},
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
|
-
function dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor) {
|
|
104
|
+
function dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor, estimateTokens) {
|
|
101
105
|
// ponytail: drop droppable groups in layout order; within history, advance a cursor and slice once.
|
|
102
106
|
// cache_aware keeps attachments longer so stable prefix stays intact while budget still allows it.
|
|
103
107
|
const order = layout === "cache_aware"
|
|
@@ -106,19 +110,19 @@ function dropNext(groups, context, skills, layout, skillContext, demotedBodies,
|
|
|
106
110
|
for (const kind of order) {
|
|
107
111
|
if (kind === "tool_results" && groups.toolResults.length > 0) {
|
|
108
112
|
const message = groups.toolResults.pop();
|
|
109
|
-
return omission("tool_results", message.id ?? toolResultId(message), message);
|
|
113
|
+
return omission("tool_results", message.id ?? toolResultId(message), message, estimateTokens);
|
|
110
114
|
}
|
|
111
115
|
if (kind === "history" && historyCursor.index < groups.history.length) {
|
|
112
116
|
const message = groups.history[historyCursor.index++];
|
|
113
|
-
return omission("history", message.id, message);
|
|
117
|
+
return omission("history", message.id, message, estimateTokens);
|
|
114
118
|
}
|
|
115
119
|
if (kind === "summaries" && groups.summaries.length > 0) {
|
|
116
120
|
const message = groups.summaries.pop();
|
|
117
|
-
return omission("summaries", message.id, message);
|
|
121
|
+
return omission("summaries", message.id, message, estimateTokens);
|
|
118
122
|
}
|
|
119
123
|
if (kind === "attachments" && groups.attachments.length > 0) {
|
|
120
124
|
const message = groups.attachments.pop();
|
|
121
|
-
return omission("attachments", message.id, message);
|
|
125
|
+
return omission("attachments", message.id, message, estimateTokens);
|
|
122
126
|
}
|
|
123
127
|
if (kind === "context" && context.length > 0) {
|
|
124
128
|
const index = pickVictimIndex(context, (block) => block.priority ?? 0);
|
|
@@ -127,7 +131,7 @@ function dropNext(groups, context, skills, layout, skillContext, demotedBodies,
|
|
|
127
131
|
return {
|
|
128
132
|
kind: "context",
|
|
129
133
|
id: block.id ?? block.title,
|
|
130
|
-
tokenEstimate:
|
|
134
|
+
tokenEstimate: estimateTokens(text),
|
|
131
135
|
byteLength: estimateTextBytes(text),
|
|
132
136
|
};
|
|
133
137
|
}
|
|
@@ -142,7 +146,7 @@ function dropNext(groups, context, skills, layout, skillContext, demotedBodies,
|
|
|
142
146
|
return {
|
|
143
147
|
kind: "skill_body",
|
|
144
148
|
id: skill.name,
|
|
145
|
-
tokenEstimate:
|
|
149
|
+
tokenEstimate: estimateTokens(beforeText) - estimateTokens(afterText),
|
|
146
150
|
byteLength: estimateTextBytes(beforeText) - estimateTextBytes(afterText),
|
|
147
151
|
};
|
|
148
152
|
}
|
|
@@ -152,31 +156,59 @@ function dropNext(groups, context, skills, layout, skillContext, demotedBodies,
|
|
|
152
156
|
return {
|
|
153
157
|
kind: "skills",
|
|
154
158
|
id: skill.name,
|
|
155
|
-
tokenEstimate:
|
|
159
|
+
tokenEstimate: estimateTokens(text),
|
|
156
160
|
byteLength: estimateTextBytes(text),
|
|
157
161
|
};
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
return undefined;
|
|
161
165
|
}
|
|
162
|
-
function omission(kind, id, message) {
|
|
166
|
+
function omission(kind, id, message, estimateTokens) {
|
|
163
167
|
return {
|
|
164
168
|
kind,
|
|
165
169
|
id,
|
|
166
|
-
tokenEstimate: estimateMessageTokens(message),
|
|
170
|
+
tokenEstimate: estimateMessageTokens(message, estimateTokens),
|
|
167
171
|
byteLength: estimateMessageBytes(message),
|
|
168
172
|
};
|
|
169
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* One O(n) cost measurement of the whole request (groups, context, skills, tool declarations).
|
|
176
|
+
* Shared with the attention-compiler gate, which measures once per turn and then subtracts a
|
|
177
|
+
* per-mutation delta instead of re-measuring — the same trick `applyContextBudget` uses.
|
|
178
|
+
*/
|
|
179
|
+
export function measureInputCost(options) {
|
|
180
|
+
return measureAll(options.groups, options.context ?? [], options.skills ?? [], options.tools, options.skillContext ?? {}, options.demotedBodies ?? EMPTY_DEMOTED_BODIES, options.estimateTokens ?? estimateTextTokens);
|
|
181
|
+
}
|
|
182
|
+
const EMPTY_DEMOTED_BODIES = new Set();
|
|
183
|
+
/**
|
|
184
|
+
* Resolves the budget's estimator, validating each return value: a host estimator that
|
|
185
|
+
* yields NaN/negative/non-finite tokens would make every eviction decision unsound, so it
|
|
186
|
+
* fails the assembly closed instead of silently keeping or dropping the wrong content.
|
|
187
|
+
*/
|
|
188
|
+
function resolveTokenEstimator(budget) {
|
|
189
|
+
const estimator = budget.tokenEstimator;
|
|
190
|
+
if (estimator === undefined)
|
|
191
|
+
return estimateTextTokens;
|
|
192
|
+
if (typeof estimator !== "function")
|
|
193
|
+
throw new TypeError("contextBudget.tokenEstimator must be a function");
|
|
194
|
+
return (text) => {
|
|
195
|
+
const tokens = estimator(text);
|
|
196
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0) {
|
|
197
|
+
throw new TypeError("contextBudget.tokenEstimator must return a non-negative finite number of tokens");
|
|
198
|
+
}
|
|
199
|
+
return tokens;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
170
202
|
function toolResultId(message) {
|
|
171
203
|
const block = message.content.find((part) => part.type === "tool_result");
|
|
172
204
|
return block && block.type === "tool_result" ? block.toolCallId : undefined;
|
|
173
205
|
}
|
|
174
|
-
function measureAll(groups, context, skills, tools, skillContext, demotedBodies) {
|
|
206
|
+
function measureAll(groups, context, skills, tools, skillContext, demotedBodies, estimateTokens) {
|
|
175
207
|
const renderContext = withDemoted(skillContext, demotedBodies);
|
|
176
208
|
let tokens = 0;
|
|
177
209
|
let bytes = 0;
|
|
178
210
|
const addMessage = (message) => {
|
|
179
|
-
tokens += estimateMessageTokens(message);
|
|
211
|
+
tokens += estimateMessageTokens(message, estimateTokens);
|
|
180
212
|
bytes += estimateMessageBytes(message);
|
|
181
213
|
};
|
|
182
214
|
for (const message of groups.instructions)
|
|
@@ -193,17 +225,17 @@ function measureAll(groups, context, skills, tools, skillContext, demotedBodies)
|
|
|
193
225
|
addMessage(message);
|
|
194
226
|
for (const block of context) {
|
|
195
227
|
const text = `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
|
|
196
|
-
tokens +=
|
|
228
|
+
tokens += estimateTokens(text);
|
|
197
229
|
bytes += estimateTextBytes(text);
|
|
198
230
|
}
|
|
199
231
|
for (const skill of skills) {
|
|
200
232
|
const text = skillPromptText(skill, renderContext) ?? "";
|
|
201
|
-
tokens +=
|
|
233
|
+
tokens += estimateTokens(text);
|
|
202
234
|
bytes += estimateTextBytes(text);
|
|
203
235
|
}
|
|
204
236
|
if (tools?.length) {
|
|
205
237
|
const text = `Available tools:\n${tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`).join("\n")}`;
|
|
206
|
-
tokens +=
|
|
238
|
+
tokens += estimateTokens(text);
|
|
207
239
|
bytes += estimateTextBytes(text);
|
|
208
240
|
}
|
|
209
241
|
return { tokens, bytes };
|
|
@@ -33,6 +33,8 @@ export interface AgentDefinition {
|
|
|
33
33
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
34
34
|
/** Optional escape hatch. When present, overrides declarative resolution. */
|
|
35
35
|
create?(config?: AgentConfig): Promise<Agent> | Agent;
|
|
36
|
+
/** Opt-in attention compiler for the agent this definition resolves to (plan 074 C1). */
|
|
37
|
+
readonly attentionCompiler?: import("./attention.js").AttentionCompilerSetting;
|
|
36
38
|
}
|
|
37
39
|
/** Input to {@link resolveAgentDefinition}. All fields are optional; the host
|
|
38
40
|
* controls scope by which registries it passes. */
|
|
@@ -64,6 +66,9 @@ export interface AgentConfig {
|
|
|
64
66
|
readonly toolsSearch?: import("../tool-search.js").ToolsSearchOptions;
|
|
65
67
|
/** Opt-in projection-only fold for aged large tool results in provider view; store untouched. */
|
|
66
68
|
readonly toolResultFold?: import("../tool-result-fold.js").ToolResultFoldOptions;
|
|
69
|
+
/** Opt-in attention compiler (plan 074): `true` for defaults, an object to tune ratios/depth.
|
|
70
|
+
* Omitted keeps today's request bytes; per-run options may only relax this setting. */
|
|
71
|
+
readonly attentionCompiler?: import("./attention.js").AttentionCompilerSetting;
|
|
67
72
|
readonly inputBuilder?: InputBuilder;
|
|
68
73
|
readonly promptBuilder?: PromptBuilder;
|
|
69
74
|
readonly middleware?: MiddlewareRegistry;
|
|
@@ -116,6 +121,8 @@ export interface SecureAgentOptions extends Omit<AgentConfig, "tools" | "validat
|
|
|
116
121
|
readonly limits: RunLimits;
|
|
117
122
|
readonly definitionRevision: string;
|
|
118
123
|
readonly runState: Omit<AgentRunStateOptions, "definitionRevision" | "interruptBeforeTool">;
|
|
124
|
+
/** Optional host composition readiness assertions evaluated on creation. */
|
|
125
|
+
readonly composition?: import("../host-composition.js").HostCompositionOptions;
|
|
119
126
|
}
|
|
120
127
|
export interface Agent {
|
|
121
128
|
readonly config: AgentConfig;
|
|
@@ -127,7 +134,18 @@ export interface AgentSessionConfig {
|
|
|
127
134
|
readonly store?: SessionStore;
|
|
128
135
|
readonly leafId?: string;
|
|
129
136
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
137
|
+
/**
|
|
138
|
+
* TTL of the in-memory `session.snapshot()` branch cache in milliseconds.
|
|
139
|
+
* Default `DEFAULT_SNAPSHOT_CACHE_TTL_MS`; `0` disables the cache (every snapshot read
|
|
140
|
+
* rebuilds from the store); at most `HARD_MAX_SNAPSHOT_CACHE_TTL_MS`. The cache is always
|
|
141
|
+
* invalidated by a new leaf or a mutation, so the TTL only bounds staleness-free reuse.
|
|
142
|
+
*/
|
|
143
|
+
readonly snapshotCacheTtlMs?: number;
|
|
130
144
|
}
|
|
145
|
+
/** Default `session.snapshot()` branch-cache TTL (milliseconds). */
|
|
146
|
+
export declare const DEFAULT_SNAPSHOT_CACHE_TTL_MS = 1000;
|
|
147
|
+
/** Upper bound for `AgentSessionConfig.snapshotCacheTtlMs` (milliseconds). */
|
|
148
|
+
export declare const HARD_MAX_SNAPSHOT_CACHE_TTL_MS = 30000;
|
|
131
149
|
export interface AgentSessionForkOptions {
|
|
132
150
|
readonly leafId?: string;
|
|
133
151
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Contracts-core agent family (0.2.5 plan 025 Task 1 split).
|
|
2
2
|
* Moved verbatim from contracts-core.ts; public surface unchanged behind the barrel. */
|
|
3
|
-
|
|
3
|
+
/** Default `session.snapshot()` branch-cache TTL (milliseconds). */
|
|
4
|
+
export const DEFAULT_SNAPSHOT_CACHE_TTL_MS = 1_000;
|
|
5
|
+
/** Upper bound for `AgentSessionConfig.snapshotCacheTtlMs` (milliseconds). */
|
|
6
|
+
export const HARD_MAX_SNAPSHOT_CACHE_TTL_MS = 30_000;
|
|
4
7
|
/** Directory-name spelling for discovered contribution kinds. Maps to a
|
|
5
8
|
* {@link ManifestContributionDeclaration} kind for non-skill kinds:
|
|
6
9
|
* `context` → `contextProvider`, `instructions` → `systemPromptContribution`. */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Attention Compiler contracts (plan 074 Tasks 2–5). Opt-in per-turn gate that measures the
|
|
2
|
+
* assembled input and mutates a history clone only after a host ratio of the model input
|
|
3
|
+
* cap. `resolveRunAttentionCompiler` turns the agent setting plus an optional run overlay
|
|
4
|
+
* into the frozen handle the session hands to assembly; nothing declared here runs on its
|
|
5
|
+
* own, and omitting the option keeps today's request bytes. */
|
|
6
|
+
import type { CompactionTrigger } from "./compaction.js";
|
|
7
|
+
import type { ModelConfig } from "./content.js";
|
|
8
|
+
/** Input-cap resolution inputs shared by the compiler and `input_ratio` compaction triggers. */
|
|
9
|
+
export interface AttentionInputCapOptions {
|
|
10
|
+
/** Host cap; when set it wins over `model.limits.contextWindow` (C2). */
|
|
11
|
+
readonly maxInputTokens?: number;
|
|
12
|
+
/** Output + next-turn headroom subtracted from the window (default 1024). */
|
|
13
|
+
readonly reserveTokens?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface AttentionCompilerOptions extends AttentionInputCapOptions {
|
|
16
|
+
/** Fraction of `inputCap` that triggers mutation; in `(0, 1)` (default 0.75). */
|
|
17
|
+
readonly triggerRatio?: number;
|
|
18
|
+
/** Where compaction should fire relative to `triggerRatio`; must exceed it (default 0.9). */
|
|
19
|
+
readonly compactRatio?: number;
|
|
20
|
+
/** Newest thinking-bearing assistant turns kept intact (default 1). */
|
|
21
|
+
readonly thinkingKeepTurns?: number;
|
|
22
|
+
/** Newest tool results kept full (default 3). */
|
|
23
|
+
readonly keepLast?: number;
|
|
24
|
+
/** Tool names whose results are never stubbed, whatever the ratio. */
|
|
25
|
+
readonly excludeTools?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** Where the compiler is switched on: `true` uses the defaults, an object tunes them, `false`
|
|
28
|
+
* (or omitted) leaves requests byte-for-byte as they are without the compiler. On
|
|
29
|
+
* `RunOptions` the same shape is an overlay: `false` disables, `true` is a no-op, and an
|
|
30
|
+
* object may only *relax* the agent setting (see `resolveRunAttentionCompiler`). */
|
|
31
|
+
export type AttentionCompilerSetting = boolean | AttentionCompilerOptions;
|
|
32
|
+
export interface AttentionCompilerContext {
|
|
33
|
+
/** Model limits used to resolve the input cap; ignored when `maxInputTokens` is set. */
|
|
34
|
+
readonly model?: Pick<ModelConfig, "limits">;
|
|
35
|
+
/** Validated at create so an unknown trigger type fails at config time, not on turn one. */
|
|
36
|
+
readonly compactionTrigger?: CompactionTrigger;
|
|
37
|
+
}
|
|
38
|
+
/** Validated, frozen configuration returned by `createAttentionCompiler`. */
|
|
39
|
+
export interface AttentionCompiler {
|
|
40
|
+
readonly inputCap: number;
|
|
41
|
+
readonly reserveTokens: number;
|
|
42
|
+
readonly triggerRatio: number;
|
|
43
|
+
readonly compactRatio: number;
|
|
44
|
+
readonly thinkingKeepTurns: number;
|
|
45
|
+
readonly keepLast: number;
|
|
46
|
+
readonly excludeTools: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
/** One mutated turn. Under-ratio turns emit nothing and produce no report (C14). */
|
|
49
|
+
export interface AttentionReport {
|
|
50
|
+
/** Estimated tokens measured before this turn's mutation — the value compared to the ratio. */
|
|
51
|
+
readonly used: number;
|
|
52
|
+
/** Estimated tokens of the same request after this turn's mutation: the cost curve is `used` → `usedAfter`. */
|
|
53
|
+
readonly usedAfter: number;
|
|
54
|
+
readonly inputCap: number;
|
|
55
|
+
readonly triggerRatio: number;
|
|
56
|
+
/** Thinking turns absent from this request; rows re-applied from the sticky frontier count again. */
|
|
57
|
+
readonly droppedThinkingTurns: number;
|
|
58
|
+
/** Tool results stubbed in this request; rows re-applied from the sticky frontier count again. */
|
|
59
|
+
readonly stubbedToolResults: number;
|
|
60
|
+
/** Payload bytes the stubs took out of this request (never the stub text itself). */
|
|
61
|
+
readonly stubbedBytes: number;
|
|
62
|
+
/** True when the gate stopped with eligible rows left: the sticky frontier is partial. */
|
|
63
|
+
readonly truncated: boolean;
|
|
64
|
+
readonly runId?: string;
|
|
65
|
+
readonly sessionId?: string;
|
|
66
|
+
}
|
|
@@ -21,15 +21,74 @@ export interface CompactionResult {
|
|
|
21
21
|
readonly entries?: readonly SessionEntry[];
|
|
22
22
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
23
23
|
}
|
|
24
|
+
/** Inputs a compaction trigger decides on. Estimates and ids only — never raw payloads. */
|
|
25
|
+
export interface CompactionTriggerContext {
|
|
26
|
+
readonly sessionId: string;
|
|
27
|
+
readonly entryCount: number;
|
|
28
|
+
readonly estimatedInputTokens: number;
|
|
29
|
+
readonly inputCapTokens: number;
|
|
30
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/** Host-programmable compact-when gate (plan 074 C11). Omitted → `thresholdEntries` only. */
|
|
34
|
+
export type CompactionTrigger = {
|
|
35
|
+
readonly type: "threshold_entries";
|
|
36
|
+
readonly entries: number;
|
|
37
|
+
} | {
|
|
38
|
+
readonly type: "input_ratio";
|
|
39
|
+
readonly ratio: number;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "custom";
|
|
42
|
+
readonly shouldCompact: (context: CompactionTriggerContext) => boolean | Promise<boolean>;
|
|
43
|
+
};
|
|
44
|
+
/** Validate a host trigger at config time, so an unknown `type` fails at create (C11). */
|
|
45
|
+
export declare function assertCompactionTrigger(trigger: CompactionTrigger): CompactionTrigger;
|
|
24
46
|
export interface CompactionOptions {
|
|
25
47
|
readonly strategy?: CompactionStrategy;
|
|
26
48
|
readonly thresholdEntries?: number;
|
|
49
|
+
/** Replaces the `thresholdEntries` gate when set; omitted keeps today's entry-count gate. */
|
|
50
|
+
readonly trigger?: CompactionTrigger;
|
|
27
51
|
readonly keepRecentEntries?: number;
|
|
28
52
|
readonly maxSummaryChars?: number;
|
|
29
53
|
readonly secrets?: readonly (string | undefined)[];
|
|
30
54
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
31
55
|
readonly signal?: AbortSignal;
|
|
32
56
|
}
|
|
57
|
+
/** Legacy gates a compaction decision can fall back to when no `trigger` is configured. */
|
|
58
|
+
export interface ResolveShouldCompactOptions {
|
|
59
|
+
/** Host trigger; when set it replaces the legacy gates below. */
|
|
60
|
+
readonly trigger?: CompactionTrigger;
|
|
61
|
+
/** Session gate: compact when the branch holds more than this many entries. */
|
|
62
|
+
readonly thresholdEntries?: number;
|
|
63
|
+
/** Attach-loop gate: compact when the estimated input is at or above this many tokens. */
|
|
64
|
+
readonly compactAfterTokens?: number;
|
|
65
|
+
}
|
|
66
|
+
/** Everything a compaction decision reads. Estimates only — never raw payloads. */
|
|
67
|
+
export interface ResolveShouldCompactInput {
|
|
68
|
+
readonly sessionId: string;
|
|
69
|
+
readonly entryCount: number;
|
|
70
|
+
/** Estimated tokens of the would-be input; called at most once, and only when a ratio or custom trigger reads it. */
|
|
71
|
+
readonly estimateInputTokens: () => number;
|
|
72
|
+
/** Resolved input cap (the attention compiler's `resolveInputCap`); called at most once, and only when a ratio or custom trigger reads it. */
|
|
73
|
+
readonly resolveInputCapTokens: () => number;
|
|
74
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
75
|
+
readonly signal?: AbortSignal;
|
|
76
|
+
/** Receives the failure behind a fail-closed `false`. */
|
|
77
|
+
readonly onError?: (error: unknown) => void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The single compact-when decision used by `autoCompact` and by host attach loops that gate their
|
|
81
|
+
* own post-run compaction (plan 074 C11).
|
|
82
|
+
*
|
|
83
|
+
* Precedence: an explicit `trigger` replaces the legacy gates. Only the token gates below a `trigger`
|
|
84
|
+
* replace are lazy — a `threshold_entries` trigger, or a `custom` callback that only reads counts,
|
|
85
|
+
* never pays for the token estimate or the input cap.
|
|
86
|
+
*
|
|
87
|
+
* Failure policy: a malformed trigger throws (`assertCompactionTrigger`, config error), while a
|
|
88
|
+
* throwing `custom.shouldCompact` — including a callback that reads an unresolvable cap — decides
|
|
89
|
+
* `false` and reports through `onError`, so a host bug can never compact on a guess.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveShouldCompact(options: ResolveShouldCompactOptions, input: ResolveShouldCompactInput): Promise<boolean>;
|
|
33
92
|
export interface CompactionMiddlewarePayload {
|
|
34
93
|
readonly context: CompactionContext;
|
|
35
94
|
readonly result: CompactionResult;
|
|
@@ -1,2 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
/** Validate a host trigger at config time, so an unknown `type` fails at create (C11). */
|
|
2
|
+
export function assertCompactionTrigger(trigger) {
|
|
3
|
+
if (typeof trigger !== "object" || trigger === null) {
|
|
4
|
+
throw new TypeError("compaction trigger must be an object");
|
|
5
|
+
}
|
|
6
|
+
switch (trigger.type) {
|
|
7
|
+
case "threshold_entries":
|
|
8
|
+
if (!Number.isSafeInteger(trigger.entries) || trigger.entries < 1) {
|
|
9
|
+
throw new TypeError("compaction trigger threshold_entries.entries must be a positive safe integer");
|
|
10
|
+
}
|
|
11
|
+
return trigger;
|
|
12
|
+
case "input_ratio":
|
|
13
|
+
if (!Number.isFinite(trigger.ratio) || trigger.ratio <= 0 || trigger.ratio >= 1) {
|
|
14
|
+
throw new TypeError("compaction trigger input_ratio.ratio must be a number in (0, 1)");
|
|
15
|
+
}
|
|
16
|
+
return trigger;
|
|
17
|
+
case "custom":
|
|
18
|
+
if (typeof trigger.shouldCompact !== "function") {
|
|
19
|
+
throw new TypeError("compaction trigger custom.shouldCompact must be a function");
|
|
20
|
+
}
|
|
21
|
+
return trigger;
|
|
22
|
+
default:
|
|
23
|
+
throw new TypeError(`unknown compaction trigger type: ${String(trigger.type)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The single compact-when decision used by `autoCompact` and by host attach loops that gate their
|
|
28
|
+
* own post-run compaction (plan 074 C11).
|
|
29
|
+
*
|
|
30
|
+
* Precedence: an explicit `trigger` replaces the legacy gates. Only the token gates below a `trigger`
|
|
31
|
+
* replace are lazy — a `threshold_entries` trigger, or a `custom` callback that only reads counts,
|
|
32
|
+
* never pays for the token estimate or the input cap.
|
|
33
|
+
*
|
|
34
|
+
* Failure policy: a malformed trigger throws (`assertCompactionTrigger`, config error), while a
|
|
35
|
+
* throwing `custom.shouldCompact` — including a callback that reads an unresolvable cap — decides
|
|
36
|
+
* `false` and reports through `onError`, so a host bug can never compact on a guess.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveShouldCompact(options, input) {
|
|
39
|
+
let estimated;
|
|
40
|
+
let cap;
|
|
41
|
+
const estimateOnce = () => (estimated ??= input.estimateInputTokens());
|
|
42
|
+
const capOnce = () => (cap ??= input.resolveInputCapTokens());
|
|
43
|
+
const trigger = options.trigger;
|
|
44
|
+
if (trigger === undefined) {
|
|
45
|
+
if (options.thresholdEntries !== undefined)
|
|
46
|
+
return input.entryCount > options.thresholdEntries;
|
|
47
|
+
if (options.compactAfterTokens !== undefined)
|
|
48
|
+
return estimateOnce() >= options.compactAfterTokens;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
assertCompactionTrigger(trigger);
|
|
52
|
+
if (trigger.type === "threshold_entries")
|
|
53
|
+
return input.entryCount > trigger.entries;
|
|
54
|
+
if (trigger.type === "input_ratio")
|
|
55
|
+
return estimateOnce() >= trigger.ratio * capOnce();
|
|
56
|
+
// Getter-backed so a callback that only reads counts never forces cap resolution, which throws
|
|
57
|
+
// when the active model declares no context window.
|
|
58
|
+
const context = {
|
|
59
|
+
sessionId: input.sessionId,
|
|
60
|
+
entryCount: input.entryCount,
|
|
61
|
+
get estimatedInputTokens() {
|
|
62
|
+
return estimateOnce();
|
|
63
|
+
},
|
|
64
|
+
get inputCapTokens() {
|
|
65
|
+
return capOnce();
|
|
66
|
+
},
|
|
67
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
68
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
return (await trigger.shouldCompact(context)) === true;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
input.onError?.(error);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
2
78
|
//# sourceMappingURL=compaction.js.map
|