@arnilo/prism 0.6.0 → 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 +44 -5
- package/README.md +10 -10
- package/dist/agent-definitions.js +1 -0
- 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 +14 -0
- package/dist/agent-session/session.js +40 -3
- 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/context-budget.d.ts +20 -1
- package/dist/context-budget.js +10 -1
- package/dist/contracts-core/agent.d.ts +7 -0
- 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 +9 -6
- package/dist/index.js +5 -4
- package/dist/input.d.ts +13 -1
- package/dist/input.js +40 -1
- package/dist/secure-agent.d.ts +2 -0
- package/dist/secure-agent.js +6 -1
- 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 +2 -1
- package/docs/ag-ui.md +5 -3
- package/docs/agent-definitions.md +9 -1
- package/docs/agent-events.md +4 -1
- package/docs/agent-session-runtime.md +6 -6
- package/docs/attention-compiler.md +272 -0
- 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/conversations.md +1 -1
- package/docs/dev-inspector.md +4 -0
- package/docs/device-adapters.md +1 -0
- package/docs/document-reader.md +11 -3
- package/docs/documents.md +10 -2
- package/docs/enterprise-postgres-state.md +2 -2
- package/docs/evaluations.md +168 -4
- package/docs/execution-timeline.md +180 -0
- 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 +37 -1
- package/docs/host-compositions.md +147 -0
- package/docs/hosted-sandboxes.md +94 -0
- package/docs/index.md +58 -39
- package/docs/input-and-prompt-assembly.md +1 -0
- package/docs/knowledge-sync.md +84 -0
- package/docs/language-intelligence.md +1 -1
- 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 +1 -1
- package/docs/migrate-to-0.6.md +1 -0
- package/docs/migrate-to-0.7.md +345 -0
- package/docs/migration.md +13 -1
- package/docs/model-routing.md +79 -4
- package/docs/multi-agent-patterns.md +20 -6
- package/docs/observability.md +52 -1
- package/docs/operations.md +13 -1
- package/docs/options-index.md +13 -1
- package/docs/peer-dependencies.md +6 -4
- 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 +1 -1
- package/docs/provider-packages.md +22 -22
- package/docs/providers/bedrock.md +71 -7
- package/docs/providers/openai.md +1 -1
- package/docs/rag.md +24 -8
- package/docs/realtime-voice.md +87 -0
- package/docs/release-and-install.md +36 -34
- package/docs/runs-and-usage.md +3 -2
- package/docs/server.md +5 -3
- package/docs/speech.md +2 -0
- package/docs/supervisors.md +33 -5
- package/docs/testing.md +1 -1
- package/docs/thinking-and-reasoning.md +3 -1
- package/docs/tools.md +6 -5
- package/docs/web-tools.md +2 -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 +1 -1
- 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Attention Compiler (plan 074 Tasks 2–3): frozen contracts, the ratio gate, and the two
|
|
2
|
+
* sticky stages (thinking strip, then old tool-result stubs). Creation is synchronous and
|
|
3
|
+
* fails closed; `compileAttention` measures the assembled request once, rewrites only the
|
|
4
|
+
* `history` / `toolResults` groups, and never touches the store, the om ledger, or the
|
|
5
|
+
* frozen prefix. The assembly branch lives in `input.ts`; opt-in agent wiring (Task 5) is not
|
|
6
|
+
* part of this module. */
|
|
7
|
+
import { type ContextBudgetMessageGroups } from "./context-budget.js";
|
|
8
|
+
import type { AttentionCompiler, AttentionCompilerContext, AttentionCompilerOptions, AttentionCompilerSetting, AttentionInputCapOptions, AttentionReport, ContextBlock, Skill, ToolDefinition } from "./contracts.js";
|
|
9
|
+
import { type CompactionTrigger } from "./contracts-core/compaction.js";
|
|
10
|
+
import type { SecretRedactor } from "./redaction.js";
|
|
11
|
+
import { type ResolvedToolResultFoldOptions } from "./tool-result-fold.js";
|
|
12
|
+
export declare const ATTENTION_BUDGET_ERROR_CODE: "attention_budget_exceeded";
|
|
13
|
+
/** C9: still over `triggerRatio` after every eligible stage — host should compact, not delete. */
|
|
14
|
+
export declare class AttentionBudgetError extends Error {
|
|
15
|
+
readonly code: "attention_budget_exceeded";
|
|
16
|
+
constructor(message?: string);
|
|
17
|
+
}
|
|
18
|
+
export declare function isAttentionBudgetError(error: unknown): error is AttentionBudgetError;
|
|
19
|
+
export declare const DEFAULT_ATTENTION_TRIGGER_RATIO = 0.75;
|
|
20
|
+
export declare const DEFAULT_ATTENTION_COMPACT_RATIO = 0.9;
|
|
21
|
+
export declare const DEFAULT_ATTENTION_THINKING_KEEP_TURNS = 1;
|
|
22
|
+
export declare const DEFAULT_ATTENTION_KEEP_LAST = 3;
|
|
23
|
+
export declare const DEFAULT_ATTENTION_RESERVE_TOKENS = 1024;
|
|
24
|
+
export declare function resolveAttentionReserveTokens(value: unknown): number;
|
|
25
|
+
/** C2: host `maxInputTokens` wins; otherwise `contextWindow - (maxOutputTokens ?? 0) - reserve`.
|
|
26
|
+
* Shared with the `input_ratio` compaction trigger (Task 4). Throws when no cap can be derived. */
|
|
27
|
+
export declare function resolveInputCap(options?: AttentionInputCapOptions, model?: AttentionCompilerContext["model"]): number;
|
|
28
|
+
/** Validate compiler options + resolve the input cap. No provider I/O; unknown or
|
|
29
|
+
* unresolvable input throws here rather than on the first turn. */
|
|
30
|
+
export declare function createAttentionCompiler(options?: AttentionCompilerOptions, context?: AttentionCompilerContext): AttentionCompiler;
|
|
31
|
+
/** Resolve the run's compiler from the agent setting plus an optional run overlay, validating
|
|
32
|
+
* both eagerly (no provider I/O) so a typo fails at run start, not on some later turn (C12).
|
|
33
|
+
* Returns `undefined` when the compiler is off — the assembly path then allocates nothing. */
|
|
34
|
+
export declare function resolveRunAttentionCompiler(agent: AttentionCompilerSetting | undefined, run: AttentionCompilerSetting | undefined, model: AttentionCompilerContext["model"]): AttentionCompiler | undefined;
|
|
35
|
+
/** Caller-owned sticky frontier: what this session leaf already mutated (C10). Mutations are
|
|
36
|
+
* monotonic, so a stubbed call stays stubbed and stripped thinking stays stripped even on a
|
|
37
|
+
* later under-ratio turn — restoring either would rewrite the prompt-cache prefix. */
|
|
38
|
+
export interface AttentionStickyFrontier {
|
|
39
|
+
/** SHA-256 keys of assistant messages whose thinking blocks were stripped. */
|
|
40
|
+
readonly thinking: Set<string>;
|
|
41
|
+
/** Tool call ids whose results were stubbed. */
|
|
42
|
+
readonly toolCallIds: Set<string>;
|
|
43
|
+
}
|
|
44
|
+
export declare function createAttentionStickyFrontier(): AttentionStickyFrontier;
|
|
45
|
+
export interface AttentionCompileOptions {
|
|
46
|
+
readonly compiler: AttentionCompiler;
|
|
47
|
+
/** The exact groups the assembler will send; only `history` and `toolResults` are rewritten. */
|
|
48
|
+
readonly groups: ContextBudgetMessageGroups;
|
|
49
|
+
readonly context?: readonly ContextBlock[];
|
|
50
|
+
readonly skills?: readonly Skill[];
|
|
51
|
+
readonly tools?: readonly ToolDefinition[];
|
|
52
|
+
/** Host fold: its `summarize` wins for the rows the compiler picks, and its age/byte gates
|
|
53
|
+
* decide fold-eligibility. Omitted → the deterministic stub and `keepLast` alone. */
|
|
54
|
+
readonly fold?: ResolvedToolResultFoldOptions;
|
|
55
|
+
readonly frontier?: AttentionStickyFrontier;
|
|
56
|
+
readonly redactor?: SecretRedactor;
|
|
57
|
+
readonly signal?: AbortSignal;
|
|
58
|
+
readonly turn?: number;
|
|
59
|
+
readonly sessionId?: string;
|
|
60
|
+
readonly runId?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface AttentionCompilation {
|
|
63
|
+
/** The input groups unchanged when nothing was mutated; otherwise a new object with the
|
|
64
|
+
* rewritten `history` / `toolResults` arrays. The input groups are never mutated. */
|
|
65
|
+
readonly groups: ContextBudgetMessageGroups;
|
|
66
|
+
readonly mutated: boolean;
|
|
67
|
+
readonly report: AttentionReport;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Measure once, then mutate oldest-first until back under `triggerRatio` (C4), or throw
|
|
71
|
+
* `AttentionBudgetError` when every eligible row is exhausted (C9). Front-of-frozen-prefix
|
|
72
|
+
* (instructions, summaries, input) is never touched, and neither are the caller's arrays.
|
|
73
|
+
*/
|
|
74
|
+
export declare function compileAttention(options: AttentionCompileOptions): Promise<AttentionCompilation>;
|
|
75
|
+
/** Serialized sticky frontier (plan 074 P3). Hashes and tool-call ids only: no model output,
|
|
76
|
+
* no payload, nothing that needs redaction, so a durable resume can restore it verbatim. */
|
|
77
|
+
export interface PersistedAttentionStickyFrontier {
|
|
78
|
+
readonly v: 1;
|
|
79
|
+
/** Newest-last, so a restored frontier is the tail of the mutations the session made. */
|
|
80
|
+
readonly thinking: readonly string[];
|
|
81
|
+
readonly toolCallIds: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
/** Bounded snapshot of a live frontier; caller-owned (the runtime persists it, the compiler
|
|
84
|
+
* never writes anywhere). */
|
|
85
|
+
export declare function serializeAttentionStickyFrontier(frontier: AttentionStickyFrontier): PersistedAttentionStickyFrontier;
|
|
86
|
+
/** Validate a persisted frontier from an untrusted store (plan 074 P3). Malformed *entries* are
|
|
87
|
+
* dropped one by one — a key the compiler cannot trust simply re-decides on the next turn —
|
|
88
|
+
* while a malformed *shape* yields `undefined` so the caller starts from an empty frontier.
|
|
89
|
+
* Never throws: a resume must not fail because a checkpoint was hand-edited. */
|
|
90
|
+
export declare function parseAttentionStickyFrontier(value: unknown): PersistedAttentionStickyFrontier | undefined;
|
|
91
|
+
/** Rebuild a frontier from a restored snapshot; callers hold the session that owns it. */
|
|
92
|
+
export declare function restoreAttentionStickyFrontier(persisted: PersistedAttentionStickyFrontier): AttentionStickyFrontier;
|
|
93
|
+
/** Consecutive `truncated` turns that arm compaction by default. */
|
|
94
|
+
export declare const DEFAULT_ATTENTION_TRUNCATION_THRESHOLD = 2;
|
|
95
|
+
export interface AttentionTruncationTriggerOptions {
|
|
96
|
+
/** Truncated turns in a row before the trigger fires (default 2). */
|
|
97
|
+
readonly threshold?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Host-side follow-up policy for `truncated` turns (plan 074 P4). `truncated: true` means the
|
|
101
|
+
* gate ran out of *eligible* rows: stubs cannot hold the request under the ratio, so the honest
|
|
102
|
+
* answer is a new prefix at the next task boundary rather than a silent eviction.
|
|
103
|
+
*
|
|
104
|
+
* Wire it by feeding every `attention_compiled` event to `observe` and handing `trigger` to
|
|
105
|
+
* `CompactionOptions.trigger` (agent config or run options). It fires **once per armed streak**,
|
|
106
|
+
* so a branch is compacted once and then left alone until new truncated turns arrive; a mutated
|
|
107
|
+
* turn that was not truncated clears the streak because the pressure was relieved.
|
|
108
|
+
*/
|
|
109
|
+
export interface AttentionTruncationTrigger {
|
|
110
|
+
/** Drop-in `CompactionOptions.trigger`. */
|
|
111
|
+
readonly trigger: CompactionTrigger;
|
|
112
|
+
/** Feed an `attention_compiled` event or an `AttentionReport`; returns the resulting streak. */
|
|
113
|
+
observe(report: {
|
|
114
|
+
readonly truncated?: unknown;
|
|
115
|
+
}): number;
|
|
116
|
+
/** Truncated turns in a row since the last fire or relief. */
|
|
117
|
+
readonly streak: () => number;
|
|
118
|
+
/** Clear the streak (host compacted for its own reasons). */
|
|
119
|
+
reset(): void;
|
|
120
|
+
}
|
|
121
|
+
export declare function createAttentionTruncationTrigger(options?: AttentionTruncationTriggerOptions): AttentionTruncationTrigger;
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/** Attention Compiler (plan 074 Tasks 2–3): frozen contracts, the ratio gate, and the two
|
|
2
|
+
* sticky stages (thinking strip, then old tool-result stubs). Creation is synchronous and
|
|
3
|
+
* fails closed; `compileAttention` measures the assembled request once, rewrites only the
|
|
4
|
+
* `history` / `toolResults` groups, and never touches the store, the om ledger, or the
|
|
5
|
+
* frozen prefix. The assembly branch lives in `input.ts`; opt-in agent wiring (Task 5) is not
|
|
6
|
+
* part of this module. */
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { estimateMessageBytes, estimateMessageTokens, estimateTextBytes, measureInputCost, } from "./context-budget.js";
|
|
9
|
+
import { assertCompactionTrigger } from "./contracts-core/compaction.js";
|
|
10
|
+
import { capToolResultSummary, foldedToolResultHeader, inferToolResultTurns, toolResultFoldText, } from "./tool-result-fold.js";
|
|
11
|
+
export const ATTENTION_BUDGET_ERROR_CODE = "attention_budget_exceeded";
|
|
12
|
+
/** C9: still over `triggerRatio` after every eligible stage — host should compact, not delete. */
|
|
13
|
+
export class AttentionBudgetError extends Error {
|
|
14
|
+
code = ATTENTION_BUDGET_ERROR_CODE;
|
|
15
|
+
constructor(message = "attention budget exceeded: still over triggerRatio after stub stages") {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "AttentionBudgetError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isAttentionBudgetError(error) {
|
|
21
|
+
return error instanceof Error && error.code === ATTENTION_BUDGET_ERROR_CODE;
|
|
22
|
+
}
|
|
23
|
+
export const DEFAULT_ATTENTION_TRIGGER_RATIO = 0.75;
|
|
24
|
+
export const DEFAULT_ATTENTION_COMPACT_RATIO = 0.9;
|
|
25
|
+
export const DEFAULT_ATTENTION_THINKING_KEEP_TURNS = 1;
|
|
26
|
+
export const DEFAULT_ATTENTION_KEEP_LAST = 3;
|
|
27
|
+
export const DEFAULT_ATTENTION_RESERVE_TOKENS = 1024;
|
|
28
|
+
/** Matches the run allow-list caps in `tools.ts`: no more tool names than a run could hold. */
|
|
29
|
+
const MAX_EXCLUDE_TOOL_NAMES = 1_024;
|
|
30
|
+
const MAX_TOOL_NAME_CHARS = 256;
|
|
31
|
+
function resolveRatio(value, fallback, name) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return fallback;
|
|
34
|
+
if (!Number.isFinite(value) || value <= 0 || value >= 1) {
|
|
35
|
+
throw new TypeError(`${name} must be a number in (0, 1)`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function resolveCount(value, fallback, name) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return fallback;
|
|
42
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
43
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export function resolveAttentionReserveTokens(value) {
|
|
48
|
+
return resolveCount(value, DEFAULT_ATTENTION_RESERVE_TOKENS, "attentionCompiler.reserveTokens");
|
|
49
|
+
}
|
|
50
|
+
/** C2: host `maxInputTokens` wins; otherwise `contextWindow - (maxOutputTokens ?? 0) - reserve`.
|
|
51
|
+
* Shared with the `input_ratio` compaction trigger (Task 4). Throws when no cap can be derived. */
|
|
52
|
+
export function resolveInputCap(options = {}, model) {
|
|
53
|
+
const reserveTokens = resolveAttentionReserveTokens(options.reserveTokens);
|
|
54
|
+
if (options.maxInputTokens !== undefined) {
|
|
55
|
+
if (!Number.isSafeInteger(options.maxInputTokens) || options.maxInputTokens < 1) {
|
|
56
|
+
throw new TypeError("attentionCompiler.maxInputTokens must be a positive safe integer");
|
|
57
|
+
}
|
|
58
|
+
return options.maxInputTokens;
|
|
59
|
+
}
|
|
60
|
+
const window = model?.limits?.contextWindow;
|
|
61
|
+
if (!Number.isSafeInteger(window) || window <= 0) {
|
|
62
|
+
throw new TypeError("attentionCompiler requires maxInputTokens or model.limits.contextWindow");
|
|
63
|
+
}
|
|
64
|
+
const maxOutputTokens = model?.limits?.maxOutputTokens;
|
|
65
|
+
if (maxOutputTokens !== undefined && (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens < 0)) {
|
|
66
|
+
throw new TypeError("attentionCompiler requires model.limits.maxOutputTokens to be a non-negative safe integer");
|
|
67
|
+
}
|
|
68
|
+
const cap = window - (maxOutputTokens ?? 0) - reserveTokens;
|
|
69
|
+
if (cap <= 0) {
|
|
70
|
+
throw new TypeError(`attentionCompiler resolved a non-positive input cap (${cap}): raise contextWindow or lower reserveTokens`);
|
|
71
|
+
}
|
|
72
|
+
return cap;
|
|
73
|
+
}
|
|
74
|
+
function resolveExcludeTools(value) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return Object.freeze([]);
|
|
77
|
+
if (!Array.isArray(value))
|
|
78
|
+
throw new TypeError("attentionCompiler.excludeTools must be an array of tool names");
|
|
79
|
+
if (value.length > MAX_EXCLUDE_TOOL_NAMES) {
|
|
80
|
+
throw new TypeError(`attentionCompiler.excludeTools exceeds ${MAX_EXCLUDE_TOOL_NAMES} entries`);
|
|
81
|
+
}
|
|
82
|
+
const out = [];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const name of value) {
|
|
85
|
+
if (typeof name !== "string" || name.length === 0 || name.length > MAX_TOOL_NAME_CHARS) {
|
|
86
|
+
throw new TypeError(`attentionCompiler.excludeTools entries must be non-empty strings of at most ${MAX_TOOL_NAME_CHARS} characters`);
|
|
87
|
+
}
|
|
88
|
+
if (!seen.has(name)) {
|
|
89
|
+
seen.add(name);
|
|
90
|
+
out.push(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze(out);
|
|
94
|
+
}
|
|
95
|
+
/** Validate compiler options + resolve the input cap. No provider I/O; unknown or
|
|
96
|
+
* unresolvable input throws here rather than on the first turn. */
|
|
97
|
+
export function createAttentionCompiler(options = {}, context = {}) {
|
|
98
|
+
if (typeof options !== "object" || options === null)
|
|
99
|
+
throw new TypeError("attentionCompiler options must be an object");
|
|
100
|
+
const triggerRatio = resolveRatio(options.triggerRatio, DEFAULT_ATTENTION_TRIGGER_RATIO, "attentionCompiler.triggerRatio");
|
|
101
|
+
const compactRatio = resolveRatio(options.compactRatio, DEFAULT_ATTENTION_COMPACT_RATIO, "attentionCompiler.compactRatio");
|
|
102
|
+
if (compactRatio <= triggerRatio) {
|
|
103
|
+
throw new TypeError(`attentionCompiler.compactRatio (${compactRatio}) must exceed triggerRatio (${triggerRatio})`);
|
|
104
|
+
}
|
|
105
|
+
const thinkingKeepTurns = resolveCount(options.thinkingKeepTurns, DEFAULT_ATTENTION_THINKING_KEEP_TURNS, "attentionCompiler.thinkingKeepTurns");
|
|
106
|
+
const keepLast = resolveCount(options.keepLast, DEFAULT_ATTENTION_KEEP_LAST, "attentionCompiler.keepLast");
|
|
107
|
+
const inputCap = resolveInputCap(options, context.model);
|
|
108
|
+
const reserveTokens = resolveAttentionReserveTokens(options.reserveTokens);
|
|
109
|
+
const excludeTools = resolveExcludeTools(options.excludeTools);
|
|
110
|
+
const trigger = context.compactionTrigger;
|
|
111
|
+
if (trigger !== undefined) {
|
|
112
|
+
assertCompactionTrigger(trigger);
|
|
113
|
+
if (trigger.type === "input_ratio" && trigger.ratio <= triggerRatio) {
|
|
114
|
+
throw new TypeError(`compaction input_ratio (${trigger.ratio}) must exceed attentionCompiler.triggerRatio (${triggerRatio})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return Object.freeze({ inputCap, reserveTokens, triggerRatio, compactRatio, thinkingKeepTurns, keepLast, excludeTools });
|
|
118
|
+
}
|
|
119
|
+
/* ------------------------------------------------------------------------------------------------
|
|
120
|
+
* Opt-in wiring (Task 5)
|
|
121
|
+
* ------------------------------------------------------------------------------------------------ */
|
|
122
|
+
/** Effective agent setting: `true` → defaults, object → as given, anything falsy → off. */
|
|
123
|
+
function enabledAttentionSetting(value) {
|
|
124
|
+
if (value === true)
|
|
125
|
+
return {};
|
|
126
|
+
if (value === undefined || value === false)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
129
|
+
throw new TypeError("attentionCompiler must be a boolean or an options object");
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
/** Run overlays may only lower the gate (higher ratios) and deepen the stages (lower keep
|
|
134
|
+
* counts); extending `excludeTools` protects more rows, so it is additive-only. Cap inputs
|
|
135
|
+
* stay config-level: moving them would move the gate itself. */
|
|
136
|
+
function mergeAttentionRunOverlay(base, overlay) {
|
|
137
|
+
const gate = (key, fallback) => {
|
|
138
|
+
const value = overlay[key];
|
|
139
|
+
if (value === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
const floor = base[key] ?? fallback;
|
|
142
|
+
if (value < floor) {
|
|
143
|
+
throw new TypeError(`RunOptions.attentionCompiler.${key} (${String(value)}) must not be more aggressive than the agent setting (${floor})`);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
};
|
|
147
|
+
const depth = (key, fallback) => {
|
|
148
|
+
const value = overlay[key];
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
const ceiling = base[key] ?? fallback;
|
|
152
|
+
if (value > ceiling) {
|
|
153
|
+
throw new TypeError(`RunOptions.attentionCompiler.${key} (${String(value)}) must not protect more than the agent setting (${ceiling})`);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
if (overlay.maxInputTokens !== undefined || overlay.reserveTokens !== undefined) {
|
|
158
|
+
throw new TypeError("RunOptions.attentionCompiler must not set maxInputTokens or reserveTokens: the input cap is agent-config only");
|
|
159
|
+
}
|
|
160
|
+
const triggerRatio = gate("triggerRatio", DEFAULT_ATTENTION_TRIGGER_RATIO);
|
|
161
|
+
const compactRatio = gate("compactRatio", DEFAULT_ATTENTION_COMPACT_RATIO);
|
|
162
|
+
const keepLast = depth("keepLast", DEFAULT_ATTENTION_KEEP_LAST);
|
|
163
|
+
const thinkingKeepTurns = depth("thinkingKeepTurns", DEFAULT_ATTENTION_THINKING_KEEP_TURNS);
|
|
164
|
+
const excludeTools = [...(base.excludeTools ?? []), ...(overlay.excludeTools ?? [])];
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
...(triggerRatio !== undefined && { triggerRatio }),
|
|
168
|
+
...(compactRatio !== undefined && { compactRatio }),
|
|
169
|
+
...(keepLast !== undefined && { keepLast }),
|
|
170
|
+
...(thinkingKeepTurns !== undefined && { thinkingKeepTurns }),
|
|
171
|
+
...(excludeTools.length > 0 && { excludeTools }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Resolve the run's compiler from the agent setting plus an optional run overlay, validating
|
|
175
|
+
* both eagerly (no provider I/O) so a typo fails at run start, not on some later turn (C12).
|
|
176
|
+
* Returns `undefined` when the compiler is off — the assembly path then allocates nothing. */
|
|
177
|
+
export function resolveRunAttentionCompiler(agent, run, model) {
|
|
178
|
+
const enabled = enabledAttentionSetting(agent);
|
|
179
|
+
if (enabled === undefined) {
|
|
180
|
+
if (run !== undefined && run !== false) {
|
|
181
|
+
throw new TypeError("RunOptions.attentionCompiler requires AgentConfig.attentionCompiler: a run may disable or relax the compiler, never enable it");
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (run === false)
|
|
186
|
+
return undefined;
|
|
187
|
+
if (run === undefined || run === true)
|
|
188
|
+
return createAttentionCompiler(enabled, { model });
|
|
189
|
+
if (typeof run !== "object" || run === null || Array.isArray(run)) {
|
|
190
|
+
throw new TypeError("RunOptions.attentionCompiler must be false or an options object");
|
|
191
|
+
}
|
|
192
|
+
return createAttentionCompiler(mergeAttentionRunOverlay(enabled, run), { model });
|
|
193
|
+
}
|
|
194
|
+
export function createAttentionStickyFrontier() {
|
|
195
|
+
return { thinking: new Set(), toolCallIds: new Set() };
|
|
196
|
+
}
|
|
197
|
+
/** Host decision/effect payloads are never stubbed (C6): any of these metadata keys on a tool
|
|
198
|
+
* message protects the row. Hosts with decision tools should list them in `excludeTools`. */
|
|
199
|
+
const PROTECTED_TOOL_RESULT_METADATA_KEYS = [
|
|
200
|
+
"approval",
|
|
201
|
+
"approvalId",
|
|
202
|
+
"prismApproval",
|
|
203
|
+
"decision",
|
|
204
|
+
"decisions",
|
|
205
|
+
"pendingDecisions",
|
|
206
|
+
"elicitation",
|
|
207
|
+
];
|
|
208
|
+
const THINKING_KEY_BYTES = 32;
|
|
209
|
+
/**
|
|
210
|
+
* Measure once, then mutate oldest-first until back under `triggerRatio` (C4), or throw
|
|
211
|
+
* `AttentionBudgetError` when every eligible row is exhausted (C9). Front-of-frozen-prefix
|
|
212
|
+
* (instructions, summaries, input) is never touched, and neither are the caller's arrays.
|
|
213
|
+
*/
|
|
214
|
+
export async function compileAttention(options) {
|
|
215
|
+
const { compiler, groups } = options;
|
|
216
|
+
const turn = options.turn ?? 1;
|
|
217
|
+
const frontier = options.frontier;
|
|
218
|
+
const triggerTokens = compiler.triggerRatio * compiler.inputCap;
|
|
219
|
+
const usedAtStart = measureInputCost({ groups, context: options.context, skills: options.skills, tools: options.tools }).tokens;
|
|
220
|
+
let used = usedAtStart;
|
|
221
|
+
const over = () => used >= triggerTokens;
|
|
222
|
+
const history = [...groups.history];
|
|
223
|
+
const toolResults = [...groups.toolResults];
|
|
224
|
+
let droppedThinkingTurns = 0;
|
|
225
|
+
let stubbedToolResults = 0;
|
|
226
|
+
let stubbedBytes = 0;
|
|
227
|
+
let leftEligible = 0;
|
|
228
|
+
// Stage 1 — strip thinking from every assistant turn except the newest `thinkingKeepTurns`.
|
|
229
|
+
const rowTurns = inferToolResultTurns(history);
|
|
230
|
+
for (const index of thinkingTargetIndexes(history, compiler.thinkingKeepTurns)) {
|
|
231
|
+
const message = history[index];
|
|
232
|
+
if (!message)
|
|
233
|
+
continue;
|
|
234
|
+
const key = thinkingKey(message, options.redactor);
|
|
235
|
+
const sticky = frontier?.thinking.has(key) === true;
|
|
236
|
+
if (!sticky && !over()) {
|
|
237
|
+
leftEligible += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const stripped = { ...message, content: message.content.filter((part) => part.type !== "thinking") };
|
|
241
|
+
history[index] = stripped;
|
|
242
|
+
used -= estimateMessageTokens(message) - estimateMessageTokens(stripped);
|
|
243
|
+
droppedThinkingTurns += 1;
|
|
244
|
+
rememberThinking(frontier, key);
|
|
245
|
+
}
|
|
246
|
+
// Stage 2 — stub the oldest tool results beyond the newest `keepLast` rows, across history
|
|
247
|
+
// and the in-flight group (which is always newer than history).
|
|
248
|
+
for (const target of toolResultTargets({ history, toolResults, rowTurns, turn, compiler, fold: options.fold })) {
|
|
249
|
+
const sticky = frontier?.toolCallIds.has(target.toolCallId) === true;
|
|
250
|
+
if (!sticky && !over()) {
|
|
251
|
+
leftEligible += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const stubbed = await stubToolResultMessage(target, options);
|
|
255
|
+
const before = estimateMessageTokens(target.message);
|
|
256
|
+
const after = estimateMessageTokens(stubbed);
|
|
257
|
+
if (after >= before && !sticky) {
|
|
258
|
+
// The stub would cost more than the payload it replaces: leave the row alone.
|
|
259
|
+
leftEligible += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const beforeBytes = estimateMessageBytes(target.message);
|
|
263
|
+
target.update(stubbed);
|
|
264
|
+
used -= before - after;
|
|
265
|
+
stubbedToolResults += 1;
|
|
266
|
+
stubbedBytes += Math.max(0, beforeBytes - estimateMessageBytes(stubbed));
|
|
267
|
+
rememberToolCall(frontier, target.toolCallId);
|
|
268
|
+
}
|
|
269
|
+
if (over()) {
|
|
270
|
+
throw new AttentionBudgetError(`attention budget exceeded: estimated ${used} tokens >= ${Math.ceil(triggerTokens)} (triggerRatio ${compiler.triggerRatio} of inputCap ${compiler.inputCap}) after dropping ${droppedThinkingTurns} thinking turns and stubbing ${stubbedToolResults} tool results`);
|
|
271
|
+
}
|
|
272
|
+
const mutated = droppedThinkingTurns > 0 || stubbedToolResults > 0;
|
|
273
|
+
return {
|
|
274
|
+
groups: mutated ? { ...groups, history, toolResults } : groups,
|
|
275
|
+
mutated,
|
|
276
|
+
report: {
|
|
277
|
+
used: usedAtStart,
|
|
278
|
+
usedAfter: Math.max(0, used),
|
|
279
|
+
inputCap: compiler.inputCap,
|
|
280
|
+
triggerRatio: compiler.triggerRatio,
|
|
281
|
+
droppedThinkingTurns,
|
|
282
|
+
stubbedToolResults,
|
|
283
|
+
stubbedBytes,
|
|
284
|
+
truncated: leftEligible > 0,
|
|
285
|
+
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
286
|
+
...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }),
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/** Thinking-bearing assistant-message indexes, oldest first, excluding the newest `keepTurns`.
|
|
291
|
+
* Counts thinking turns, not assistant messages, so a thinking-free reply does not push an
|
|
292
|
+
* older reasoning block out of the keep window. */
|
|
293
|
+
function thinkingTargetIndexes(history, keepTurns) {
|
|
294
|
+
const withThinking = history.flatMap((message, index) => message.role === "assistant" && message.content.some((part) => part.type === "thinking") ? [index] : []);
|
|
295
|
+
return withThinking.slice(0, Math.max(0, withThinking.length - keepTurns));
|
|
296
|
+
}
|
|
297
|
+
/** Identity of one assistant turn's thinking, hashed so the frontier retains no model output. */
|
|
298
|
+
function thinkingKey(message, redactor) {
|
|
299
|
+
const text = message.content
|
|
300
|
+
.flatMap((part) => (part.type === "thinking" ? [`${part.signature ?? ""}\u0000${part.text}`] : []))
|
|
301
|
+
.join("\u0001");
|
|
302
|
+
return digest(redactor?.redact(text) ?? text);
|
|
303
|
+
}
|
|
304
|
+
/** Rows beyond the newest `keepLast`, in oldest-first order, minus everything C6 protects. */
|
|
305
|
+
function toolResultTargets(options) {
|
|
306
|
+
const rows = [];
|
|
307
|
+
const add = (message, rowTurn, update) => {
|
|
308
|
+
const block = toolResultBlock(message);
|
|
309
|
+
if (!block)
|
|
310
|
+
return;
|
|
311
|
+
rows.push({ message, block, toolCallId: block.toolCallId, toolName: block.name, rowTurn, update });
|
|
312
|
+
};
|
|
313
|
+
for (let index = 0; index < options.history.length; index += 1) {
|
|
314
|
+
const message = options.history[index];
|
|
315
|
+
if (message)
|
|
316
|
+
add(message, options.rowTurns[index] ?? options.turn, (next) => void (options.history[index] = next));
|
|
317
|
+
}
|
|
318
|
+
for (let index = 0; index < options.toolResults.length; index += 1) {
|
|
319
|
+
const message = options.toolResults[index];
|
|
320
|
+
if (message)
|
|
321
|
+
add(message, options.turn, (next) => void (options.toolResults[index] = next));
|
|
322
|
+
}
|
|
323
|
+
const excluded = new Set(options.compiler.excludeTools);
|
|
324
|
+
const keepFrom = Math.max(0, rows.length - options.compiler.keepLast);
|
|
325
|
+
return rows.slice(0, keepFrom).filter((target) => {
|
|
326
|
+
const { block, message } = target;
|
|
327
|
+
if (block.error !== undefined && block.error !== null)
|
|
328
|
+
return false;
|
|
329
|
+
if (excluded.has(target.toolName))
|
|
330
|
+
return false;
|
|
331
|
+
if (isProtectedToolResult(message.metadata))
|
|
332
|
+
return false;
|
|
333
|
+
const fold = options.fold;
|
|
334
|
+
if (!fold)
|
|
335
|
+
return true;
|
|
336
|
+
if (options.turn - target.rowTurn < fold.minAgeTurns)
|
|
337
|
+
return false;
|
|
338
|
+
return estimateTextBytes(toolResultFoldText(block.result, block.error, message.content)) >= fold.minBytes;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function stubToolResultMessage(target, options) {
|
|
342
|
+
const { block, message } = target;
|
|
343
|
+
const text = toolResultFoldText(block.result, block.error, message.content);
|
|
344
|
+
const fold = options.fold;
|
|
345
|
+
const summary = fold
|
|
346
|
+
? capToolResultSummary(String(await fold.summarize({
|
|
347
|
+
sessionId: options.sessionId ?? "",
|
|
348
|
+
runId: options.runId ?? "",
|
|
349
|
+
turn: target.rowTurn,
|
|
350
|
+
toolCallId: target.toolCallId,
|
|
351
|
+
toolName: target.toolName,
|
|
352
|
+
text,
|
|
353
|
+
})), fold.maxSummaryBytes)
|
|
354
|
+
: attentionStubText(options.redactor?.redact(text) ?? text);
|
|
355
|
+
return {
|
|
356
|
+
...message,
|
|
357
|
+
content: message.content.map((part) => part.type === "tool_result" ? { ...part, result: foldedToolResultHeader(target.toolName, target.toolCallId, summary) } : part),
|
|
358
|
+
metadata: { ...message.metadata, prismFolded: true },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Deterministic default stub (C5): byte count plus a digest of the already-redacted text, so
|
|
362
|
+
* the model keeps a stable placeholder and never sees the payload again. */
|
|
363
|
+
function attentionStubText(redactedText) {
|
|
364
|
+
return `omitted ${estimateTextBytes(redactedText)} bytes (sha256 ${digest(redactedText)})`;
|
|
365
|
+
}
|
|
366
|
+
function toolResultBlock(message) {
|
|
367
|
+
const block = message.content.find((part) => part.type === "tool_result");
|
|
368
|
+
return block?.type === "tool_result" ? block : undefined;
|
|
369
|
+
}
|
|
370
|
+
function isProtectedToolResult(metadata) {
|
|
371
|
+
if (!metadata)
|
|
372
|
+
return false;
|
|
373
|
+
return PROTECTED_TOOL_RESULT_METADATA_KEYS.some((key) => metadata[key] !== undefined);
|
|
374
|
+
}
|
|
375
|
+
function digest(text) {
|
|
376
|
+
return createHash("sha256").update(text).digest("hex").slice(0, THINKING_KEY_BYTES);
|
|
377
|
+
}
|
|
378
|
+
/* ------------------------------------------------------------------------------------------------
|
|
379
|
+
* Frontier hygiene, persistence, and the truncated-turn follow-up (plan 074, Further Actions)
|
|
380
|
+
* ------------------------------------------------------------------------------------------------ */
|
|
381
|
+
/** Live and persisted frontier caps. Far above any realistic session (one entry per mutation),
|
|
382
|
+
* so eviction only ever drops mutations whose rows have long left the input window. */
|
|
383
|
+
const MAX_ATTENTION_THINKING_KEYS = 256;
|
|
384
|
+
const MAX_ATTENTION_TOOL_CALL_IDS = 256;
|
|
385
|
+
const MAX_ATTENTION_TOOL_CALL_ID_CHARS = 256;
|
|
386
|
+
const ATTENTION_STICKY_SCHEMA_VERSION = 1;
|
|
387
|
+
/** Keep the newest `max` keys: a session that runs long enough to overflow the cap should
|
|
388
|
+
* re-apply its recent mutations, not its oldest. */
|
|
389
|
+
function trimNewest(set, max) {
|
|
390
|
+
while (set.size > max) {
|
|
391
|
+
const oldest = set.values().next().value;
|
|
392
|
+
if (oldest === undefined)
|
|
393
|
+
break;
|
|
394
|
+
set.delete(oldest);
|
|
395
|
+
}
|
|
396
|
+
return [...set];
|
|
397
|
+
}
|
|
398
|
+
/** Record one mutation, keeping the live frontier inside its cap. The evicted key is the
|
|
399
|
+
* oldest one, whose row has almost certainly left the input window; if it ever comes back the
|
|
400
|
+
* compiler re-decides (and the deterministic stub re-applies byte-identically when over). */
|
|
401
|
+
function rememberThinking(frontier, key) {
|
|
402
|
+
if (!frontier)
|
|
403
|
+
return;
|
|
404
|
+
frontier.thinking.add(key);
|
|
405
|
+
trimNewest(frontier.thinking, MAX_ATTENTION_THINKING_KEYS);
|
|
406
|
+
}
|
|
407
|
+
function rememberToolCall(frontier, toolCallId) {
|
|
408
|
+
if (!frontier)
|
|
409
|
+
return;
|
|
410
|
+
frontier.toolCallIds.add(toolCallId);
|
|
411
|
+
trimNewest(frontier.toolCallIds, MAX_ATTENTION_TOOL_CALL_IDS);
|
|
412
|
+
}
|
|
413
|
+
/** Bounded snapshot of a live frontier; caller-owned (the runtime persists it, the compiler
|
|
414
|
+
* never writes anywhere). */
|
|
415
|
+
export function serializeAttentionStickyFrontier(frontier) {
|
|
416
|
+
return {
|
|
417
|
+
v: ATTENTION_STICKY_SCHEMA_VERSION,
|
|
418
|
+
thinking: trimNewest(frontier.thinking, MAX_ATTENTION_THINKING_KEYS),
|
|
419
|
+
toolCallIds: trimNewest(frontier.toolCallIds, MAX_ATTENTION_TOOL_CALL_IDS),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const THINKING_KEY_PATTERN = /^[0-9a-f]{32}$/;
|
|
423
|
+
/** Validate a persisted frontier from an untrusted store (plan 074 P3). Malformed *entries* are
|
|
424
|
+
* dropped one by one — a key the compiler cannot trust simply re-decides on the next turn —
|
|
425
|
+
* while a malformed *shape* yields `undefined` so the caller starts from an empty frontier.
|
|
426
|
+
* Never throws: a resume must not fail because a checkpoint was hand-edited. */
|
|
427
|
+
export function parseAttentionStickyFrontier(value) {
|
|
428
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
429
|
+
return undefined;
|
|
430
|
+
const record = value;
|
|
431
|
+
if (!Array.isArray(record.thinking) || !Array.isArray(record.toolCallIds))
|
|
432
|
+
return undefined;
|
|
433
|
+
const thinking = record.thinking.filter((key) => typeof key === "string" && THINKING_KEY_PATTERN.test(key));
|
|
434
|
+
const toolCallIds = record.toolCallIds.filter((id) => typeof id === "string" && id.length > 0 && id.length <= MAX_ATTENTION_TOOL_CALL_ID_CHARS && !id.includes("\u0000"));
|
|
435
|
+
return {
|
|
436
|
+
v: ATTENTION_STICKY_SCHEMA_VERSION,
|
|
437
|
+
// The tail is the newest window; a long-lived checkpoint cannot grow past the live caps.
|
|
438
|
+
thinking: thinking.slice(-MAX_ATTENTION_THINKING_KEYS),
|
|
439
|
+
toolCallIds: toolCallIds.slice(-MAX_ATTENTION_TOOL_CALL_IDS),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/** Rebuild a frontier from a restored snapshot; callers hold the session that owns it. */
|
|
443
|
+
export function restoreAttentionStickyFrontier(persisted) {
|
|
444
|
+
const frontier = createAttentionStickyFrontier();
|
|
445
|
+
for (const key of persisted.thinking)
|
|
446
|
+
frontier.thinking.add(key);
|
|
447
|
+
for (const id of persisted.toolCallIds)
|
|
448
|
+
frontier.toolCallIds.add(id);
|
|
449
|
+
return frontier;
|
|
450
|
+
}
|
|
451
|
+
/** Consecutive `truncated` turns that arm compaction by default. */
|
|
452
|
+
export const DEFAULT_ATTENTION_TRUNCATION_THRESHOLD = 2;
|
|
453
|
+
export function createAttentionTruncationTrigger(options = {}) {
|
|
454
|
+
const threshold = options.threshold ?? DEFAULT_ATTENTION_TRUNCATION_THRESHOLD;
|
|
455
|
+
if (!Number.isSafeInteger(threshold) || threshold < 1) {
|
|
456
|
+
throw new TypeError("attention truncation threshold must be a positive safe integer");
|
|
457
|
+
}
|
|
458
|
+
let streak = 0;
|
|
459
|
+
return {
|
|
460
|
+
trigger: {
|
|
461
|
+
type: "custom",
|
|
462
|
+
shouldCompact: () => {
|
|
463
|
+
if (streak < threshold)
|
|
464
|
+
return false;
|
|
465
|
+
streak = 0;
|
|
466
|
+
return true;
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
observe: (report) => {
|
|
470
|
+
streak = report.truncated === true ? streak + 1 : 0;
|
|
471
|
+
return streak;
|
|
472
|
+
},
|
|
473
|
+
streak: () => streak,
|
|
474
|
+
reset: () => {
|
|
475
|
+
streak = 0;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
//# sourceMappingURL=attention-compiler.js.map
|
package/dist/cli-init.js
CHANGED
|
@@ -283,6 +283,7 @@ export async function createInitProject(options, runtime = {
|
|
|
283
283
|
version,
|
|
284
284
|
templateName,
|
|
285
285
|
provider,
|
|
286
|
+
templateDir,
|
|
286
287
|
});
|
|
287
288
|
}
|
|
288
289
|
else {
|
|
@@ -360,12 +361,25 @@ async function planTemplateFiles(templateDir) {
|
|
|
360
361
|
return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
361
362
|
}
|
|
362
363
|
function buildTokensForTemplate(input) {
|
|
363
|
-
const dependencies = {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
const dependencies = {};
|
|
365
|
+
let packages = ["@arnilo/prism", "@arnilo/prism-memory", "@arnilo/prism-web-tools", "@arnilo/prism-workflows"];
|
|
366
|
+
if (input.templateDir) {
|
|
367
|
+
const manifestPath = join(input.templateDir, "manifest.json");
|
|
368
|
+
if (existsSync(manifestPath)) {
|
|
369
|
+
try {
|
|
370
|
+
const raw = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
371
|
+
if (Array.isArray(raw.packages) && raw.packages.length > 0) {
|
|
372
|
+
packages = raw.packages;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// fallback
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const pkg of packages) {
|
|
381
|
+
dependencies[pkg] = input.version;
|
|
382
|
+
}
|
|
369
383
|
const dependencyLines = Object.entries(dependencies)
|
|
370
384
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
371
385
|
.map(([name, ver]) => ` ${JSON.stringify(name)}: ${JSON.stringify(ver)}`)
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
3
|
/**
|
|
4
4
|
* Host-supplied token estimator. Budget-only: it never reaches billing, provider
|
|
5
5
|
* usage, or the wire — it decides what the assembler evicts and nothing else.
|
|
@@ -76,3 +76,22 @@ export declare function applyContextBudget(options: {
|
|
|
76
76
|
readonly demotedSkillBodies: readonly string[];
|
|
77
77
|
readonly report: ContextBudgetReport;
|
|
78
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
|
+
};
|