@alisio/core 0.8.0 → 0.10.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.
Files changed (58) hide show
  1. package/dist/analysis/data/engine-client.d.ts +2 -0
  2. package/dist/analysis/data/engine-client.js +11 -4
  3. package/dist/application.d.ts +11 -1
  4. package/dist/application.js +46 -0
  5. package/dist/changelog/data.js +42 -0
  6. package/dist/commands/builtins.js +6 -0
  7. package/dist/commands/catalog.d.ts +4 -2
  8. package/dist/commands/catalog.js +23 -1
  9. package/dist/config.d.ts +31 -0
  10. package/dist/config.js +31 -1
  11. package/dist/context/elide.d.ts +50 -0
  12. package/dist/context/elide.js +222 -0
  13. package/dist/context/format.d.ts +21 -0
  14. package/dist/context/format.js +81 -0
  15. package/dist/context/governance.d.ts +58 -0
  16. package/dist/context/governance.js +96 -0
  17. package/dist/context/invariants.d.ts +88 -0
  18. package/dist/context/invariants.js +141 -0
  19. package/dist/context/inventory.d.ts +22 -0
  20. package/dist/context/inventory.js +178 -0
  21. package/dist/context/report.d.ts +35 -0
  22. package/dist/context/report.js +120 -0
  23. package/dist/context/sources.d.ts +20 -0
  24. package/dist/context/sources.js +41 -0
  25. package/dist/context/types.d.ts +43 -0
  26. package/dist/context/types.js +5 -0
  27. package/dist/context/usage.d.ts +96 -0
  28. package/dist/context/usage.js +264 -0
  29. package/dist/context/util.d.ts +22 -0
  30. package/dist/context/util.js +36 -0
  31. package/dist/core/compaction.d.ts +7 -2
  32. package/dist/core/compaction.js +9 -3
  33. package/dist/core/contracts.d.ts +8 -0
  34. package/dist/core/runner.d.ts +67 -0
  35. package/dist/core/runner.js +272 -10
  36. package/dist/eval/compare.d.ts +1 -1
  37. package/dist/eval/compare.js +18 -1
  38. package/dist/eval/engine.js +31 -1
  39. package/dist/eval/report.js +44 -1
  40. package/dist/forge/javascript/hosts/process-host.d.ts +1 -1
  41. package/dist/forge/javascript/hosts/process-host.js +4 -1
  42. package/dist/forge/python/wasm-node.d.ts +1 -1
  43. package/dist/forge/python/wasm-node.js +4 -1
  44. package/dist/index.d.ts +8 -2
  45. package/dist/index.js +8 -2
  46. package/dist/plugins/host.d.ts +5 -0
  47. package/dist/plugins/host.js +15 -1
  48. package/dist/plugins/install.d.ts +50 -2
  49. package/dist/plugins/install.js +102 -12
  50. package/dist/plugins/local-install.d.ts +33 -0
  51. package/dist/plugins/local-install.js +121 -0
  52. package/dist/plugins/npm-runner.d.ts +30 -0
  53. package/dist/plugins/npm-runner.js +66 -0
  54. package/dist/plugins/tarball.d.ts +39 -0
  55. package/dist/plugins/tarball.js +277 -0
  56. package/dist/plugins/version-compat.d.ts +20 -0
  57. package/dist/plugins/version-compat.js +154 -0
  58. package/package.json +2 -2
@@ -0,0 +1,222 @@
1
+ import { DEFAULT_READ_BYTES } from "../background/output.js";
2
+ import { estimateTokens } from "../core/compaction.js";
3
+ import { describeCall, parseObject, resultText, segmentId, sha, stableStringify, WRITE_TOOLS, } from "./util.js";
4
+ /**
5
+ * Deterministic elision by substitution (spec 012 phase 2). Pure functions over `Message[]`: they
6
+ * never touch the persisted transcript, only compute what a request MAY send instead.
7
+ */
8
+ /** Version of the elision rules. Bump it whenever a rule or a stub format changes. */
9
+ export const CONTEXT_POLICY_VERSION = 1;
10
+ const READ_STUB_DEFAULT_START = 1;
11
+ const READ_STUB_DEFAULT_LIMIT = 200;
12
+ const HEAD_SHA = /^\{"path":"(?:[^"\\]|\\.)*","sha256":"([0-9a-f]{64})"/;
13
+ /** `sha256` of a read_file result, from the parsed JSON or (clipped results) from its head. */
14
+ function readSha(text) {
15
+ const value = parseObject(text)?.sha256;
16
+ if (typeof value === "string" && value.length > 0)
17
+ return value;
18
+ if (value !== undefined)
19
+ return undefined;
20
+ return HEAD_SHA.exec(text.slice(0, 4096))?.[1];
21
+ }
22
+ /** bg_output arguments with the tool's own defaults applied, as a comparison key. */
23
+ function bgKey(call) {
24
+ const id = call.args?.id;
25
+ if (typeof id !== "string")
26
+ return undefined;
27
+ const offset = typeof call.args?.offset === "number" ? call.args.offset : 0;
28
+ const limit = typeof call.args?.limit === "number" ? call.args.limit : DEFAULT_READ_BYTES;
29
+ return { id, offset, key: `${id}\0${offset}\0${limit}` };
30
+ }
31
+ /**
32
+ * Hash of what a search found: the exit code and the ripgrep `match`/`context` lines. The summary
33
+ * lines carry the elapsed time and would make identical searches look different. Text that is not
34
+ * the expected shape is hashed as is.
35
+ */
36
+ function searchHash(text) {
37
+ const out = parseObject(text);
38
+ if (!out || typeof out.stdout !== "string" || typeof out.exitCode !== "number")
39
+ return sha(text);
40
+ const kept = [];
41
+ for (const line of out.stdout.split("\n")) {
42
+ if (line === "")
43
+ continue;
44
+ const event = parseObject(line);
45
+ if (!event)
46
+ return sha(text);
47
+ if (event.type === "match" || event.type === "context")
48
+ kept.push(line);
49
+ }
50
+ return sha(`${out.exitCode}\n${kept.join("\n")}`);
51
+ }
52
+ const searchKey = (e) => `${e.call.args ? stableStringify(e.call.args) : e.call.arguments}\0${searchHash(e.text)}`;
53
+ /**
54
+ * Which tool results a later result makes redundant, regardless of age. `witness` is the index of
55
+ * the EARLIEST later result that proves it, so it never changes when messages are appended. Every
56
+ * rule is existential ("some later result exists"), which is what makes the set grow monotonically.
57
+ * Error results are neither candidates nor witnesses.
58
+ */
59
+ export function elisionCandidates(messages) {
60
+ return analyze(messages).candidates;
61
+ }
62
+ function analyze(messages) {
63
+ const calls = new Map();
64
+ const entries = [];
65
+ messages.forEach((m, index) => {
66
+ if (m.role === "assistant") {
67
+ for (const c of m.calls)
68
+ calls.set(c.id, describeCall(c.name, c.arguments));
69
+ }
70
+ else if (m.role === "tool" && m.result.isError !== true) {
71
+ const call = calls.get(m.callId);
72
+ if (call)
73
+ entries.push({ index, callId: m.callId, call, text: resultText(m) });
74
+ }
75
+ });
76
+ const out = new Map();
77
+ const byIndex = new Map(entries.map((e) => [e.index, e]));
78
+ const byPath = new Map();
79
+ const byTask = new Map();
80
+ const bySearch = new Map();
81
+ for (const e of entries) {
82
+ if (e.call.path) {
83
+ const list = byPath.get(e.call.path) ?? [];
84
+ list.push(e);
85
+ byPath.set(e.call.path, list);
86
+ }
87
+ else if (e.call.name === "bg_output") {
88
+ const id = bgKey(e.call)?.id;
89
+ if (id !== undefined)
90
+ byTask.set(id, [...(byTask.get(id) ?? []), e]);
91
+ }
92
+ else if (e.call.name === "search_text") {
93
+ const key = searchKey(e);
94
+ bySearch.set(key, [...(bySearch.get(key) ?? []), e]);
95
+ }
96
+ }
97
+ for (const list of byPath.values()) {
98
+ list.forEach((x, at) => {
99
+ if (x.call.name !== "read_file")
100
+ return;
101
+ const shaX = readSha(x.text);
102
+ const w = list.slice(at + 1).find((later) => {
103
+ if (WRITE_TOOLS.has(later.call.name))
104
+ return true;
105
+ if (later.call.name !== "read_file")
106
+ return false;
107
+ const shaW = readSha(later.text);
108
+ return shaX !== undefined && shaW !== undefined && shaX !== shaW;
109
+ });
110
+ if (w)
111
+ out.set(x.index, { reason: "superseded_read", witness: w.index });
112
+ });
113
+ }
114
+ for (const list of byTask.values()) {
115
+ list.forEach((x, at) => {
116
+ const mine = bgKey(x.call);
117
+ if (!mine)
118
+ return;
119
+ const emptyPoll = parseObject(x.text)?.text === "";
120
+ const w = list
121
+ .slice(at + 1)
122
+ .find((later) => emptyPoll || bgKey(later.call)?.key === mine.key);
123
+ if (w)
124
+ out.set(x.index, { reason: "stale_bg_output", witness: w.index });
125
+ });
126
+ }
127
+ for (const list of bySearch.values()) {
128
+ list.forEach((x, at) => {
129
+ const w = list[at + 1];
130
+ if (w)
131
+ out.set(x.index, { reason: "duplicate_search", witness: w.index });
132
+ });
133
+ }
134
+ return { candidates: out, entries: byIndex };
135
+ }
136
+ /** JSON text restricted to printable ASCII on one line, so a stub never needs escaping again. */
137
+ const quote = (value) => (JSON.stringify(value) ?? "null").replace(/[^\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
138
+ /** The replacement text. It depends only on the elided call's arguments and its own result head. */
139
+ function stubFor(reason, x) {
140
+ if (reason === "superseded_read") {
141
+ const { startLine, limit } = x.call.args ?? {};
142
+ const window = typeof startLine === "number" || typeof limit === "number"
143
+ ? ` lines ${typeof startLine === "number" ? startLine : READ_STUB_DEFAULT_START}+${typeof limit === "number" ? limit : READ_STUB_DEFAULT_LIMIT}`
144
+ : "";
145
+ return `[elided: read_file ${quote(x.call.path)}@${(readSha(x.text) ?? "unknown").slice(0, 8)}${window}; re-read to view]`;
146
+ }
147
+ if (reason === "stale_bg_output") {
148
+ const mine = bgKey(x.call);
149
+ return `[elided: bg_output ${quote(mine?.id)} offset ${mine?.offset ?? 0}; a later bg_output covers it]`;
150
+ }
151
+ return `[elided: search_text ${quote(x.call.args?.pattern)}; identical to a later result]`;
152
+ }
153
+ /** Estimated tokens of everything a message puts in front of the model. */
154
+ function visibleTokens(m) {
155
+ if (m.role === "tool")
156
+ return estimateTokens(resultText(m));
157
+ if (m.role === "user")
158
+ return estimateTokens(m.text);
159
+ return estimateTokens(m.calls.reduce((text, c) => `${text}\n${c.name} ${c.arguments}`, m.text));
160
+ }
161
+ /**
162
+ * Projects the history a request would carry: tool results that a later result made redundant are
163
+ * replaced by a one-line stub once they are `minAgeTurns` assistant messages old (counted inside the
164
+ * eligible prefix). With `epochMessages` E > 0 only the prefix `floor(n / E) * E` is eligible, so the
165
+ * projected history grows by appending between epochs and a provider prompt cache stays valid.
166
+ */
167
+ export function projectContext(messages, o) {
168
+ const n = messages.length;
169
+ const epoch = Number.isFinite(o.epochMessages) ? Math.max(0, Math.floor(o.epochMessages)) : 0;
170
+ const minAge = Number.isFinite(o.minAgeTurns) ? Math.max(1, Math.floor(o.minAgeTurns)) : 1;
171
+ const boundary = epoch > 0 ? Math.floor(n / epoch) * epoch : n;
172
+ const prefix = boundary === n ? messages : messages.slice(0, boundary);
173
+ const { candidates, entries } = analyze(prefix);
174
+ const tokensBefore = messages.reduce((t, m) => t + visibleTokens(m), 0);
175
+ if (candidates.size === 0)
176
+ return { messages, elisions: [], tokensBefore, tokensAfter: tokensBefore, boundary };
177
+ // Assistant messages after each index, inside the prefix.
178
+ const assistantsAfter = new Array(boundary).fill(0);
179
+ let seen = 0;
180
+ for (let i = boundary - 1; i >= 0; i--) {
181
+ assistantsAfter[i] = seen;
182
+ if (prefix[i]?.role === "assistant")
183
+ seen++;
184
+ }
185
+ const elisions = [];
186
+ let projected;
187
+ for (const [index, c] of [...candidates].sort(([a], [b]) => a - b)) {
188
+ if ((assistantsAfter[index] ?? 0) < minAge)
189
+ continue;
190
+ const x = entries.get(index);
191
+ const m = prefix[index];
192
+ const w = prefix[c.witness];
193
+ if (!x || m?.role !== "tool" || w?.role !== "tool")
194
+ continue;
195
+ const text = x.text;
196
+ const stub = stubFor(c.reason, x);
197
+ // Size guard: a stub that is not shorter would cost more than it saves. It depends on the
198
+ // elided entry alone, so it never makes a decision flip under append.
199
+ if (stub.length >= text.length)
200
+ continue;
201
+ projected ??= messages.slice();
202
+ projected[index] = { ...m, result: { ...m.result, content: [{ type: "text", text: stub }] } };
203
+ elisions.push({
204
+ index,
205
+ callId: m.callId,
206
+ segmentId: segmentId("tool_result", m.callId, text),
207
+ reason: c.reason,
208
+ witnessCallId: w.callId,
209
+ tokensSaved: estimateTokens(text) - estimateTokens(stub),
210
+ });
211
+ }
212
+ if (!projected)
213
+ return { messages, elisions: [], tokensBefore, tokensAfter: tokensBefore, boundary };
214
+ const saved = elisions.reduce((t, e) => t + e.tokensSaved, 0);
215
+ return {
216
+ messages: projected,
217
+ elisions,
218
+ tokensBefore,
219
+ tokensAfter: tokensBefore - saved,
220
+ boundary,
221
+ };
222
+ }
@@ -0,0 +1,21 @@
1
+ import type { ContextReport, ContextSegment } from "@alisio/sdk";
2
+ import type { GovernanceSnapshot } from "./governance.ts";
3
+ import type { UsageSummary } from "./usage.ts";
4
+ /**
5
+ * The "Context Governor" block of `/stats`, shared by the core handler and the TUI report.
6
+ * `undefined` when there is nothing to show (mode `off`, or no request has been measured yet), so
7
+ * the section is omitted. Counts and ratios only; the usage figures are a path heuristic for this
8
+ * process and session (spec 012 section 3.4).
9
+ */
10
+ export declare function formatContextGovernorStatsSection(input: {
11
+ governance?: GovernanceSnapshot;
12
+ usage?: UsageSummary;
13
+ }): string | undefined;
14
+ /** The segments a reader needs explained: pinned, elided and low ones. The rest is counted. */
15
+ export declare const notableSegments: (report: ContextReport) => ContextSegment[];
16
+ /**
17
+ * `/context` as markdown, for the TUI and the core command catalog. It renders the same
18
+ * `ContextReport` the web panel and the route show: every elided, pinned and low segment with its
19
+ * reason, and counts for the rest. Ids, counts and estimates only.
20
+ */
21
+ export declare function formatContextReport(report: ContextReport): string;
@@ -0,0 +1,81 @@
1
+ const REASONS = ["superseded_read", "stale_bg_output", "duplicate_search"];
2
+ const percent = (used, observed) => Math.round((used / observed) * 100);
3
+ /**
4
+ * The "Context Governor" block of `/stats`, shared by the core handler and the TUI report.
5
+ * `undefined` when there is nothing to show (mode `off`, or no request has been measured yet), so
6
+ * the section is omitted. Counts and ratios only; the usage figures are a path heuristic for this
7
+ * process and session (spec 012 section 3.4).
8
+ */
9
+ export function formatContextGovernorStatsSection(input) {
10
+ const { governance: g, usage: u } = input;
11
+ const hasUsage = !!u && u.observed + u.pending + u.unattributed + u.stubOnly + u.rereads > 0;
12
+ if (!g && !hasUsage)
13
+ return undefined;
14
+ const lines = ["**Context Governor**", ""];
15
+ if (g) {
16
+ lines.push(`- Mode: ${g.mode} · policy v${g.policyVersion}`);
17
+ const reasons = REASONS.filter((r) => g.byReason[r] > 0).map((r) => `${r} ${g.byReason[r]}`);
18
+ lines.push(`- Last request: ${g.messages} messages · ${g.elided} elided${reasons.length ? ` (${reasons.join(", ")})` : ""} · ~${g.tokensSaved} tokens saved (estimate)`);
19
+ const inv = g.invariants;
20
+ if (inv.count > 0 || inv.chars > 0 || inv.omitted > 0)
21
+ lines.push(`- Invariants: ${inv.count} (${inv.chars} chars, ${inv.omitted} omitted)`);
22
+ }
23
+ if (u && hasUsage) {
24
+ if (u.observed > 0) {
25
+ const tools = Object.entries(u.byTool)
26
+ .filter(([, c]) => c.observed > 0)
27
+ .sort(([an, a], [bn, b]) => b.observed - a.observed || (an < bn ? -1 : an > bn ? 1 : 0))
28
+ .map(([name, c]) => `${name} ${c.used}/${c.observed}`);
29
+ lines.push(`- Tool results used later: ${u.used} of ${u.observed} (${percent(u.used, u.observed)}%)${tools.length ? ` · ${tools.join(" · ")}` : ""}`);
30
+ }
31
+ lines.push(`- Not attributable: ${u.unattributed} · pending: ${u.pending} · re-reads: ${u.rereads}${u.stubOnly > 0 ? ` · stub only: ${u.stubOnly}` : ""}`);
32
+ }
33
+ lines.push('- Counts cover this process only; "used" is a path heuristic, not a measure of what the model needed.');
34
+ return lines.join("\n");
35
+ }
36
+ const cell = (text) => text.replace(/[|\r\n]+/g, " ").trim();
37
+ /** The segments a reader needs explained: pinned, elided and low ones. The rest is counted. */
38
+ export const notableSegments = (report) => report.segments.filter((s) => s.criticality === "pinned" || s.criticality === "low" || s.elided);
39
+ /**
40
+ * `/context` as markdown, for the TUI and the core command catalog. It renders the same
41
+ * `ContextReport` the web panel and the route show: every elided, pinned and low segment with its
42
+ * reason, and counts for the rest. Ids, counts and estimates only.
43
+ */
44
+ export function formatContextReport(report) {
45
+ if (report.mode === "off")
46
+ return [
47
+ "**Context**",
48
+ "",
49
+ "- The Context Governor is off, so nothing is measured for this session.",
50
+ '- Set `context.governor` to `"shadow"` (measure only) or `"on"` (also elide) to see which segments the next request carries and why.',
51
+ ].join("\n");
52
+ const reasons = REASONS.filter((r) => report.byReason[r] > 0).map((r) => `${r} ${report.byReason[r]}`);
53
+ const lines = [
54
+ "**Context**",
55
+ "",
56
+ `- Mode: ${report.mode} · policy v${report.policyVersion}${report.request ? ` · latest request #${report.request}` : ""}`,
57
+ `- History: ${report.messages} messages · ~${report.tokensBefore} tokens (estimate) -> ~${report.tokensAfter} after the policy (${report.mode === "on" ? "applied" : "measured only, not applied"})`,
58
+ `- Elided${report.mode === "on" ? "" : " (would be)"}: ${report.elided}${reasons.length ? ` (${reasons.join(", ")})` : ""} · ~${report.tokensSaved} tokens saved (estimate)`,
59
+ ];
60
+ const ratios = Object.entries(report.usedRatioByKind).map(([kind, ratio]) => `${kind} ${Math.round((ratio ?? 0) * 100)}%`);
61
+ if (ratios.length)
62
+ lines.push(`- Used later: ${ratios.join(" · ")} (path heuristic)`);
63
+ const inv = report.invariants;
64
+ const kinds = Object.entries(inv.byKind).map(([kind, n]) => `${kind} ${n}`);
65
+ lines.push(`- Invariants: ${inv.count}${inv.omitted ? ` (${inv.omitted} omitted)` : ""}${kinds.length ? ` · ${kinds.join(" · ")}` : ""} (counts only)`);
66
+ const notable = notableSegments(report);
67
+ if (notable.length) {
68
+ lines.push("", "| Segment | Kind | Origin | Tokens | Criticality | Decision |", "| --- | --- | --- | --- | --- | --- |");
69
+ for (const s of notable) {
70
+ const decision = s.elided
71
+ ? `${report.mode === "on" ? "elided" : "would elide"}: ${s.elided.reason}${s.elided.replacedBy ? ` (replaced by ${cell(s.elided.replacedBy)})` : ""} · ~${s.elided.tokensSaved} saved`
72
+ : (s.reason ?? "");
73
+ lines.push(`| \`${s.id}\` | ${s.kind} | ${cell(s.origin ?? "")}${s.callId ? ` (${cell(s.callId)})` : ""} | ${s.tokens} | ${s.criticality} | ${cell(decision)} |`);
74
+ }
75
+ }
76
+ const rest = report.segments.length - notable.length + (report.segmentsOmitted ?? 0);
77
+ if (rest > 0)
78
+ lines.push("", `${rest} more segments are ordinary or recent and not listed.`);
79
+ lines.push("", 'Token figures are estimates; "used" is a path heuristic for this process.');
80
+ return lines.join("\n");
81
+ }
@@ -0,0 +1,58 @@
1
+ import type { ContextGovernedData } from "@alisio/sdk";
2
+ import type { Elision, ElisionReason, Projection } from "./elide.ts";
3
+ import type { UsageSummary } from "./usage.ts";
4
+ /** Mode of the Context Governor (spec 012): `off` is inert, `shadow` measures, `on` applies. */
5
+ export type GovernorMode = "off" | "shadow" | "on";
6
+ /** Settings the runner needs; `application.ts` builds them from `config.context`. */
7
+ export interface ContextGovernorOptions {
8
+ mode: GovernorMode;
9
+ minAgeTurns: number;
10
+ epochMessages: number;
11
+ policyVersion: number;
12
+ /** Size cap of the `<invariants>` block (`context.invariants.maxChars`); default 4000. */
13
+ invariantMaxChars?: number;
14
+ }
15
+ /** Counts of the invariants block; never its content (spec 012 D7). */
16
+ export interface InvariantCounts {
17
+ /** Invariants rendered into the latest request's block. */
18
+ count: number;
19
+ /** Length of that block in characters (0 when none was built). */
20
+ chars: number;
21
+ /** Invariants left out because they did not fit `maxChars`. */
22
+ omitted: number;
23
+ /** Instruction quotes promoted by the compactions of this process (`on` only). */
24
+ promoted: number;
25
+ /** Proposed quotes refused by the substring check or the cap (`on` only). */
26
+ rejected: number;
27
+ }
28
+ /**
29
+ * What the governor decided for the latest request of a session. Counts, ids and token estimates
30
+ * only: never file contents, arguments or stub text (spec 012 D7). `governedEventData` turns it
31
+ * into the `context_governed` event.
32
+ */
33
+ export interface GovernanceSnapshot {
34
+ mode: Exclude<GovernorMode, "off">;
35
+ policyVersion: number;
36
+ /** 1-based number of the model request inside the current run. */
37
+ request: number;
38
+ messages: number;
39
+ /** Inventory segments of the history (the rule of `buildInventory`, without pinned context). */
40
+ segments: number;
41
+ /** Messages `[0, boundary)` were eligible for elision. */
42
+ boundary: number;
43
+ elided: number;
44
+ byReason: Record<ElisionReason, number>;
45
+ tokensBefore: number;
46
+ tokensAfter: number;
47
+ tokensSaved: number;
48
+ elisions: Array<Pick<Elision, "index" | "callId" | "segmentId" | "reason" | "tokensSaved">>;
49
+ invariants: InvariantCounts;
50
+ /** Call ids elided at this request but not at the previous snapshot of the session. */
51
+ newlyElided: string[];
52
+ }
53
+ export declare function governanceSnapshot(projection: Projection, options: ContextGovernorOptions, request: number, previous?: GovernanceSnapshot): GovernanceSnapshot;
54
+ /**
55
+ * The `context_governed` payload of one request. Integers, enum keys, tool names and provider call
56
+ * ids only; bounded (32 ids, 16 tools), so it stays well under 2 KB.
57
+ */
58
+ export declare function governedEventData(snapshot: GovernanceSnapshot, usage?: UsageSummary): ContextGovernedData;
@@ -0,0 +1,96 @@
1
+ import { countSegments } from "./inventory.js";
2
+ export function governanceSnapshot(projection, options, request, previous) {
3
+ const byReason = {
4
+ superseded_read: 0,
5
+ stale_bg_output: 0,
6
+ duplicate_search: 0,
7
+ };
8
+ for (const e of projection.elisions)
9
+ byReason[e.reason]++;
10
+ const before = new Set(previous?.elisions.map((e) => e.callId));
11
+ return {
12
+ mode: options.mode === "on" ? "on" : "shadow",
13
+ policyVersion: options.policyVersion,
14
+ request,
15
+ messages: projection.messages.length,
16
+ segments: countSegments(projection.messages),
17
+ boundary: projection.boundary,
18
+ elided: projection.elisions.length,
19
+ byReason,
20
+ tokensBefore: projection.tokensBefore,
21
+ tokensAfter: projection.tokensAfter,
22
+ tokensSaved: projection.tokensBefore - projection.tokensAfter,
23
+ elisions: projection.elisions.map(({ index, callId, segmentId, reason, tokensSaved }) => ({
24
+ index,
25
+ callId,
26
+ segmentId,
27
+ reason,
28
+ tokensSaved,
29
+ })),
30
+ invariants: { count: 0, chars: 0, omitted: 0, promoted: 0, rejected: 0 },
31
+ newlyElided: projection.elisions.map((e) => e.callId).filter((id) => !before.has(id)),
32
+ };
33
+ }
34
+ const MAX_EVENT_IDS = 32;
35
+ const MAX_EVENT_TOOLS = 16;
36
+ /**
37
+ * The `context_governed` payload of one request. Integers, enum keys, tool names and provider call
38
+ * ids only; bounded (32 ids, 16 tools), so it stays well under 2 KB.
39
+ */
40
+ export function governedEventData(snapshot, usage) {
41
+ const { invariants } = snapshot;
42
+ const omitted = snapshot.newlyElided.length - MAX_EVENT_IDS;
43
+ const data = {
44
+ policyVersion: snapshot.policyVersion,
45
+ mode: snapshot.mode,
46
+ request: snapshot.request,
47
+ messages: snapshot.messages,
48
+ segments: snapshot.segments,
49
+ elided: snapshot.elided,
50
+ byReason: { ...snapshot.byReason },
51
+ tokensBefore: snapshot.tokensBefore,
52
+ tokensAfter: snapshot.tokensAfter,
53
+ tokensSaved: snapshot.tokensSaved,
54
+ };
55
+ if (snapshot.newlyElided.length > 0)
56
+ data.elidedCallIds = snapshot.newlyElided.slice(0, MAX_EVENT_IDS);
57
+ if (omitted > 0)
58
+ data.elidedCallIdsOmitted = omitted;
59
+ if (invariants.count > 0 || invariants.chars > 0 || invariants.omitted > 0)
60
+ data.invariants = {
61
+ count: invariants.count,
62
+ chars: invariants.chars,
63
+ omitted: invariants.omitted,
64
+ };
65
+ if (usage) {
66
+ const byTool = capTools(usage.byTool);
67
+ data.usage = {
68
+ observed: usage.observed,
69
+ used: usage.used,
70
+ pending: usage.pending,
71
+ unattributed: usage.unattributed,
72
+ ...(Object.keys(usage.byKind).length > 0 ? { byKind: usage.byKind } : {}),
73
+ ...(Object.keys(byTool).length > 0 ? { byTool } : {}),
74
+ };
75
+ }
76
+ return data;
77
+ }
78
+ /** At most `MAX_EVENT_TOOLS` tools: the busiest ones, the rest folded into `other`. */
79
+ function capTools(byTool) {
80
+ const all = Object.entries(byTool);
81
+ if (all.length <= MAX_EVENT_TOOLS)
82
+ return { ...byTool };
83
+ const ranked = all.sort(([an, a], [bn, b]) => b.observed - a.observed || (an < bn ? -1 : an > bn ? 1 : 0));
84
+ const out = {};
85
+ const other = { observed: 0, used: 0 };
86
+ ranked.forEach(([name, counts], i) => {
87
+ if (i < MAX_EVENT_TOOLS - 1 && name !== "other")
88
+ out[name] = { ...counts };
89
+ else {
90
+ other.observed += counts.observed;
91
+ other.used += counts.used;
92
+ }
93
+ });
94
+ out.other = other;
95
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
96
+ }
@@ -0,0 +1,88 @@
1
+ import type { Message } from "@alisio/sdk";
2
+ /**
3
+ * Context invariants (spec 012, phase 3): the standing context that must survive every
4
+ * compaction. They are NEVER written into a saved message (the summarizer would paraphrase them,
5
+ * D4): user instructions live in `sessions.options.contextInvariants` and the rest is read live,
6
+ * and all of it is rendered into the request as one transient block.
7
+ */
8
+ export type InvariantKind = "instruction" | "goal" | "plan" | "criterion" | "grant";
9
+ export interface Invariant {
10
+ kind: InvariantKind;
11
+ /** Where it came from: `user:<sha8>`, `goal:<id>`, `plan:<id>@<rev>`, `proof:<id>`, `grant:<name>`. */
12
+ origin: string;
13
+ text: string;
14
+ }
15
+ export interface InvariantInputs {
16
+ /** Saved user instructions, oldest first. */
17
+ instructions: readonly string[];
18
+ /** Only a goal that is still live (active, paused, blocked or budget limited). */
19
+ goal?: {
20
+ goalId: string;
21
+ objective: string;
22
+ };
23
+ /** Only an approved or implementing plan. */
24
+ plan?: {
25
+ planId: string;
26
+ revision: number;
27
+ hash: string;
28
+ title: string;
29
+ artifactId?: string;
30
+ };
31
+ /** `undefined` means Proof is not wired: the section does not exist. */
32
+ criteria?: readonly {
33
+ criterionId: string;
34
+ text: string;
35
+ required: boolean;
36
+ }[];
37
+ grants?: {
38
+ effects: readonly ("write" | "process" | "external")[];
39
+ capabilities: readonly string[];
40
+ externalDirectories: number;
41
+ };
42
+ }
43
+ /** Live material the runner asks for on every request; a missing source leaves its section out. */
44
+ export interface InvariantSources {
45
+ goal?(sessionId: string): InvariantInputs["goal"];
46
+ plan?(sessionId: string): InvariantInputs["plan"];
47
+ criteria?(sessionId: string): InvariantInputs["criteria"];
48
+ grants?(sessionId: string): {
49
+ capabilities: readonly string[];
50
+ externalDirectories: number;
51
+ } | undefined;
52
+ }
53
+ /** `sessions.options.contextInvariants`: server-owned, written only by compaction. */
54
+ export interface InvariantState {
55
+ version: 1;
56
+ instructions: string[];
57
+ compactions: number;
58
+ updatedAt: number;
59
+ }
60
+ export declare const INVARIANT_LIMITS: {
61
+ readonly maxItems: 20;
62
+ readonly maxItemChars: 500;
63
+ };
64
+ export declare function collectInvariants(input: InvariantInputs): Invariant[];
65
+ /**
66
+ * The `<invariants>` block, at most `maxChars` long. Items are never cut: they are taken
67
+ * first-fit in section order, and one that does not fit is skipped and counted.
68
+ */
69
+ export declare function renderInvariants(list: readonly Invariant[], maxChars: number): {
70
+ text: string;
71
+ included: number;
72
+ omitted: number;
73
+ };
74
+ /** The stored state, or undefined when absent or malformed (old sessions). Never throws. */
75
+ export declare function readInvariantState(options: Record<string, unknown> | undefined): InvariantState | undefined;
76
+ /**
77
+ * Accepts the summarizer's proposed instruction quotes only when each one is an exact substring
78
+ * (whitespace collapsed) of a single plain user message that is being summarized. Checkpoints,
79
+ * assistant text and tool results can never be promoted. A full list keeps the OLDEST items.
80
+ */
81
+ export declare function promoteInstructions(previous: readonly string[], proposed: unknown, summarized: readonly Message[], limits?: {
82
+ maxItems: number;
83
+ maxItemChars: number;
84
+ }): {
85
+ instructions: string[];
86
+ accepted: number;
87
+ rejected: number;
88
+ };