@compaction/cli 0.1.4 → 0.2.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/README.md +11 -1
- package/dist/cli/commands/compact.js +15 -6
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +130 -0
- package/dist/cli/commands/import.js +3 -1
- package/dist/cli/commands/run.js +13 -6
- package/dist/cli/index.js +4 -1
- package/dist/core/context-store-eval-cases.d.ts +6 -0
- package/dist/core/context-store-eval-cases.js +331 -0
- package/dist/core/context-store-eval.d.ts +140 -0
- package/dist/core/context-store-eval.js +138 -0
- package/dist/core/context-store-fs.d.ts +73 -0
- package/dist/core/context-store-fs.js +157 -0
- package/dist/core/context-store-sufficiency.d.ts +98 -0
- package/dist/core/context-store-sufficiency.js +135 -0
- package/dist/core/context-store.d.ts +155 -0
- package/dist/core/context-store.js +228 -0
- package/dist/core/report-generator.d.ts +19 -1
- package/dist/core/report-generator.js +51 -0
- package/dist/core/run-aggregator.d.ts +45 -0
- package/dist/core/run-aggregator.js +64 -0
- package/dist/core/trace-adapters.d.ts +8 -0
- package/dist/core/trace-adapters.js +4 -0
- package/dist/core/types.d.ts +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { evaluateContextStoreFixture } from "./context-store-eval.js";
|
|
2
|
+
/**
|
|
3
|
+
* V0.4 context-store INTERNAL sufficiency report (rung-1).
|
|
4
|
+
*
|
|
5
|
+
* Applies the founder-accepted threshold defaults
|
|
6
|
+
* (`docs/design/v0.4-context-store-thresholds.md`) to the eval fixtures, purely as an
|
|
7
|
+
* INTERNAL regression/confidence gate. It is consumed only by the eval harness/tests — it is
|
|
8
|
+
* NOT wired to any CLI command and produces NO user-facing output.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THIS IS NOT (load-bearing):
|
|
11
|
+
* - NOT a public claim. Passing means the deterministic retriever still behaves on SYNTHETIC
|
|
12
|
+
* fixtures — it is NOT evidence of retrieval correctness for users, savings, semantic
|
|
13
|
+
* preservation, or any large-memory / 300M readiness. Those remain gated.
|
|
14
|
+
* - NO model replay, NO embeddings, NO network.
|
|
15
|
+
*
|
|
16
|
+
* Model:
|
|
17
|
+
* - `sufficiency` fixtures are GATED against the thresholds below (hard axes fail; soft axes
|
|
18
|
+
* warn; latency is recorded only).
|
|
19
|
+
* - `adversarial` fixtures are NOT gated — their low/partial scores are correct. They are
|
|
20
|
+
* regression ANCHORS: the measured score must keep matching the fixture's `expected`.
|
|
21
|
+
*/
|
|
22
|
+
export const CONTEXT_STORE_SUFFICIENCY_VERSION = "context-store-sufficiency-v1";
|
|
23
|
+
/**
|
|
24
|
+
* Founder-accepted defaults (2026-06-24): hard recall / source coverage / recoverability
|
|
25
|
+
* (= 1.0 on sufficiency fixtures); soft continuation (advisory ≥ 0.9 — it is a marker proxy,
|
|
26
|
+
* not model replay); latency report-only. INTERNAL thresholds, not public claims.
|
|
27
|
+
*/
|
|
28
|
+
export const CONTEXT_STORE_SUFFICIENCY_THRESHOLDS = {
|
|
29
|
+
recall: { kind: "hard", min: 1 },
|
|
30
|
+
source_coverage: { kind: "hard", min: 1 },
|
|
31
|
+
recoverability: { kind: "hard", min: 1 },
|
|
32
|
+
continuation_success: { kind: "soft", min: 0.9 },
|
|
33
|
+
latency_ms: { kind: "report" }
|
|
34
|
+
};
|
|
35
|
+
/** Float tolerance for threshold/anchor comparisons (e.g. recall = 2/3). */
|
|
36
|
+
const TOLERANCE = 1e-9;
|
|
37
|
+
/** The quality axes a fixture is gated/anchored on (latency is handled separately). */
|
|
38
|
+
const QUALITY_AXES = ["recall", "source_coverage", "recoverability", "continuation_success"];
|
|
39
|
+
/**
|
|
40
|
+
* Run every fixture and build the internal sufficiency report. Deterministic apart from the
|
|
41
|
+
* recorded latency (use an injected clock for reproducible reports).
|
|
42
|
+
*/
|
|
43
|
+
export function evaluateSufficiency(fixtures, thresholds = CONTEXT_STORE_SUFFICIENCY_THRESHOLDS, options = {}) {
|
|
44
|
+
const hard_failures = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
const anchor_failures = [];
|
|
47
|
+
const latency = [];
|
|
48
|
+
let sufficiencyCount = 0;
|
|
49
|
+
let adversarialCount = 0;
|
|
50
|
+
for (const fixture of fixtures) {
|
|
51
|
+
const result = evaluateContextStoreFixture(fixture, { clock: options.clock });
|
|
52
|
+
latency.push({ fixture: fixture.name, latency_ms: result.latency_ms }); // report-only
|
|
53
|
+
const scores = {
|
|
54
|
+
recall: result.recall,
|
|
55
|
+
source_coverage: result.source_coverage,
|
|
56
|
+
recoverability: result.recoverability,
|
|
57
|
+
continuation_success: result.continuation_success
|
|
58
|
+
};
|
|
59
|
+
if ((fixture.role ?? "sufficiency") === "adversarial") {
|
|
60
|
+
adversarialCount += 1;
|
|
61
|
+
const expected = fixture.expected ?? {};
|
|
62
|
+
for (const axis of QUALITY_AXES) {
|
|
63
|
+
const exp = expected[axis];
|
|
64
|
+
if (exp !== undefined && Math.abs(scores[axis] - exp) > TOLERANCE) {
|
|
65
|
+
anchor_failures.push({ fixture: fixture.name, axis, measured: scores[axis], expected: exp });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
sufficiencyCount += 1;
|
|
71
|
+
for (const axis of QUALITY_AXES) {
|
|
72
|
+
const gate = thresholds[axis];
|
|
73
|
+
if (gate.kind === "report")
|
|
74
|
+
continue;
|
|
75
|
+
if (scores[axis] + TOLERANCE < gate.min) {
|
|
76
|
+
if (gate.kind === "hard") {
|
|
77
|
+
hard_failures.push({ fixture: fixture.name, axis, measured: scores[axis], min: gate.min });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
warnings.push({ fixture: fixture.name, axis, measured: scores[axis], min: gate.min });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
version: CONTEXT_STORE_SUFFICIENCY_VERSION,
|
|
88
|
+
passed: hard_failures.length === 0 && anchor_failures.length === 0,
|
|
89
|
+
sufficiency_fixtures: sufficiencyCount,
|
|
90
|
+
adversarial_fixtures: adversarialCount,
|
|
91
|
+
hard_failures,
|
|
92
|
+
warnings,
|
|
93
|
+
anchor_failures,
|
|
94
|
+
latency
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function pct(value) {
|
|
98
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Internal markdown rendering of the sufficiency report. For local/CI inspection only —
|
|
102
|
+
* not a user-facing surface and not a claim.
|
|
103
|
+
*/
|
|
104
|
+
export function formatSufficiencyReportMarkdown(report) {
|
|
105
|
+
const lines = [
|
|
106
|
+
"## Context-store sufficiency report (INTERNAL — not a claim)",
|
|
107
|
+
"",
|
|
108
|
+
`Result: ${report.passed ? "PASS" : "FAIL"} · sufficiency fixtures: ${report.sufficiency_fixtures} · adversarial anchors: ${report.adversarial_fixtures}`,
|
|
109
|
+
""
|
|
110
|
+
];
|
|
111
|
+
if (report.hard_failures.length > 0) {
|
|
112
|
+
lines.push("### Hard-axis failures (FAIL)");
|
|
113
|
+
for (const f of report.hard_failures) {
|
|
114
|
+
lines.push(`- ${f.fixture} · ${f.axis}: ${pct(f.measured)} < required ${pct(f.min)}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
if (report.anchor_failures.length > 0) {
|
|
119
|
+
lines.push("### Adversarial anchor drift (FAIL)");
|
|
120
|
+
for (const f of report.anchor_failures) {
|
|
121
|
+
lines.push(`- ${f.fixture} · ${f.axis}: measured ${pct(f.measured)} ≠ expected ${pct(f.expected)}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
if (report.warnings.length > 0) {
|
|
126
|
+
lines.push("### Soft warnings (advisory — do not fail)");
|
|
127
|
+
for (const w of report.warnings) {
|
|
128
|
+
lines.push(`- ${w.fixture} · ${w.axis}: ${pct(w.measured)} < advisory ${pct(w.min)}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
lines.push("_Internal regression gate over synthetic fixtures. Hard axes: recall, source coverage,", "recoverability. Soft (advisory): continuation success (a marker proxy, NOT model replay).", "Latency: recorded only. Passing is NOT evidence of user-facing retrieval correctness,", "savings, semantic preservation, or any large-memory / 300M readiness._");
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=context-store-sufficiency.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { AgentTrace, StateCapsule, StateCapsuleProvenanceEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* V0.4 context store — rung-1 (local, deterministic, source-grounded).
|
|
4
|
+
*
|
|
5
|
+
* SCOPE (accepted design doc `docs/design/v0.4-context-store-retrieval.md`,
|
|
6
|
+
* 2026-06-24): a durable, local, file-based store of source-pointed context items
|
|
7
|
+
* accumulated across runs, plus DETERMINISTIC local retrieval and assembly under a
|
|
8
|
+
* token budget. This module is the rung-1 core.
|
|
9
|
+
*
|
|
10
|
+
* HARD NON-GOALS (load-bearing — do not add here):
|
|
11
|
+
* - NO semantic / embedding retrieval. Retrieval ranks by deterministic signals only
|
|
12
|
+
* (source-pointer match, lexical overlap, recency). Semantic retrieval needs a model
|
|
13
|
+
* → not free-CLI local-first → deferred to the gated private engine/API.
|
|
14
|
+
* - NO network call, credential, upload, or model invocation. This module is pure +
|
|
15
|
+
* local-file I/O only (see `loadContextStore` / `appendContextItems`).
|
|
16
|
+
* - NO hosted DB / vector store / auth.
|
|
17
|
+
* - NO large-memory / 300M-token / retrieval-correctness claim. Those are gated on the
|
|
18
|
+
* four-axis eval harness (a follow-up increment) and rung-6 evidence.
|
|
19
|
+
*
|
|
20
|
+
* HONESTY: token figures here are `local_estimate` (chars/4), never provider-reported.
|
|
21
|
+
* Every stored item retains its `source_pointer` + recoverability so the assembled
|
|
22
|
+
* context stays recoverable to its sources (the substrate invariant).
|
|
23
|
+
*/
|
|
24
|
+
/** Algorithm + schema version, so a persisted store is self-describing. */
|
|
25
|
+
export declare const CONTEXT_STORE_ITEM_VERSION = "context-store-item-v1";
|
|
26
|
+
/**
|
|
27
|
+
* A single source-pointed context item accumulated in the store.
|
|
28
|
+
*
|
|
29
|
+
* `content` is local-only (like a captured trace) and is NEVER uploaded; the store
|
|
30
|
+
* lives under the gitignored `.compaction/` tree. `content_sha256` is the dedup key.
|
|
31
|
+
*/
|
|
32
|
+
export interface ContextStoreItem {
|
|
33
|
+
/** Schema version for forward-compatible reads. */
|
|
34
|
+
version: string;
|
|
35
|
+
/** SHA-256 of `content` — the dedup key (reuses the fingerprint discipline). */
|
|
36
|
+
content_sha256: string;
|
|
37
|
+
/** The retained context text (local-only; never uploaded). */
|
|
38
|
+
content: string;
|
|
39
|
+
/** Local token estimate (chars/4) for assembly budgeting. NOT provider-reported. */
|
|
40
|
+
estimated_tokens: number;
|
|
41
|
+
/** Recoverable pointer back to the source (kept so assembly stays recoverable). */
|
|
42
|
+
source_pointer?: string;
|
|
43
|
+
/** Source recoverability, carried straight from the substrate. */
|
|
44
|
+
recoverability: boolean | "unknown";
|
|
45
|
+
/** Per-run distinctness digest (from `computeTraceFingerprint`), if known. */
|
|
46
|
+
trace_fingerprint?: string;
|
|
47
|
+
/** The originating trace id, when known (a deterministic retrieval signal). */
|
|
48
|
+
source_trace_id?: string;
|
|
49
|
+
/** Provenance entry carried from the state-capsule, when available. */
|
|
50
|
+
provenance?: StateCapsuleProvenanceEntry;
|
|
51
|
+
/** Free-form deterministic labels (e.g. "retained_fact", "open_question"). */
|
|
52
|
+
labels: string[];
|
|
53
|
+
/** ISO-8601 creation timestamp (recency signal). Stamped at append time. */
|
|
54
|
+
created_at: string;
|
|
55
|
+
}
|
|
56
|
+
/** Input to `makeContextItem` — everything except the derived hash/token fields. */
|
|
57
|
+
export interface ContextItemInput {
|
|
58
|
+
content: string;
|
|
59
|
+
source_pointer?: string;
|
|
60
|
+
recoverability?: boolean | "unknown";
|
|
61
|
+
trace_fingerprint?: string;
|
|
62
|
+
source_trace_id?: string;
|
|
63
|
+
provenance?: StateCapsuleProvenanceEntry;
|
|
64
|
+
labels?: string[];
|
|
65
|
+
created_at: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a single context item, deriving the dedup hash + local token estimate.
|
|
69
|
+
* Pure — no I/O, no clock (the caller supplies `created_at` so this stays
|
|
70
|
+
* deterministic and testable).
|
|
71
|
+
*/
|
|
72
|
+
export declare function makeContextItem(input: ContextItemInput): ContextStoreItem;
|
|
73
|
+
/**
|
|
74
|
+
* Derive context items from a `StateCapsule`. Each retained fact / open question /
|
|
75
|
+
* safety note becomes a source-pointed item, inheriting the capsule's source pointer +
|
|
76
|
+
* recoverability + the originating trace id (and the supplied per-run fingerprint).
|
|
77
|
+
* Pure — `created_at` is supplied by the caller.
|
|
78
|
+
*/
|
|
79
|
+
export declare function contextItemsFromCapsule(capsule: StateCapsule, opts: {
|
|
80
|
+
created_at: string;
|
|
81
|
+
trace_fingerprint?: string;
|
|
82
|
+
}): ContextStoreItem[];
|
|
83
|
+
/**
|
|
84
|
+
* Derive context items from a normalized `AgentTrace` (the local artifact `compaction capture`
|
|
85
|
+
* / `import` produces). Each non-empty message becomes a source-pointed item recoverable to its
|
|
86
|
+
* `trace=<id> message=<id>` origin, tagged with the run's distinctness fingerprint. This is the
|
|
87
|
+
* free-CLI populate path (state-capsules require the gated engine; trace messages do not). Pure —
|
|
88
|
+
* `created_at` is supplied by the caller.
|
|
89
|
+
*/
|
|
90
|
+
export declare function contextItemsFromTrace(trace: AgentTrace, opts: {
|
|
91
|
+
created_at: string;
|
|
92
|
+
}): ContextStoreItem[];
|
|
93
|
+
/**
|
|
94
|
+
* Dedup by `content_sha256`, keeping the EARLIEST item deterministically (ties broken
|
|
95
|
+
* by `created_at`, then by the existing order). Reuses the fingerprint discipline:
|
|
96
|
+
* byte-identical content is one item, not many. Output order follows the
|
|
97
|
+
* first-occurrence order of each distinct hash in the input array.
|
|
98
|
+
*/
|
|
99
|
+
export declare function dedupeContextItems(items: ContextStoreItem[]): ContextStoreItem[];
|
|
100
|
+
/** A retrieval query: the current step's context + optional source/trace hints. */
|
|
101
|
+
export interface RetrievalQuery {
|
|
102
|
+
/** Free text describing the current step (task, recent messages, etc.). */
|
|
103
|
+
text: string;
|
|
104
|
+
/** Source pointers known to be relevant to the current step (exact-match boost). */
|
|
105
|
+
source_pointers?: string[];
|
|
106
|
+
/** Trace ids known to be relevant to the current step (exact-match boost). */
|
|
107
|
+
trace_ids?: string[];
|
|
108
|
+
}
|
|
109
|
+
export interface RetrievalOptions {
|
|
110
|
+
/** Max items to return (after ranking). Default: all candidates with score > 0. */
|
|
111
|
+
limit?: number;
|
|
112
|
+
/** Weight on exact source-pointer / trace-id match. Default 1. */
|
|
113
|
+
sourceMatchWeight?: number;
|
|
114
|
+
/** Weight on lexical overlap (overlap coefficient in [0,1]). Default 1. */
|
|
115
|
+
lexicalWeight?: number;
|
|
116
|
+
}
|
|
117
|
+
export interface RankedContextItem {
|
|
118
|
+
item: ContextStoreItem;
|
|
119
|
+
score: number;
|
|
120
|
+
source_match: boolean;
|
|
121
|
+
lexical_overlap: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Deterministic, local retrieval. Ranks items by:
|
|
125
|
+
* score = sourceMatchWeight * (exact source/trace match ? 1 : 0)
|
|
126
|
+
* + lexicalWeight * lexical overlap coefficient in [0,1]
|
|
127
|
+
*
|
|
128
|
+
* Ties are broken deterministically by recency (newer first) then `content_sha256`
|
|
129
|
+
* ascending, so the same store + query always yields the same order. No model, no
|
|
130
|
+
* embeddings, no network — overlap is a pure word-set intersection.
|
|
131
|
+
*/
|
|
132
|
+
export declare function retrieveContextItems(items: ContextStoreItem[], query: RetrievalQuery, options?: RetrievalOptions): RankedContextItem[];
|
|
133
|
+
export interface AssembleOptions {
|
|
134
|
+
/** Token budget for the assembled context (local estimate). */
|
|
135
|
+
tokenBudget: number;
|
|
136
|
+
}
|
|
137
|
+
export interface AssembledContext {
|
|
138
|
+
/** The assembled context text (included items joined, in ranked order). */
|
|
139
|
+
text: string;
|
|
140
|
+
/** Items that fit within the budget (provenance preserved on each). */
|
|
141
|
+
included: ContextStoreItem[];
|
|
142
|
+
/** Items dropped because the budget was exhausted (still recoverable from source). */
|
|
143
|
+
dropped: ContextStoreItem[];
|
|
144
|
+
/** Local token estimate of the assembled context. */
|
|
145
|
+
estimated_tokens: number;
|
|
146
|
+
/** Whether everything offered fit within the budget. */
|
|
147
|
+
within_budget: boolean;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Assemble ranked items into the sufficient active context under a token budget.
|
|
151
|
+
* Greedy in ranked order; dedups by `content_sha256`; PRESERVES the source pointer +
|
|
152
|
+
* provenance on every included item (so the assembled context stays recoverable).
|
|
153
|
+
* Token figures are local estimates.
|
|
154
|
+
*/
|
|
155
|
+
export declare function assembleContext(ranked: Array<RankedContextItem | ContextStoreItem>, options: AssembleOptions): AssembledContext;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { contentHash } from "./content-hash.js";
|
|
2
|
+
import { estimateTextTokens } from "./token-estimator.js";
|
|
3
|
+
import { computeTraceFingerprint } from "./trace-fingerprint.js";
|
|
4
|
+
/**
|
|
5
|
+
* V0.4 context store — rung-1 (local, deterministic, source-grounded).
|
|
6
|
+
*
|
|
7
|
+
* SCOPE (accepted design doc `docs/design/v0.4-context-store-retrieval.md`,
|
|
8
|
+
* 2026-06-24): a durable, local, file-based store of source-pointed context items
|
|
9
|
+
* accumulated across runs, plus DETERMINISTIC local retrieval and assembly under a
|
|
10
|
+
* token budget. This module is the rung-1 core.
|
|
11
|
+
*
|
|
12
|
+
* HARD NON-GOALS (load-bearing — do not add here):
|
|
13
|
+
* - NO semantic / embedding retrieval. Retrieval ranks by deterministic signals only
|
|
14
|
+
* (source-pointer match, lexical overlap, recency). Semantic retrieval needs a model
|
|
15
|
+
* → not free-CLI local-first → deferred to the gated private engine/API.
|
|
16
|
+
* - NO network call, credential, upload, or model invocation. This module is pure +
|
|
17
|
+
* local-file I/O only (see `loadContextStore` / `appendContextItems`).
|
|
18
|
+
* - NO hosted DB / vector store / auth.
|
|
19
|
+
* - NO large-memory / 300M-token / retrieval-correctness claim. Those are gated on the
|
|
20
|
+
* four-axis eval harness (a follow-up increment) and rung-6 evidence.
|
|
21
|
+
*
|
|
22
|
+
* HONESTY: token figures here are `local_estimate` (chars/4), never provider-reported.
|
|
23
|
+
* Every stored item retains its `source_pointer` + recoverability so the assembled
|
|
24
|
+
* context stays recoverable to its sources (the substrate invariant).
|
|
25
|
+
*/
|
|
26
|
+
/** Algorithm + schema version, so a persisted store is self-describing. */
|
|
27
|
+
export const CONTEXT_STORE_ITEM_VERSION = "context-store-item-v1";
|
|
28
|
+
function normalizeContent(content) {
|
|
29
|
+
return content.trim().replace(/\s+/g, " ");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a single context item, deriving the dedup hash + local token estimate.
|
|
33
|
+
* Pure — no I/O, no clock (the caller supplies `created_at` so this stays
|
|
34
|
+
* deterministic and testable).
|
|
35
|
+
*/
|
|
36
|
+
export function makeContextItem(input) {
|
|
37
|
+
const content = normalizeContent(input.content);
|
|
38
|
+
return {
|
|
39
|
+
version: CONTEXT_STORE_ITEM_VERSION,
|
|
40
|
+
content_sha256: contentHash(content),
|
|
41
|
+
content,
|
|
42
|
+
estimated_tokens: estimateTextTokens(content),
|
|
43
|
+
source_pointer: input.source_pointer,
|
|
44
|
+
recoverability: input.recoverability ?? "unknown",
|
|
45
|
+
trace_fingerprint: input.trace_fingerprint,
|
|
46
|
+
source_trace_id: input.source_trace_id,
|
|
47
|
+
provenance: input.provenance,
|
|
48
|
+
labels: input.labels ? [...input.labels] : [],
|
|
49
|
+
created_at: input.created_at
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Derive context items from a `StateCapsule`. Each retained fact / open question /
|
|
54
|
+
* safety note becomes a source-pointed item, inheriting the capsule's source pointer +
|
|
55
|
+
* recoverability + the originating trace id (and the supplied per-run fingerprint).
|
|
56
|
+
* Pure — `created_at` is supplied by the caller.
|
|
57
|
+
*/
|
|
58
|
+
export function contextItemsFromCapsule(capsule, opts) {
|
|
59
|
+
const primaryProvenance = capsule.provenance_entries?.[0];
|
|
60
|
+
const base = {
|
|
61
|
+
source_pointer: capsule.source_pointer ?? primaryProvenance?.source_pointer,
|
|
62
|
+
recoverability: capsule.source_recoverable ?? primaryProvenance?.source_recoverable ?? "unknown",
|
|
63
|
+
source_trace_id: capsule.source_trace_id ?? capsule.traceId,
|
|
64
|
+
provenance: primaryProvenance,
|
|
65
|
+
trace_fingerprint: opts.trace_fingerprint,
|
|
66
|
+
created_at: opts.created_at
|
|
67
|
+
};
|
|
68
|
+
const items = [];
|
|
69
|
+
for (const fact of capsule.retainedFacts) {
|
|
70
|
+
if (fact.trim().length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
items.push(makeContextItem({ ...base, content: fact, labels: ["retained_fact"] }));
|
|
73
|
+
}
|
|
74
|
+
for (const question of capsule.openQuestions) {
|
|
75
|
+
if (question.trim().length === 0)
|
|
76
|
+
continue;
|
|
77
|
+
items.push(makeContextItem({ ...base, content: question, labels: ["open_question"] }));
|
|
78
|
+
}
|
|
79
|
+
for (const note of capsule.safetyNotes) {
|
|
80
|
+
if (note.trim().length === 0)
|
|
81
|
+
continue;
|
|
82
|
+
items.push(makeContextItem({ ...base, content: note, labels: ["safety_note"] }));
|
|
83
|
+
}
|
|
84
|
+
return dedupeContextItems(items);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Derive context items from a normalized `AgentTrace` (the local artifact `compaction capture`
|
|
88
|
+
* / `import` produces). Each non-empty message becomes a source-pointed item recoverable to its
|
|
89
|
+
* `trace=<id> message=<id>` origin, tagged with the run's distinctness fingerprint. This is the
|
|
90
|
+
* free-CLI populate path (state-capsules require the gated engine; trace messages do not). Pure —
|
|
91
|
+
* `created_at` is supplied by the caller.
|
|
92
|
+
*/
|
|
93
|
+
export function contextItemsFromTrace(trace, opts) {
|
|
94
|
+
const trace_fingerprint = computeTraceFingerprint(trace).content_sha256;
|
|
95
|
+
const items = [];
|
|
96
|
+
for (const message of trace.messages) {
|
|
97
|
+
if (message.content.trim().length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const labels = [message.role];
|
|
100
|
+
if (message.toolName)
|
|
101
|
+
labels.push(message.toolName);
|
|
102
|
+
items.push(makeContextItem({
|
|
103
|
+
content: message.content,
|
|
104
|
+
source_pointer: `trace=${trace.id} message=${message.id}`,
|
|
105
|
+
source_trace_id: trace.id,
|
|
106
|
+
recoverability: true,
|
|
107
|
+
trace_fingerprint,
|
|
108
|
+
labels,
|
|
109
|
+
created_at: opts.created_at
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return dedupeContextItems(items);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Dedup by `content_sha256`, keeping the EARLIEST item deterministically (ties broken
|
|
116
|
+
* by `created_at`, then by the existing order). Reuses the fingerprint discipline:
|
|
117
|
+
* byte-identical content is one item, not many. Output order follows the
|
|
118
|
+
* first-occurrence order of each distinct hash in the input array.
|
|
119
|
+
*/
|
|
120
|
+
export function dedupeContextItems(items) {
|
|
121
|
+
const byHash = new Map();
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
const existing = byHash.get(item.content_sha256);
|
|
124
|
+
if (!existing || item.created_at < existing.created_at) {
|
|
125
|
+
// Merge labels so a later duplicate's labels are not lost.
|
|
126
|
+
if (existing) {
|
|
127
|
+
const labels = Array.from(new Set([...item.labels, ...existing.labels])).sort();
|
|
128
|
+
byHash.set(item.content_sha256, { ...item, labels });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
byHash.set(item.content_sha256, item);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const labels = Array.from(new Set([...existing.labels, ...item.labels])).sort();
|
|
136
|
+
byHash.set(item.content_sha256, { ...existing, labels });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Array.from(byHash.values());
|
|
140
|
+
}
|
|
141
|
+
const WORD_RE = /[a-z0-9]+/g;
|
|
142
|
+
function tokenSet(text) {
|
|
143
|
+
const set = new Set();
|
|
144
|
+
const matches = text.toLowerCase().match(WORD_RE);
|
|
145
|
+
if (matches) {
|
|
146
|
+
for (const word of matches)
|
|
147
|
+
set.add(word);
|
|
148
|
+
}
|
|
149
|
+
return set;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Deterministic, local retrieval. Ranks items by:
|
|
153
|
+
* score = sourceMatchWeight * (exact source/trace match ? 1 : 0)
|
|
154
|
+
* + lexicalWeight * lexical overlap coefficient in [0,1]
|
|
155
|
+
*
|
|
156
|
+
* Ties are broken deterministically by recency (newer first) then `content_sha256`
|
|
157
|
+
* ascending, so the same store + query always yields the same order. No model, no
|
|
158
|
+
* embeddings, no network — overlap is a pure word-set intersection.
|
|
159
|
+
*/
|
|
160
|
+
export function retrieveContextItems(items, query, options = {}) {
|
|
161
|
+
const sourceWeight = options.sourceMatchWeight ?? 1;
|
|
162
|
+
const lexicalWeight = options.lexicalWeight ?? 1;
|
|
163
|
+
const queryWords = tokenSet(query.text);
|
|
164
|
+
const wantedPointers = new Set(query.source_pointers ?? []);
|
|
165
|
+
const wantedTraces = new Set(query.trace_ids ?? []);
|
|
166
|
+
const ranked = items.map((item) => {
|
|
167
|
+
const sourceMatch = (item.source_pointer !== undefined && wantedPointers.has(item.source_pointer)) ||
|
|
168
|
+
(item.source_trace_id !== undefined && wantedTraces.has(item.source_trace_id));
|
|
169
|
+
let overlap = 0;
|
|
170
|
+
if (queryWords.size > 0) {
|
|
171
|
+
const itemWords = tokenSet(item.content);
|
|
172
|
+
if (itemWords.size > 0) {
|
|
173
|
+
let intersection = 0;
|
|
174
|
+
const [small, large] = queryWords.size <= itemWords.size ? [queryWords, itemWords] : [itemWords, queryWords];
|
|
175
|
+
for (const word of small) {
|
|
176
|
+
if (large.has(word))
|
|
177
|
+
intersection += 1;
|
|
178
|
+
}
|
|
179
|
+
// Overlap coefficient: intersection / min(|q|, |item|) — in [0, 1].
|
|
180
|
+
overlap = intersection / Math.min(queryWords.size, itemWords.size);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const score = sourceWeight * (sourceMatch ? 1 : 0) + lexicalWeight * overlap;
|
|
184
|
+
return { item, score, source_match: sourceMatch, lexical_overlap: overlap };
|
|
185
|
+
});
|
|
186
|
+
ranked.sort((a, b) => {
|
|
187
|
+
if (b.score !== a.score)
|
|
188
|
+
return b.score - a.score;
|
|
189
|
+
if (a.item.created_at !== b.item.created_at)
|
|
190
|
+
return a.item.created_at < b.item.created_at ? 1 : -1;
|
|
191
|
+
return a.item.content_sha256 < b.item.content_sha256 ? -1 : 1;
|
|
192
|
+
});
|
|
193
|
+
const withSignal = ranked.filter((entry) => entry.score > 0);
|
|
194
|
+
return options.limit !== undefined ? withSignal.slice(0, options.limit) : withSignal;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Assemble ranked items into the sufficient active context under a token budget.
|
|
198
|
+
* Greedy in ranked order; dedups by `content_sha256`; PRESERVES the source pointer +
|
|
199
|
+
* provenance on every included item (so the assembled context stays recoverable).
|
|
200
|
+
* Token figures are local estimates.
|
|
201
|
+
*/
|
|
202
|
+
export function assembleContext(ranked, options) {
|
|
203
|
+
const items = ranked.map((entry) => ("item" in entry ? entry.item : entry));
|
|
204
|
+
const included = [];
|
|
205
|
+
const dropped = [];
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
let total = 0;
|
|
208
|
+
for (const item of items) {
|
|
209
|
+
if (seen.has(item.content_sha256))
|
|
210
|
+
continue;
|
|
211
|
+
seen.add(item.content_sha256);
|
|
212
|
+
if (total + item.estimated_tokens <= options.tokenBudget) {
|
|
213
|
+
included.push(item);
|
|
214
|
+
total += item.estimated_tokens;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
dropped.push(item);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
text: included.map((item) => item.content).join("\n"),
|
|
222
|
+
included,
|
|
223
|
+
dropped,
|
|
224
|
+
estimated_tokens: total,
|
|
225
|
+
within_budget: dropped.length === 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=context-store.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillInjectionAdvisory } from "./skill-injection-detector.js";
|
|
2
|
-
import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, TokenEstimate, WasteFinding } from "./types.js";
|
|
2
|
+
import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, SavingsEvidence, TokenEstimate, WasteFinding } from "./types.js";
|
|
3
3
|
import type { UsageMetadata } from "./usage-metadata.js";
|
|
4
4
|
/**
|
|
5
5
|
* Attach the content-addressed per-run trace fingerprint to a compaction report (PURE,
|
|
@@ -14,6 +14,24 @@ import type { UsageMetadata } from "./usage-metadata.js";
|
|
|
14
14
|
* attribution/integrity signal, NOT a billing, provider, or semantic-preservation claim.
|
|
15
15
|
*/
|
|
16
16
|
export declare function withTraceFingerprint(report: CompactionReport, trace: AgentTrace): CompactionReport;
|
|
17
|
+
/**
|
|
18
|
+
* Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
|
|
19
|
+
* additive, backward-compatible). V0.2 "evidence labels per result": it composes the
|
|
20
|
+
* report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
|
|
21
|
+
* honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
|
|
22
|
+
* `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
|
|
23
|
+
* the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
|
|
24
|
+
* the aggregate increment across N≥3 distinct runs).
|
|
25
|
+
*
|
|
26
|
+
* `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
|
|
27
|
+
* source ONLY when the run genuinely carries it. `recoverability` defaults to
|
|
28
|
+
* `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
|
|
29
|
+
* when one ran. Returns a new object; the input report is not mutated.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withSavingsEvidence(report: CompactionReport, opts?: {
|
|
32
|
+
costSource?: SavingsEvidence["cost_source"];
|
|
33
|
+
recoverability?: SavingsEvidence["recoverability"];
|
|
34
|
+
}): CompactionReport;
|
|
17
35
|
export declare function formatAnalyzeReport(trace: AgentTrace, tokens: TokenEstimate, cost: CostEstimate, findings?: WasteFinding[], usage?: UsageMetadata, skillInjectionAdvisory?: SkillInjectionAdvisory): string;
|
|
18
36
|
export declare function formatCompactionReport(report: CompactionReport): string;
|
|
19
37
|
export declare function formatCompactionMarkdownReport(report: CompactionReport, policy: CompactionPolicy): string;
|
|
@@ -16,6 +16,56 @@ import { computeTraceFingerprint } from "./trace-fingerprint.js";
|
|
|
16
16
|
export function withTraceFingerprint(report, trace) {
|
|
17
17
|
return { ...report, trace_fingerprint: computeTraceFingerprint(trace) };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
|
|
21
|
+
* additive, backward-compatible). V0.2 "evidence labels per result": it composes the
|
|
22
|
+
* report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
|
|
23
|
+
* honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
|
|
24
|
+
* `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
|
|
25
|
+
* the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
|
|
26
|
+
* the aggregate increment across N≥3 distinct runs).
|
|
27
|
+
*
|
|
28
|
+
* `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
|
|
29
|
+
* source ONLY when the run genuinely carries it. `recoverability` defaults to
|
|
30
|
+
* `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
|
|
31
|
+
* when one ran. Returns a new object; the input report is not mutated.
|
|
32
|
+
*/
|
|
33
|
+
export function withSavingsEvidence(report, opts = {}) {
|
|
34
|
+
const cost_source = opts.costSource ?? "local_estimate";
|
|
35
|
+
const recoverability = opts.recoverability ?? "not_evaluated";
|
|
36
|
+
const savings_evidence = {
|
|
37
|
+
trace_identity: report.trace_fingerprint ? "fingerprinted" : "unidentified",
|
|
38
|
+
cost_source,
|
|
39
|
+
recoverability,
|
|
40
|
+
semantic_preservation: "not_evaluated",
|
|
41
|
+
evidence_rung: "local_estimate_single_run",
|
|
42
|
+
billing_confirmed: false,
|
|
43
|
+
label: `Per-run ${cost_source.replace(/_/g, " ")} delta (single run → local-estimate rung). ` +
|
|
44
|
+
`Recoverability: ${recoverability}` +
|
|
45
|
+
(recoverability === "not_evaluated" ? " (run `compact --eval`)" : "") +
|
|
46
|
+
". Semantic preservation: not_evaluated. NOT billing-confirmed, NOT realized savings, NOT extrapolated."
|
|
47
|
+
};
|
|
48
|
+
return { ...report, savings_evidence };
|
|
49
|
+
}
|
|
50
|
+
/** Render the composed per-run savings-evidence record for the markdown report. */
|
|
51
|
+
function formatSavingsEvidenceLines(report) {
|
|
52
|
+
const se = report.savings_evidence;
|
|
53
|
+
if (!se) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
"",
|
|
58
|
+
"## Savings Evidence (per-run, labeled)",
|
|
59
|
+
"",
|
|
60
|
+
`- Trace identity: ${se.trace_identity}`,
|
|
61
|
+
`- Cost source: ${se.cost_source}`,
|
|
62
|
+
`- Recoverability: ${se.recoverability}`,
|
|
63
|
+
`- Semantic preservation: ${se.semantic_preservation}`,
|
|
64
|
+
`- Evidence rung: ${se.evidence_rung}`,
|
|
65
|
+
`- Billing-confirmed: ${se.billing_confirmed}`,
|
|
66
|
+
`- ${se.label}`
|
|
67
|
+
];
|
|
68
|
+
}
|
|
19
69
|
function formatTokenBreakdown(tokens) {
|
|
20
70
|
return `${tokens.totalTokens} total (${tokens.inputTokens} input, ${tokens.outputTokens} output)`;
|
|
21
71
|
}
|
|
@@ -217,6 +267,7 @@ export function formatCompactionMarkdownReport(report, policy) {
|
|
|
217
267
|
`- Saving per run: $${report.saving_per_run.toFixed(6)} (estimated)`,
|
|
218
268
|
...formatValueProofLines(report),
|
|
219
269
|
...formatSpendBySourceLines(report),
|
|
270
|
+
...formatSavingsEvidenceLines(report),
|
|
220
271
|
"",
|
|
221
272
|
"## Waste Pattern",
|
|
222
273
|
"",
|
|
@@ -35,8 +35,44 @@ export interface SkippedReportFile {
|
|
|
35
35
|
path: string;
|
|
36
36
|
reason: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* V0.2 Increment 2 — the MEASURED-across-distinct-runs estimate delta. Aggregates the per-run
|
|
40
|
+
* estimate reductions over N DISTINCT runs (dedup by content fingerprint) into a mean + dispersion,
|
|
41
|
+
* and applies the accepted criterion (`docs/design/billing-delta-loop.md`): **N ≥ 3 distinct runs
|
|
42
|
+
* AND the mean ±2·SE interval excludes zero**. When BOTH hold, the figure earns the
|
|
43
|
+
* `measured_caveated_estimate_delta` rung (claims-and-evidence-ladder rung 1.5) — a measured
|
|
44
|
+
* estimate, NOT a single anecdote. Below the bar it stays rung 1 (`local_estimate_single_run`).
|
|
45
|
+
*
|
|
46
|
+
* It is STILL an estimate (chars/4 + price-table): explicitly NOT `provider-reported`, NOT
|
|
47
|
+
* eval-backed (rung 3), and NEVER `billing-confirmed` (rung 5). No time-period extrapolation.
|
|
48
|
+
*/
|
|
49
|
+
export interface MeasuredEstimateDelta {
|
|
50
|
+
/** N distinct runs used (≥3 required to qualify). */
|
|
51
|
+
distinct_run_count: number;
|
|
52
|
+
/** Mean per-run percent reduction across the distinct runs (estimate). */
|
|
53
|
+
mean_percent_reduction: number;
|
|
54
|
+
/** Standard error of the mean percent reduction (0 when N<2 — no dispersion). */
|
|
55
|
+
std_error: number;
|
|
56
|
+
/** Lower / upper bound of the mean ±2·SE interval. */
|
|
57
|
+
ci_low: number;
|
|
58
|
+
ci_high: number;
|
|
59
|
+
/** Does the ±2·SE interval exclude zero (lower bound > 0)? */
|
|
60
|
+
excludes_zero: boolean;
|
|
61
|
+
/** Criterion met: N≥3 AND the ±2·SE interval excludes zero. */
|
|
62
|
+
qualifies: boolean;
|
|
63
|
+
/** The claims-ladder rung this aggregate stands on. */
|
|
64
|
+
evidence_rung: "measured_caveated_estimate_delta" | "local_estimate_single_run";
|
|
65
|
+
/** One-line honest label — estimate-class, never billing-confirmed, never eval-backed. */
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
38
68
|
export interface RunSummary extends RunSummaryBucket {
|
|
39
69
|
average_percent_reduction: number;
|
|
70
|
+
/**
|
|
71
|
+
* V0.2 measured-across-distinct-runs estimate delta (the "measured" rung). Mean ±2·SE over the
|
|
72
|
+
* distinct runs' estimate reductions; `qualifies` only when N≥3 AND the interval excludes zero.
|
|
73
|
+
* Estimate-class — never billing-confirmed, never eval-backed.
|
|
74
|
+
*/
|
|
75
|
+
measured_estimate_delta: MeasuredEstimateDelta;
|
|
40
76
|
savings_by_policy: Record<string, RunSummaryBucket>;
|
|
41
77
|
savings_by_waste_pattern: Record<string, RunSummaryBucket>;
|
|
42
78
|
top_savings_runs: TopSavingsRun[];
|
|
@@ -54,6 +90,15 @@ interface LoadedReport {
|
|
|
54
90
|
path: string;
|
|
55
91
|
report: CompactionReport;
|
|
56
92
|
}
|
|
93
|
+
/** Minimum DISTINCT runs for a measured (not single-anecdote) estimate delta. */
|
|
94
|
+
export declare const MIN_RUNS_FOR_MEASURED_DELTA = 3;
|
|
95
|
+
/**
|
|
96
|
+
* Compute the measured-across-distinct-runs estimate delta over a set of per-run percent
|
|
97
|
+
* reductions (PURE). Applies the accepted criterion: N ≥ 3 AND the mean ±2·SE interval excludes
|
|
98
|
+
* zero. Estimate-class only — the result is NEVER billing-confirmed or eval-backed. Uses the
|
|
99
|
+
* sample standard deviation (n−1); with n<2 there is no dispersion (SE 0) so it cannot qualify.
|
|
100
|
+
*/
|
|
101
|
+
export declare function computeMeasuredEstimateDelta(percentReductions: number[]): MeasuredEstimateDelta;
|
|
57
102
|
export declare function validateCompactionReport(value: unknown): {
|
|
58
103
|
report?: CompactionReport;
|
|
59
104
|
reason?: string;
|