open-multi-agent-kit 1.0.0 → 1.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/CHANGELOG.md +6 -1
- package/README.md +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +1 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/coordination/awareness.d.ts +32 -0
- package/dist/coordination/awareness.d.ts.map +1 -0
- package/dist/coordination/awareness.js +55 -0
- package/dist/coordination/awareness.js.map +1 -0
- package/dist/coordination/broker.d.ts +79 -0
- package/dist/coordination/broker.d.ts.map +1 -0
- package/dist/coordination/broker.js +195 -0
- package/dist/coordination/broker.js.map +1 -0
- package/dist/coordination/index.d.ts +16 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +16 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/integration.d.ts +59 -0
- package/dist/coordination/integration.d.ts.map +1 -0
- package/dist/coordination/integration.js +126 -0
- package/dist/coordination/integration.js.map +1 -0
- package/dist/coordination/operation.d.ts +106 -0
- package/dist/coordination/operation.d.ts.map +1 -0
- package/dist/coordination/operation.js +179 -0
- package/dist/coordination/operation.js.map +1 -0
- package/dist/coordination/resource.d.ts +33 -0
- package/dist/coordination/resource.d.ts.map +1 -0
- package/dist/coordination/resource.js +89 -0
- package/dist/coordination/resource.js.map +1 -0
- package/dist/coordination/session.d.ts +74 -0
- package/dist/coordination/session.d.ts.map +1 -0
- package/dist/coordination/session.js +143 -0
- package/dist/coordination/session.js.map +1 -0
- package/dist/coordination/types.d.ts +68 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +26 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +28 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/control-state.d.ts +41 -0
- package/dist/core/compaction/control-state.d.ts.map +1 -0
- package/dist/core/compaction/control-state.js +85 -0
- package/dist/core/compaction/control-state.js.map +1 -0
- package/dist/core/compaction/fallback.d.ts +60 -0
- package/dist/core/compaction/fallback.d.ts.map +1 -0
- package/dist/core/compaction/fallback.js +117 -0
- package/dist/core/compaction/fallback.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/context-budget-headroom-candidates.d.ts +16 -1
- package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-candidates.js +31 -13
- package/dist/core/context-budget-headroom-candidates.js.map +1 -1
- package/dist/core/context-budget-headroom-types.d.ts +7 -1
- package/dist/core/context-budget-headroom-types.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-types.js +0 -1
- package/dist/core/context-budget-headroom-types.js.map +1 -1
- package/dist/core/context-budget-headroom.d.ts +10 -0
- package/dist/core/context-budget-headroom.d.ts.map +1 -1
- package/dist/core/context-budget-headroom.js +11 -2
- package/dist/core/context-budget-headroom.js.map +1 -1
- package/dist/core/context-budget-v2-global-pass.d.ts +21 -0
- package/dist/core/context-budget-v2-global-pass.d.ts.map +1 -0
- package/dist/core/context-budget-v2-global-pass.js +203 -0
- package/dist/core/context-budget-v2-global-pass.js.map +1 -0
- package/dist/core/context-budget-v2-planned-items.d.ts +12 -0
- package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planned-items.js +21 -2
- package/dist/core/context-budget-v2-planned-items.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +18 -19
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +7 -1
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +3 -0
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +6 -2
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +6 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +6 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/provider-default-models.d.ts +1 -0
- package/dist/core/provider-default-models.d.ts.map +1 -1
- package/dist/core/provider-default-models.js +1 -0
- package/dist/core/provider-default-models.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/reasoning-router-resolver.d.ts +7 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
- package/dist/core/reasoning-router-resolver.js +21 -3
- package/dist/core/reasoning-router-resolver.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +8 -5
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +8 -5
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/session-compaction-service.d.ts +15 -0
- package/dist/core/session-compaction-service.d.ts.map +1 -1
- package/dist/core/session-compaction-service.js +17 -4
- package/dist/core/session-compaction-service.js.map +1 -1
- package/dist/core/todo-runtime-state.d.ts +12 -0
- package/dist/core/todo-runtime-state.d.ts.map +1 -1
- package/dist/core/todo-runtime-state.js +23 -0
- package/dist/core/todo-runtime-state.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/metacognition/calibration-selective.d.ts +78 -0
- package/dist/metacognition/calibration-selective.d.ts.map +1 -0
- package/dist/metacognition/calibration-selective.js +161 -0
- package/dist/metacognition/calibration-selective.js.map +1 -0
- package/dist/metacognition/calibration.d.ts +62 -0
- package/dist/metacognition/calibration.d.ts.map +1 -0
- package/dist/metacognition/calibration.js +122 -0
- package/dist/metacognition/calibration.js.map +1 -0
- package/dist/metacognition/checkpoint.d.ts +60 -0
- package/dist/metacognition/checkpoint.d.ts.map +1 -0
- package/dist/metacognition/checkpoint.js +57 -0
- package/dist/metacognition/checkpoint.js.map +1 -0
- package/dist/metacognition/context7.d.ts +48 -0
- package/dist/metacognition/context7.d.ts.map +1 -0
- package/dist/metacognition/context7.js +154 -0
- package/dist/metacognition/context7.js.map +1 -0
- package/dist/metacognition/decision.d.ts +44 -0
- package/dist/metacognition/decision.d.ts.map +1 -0
- package/dist/metacognition/decision.js +111 -0
- package/dist/metacognition/decision.js.map +1 -0
- package/dist/metacognition/evaluation.d.ts +73 -0
- package/dist/metacognition/evaluation.d.ts.map +1 -0
- package/dist/metacognition/evaluation.js +97 -0
- package/dist/metacognition/evaluation.js.map +1 -0
- package/dist/metacognition/index.d.ts +31 -0
- package/dist/metacognition/index.d.ts.map +1 -0
- package/dist/metacognition/index.js +31 -0
- package/dist/metacognition/index.js.map +1 -0
- package/dist/metacognition/knowledge-action.d.ts +43 -0
- package/dist/metacognition/knowledge-action.d.ts.map +1 -0
- package/dist/metacognition/knowledge-action.js +60 -0
- package/dist/metacognition/knowledge-action.js.map +1 -0
- package/dist/metacognition/knowledge.d.ts +84 -0
- package/dist/metacognition/knowledge.d.ts.map +1 -0
- package/dist/metacognition/knowledge.js +165 -0
- package/dist/metacognition/knowledge.js.map +1 -0
- package/dist/metacognition/obligations.d.ts +82 -0
- package/dist/metacognition/obligations.d.ts.map +1 -0
- package/dist/metacognition/obligations.js +139 -0
- package/dist/metacognition/obligations.js.map +1 -0
- package/dist/metacognition/observation-validity.d.ts +83 -0
- package/dist/metacognition/observation-validity.d.ts.map +1 -0
- package/dist/metacognition/observation-validity.js +118 -0
- package/dist/metacognition/observation-validity.js.map +1 -0
- package/dist/metacognition/observe.d.ts +45 -0
- package/dist/metacognition/observe.d.ts.map +1 -0
- package/dist/metacognition/observe.js +59 -0
- package/dist/metacognition/observe.js.map +1 -0
- package/dist/metacognition/policy.d.ts +53 -0
- package/dist/metacognition/policy.d.ts.map +1 -0
- package/dist/metacognition/policy.js +155 -0
- package/dist/metacognition/policy.js.map +1 -0
- package/dist/metacognition/predictions.d.ts +78 -0
- package/dist/metacognition/predictions.d.ts.map +1 -0
- package/dist/metacognition/predictions.js +181 -0
- package/dist/metacognition/predictions.js.map +1 -0
- package/dist/metacognition/retrieval.d.ts +53 -0
- package/dist/metacognition/retrieval.d.ts.map +1 -0
- package/dist/metacognition/retrieval.js +170 -0
- package/dist/metacognition/retrieval.js.map +1 -0
- package/dist/metacognition/risk.d.ts +48 -0
- package/dist/metacognition/risk.d.ts.map +1 -0
- package/dist/metacognition/risk.js +165 -0
- package/dist/metacognition/risk.js.map +1 -0
- package/dist/metacognition/route-economics.d.ts +95 -0
- package/dist/metacognition/route-economics.d.ts.map +1 -0
- package/dist/metacognition/route-economics.js +110 -0
- package/dist/metacognition/route-economics.js.map +1 -0
- package/dist/metacognition/runtime-bridge.d.ts +54 -0
- package/dist/metacognition/runtime-bridge.d.ts.map +1 -0
- package/dist/metacognition/runtime-bridge.js +140 -0
- package/dist/metacognition/runtime-bridge.js.map +1 -0
- package/dist/metacognition/skills.d.ts +42 -0
- package/dist/metacognition/skills.d.ts.map +1 -0
- package/dist/metacognition/skills.js +220 -0
- package/dist/metacognition/skills.js.map +1 -0
- package/dist/metacognition/state.d.ts +110 -0
- package/dist/metacognition/state.d.ts.map +1 -0
- package/dist/metacognition/state.js +52 -0
- package/dist/metacognition/state.js.map +1 -0
- package/dist/metacognition/validation.d.ts +19 -0
- package/dist/metacognition/validation.d.ts.map +1 -0
- package/dist/metacognition/validation.js +58 -0
- package/dist/metacognition/validation.js.map +1 -0
- package/dist/metacognition/verification.d.ts +93 -0
- package/dist/metacognition/verification.d.ts.map +1 -0
- package/dist/metacognition/verification.js +132 -0
- package/dist/metacognition/verification.js.map +1 -0
- package/dist/metacognition/verifier.d.ts +49 -0
- package/dist/metacognition/verifier.d.ts.map +1 -0
- package/dist/metacognition/verifier.js +88 -0
- package/dist/metacognition/verifier.js.map +1 -0
- package/dist/observation/identity.d.ts +14 -0
- package/dist/observation/identity.d.ts.map +1 -0
- package/dist/observation/identity.js +29 -0
- package/dist/observation/identity.js.map +1 -0
- package/dist/observation/index.d.ts +13 -0
- package/dist/observation/index.d.ts.map +1 -0
- package/dist/observation/index.js +13 -0
- package/dist/observation/index.js.map +1 -0
- package/dist/observation/observe-mode.d.ts +46 -0
- package/dist/observation/observe-mode.d.ts.map +1 -0
- package/dist/observation/observe-mode.js +83 -0
- package/dist/observation/observe-mode.js.map +1 -0
- package/dist/observation/store.d.ts +53 -0
- package/dist/observation/store.d.ts.map +1 -0
- package/dist/observation/store.js +126 -0
- package/dist/observation/store.js.map +1 -0
- package/dist/observation/types.d.ts +72 -0
- package/dist/observation/types.d.ts.map +1 -0
- package/dist/observation/types.js +9 -0
- package/dist/observation/types.js.map +1 -0
- package/dist/observation/view.d.ts +39 -0
- package/dist/observation/view.d.ts.map +1 -0
- package/dist/observation/view.js +173 -0
- package/dist/observation/view.js.map +1 -0
- package/docs/metacognition.md +51 -0
- package/docs/model-catalog-refresh.md +59 -0
- package/docs/providers.md +53 -0
- package/docs/runtime-algorithms.md +107 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host control snapshot bound into a compaction envelope's preserved provenance.
|
|
3
|
+
*
|
|
4
|
+
* A natural-language summary cannot restore the obligation set: if the only
|
|
5
|
+
* record of "what is still open" is prose the summarizer wrote, a dropped line
|
|
6
|
+
* silently closes real work. The host supplies this structured snapshot
|
|
7
|
+
* instead, and the compaction commit compares its digest so a summary generated
|
|
8
|
+
* under different control state is discarded rather than committed.
|
|
9
|
+
*
|
|
10
|
+
* Producer output is untrusted input here: malformed shapes fail closed instead
|
|
11
|
+
* of degrading into an empty-but-valid checkpoint, which would look like
|
|
12
|
+
* "nothing was open" to every later reader.
|
|
13
|
+
*/
|
|
14
|
+
export interface CompactionControlState {
|
|
15
|
+
readonly openTasks: readonly string[];
|
|
16
|
+
readonly blockerReasons: readonly string[];
|
|
17
|
+
readonly branch: string | null;
|
|
18
|
+
}
|
|
19
|
+
/** Validate producer output; malformed input fails closed, never degrades to an empty checkpoint. */
|
|
20
|
+
export declare function validateControlState(value: CompactionControlState | null): CompactionControlState | null;
|
|
21
|
+
/**
|
|
22
|
+
* Digest the snapshot deterministically. Null stays null so "no control
|
|
23
|
+
* authority attached" stays distinguishable from "authority reporting nothing
|
|
24
|
+
* open" — the transition between them is itself a change worth discarding on.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Make one control-state string safe for the envelope validators.
|
|
28
|
+
*
|
|
29
|
+
* Returns `[REDACTED]` rather than an empty string when sanitization consumes
|
|
30
|
+
* everything: the validators reject empty entries, and dropping the entry would
|
|
31
|
+
* quietly reduce the open-task count instead of preserving that work exists.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sanitizeProvenanceText(value: string): string;
|
|
34
|
+
/** Project a control snapshot onto the envelope's preserved-provenance fields. */
|
|
35
|
+
export declare function controlStateProvenance(state: CompactionControlState | null): {
|
|
36
|
+
readonly openTasks: readonly string[];
|
|
37
|
+
readonly blockerReasons: readonly string[];
|
|
38
|
+
readonly branch: string | null;
|
|
39
|
+
};
|
|
40
|
+
export declare function controlStateDigest(state: CompactionControlState | null): string | null;
|
|
41
|
+
//# sourceMappingURL=control-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-state.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/control-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAWD,qGAAqG;AACrG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,GAAG,sBAAsB,GAAG,IAAI,CAcxG;AAED;;;;GAIG;AACH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO5D;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,GAAG;IAC7E,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAOA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAYtF","sourcesContent":["/**\n * Host control snapshot bound into a compaction envelope's preserved provenance.\n *\n * A natural-language summary cannot restore the obligation set: if the only\n * record of \"what is still open\" is prose the summarizer wrote, a dropped line\n * silently closes real work. The host supplies this structured snapshot\n * instead, and the compaction commit compares its digest so a summary generated\n * under different control state is discarded rather than committed.\n *\n * Producer output is untrusted input here: malformed shapes fail closed instead\n * of degrading into an empty-but-valid checkpoint, which would look like\n * \"nothing was open\" to every later reader.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { redactSensitiveTextForced } from \"../redaction.ts\";\nimport { redactCredentialShapedContent } from \"./transaction.ts\";\n\n/** Envelope validators reject credential shapes and control characters outright. */\nconst MAX_PROVENANCE_TEXT = 4096;\n\nexport interface CompactionControlState {\n\treadonly openTasks: readonly string[];\n\treadonly blockerReasons: readonly string[];\n\treadonly branch: string | null;\n}\n\nconst MAX_CONTROL_STATE_ITEMS = 1024;\nconst MAX_CONTROL_STATE_TEXT = 16_384;\n\nfunction assertControlStateText(value: unknown, field: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.length > MAX_CONTROL_STATE_TEXT) {\n\t\tthrow new TypeError(`control state ${field} must be a bounded string`);\n\t}\n}\n\n/** Validate producer output; malformed input fails closed, never degrades to an empty checkpoint. */\nexport function validateControlState(value: CompactionControlState | null): CompactionControlState | null {\n\tif (value === null) return null;\n\tif (typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new TypeError(\"control state must be an object or null\");\n\t}\n\tfor (const field of [\"openTasks\", \"blockerReasons\"] as const) {\n\t\tconst list = value[field];\n\t\tif (!Array.isArray(list) || list.length > MAX_CONTROL_STATE_ITEMS) {\n\t\t\tthrow new TypeError(`control state ${field} must be a bounded array`);\n\t\t}\n\t\tfor (const item of list) assertControlStateText(item, `${field} item`);\n\t}\n\tif (value.branch !== null) assertControlStateText(value.branch, \"branch\");\n\treturn value;\n}\n\n/**\n * Digest the snapshot deterministically. Null stays null so \"no control\n * authority attached\" stays distinguishable from \"authority reporting nothing\n * open\" — the transition between them is itself a change worth discarding on.\n */\n/**\n * Make one control-state string safe for the envelope validators.\n *\n * Returns `[REDACTED]` rather than an empty string when sanitization consumes\n * everything: the validators reject empty entries, and dropping the entry would\n * quietly reduce the open-task count instead of preserving that work exists.\n */\nexport function sanitizeProvenanceText(value: string): string {\n\tconst sanitized = redactCredentialShapedContent(\n\t\tsanitizeBinaryOutput(redactSensitiveTextForced(value.trim()))\n\t\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t\t.slice(0, MAX_PROVENANCE_TEXT),\n\t);\n\treturn sanitized.length > 0 ? sanitized : \"[REDACTED]\";\n}\n\n/** Project a control snapshot onto the envelope's preserved-provenance fields. */\nexport function controlStateProvenance(state: CompactionControlState | null): {\n\treadonly openTasks: readonly string[];\n\treadonly blockerReasons: readonly string[];\n\treadonly branch: string | null;\n} {\n\tif (state === null) return { openTasks: [], blockerReasons: [], branch: null };\n\treturn {\n\t\topenTasks: state.openTasks.map(sanitizeProvenanceText),\n\t\tblockerReasons: state.blockerReasons.map(sanitizeProvenanceText),\n\t\tbranch: state.branch == null ? null : sanitizeProvenanceText(state.branch),\n\t};\n}\n\nexport function controlStateDigest(state: CompactionControlState | null): string | null {\n\tif (state === null) return null;\n\treturn createHash(\"sha256\")\n\t\t.update(\n\t\t\tJSON.stringify({\n\t\t\t\topenTasks: [...state.openTasks],\n\t\t\t\tblockerReasons: [...state.blockerReasons],\n\t\t\t\tbranch: state.branch,\n\t\t\t}),\n\t\t\t\"utf8\",\n\t\t)\n\t\t.digest(\"hex\");\n}\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host control snapshot bound into a compaction envelope's preserved provenance.
|
|
3
|
+
*
|
|
4
|
+
* A natural-language summary cannot restore the obligation set: if the only
|
|
5
|
+
* record of "what is still open" is prose the summarizer wrote, a dropped line
|
|
6
|
+
* silently closes real work. The host supplies this structured snapshot
|
|
7
|
+
* instead, and the compaction commit compares its digest so a summary generated
|
|
8
|
+
* under different control state is discarded rather than committed.
|
|
9
|
+
*
|
|
10
|
+
* Producer output is untrusted input here: malformed shapes fail closed instead
|
|
11
|
+
* of degrading into an empty-but-valid checkpoint, which would look like
|
|
12
|
+
* "nothing was open" to every later reader.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
|
16
|
+
import { redactSensitiveTextForced } from "../redaction.js";
|
|
17
|
+
import { redactCredentialShapedContent } from "./transaction.js";
|
|
18
|
+
/** Envelope validators reject credential shapes and control characters outright. */
|
|
19
|
+
const MAX_PROVENANCE_TEXT = 4096;
|
|
20
|
+
const MAX_CONTROL_STATE_ITEMS = 1024;
|
|
21
|
+
const MAX_CONTROL_STATE_TEXT = 16_384;
|
|
22
|
+
function assertControlStateText(value, field) {
|
|
23
|
+
if (typeof value !== "string" || value.length > MAX_CONTROL_STATE_TEXT) {
|
|
24
|
+
throw new TypeError(`control state ${field} must be a bounded string`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Validate producer output; malformed input fails closed, never degrades to an empty checkpoint. */
|
|
28
|
+
export function validateControlState(value) {
|
|
29
|
+
if (value === null)
|
|
30
|
+
return null;
|
|
31
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
32
|
+
throw new TypeError("control state must be an object or null");
|
|
33
|
+
}
|
|
34
|
+
for (const field of ["openTasks", "blockerReasons"]) {
|
|
35
|
+
const list = value[field];
|
|
36
|
+
if (!Array.isArray(list) || list.length > MAX_CONTROL_STATE_ITEMS) {
|
|
37
|
+
throw new TypeError(`control state ${field} must be a bounded array`);
|
|
38
|
+
}
|
|
39
|
+
for (const item of list)
|
|
40
|
+
assertControlStateText(item, `${field} item`);
|
|
41
|
+
}
|
|
42
|
+
if (value.branch !== null)
|
|
43
|
+
assertControlStateText(value.branch, "branch");
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Digest the snapshot deterministically. Null stays null so "no control
|
|
48
|
+
* authority attached" stays distinguishable from "authority reporting nothing
|
|
49
|
+
* open" — the transition between them is itself a change worth discarding on.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Make one control-state string safe for the envelope validators.
|
|
53
|
+
*
|
|
54
|
+
* Returns `[REDACTED]` rather than an empty string when sanitization consumes
|
|
55
|
+
* everything: the validators reject empty entries, and dropping the entry would
|
|
56
|
+
* quietly reduce the open-task count instead of preserving that work exists.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeProvenanceText(value) {
|
|
59
|
+
const sanitized = redactCredentialShapedContent(sanitizeBinaryOutput(redactSensitiveTextForced(value.trim()))
|
|
60
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|
61
|
+
.slice(0, MAX_PROVENANCE_TEXT));
|
|
62
|
+
return sanitized.length > 0 ? sanitized : "[REDACTED]";
|
|
63
|
+
}
|
|
64
|
+
/** Project a control snapshot onto the envelope's preserved-provenance fields. */
|
|
65
|
+
export function controlStateProvenance(state) {
|
|
66
|
+
if (state === null)
|
|
67
|
+
return { openTasks: [], blockerReasons: [], branch: null };
|
|
68
|
+
return {
|
|
69
|
+
openTasks: state.openTasks.map(sanitizeProvenanceText),
|
|
70
|
+
blockerReasons: state.blockerReasons.map(sanitizeProvenanceText),
|
|
71
|
+
branch: state.branch == null ? null : sanitizeProvenanceText(state.branch),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function controlStateDigest(state) {
|
|
75
|
+
if (state === null)
|
|
76
|
+
return null;
|
|
77
|
+
return createHash("sha256")
|
|
78
|
+
.update(JSON.stringify({
|
|
79
|
+
openTasks: [...state.openTasks],
|
|
80
|
+
blockerReasons: [...state.blockerReasons],
|
|
81
|
+
branch: state.branch,
|
|
82
|
+
}), "utf8")
|
|
83
|
+
.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=control-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-state.js","sourceRoot":"","sources":["../../../src/core/compaction/control-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAEjE,oFAAoF;AACpF,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAQjC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AACrC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEtC,SAAS,sBAAsB,CAAC,KAAc,EAAE,KAAa,EAA2B;IACvF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,sBAAsB,EAAE,CAAC;QACxE,MAAM,IAAI,SAAS,CAAC,iBAAiB,KAAK,2BAA2B,CAAC,CAAC;IACxE,CAAC;AAAA,CACD;AAED,qGAAqG;AACrG,MAAM,UAAU,oBAAoB,CAAC,KAAoC,EAAiC;IACzG,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,SAAS,CAAC,yCAAyC,CAAC,CAAC;IAChE,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAU,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;YACnE,MAAM,IAAI,SAAS,CAAC,iBAAiB,KAAK,0BAA0B,CAAC,CAAC;QACvE,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,IAAI;YAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,sBAAsB,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1E,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;GAIG;AACH;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAa,EAAU;IAC7D,MAAM,SAAS,GAAG,6BAA6B,CAC9C,oBAAoB,CAAC,yBAAyB,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;SAC3D,OAAO,CAAC,kDAAkD,EAAE,EAAE,CAAC;SAC/D,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAC/B,CAAC;IACF,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;AAAA,CACvD;AAED,kFAAkF;AAClF,MAAM,UAAU,sBAAsB,CAAC,KAAoC,EAIzE;IACD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC/E,OAAO;QACN,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,sBAAsB,CAAC;QACtD,cAAc,EAAE,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,sBAAsB,CAAC;QAChE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,sBAAsB,CAAC,KAAK,CAAC,MAAM,CAAC;KAC1E,CAAC;AAAA,CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAoC,EAAiB;IACvF,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,UAAU,CAAC,QAAQ,CAAC;SACzB,MAAM,CACN,IAAI,CAAC,SAAS,CAAC;QACd,SAAS,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC;QAC/B,cAAc,EAAE,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC;QACzC,MAAM,EAAE,KAAK,CAAC,MAAM;KACpB,CAAC,EACF,MAAM,CACN;SACA,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CAChB","sourcesContent":["/**\n * Host control snapshot bound into a compaction envelope's preserved provenance.\n *\n * A natural-language summary cannot restore the obligation set: if the only\n * record of \"what is still open\" is prose the summarizer wrote, a dropped line\n * silently closes real work. The host supplies this structured snapshot\n * instead, and the compaction commit compares its digest so a summary generated\n * under different control state is discarded rather than committed.\n *\n * Producer output is untrusted input here: malformed shapes fail closed instead\n * of degrading into an empty-but-valid checkpoint, which would look like\n * \"nothing was open\" to every later reader.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { redactSensitiveTextForced } from \"../redaction.ts\";\nimport { redactCredentialShapedContent } from \"./transaction.ts\";\n\n/** Envelope validators reject credential shapes and control characters outright. */\nconst MAX_PROVENANCE_TEXT = 4096;\n\nexport interface CompactionControlState {\n\treadonly openTasks: readonly string[];\n\treadonly blockerReasons: readonly string[];\n\treadonly branch: string | null;\n}\n\nconst MAX_CONTROL_STATE_ITEMS = 1024;\nconst MAX_CONTROL_STATE_TEXT = 16_384;\n\nfunction assertControlStateText(value: unknown, field: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.length > MAX_CONTROL_STATE_TEXT) {\n\t\tthrow new TypeError(`control state ${field} must be a bounded string`);\n\t}\n}\n\n/** Validate producer output; malformed input fails closed, never degrades to an empty checkpoint. */\nexport function validateControlState(value: CompactionControlState | null): CompactionControlState | null {\n\tif (value === null) return null;\n\tif (typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new TypeError(\"control state must be an object or null\");\n\t}\n\tfor (const field of [\"openTasks\", \"blockerReasons\"] as const) {\n\t\tconst list = value[field];\n\t\tif (!Array.isArray(list) || list.length > MAX_CONTROL_STATE_ITEMS) {\n\t\t\tthrow new TypeError(`control state ${field} must be a bounded array`);\n\t\t}\n\t\tfor (const item of list) assertControlStateText(item, `${field} item`);\n\t}\n\tif (value.branch !== null) assertControlStateText(value.branch, \"branch\");\n\treturn value;\n}\n\n/**\n * Digest the snapshot deterministically. Null stays null so \"no control\n * authority attached\" stays distinguishable from \"authority reporting nothing\n * open\" — the transition between them is itself a change worth discarding on.\n */\n/**\n * Make one control-state string safe for the envelope validators.\n *\n * Returns `[REDACTED]` rather than an empty string when sanitization consumes\n * everything: the validators reject empty entries, and dropping the entry would\n * quietly reduce the open-task count instead of preserving that work exists.\n */\nexport function sanitizeProvenanceText(value: string): string {\n\tconst sanitized = redactCredentialShapedContent(\n\t\tsanitizeBinaryOutput(redactSensitiveTextForced(value.trim()))\n\t\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t\t.slice(0, MAX_PROVENANCE_TEXT),\n\t);\n\treturn sanitized.length > 0 ? sanitized : \"[REDACTED]\";\n}\n\n/** Project a control snapshot onto the envelope's preserved-provenance fields. */\nexport function controlStateProvenance(state: CompactionControlState | null): {\n\treadonly openTasks: readonly string[];\n\treadonly blockerReasons: readonly string[];\n\treadonly branch: string | null;\n} {\n\tif (state === null) return { openTasks: [], blockerReasons: [], branch: null };\n\treturn {\n\t\topenTasks: state.openTasks.map(sanitizeProvenanceText),\n\t\tblockerReasons: state.blockerReasons.map(sanitizeProvenanceText),\n\t\tbranch: state.branch == null ? null : sanitizeProvenanceText(state.branch),\n\t};\n}\n\nexport function controlStateDigest(state: CompactionControlState | null): string | null {\n\tif (state === null) return null;\n\treturn createHash(\"sha256\")\n\t\t.update(\n\t\t\tJSON.stringify({\n\t\t\t\topenTasks: [...state.openTasks],\n\t\t\t\tblockerReasons: [...state.blockerReasons],\n\t\t\t\tbranch: state.branch,\n\t\t\t}),\n\t\t\t\"utf8\",\n\t\t)\n\t\t.digest(\"hex\");\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to do when compaction summarization cannot run.
|
|
3
|
+
*
|
|
4
|
+
* The resilience ladder, in order:
|
|
5
|
+
* 1. the resolved compaction model (caller's `summarize`),
|
|
6
|
+
* 2. the live session model — a pinned `compaction.model` can be quota-dead
|
|
7
|
+
* while the model the user is actively talking to still serves traffic,
|
|
8
|
+
* 3. a deterministic trim that needs no model at all.
|
|
9
|
+
*
|
|
10
|
+
* Step 3 exists because the alternative is stranding the run: once context is
|
|
11
|
+
* over the window, a failed compaction means no later turn can succeed until
|
|
12
|
+
* the billing cycle resets.
|
|
13
|
+
*
|
|
14
|
+
* Kept out of `compaction.ts` so the ladder is unit-testable without an
|
|
15
|
+
* AgentSession, and so neither module carries the other's growth.
|
|
16
|
+
*/
|
|
17
|
+
import type { Model } from "omk-ai";
|
|
18
|
+
import type { CompactionDetails, CompactionPreparation, CompactionResult } from "./compaction.ts";
|
|
19
|
+
/**
|
|
20
|
+
* Detail shape unique to the no-model path. Kept here rather than on
|
|
21
|
+
* `CompactionDetails` so a reader of a normal entry never has to wonder why a
|
|
22
|
+
* `deterministicEmergency` flag exists on it.
|
|
23
|
+
*/
|
|
24
|
+
export interface DeterministicCompactionDetails extends CompactionDetails {
|
|
25
|
+
readonly deterministicEmergency: true;
|
|
26
|
+
/** Sanitized, bounded reason the model path was unavailable. */
|
|
27
|
+
readonly deterministicReason: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compact without a model.
|
|
31
|
+
*
|
|
32
|
+
* Keeps exactly what `prepareCompaction()` already derived deterministically —
|
|
33
|
+
* the prior summary, source-bound user rules, and file operations — plus the
|
|
34
|
+
* recent window the cut point preserved. It never claims to be a model summary:
|
|
35
|
+
* the heading and `details.deterministicEmergency` mark it as a trim, because
|
|
36
|
+
* the dropped turns were discarded without semantic summarization.
|
|
37
|
+
*
|
|
38
|
+
* Takes no model, key, or stream function: the signature itself proves no
|
|
39
|
+
* provider call can occur on this path.
|
|
40
|
+
*/
|
|
41
|
+
export declare function compactDeterministic(preparation: CompactionPreparation, reason: string): CompactionResult;
|
|
42
|
+
export interface SummarizationFallbackInput {
|
|
43
|
+
readonly preparation: CompactionPreparation;
|
|
44
|
+
readonly primaryModel: Model<any>;
|
|
45
|
+
/** The model the session is actually running, when known. */
|
|
46
|
+
readonly sessionModel: Model<any> | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Summarize on one model, resolving that model's own credentials. The
|
|
49
|
+
* generic failover list reuses the failed model's key across candidates, so
|
|
50
|
+
* a cross-provider rescue must resolve auth per call instead.
|
|
51
|
+
*/
|
|
52
|
+
readonly summarize: (model: Model<any>) => Promise<CompactionResult>;
|
|
53
|
+
readonly isAborted: () => boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run the summarization ladder. Aborts and non-quota failures propagate
|
|
57
|
+
* unchanged — a transient 503 must retry its model, not silently degrade.
|
|
58
|
+
*/
|
|
59
|
+
export declare function summarizeWithFallback(input: SummarizationFallbackInput): Promise<CompactionResult>;
|
|
60
|
+
//# sourceMappingURL=fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAYlG;;;;GAIG;AACH,MAAM,WAAW,8BAA+B,SAAQ,iBAAiB;IACxE,QAAQ,CAAC,sBAAsB,EAAE,IAAI,CAAC;IACtC,gEAAgE;IAChE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACrC;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAwDzG;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;CAClC;AAMD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmBxG","sourcesContent":["/**\n * What to do when compaction summarization cannot run.\n *\n * The resilience ladder, in order:\n * 1. the resolved compaction model (caller's `summarize`),\n * 2. the live session model — a pinned `compaction.model` can be quota-dead\n * while the model the user is actively talking to still serves traffic,\n * 3. a deterministic trim that needs no model at all.\n *\n * Step 3 exists because the alternative is stranding the run: once context is\n * over the window, a failed compaction means no later turn can succeed until\n * the billing cycle resets.\n *\n * Kept out of `compaction.ts` so the ladder is unit-testable without an\n * AgentSession, and so neither module carries the other's growth.\n */\n\nimport type { Model } from \"omk-ai\";\nimport { isQuotaExhaustionMessage } from \"../provider-resilience.ts\";\nimport { redactSensitiveTextForced } from \"../redaction.ts\";\nimport type { CompactionDetails, CompactionPreparation, CompactionResult } from \"./compaction.ts\";\nimport { applyCompactionKnowledgeTriage } from \"./knowledge-triage.ts\";\nimport { redactCredentialShapedContent } from \"./transaction.ts\";\nimport { computeFileLists, formatFileOperations } from \"./utils.ts\";\n\n/** Upper bound on the reason text persisted into the compaction entry. */\nconst DETERMINISTIC_REASON_MAX_CHARS = 512;\n/** Upper bound on the prior summary carried forward verbatim. */\nconst CARRIED_SUMMARY_MAX_CHARS = 16_384;\n\nconst DETERMINISTIC_EMERGENCY_HEADING = \"## Deterministic emergency compaction\";\n\n/**\n * Detail shape unique to the no-model path. Kept here rather than on\n * `CompactionDetails` so a reader of a normal entry never has to wonder why a\n * `deterministicEmergency` flag exists on it.\n */\nexport interface DeterministicCompactionDetails extends CompactionDetails {\n\treadonly deterministicEmergency: true;\n\t/** Sanitized, bounded reason the model path was unavailable. */\n\treadonly deterministicReason: string;\n}\n\nfunction boundedSanitizedText(value: string, maxChars: number): string {\n\tconst sanitized = redactCredentialShapedContent(redactSensitiveTextForced(value))\n\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t.trim();\n\treturn sanitized.length <= maxChars ? sanitized : `${sanitized.slice(0, maxChars - 1)}\\u2026`;\n}\n\n/**\n * Compact without a model.\n *\n * Keeps exactly what `prepareCompaction()` already derived deterministically —\n * the prior summary, source-bound user rules, and file operations — plus the\n * recent window the cut point preserved. It never claims to be a model summary:\n * the heading and `details.deterministicEmergency` mark it as a trim, because\n * the dropped turns were discarded without semantic summarization.\n *\n * Takes no model, key, or stream function: the signature itself proves no\n * provider call can occur on this path.\n */\nexport function compactDeterministic(preparation: CompactionPreparation, reason: string): CompactionResult {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tpreviousRuleHistory,\n\t\tcurrentRuleEntries,\n\t\tfileOps,\n\t} = preparation;\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst safeReason = boundedSanitizedText(reason, DETERMINISTIC_REASON_MAX_CHARS);\n\tconst carried =\n\t\tpreviousSummary === undefined ? \"\" : boundedSanitizedText(previousSummary, CARRIED_SUMMARY_MAX_CHARS);\n\n\tconst sections = [\n\t\tDETERMINISTIC_EMERGENCY_HEADING,\n\t\t\"Model summarization was unavailable, so the context was trimmed deterministically \" +\n\t\t\t\"rather than leaving the session unable to continue. This is not a model-generated \" +\n\t\t\t\"summary: older turns were dropped without semantic summarization, and only the \" +\n\t\t\t\"structured state below plus the retained recent window survive.\",\n\t\t`Cause: ${safeReason.length > 0 ? safeReason : \"unspecified summarization failure\"}`,\n\t];\n\tif (carried.length > 0) {\n\t\tsections.push(`### Carried forward from the previous compaction\\n\\n${carried}`);\n\t}\n\n\tconst triage = applyCompactionKnowledgeTriage({\n\t\tgeneratedSummary: sections.join(\"\\n\\n\"),\n\t\tcurrentMessages: [...messagesToSummarize, ...turnPrefixMessages],\n\t\tcurrentEntries: currentRuleEntries,\n\t\tpreviousRules: previousRuleHistory?.rules,\n\t\tpreviousSummary,\n\t\tpreviousEntries: previousRuleHistory?.entries,\n\t});\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst summary = triage.summary + formatFileOperations(readFiles, modifiedFiles);\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: {\n\t\t\treadFiles,\n\t\t\tmodifiedFiles,\n\t\t\tpreservedRules: [...triage.preservedRules],\n\t\t\tdeterministicEmergency: true,\n\t\t\tdeterministicReason: safeReason,\n\t\t} satisfies DeterministicCompactionDetails,\n\t};\n}\n\nexport interface SummarizationFallbackInput {\n\treadonly preparation: CompactionPreparation;\n\treadonly primaryModel: Model<any>;\n\t/** The model the session is actually running, when known. */\n\treadonly sessionModel: Model<any> | undefined;\n\t/**\n\t * Summarize on one model, resolving that model's own credentials. The\n\t * generic failover list reuses the failed model's key across candidates, so\n\t * a cross-provider rescue must resolve auth per call instead.\n\t */\n\treadonly summarize: (model: Model<any>) => Promise<CompactionResult>;\n\treadonly isAborted: () => boolean;\n}\n\nfunction sameModel(a: Model<any>, b: Model<any>): boolean {\n\treturn a.provider === b.provider && a.id === b.id;\n}\n\n/**\n * Run the summarization ladder. Aborts and non-quota failures propagate\n * unchanged — a transient 503 must retry its model, not silently degrade.\n */\nexport async function summarizeWithFallback(input: SummarizationFallbackInput): Promise<CompactionResult> {\n\ttry {\n\t\treturn await input.summarize(input.primaryModel);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (input.isAborted()) throw error;\n\t\tif (!isQuotaExhaustionMessage(message)) throw error;\n\n\t\tconst session = input.sessionModel;\n\t\tif (session !== undefined && !sameModel(session, input.primaryModel)) {\n\t\t\ttry {\n\t\t\t\treturn await input.summarize(session);\n\t\t\t} catch (rescueError) {\n\t\t\t\tif (input.isAborted()) throw rescueError;\n\t\t\t\t// Session model could not help either; fall through to the trim.\n\t\t\t}\n\t\t}\n\t\treturn compactDeterministic(input.preparation, message);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to do when compaction summarization cannot run.
|
|
3
|
+
*
|
|
4
|
+
* The resilience ladder, in order:
|
|
5
|
+
* 1. the resolved compaction model (caller's `summarize`),
|
|
6
|
+
* 2. the live session model — a pinned `compaction.model` can be quota-dead
|
|
7
|
+
* while the model the user is actively talking to still serves traffic,
|
|
8
|
+
* 3. a deterministic trim that needs no model at all.
|
|
9
|
+
*
|
|
10
|
+
* Step 3 exists because the alternative is stranding the run: once context is
|
|
11
|
+
* over the window, a failed compaction means no later turn can succeed until
|
|
12
|
+
* the billing cycle resets.
|
|
13
|
+
*
|
|
14
|
+
* Kept out of `compaction.ts` so the ladder is unit-testable without an
|
|
15
|
+
* AgentSession, and so neither module carries the other's growth.
|
|
16
|
+
*/
|
|
17
|
+
import { isQuotaExhaustionMessage } from "../provider-resilience.js";
|
|
18
|
+
import { redactSensitiveTextForced } from "../redaction.js";
|
|
19
|
+
import { applyCompactionKnowledgeTriage } from "./knowledge-triage.js";
|
|
20
|
+
import { redactCredentialShapedContent } from "./transaction.js";
|
|
21
|
+
import { computeFileLists, formatFileOperations } from "./utils.js";
|
|
22
|
+
/** Upper bound on the reason text persisted into the compaction entry. */
|
|
23
|
+
const DETERMINISTIC_REASON_MAX_CHARS = 512;
|
|
24
|
+
/** Upper bound on the prior summary carried forward verbatim. */
|
|
25
|
+
const CARRIED_SUMMARY_MAX_CHARS = 16_384;
|
|
26
|
+
const DETERMINISTIC_EMERGENCY_HEADING = "## Deterministic emergency compaction";
|
|
27
|
+
function boundedSanitizedText(value, maxChars) {
|
|
28
|
+
const sanitized = redactCredentialShapedContent(redactSensitiveTextForced(value))
|
|
29
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|
30
|
+
.trim();
|
|
31
|
+
return sanitized.length <= maxChars ? sanitized : `${sanitized.slice(0, maxChars - 1)}\u2026`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compact without a model.
|
|
35
|
+
*
|
|
36
|
+
* Keeps exactly what `prepareCompaction()` already derived deterministically —
|
|
37
|
+
* the prior summary, source-bound user rules, and file operations — plus the
|
|
38
|
+
* recent window the cut point preserved. It never claims to be a model summary:
|
|
39
|
+
* the heading and `details.deterministicEmergency` mark it as a trim, because
|
|
40
|
+
* the dropped turns were discarded without semantic summarization.
|
|
41
|
+
*
|
|
42
|
+
* Takes no model, key, or stream function: the signature itself proves no
|
|
43
|
+
* provider call can occur on this path.
|
|
44
|
+
*/
|
|
45
|
+
export function compactDeterministic(preparation, reason) {
|
|
46
|
+
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, tokensBefore, previousSummary, previousRuleHistory, currentRuleEntries, fileOps, } = preparation;
|
|
47
|
+
if (!firstKeptEntryId) {
|
|
48
|
+
throw new Error("First kept entry has no UUID - session may need migration");
|
|
49
|
+
}
|
|
50
|
+
const safeReason = boundedSanitizedText(reason, DETERMINISTIC_REASON_MAX_CHARS);
|
|
51
|
+
const carried = previousSummary === undefined ? "" : boundedSanitizedText(previousSummary, CARRIED_SUMMARY_MAX_CHARS);
|
|
52
|
+
const sections = [
|
|
53
|
+
DETERMINISTIC_EMERGENCY_HEADING,
|
|
54
|
+
"Model summarization was unavailable, so the context was trimmed deterministically " +
|
|
55
|
+
"rather than leaving the session unable to continue. This is not a model-generated " +
|
|
56
|
+
"summary: older turns were dropped without semantic summarization, and only the " +
|
|
57
|
+
"structured state below plus the retained recent window survive.",
|
|
58
|
+
`Cause: ${safeReason.length > 0 ? safeReason : "unspecified summarization failure"}`,
|
|
59
|
+
];
|
|
60
|
+
if (carried.length > 0) {
|
|
61
|
+
sections.push(`### Carried forward from the previous compaction\n\n${carried}`);
|
|
62
|
+
}
|
|
63
|
+
const triage = applyCompactionKnowledgeTriage({
|
|
64
|
+
generatedSummary: sections.join("\n\n"),
|
|
65
|
+
currentMessages: [...messagesToSummarize, ...turnPrefixMessages],
|
|
66
|
+
currentEntries: currentRuleEntries,
|
|
67
|
+
previousRules: previousRuleHistory?.rules,
|
|
68
|
+
previousSummary,
|
|
69
|
+
previousEntries: previousRuleHistory?.entries,
|
|
70
|
+
});
|
|
71
|
+
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
72
|
+
const summary = triage.summary + formatFileOperations(readFiles, modifiedFiles);
|
|
73
|
+
return {
|
|
74
|
+
summary,
|
|
75
|
+
firstKeptEntryId,
|
|
76
|
+
tokensBefore,
|
|
77
|
+
details: {
|
|
78
|
+
readFiles,
|
|
79
|
+
modifiedFiles,
|
|
80
|
+
preservedRules: [...triage.preservedRules],
|
|
81
|
+
deterministicEmergency: true,
|
|
82
|
+
deterministicReason: safeReason,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function sameModel(a, b) {
|
|
87
|
+
return a.provider === b.provider && a.id === b.id;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run the summarization ladder. Aborts and non-quota failures propagate
|
|
91
|
+
* unchanged — a transient 503 must retry its model, not silently degrade.
|
|
92
|
+
*/
|
|
93
|
+
export async function summarizeWithFallback(input) {
|
|
94
|
+
try {
|
|
95
|
+
return await input.summarize(input.primaryModel);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
if (input.isAborted())
|
|
100
|
+
throw error;
|
|
101
|
+
if (!isQuotaExhaustionMessage(message))
|
|
102
|
+
throw error;
|
|
103
|
+
const session = input.sessionModel;
|
|
104
|
+
if (session !== undefined && !sameModel(session, input.primaryModel)) {
|
|
105
|
+
try {
|
|
106
|
+
return await input.summarize(session);
|
|
107
|
+
}
|
|
108
|
+
catch (rescueError) {
|
|
109
|
+
if (input.isAborted())
|
|
110
|
+
throw rescueError;
|
|
111
|
+
// Session model could not help either; fall through to the trim.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return compactDeterministic(input.preparation, message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=fallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/core/compaction/fallback.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What to do when compaction summarization cannot run.\n *\n * The resilience ladder, in order:\n * 1. the resolved compaction model (caller's `summarize`),\n * 2. the live session model — a pinned `compaction.model` can be quota-dead\n * while the model the user is actively talking to still serves traffic,\n * 3. a deterministic trim that needs no model at all.\n *\n * Step 3 exists because the alternative is stranding the run: once context is\n * over the window, a failed compaction means no later turn can succeed until\n * the billing cycle resets.\n *\n * Kept out of `compaction.ts` so the ladder is unit-testable without an\n * AgentSession, and so neither module carries the other's growth.\n */\n\nimport type { Model } from \"omk-ai\";\nimport { isQuotaExhaustionMessage } from \"../provider-resilience.ts\";\nimport { redactSensitiveTextForced } from \"../redaction.ts\";\nimport type { CompactionDetails, CompactionPreparation, CompactionResult } from \"./compaction.ts\";\nimport { applyCompactionKnowledgeTriage } from \"./knowledge-triage.ts\";\nimport { redactCredentialShapedContent } from \"./transaction.ts\";\nimport { computeFileLists, formatFileOperations } from \"./utils.ts\";\n\n/** Upper bound on the reason text persisted into the compaction entry. */\nconst DETERMINISTIC_REASON_MAX_CHARS = 512;\n/** Upper bound on the prior summary carried forward verbatim. */\nconst CARRIED_SUMMARY_MAX_CHARS = 16_384;\n\nconst DETERMINISTIC_EMERGENCY_HEADING = \"## Deterministic emergency compaction\";\n\n/**\n * Detail shape unique to the no-model path. Kept here rather than on\n * `CompactionDetails` so a reader of a normal entry never has to wonder why a\n * `deterministicEmergency` flag exists on it.\n */\nexport interface DeterministicCompactionDetails extends CompactionDetails {\n\treadonly deterministicEmergency: true;\n\t/** Sanitized, bounded reason the model path was unavailable. */\n\treadonly deterministicReason: string;\n}\n\nfunction boundedSanitizedText(value: string, maxChars: number): string {\n\tconst sanitized = redactCredentialShapedContent(redactSensitiveTextForced(value))\n\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t.trim();\n\treturn sanitized.length <= maxChars ? sanitized : `${sanitized.slice(0, maxChars - 1)}\\u2026`;\n}\n\n/**\n * Compact without a model.\n *\n * Keeps exactly what `prepareCompaction()` already derived deterministically —\n * the prior summary, source-bound user rules, and file operations — plus the\n * recent window the cut point preserved. It never claims to be a model summary:\n * the heading and `details.deterministicEmergency` mark it as a trim, because\n * the dropped turns were discarded without semantic summarization.\n *\n * Takes no model, key, or stream function: the signature itself proves no\n * provider call can occur on this path.\n */\nexport function compactDeterministic(preparation: CompactionPreparation, reason: string): CompactionResult {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tpreviousRuleHistory,\n\t\tcurrentRuleEntries,\n\t\tfileOps,\n\t} = preparation;\n\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no UUID - session may need migration\");\n\t}\n\n\tconst safeReason = boundedSanitizedText(reason, DETERMINISTIC_REASON_MAX_CHARS);\n\tconst carried =\n\t\tpreviousSummary === undefined ? \"\" : boundedSanitizedText(previousSummary, CARRIED_SUMMARY_MAX_CHARS);\n\n\tconst sections = [\n\t\tDETERMINISTIC_EMERGENCY_HEADING,\n\t\t\"Model summarization was unavailable, so the context was trimmed deterministically \" +\n\t\t\t\"rather than leaving the session unable to continue. This is not a model-generated \" +\n\t\t\t\"summary: older turns were dropped without semantic summarization, and only the \" +\n\t\t\t\"structured state below plus the retained recent window survive.\",\n\t\t`Cause: ${safeReason.length > 0 ? safeReason : \"unspecified summarization failure\"}`,\n\t];\n\tif (carried.length > 0) {\n\t\tsections.push(`### Carried forward from the previous compaction\\n\\n${carried}`);\n\t}\n\n\tconst triage = applyCompactionKnowledgeTriage({\n\t\tgeneratedSummary: sections.join(\"\\n\\n\"),\n\t\tcurrentMessages: [...messagesToSummarize, ...turnPrefixMessages],\n\t\tcurrentEntries: currentRuleEntries,\n\t\tpreviousRules: previousRuleHistory?.rules,\n\t\tpreviousSummary,\n\t\tpreviousEntries: previousRuleHistory?.entries,\n\t});\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tconst summary = triage.summary + formatFileOperations(readFiles, modifiedFiles);\n\n\treturn {\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: {\n\t\t\treadFiles,\n\t\t\tmodifiedFiles,\n\t\t\tpreservedRules: [...triage.preservedRules],\n\t\t\tdeterministicEmergency: true,\n\t\t\tdeterministicReason: safeReason,\n\t\t} satisfies DeterministicCompactionDetails,\n\t};\n}\n\nexport interface SummarizationFallbackInput {\n\treadonly preparation: CompactionPreparation;\n\treadonly primaryModel: Model<any>;\n\t/** The model the session is actually running, when known. */\n\treadonly sessionModel: Model<any> | undefined;\n\t/**\n\t * Summarize on one model, resolving that model's own credentials. The\n\t * generic failover list reuses the failed model's key across candidates, so\n\t * a cross-provider rescue must resolve auth per call instead.\n\t */\n\treadonly summarize: (model: Model<any>) => Promise<CompactionResult>;\n\treadonly isAborted: () => boolean;\n}\n\nfunction sameModel(a: Model<any>, b: Model<any>): boolean {\n\treturn a.provider === b.provider && a.id === b.id;\n}\n\n/**\n * Run the summarization ladder. Aborts and non-quota failures propagate\n * unchanged — a transient 503 must retry its model, not silently degrade.\n */\nexport async function summarizeWithFallback(input: SummarizationFallbackInput): Promise<CompactionResult> {\n\ttry {\n\t\treturn await input.summarize(input.primaryModel);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (input.isAborted()) throw error;\n\t\tif (!isQuotaExhaustionMessage(message)) throw error;\n\n\t\tconst session = input.sessionModel;\n\t\tif (session !== undefined && !sameModel(session, input.primaryModel)) {\n\t\t\ttry {\n\t\t\t\treturn await input.summarize(session);\n\t\t\t} catch (rescueError) {\n\t\t\t\tif (input.isAborted()) throw rescueError;\n\t\t\t\t// Session model could not help either; fall through to the trim.\n\t\t\t}\n\t\t}\n\t\treturn compactDeterministic(input.preparation, message);\n\t}\n}\n"]}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export * from "./branch-summarization.ts";
|
|
5
5
|
export * from "./compaction.ts";
|
|
6
|
+
export * from "./control-state.ts";
|
|
7
|
+
export * from "./fallback.ts";
|
|
6
8
|
export * from "./hysteresis.ts";
|
|
7
9
|
export * from "./knowledge-triage.ts";
|
|
8
10
|
export * from "./model-policy.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./hysteresis.ts\";\nexport * from \"./knowledge-triage.ts\";\nexport * from \"./model-policy.ts\";\nexport * from \"./resume-policy.ts\";\nexport * from \"./transaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./control-state.ts\";\nexport * from \"./fallback.ts\";\nexport * from \"./hysteresis.ts\";\nexport * from \"./knowledge-triage.ts\";\nexport * from \"./model-policy.ts\";\nexport * from \"./resume-policy.ts\";\nexport * from \"./transaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export * from "./branch-summarization.js";
|
|
5
5
|
export * from "./compaction.js";
|
|
6
|
+
export * from "./control-state.js";
|
|
7
|
+
export * from "./fallback.js";
|
|
6
8
|
export * from "./hysteresis.js";
|
|
7
9
|
export * from "./knowledge-triage.js";
|
|
8
10
|
export * from "./model-policy.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./hysteresis.ts\";\nexport * from \"./knowledge-triage.ts\";\nexport * from \"./model-policy.ts\";\nexport * from \"./resume-policy.ts\";\nexport * from \"./transaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/compaction/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC","sourcesContent":["/**\n * Compaction and summarization utilities.\n */\n\nexport * from \"./branch-summarization.ts\";\nexport * from \"./compaction.ts\";\nexport * from \"./control-state.ts\";\nexport * from \"./fallback.ts\";\nexport * from \"./hysteresis.ts\";\nexport * from \"./knowledge-triage.ts\";\nexport * from \"./model-policy.ts\";\nexport * from \"./resume-policy.ts\";\nexport * from \"./transaction.ts\";\nexport * from \"./utils.ts\";\n"]}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import { type ContextBudgetItemV2, type ContextRepresentationCandidateV2, type HeadroomQualityPolicyV2 } from "./context-budget-headroom-types.ts";
|
|
2
|
-
|
|
2
|
+
/** Prices one materialized representation string; must be the counter that produced the item's full-text cost. */
|
|
3
|
+
export type RepresentationTokenCounter = (text: string) => number;
|
|
4
|
+
/**
|
|
5
|
+
* Derive the representations the selector may choose for one item.
|
|
6
|
+
*
|
|
7
|
+
* Every non-full candidate is priced by counting the string it actually
|
|
8
|
+
* materializes with `countTokens` — the same counter that priced the full
|
|
9
|
+
* text — so the cost the selector checks against the budget is the cost of
|
|
10
|
+
* the text that would be sent. A compression ratio is never a cost: a summary
|
|
11
|
+
* that returns its source unchanged saves nothing, and a representation whose
|
|
12
|
+
* materialized text is not cheaper than the full text is not offered at all.
|
|
13
|
+
* Pass the counter that produced `item.tokenEstimate`; mixing an exact
|
|
14
|
+
* tokenizer for the full text with the heuristic for its alternatives makes
|
|
15
|
+
* the candidates incomparable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveRepresentationCandidates(item: ContextBudgetItemV2, policy?: HeadroomQualityPolicyV2, countTokens?: RepresentationTokenCounter): readonly ContextRepresentationCandidateV2[];
|
|
3
18
|
//# sourceMappingURL=context-budget-headroom-candidates.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget-headroom-candidates.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-headroom-candidates.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,mBAAmB,EACxB,KAAK,gCAAgC,EAKrC,KAAK,uBAAuB,EAE5B,MAAM,oCAAoC,CAAC;AAK5C,wBAAgB,8BAA8B,CAC7C,IAAI,EAAE,mBAAmB,EACzB,MAAM,GAAE,uBAAyD,
|
|
1
|
+
{"version":3,"file":"context-budget-headroom-candidates.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-headroom-candidates.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,mBAAmB,EACxB,KAAK,gCAAgC,EAKrC,KAAK,uBAAuB,EAE5B,MAAM,oCAAoC,CAAC;AAK5C,kHAAkH;AAClH,MAAM,MAAM,0BAA0B,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAElE;;;;;;;;;;;;GAYG;AACH,wBAAgB,8BAA8B,CAC7C,IAAI,EAAE,mBAAmB,EACzB,MAAM,GAAE,uBAAyD,EACjE,WAAW,GAAE,0BAAgD,GAC3D,SAAS,gCAAgC,EAAE,CAgE7C","sourcesContent":["import {\n\ttype ContextBudgetItemV2,\n\ttype ContextRepresentationCandidateV2,\n\ttype ContextSourceRefV2,\n\tDEFAULT_HEADROOM_QUALITY_POLICY,\n\tfnv1aHex,\n\tfullTextTokens,\n\ttype HeadroomQualityPolicyV2,\n\theuristicTokenCount,\n} from \"./context-budget-headroom-types.ts\";\n\nconst SUMMARY_HEAD_CHARS = 160;\nconst HEADROOM_HEAD_CHARS = 120;\n\n/** Prices one materialized representation string; must be the counter that produced the item's full-text cost. */\nexport type RepresentationTokenCounter = (text: string) => number;\n\n/**\n * Derive the representations the selector may choose for one item.\n *\n * Every non-full candidate is priced by counting the string it actually\n * materializes with `countTokens` — the same counter that priced the full\n * text — so the cost the selector checks against the budget is the cost of\n * the text that would be sent. A compression ratio is never a cost: a summary\n * that returns its source unchanged saves nothing, and a representation whose\n * materialized text is not cheaper than the full text is not offered at all.\n * Pass the counter that produced `item.tokenEstimate`; mixing an exact\n * tokenizer for the full text with the heuristic for its alternatives makes\n * the candidates incomparable.\n */\nexport function deriveRepresentationCandidates(\n\titem: ContextBudgetItemV2,\n\tpolicy: HeadroomQualityPolicyV2 = DEFAULT_HEADROOM_QUALITY_POLICY,\n\tcountTokens: RepresentationTokenCounter = heuristicTokenCount,\n): readonly ContextRepresentationCandidateV2[] {\n\tconst full = fullTextTokens(item);\n\tconst sourceRef = item.sourceRef;\n\tconst retrievable = sourceRef?.retrievable === true;\n\tconst candidates: ContextRepresentationCandidateV2[] = [];\n\tconst offerIfCheaper = (candidate: ContextRepresentationCandidateV2): void => {\n\t\tif (candidate.text !== item.text && candidate.estimatedTokens < full) candidates.push(candidate);\n\t};\n\n\tcandidates.push({\n\t\tkind: \"full\",\n\t\ttext: item.text,\n\t\testimatedTokens: full,\n\t\tfidelity: item.tokenEstimate !== undefined ? \"exact\" : \"bounded\",\n\t\tsourceRef,\n\t});\n\n\tif (retrievable && sourceRef) {\n\t\tconst pointerText = formatPointer(sourceRef);\n\t\tofferIfCheaper({\n\t\t\tkind: \"pointer\",\n\t\t\ttext: pointerText,\n\t\t\testimatedTokens: countTokens(pointerText),\n\t\t\tfidelity: \"bounded\",\n\t\t\tsourceRef,\n\t\t});\n\t}\n\n\tif (isSummaryEligible(item, full, policy)) {\n\t\tconst summaryText = summarizeText(item.text);\n\t\tofferIfCheaper({\n\t\t\tkind: \"summary\",\n\t\t\ttext: summaryText,\n\t\t\testimatedTokens: countTokens(summaryText),\n\t\t\tfidelity: \"lossy\",\n\t\t\tsummaryHash: fnv1aHex(item.text),\n\t\t});\n\t}\n\n\tif (full > policy.headroomThresholdTokens && retrievable) {\n\t\tconst headroomText = formatHeadroom(item.text, sourceRef);\n\t\t// The shadow compressor's reversible payload is larger than the visible\n\t\t// head, so its price keeps a floor above the counted head text.\n\t\tofferIfCheaper({\n\t\t\tkind: \"headroom-compressed\",\n\t\t\ttext: headroomText,\n\t\t\testimatedTokens: Math.max(Math.ceil(full * 0.35) + 16, countTokens(headroomText)),\n\t\t\tfidelity: \"reversible\",\n\t\t\tsourceRef,\n\t\t\tcompressorId: \"headroom-shadow\",\n\t\t});\n\t}\n\n\tif (policy.allowOmit) {\n\t\tcandidates.push({\n\t\t\tkind: \"omit\",\n\t\t\ttext: \"\",\n\t\t\testimatedTokens: 0,\n\t\t\tfidelity: \"lossy\",\n\t\t\tsourceRef,\n\t\t});\n\t}\n\n\treturn candidates;\n}\n\nfunction isSummaryEligible(item: ContextBudgetItemV2, fullTokens: number, policy: HeadroomQualityPolicyV2): boolean {\n\treturn (\n\t\titem.tier === \"history\" ||\n\t\titem.tier === \"evidence\" ||\n\t\titem.tier === \"scratch\" ||\n\t\t(item.ageTurns ?? 0) >= policy.summaryMaxAgeTurns ||\n\t\tfullTokens > policy.headroomThresholdTokens\n\t);\n}\n\nfunction formatPointer(ref: ContextSourceRefV2): string {\n\tconst parts: string[] = [`uri=\"${escapeMetadataValue(ref.uri)}\"`];\n\tif (ref.symbol) parts.push(`symbol=\"${escapeMetadataValue(ref.symbol)}\"`);\n\tif (ref.range) parts.push(`lines=\"${ref.range.startLine}-${ref.range.endLine}\"`);\n\tparts.push(`hash=\"${escapeMetadataValue(ref.contentHash)}\"`);\n\treturn `<pointer ${parts.join(\" \")} />`;\n}\n\nfunction summarizeText(text: string): string {\n\tif (text.length <= SUMMARY_HEAD_CHARS) {\n\t\treturn text;\n\t}\n\treturn `${text.slice(0, SUMMARY_HEAD_CHARS)} …[summary]`;\n}\n\nfunction formatHeadroom(text: string, ref?: ContextSourceRefV2): string {\n\tconst head = text.slice(0, HEADROOM_HEAD_CHARS);\n\tconst hash = fnv1aHex(text);\n\tconst where = ref ? ` uri=\"${escapeMetadataValue(ref.uri)}\"` : \"\";\n\treturn `${head} …[headroom-compressed${where} hash=\"${hash}\"]`;\n}\n\nfunction escapeMetadataValue(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n"]}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { DEFAULT_HEADROOM_QUALITY_POLICY, fnv1aHex, fullTextTokens, heuristicTokenCount, } from "./context-budget-headroom-types.js";
|
|
2
2
|
const SUMMARY_HEAD_CHARS = 160;
|
|
3
3
|
const HEADROOM_HEAD_CHARS = 120;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Derive the representations the selector may choose for one item.
|
|
6
|
+
*
|
|
7
|
+
* Every non-full candidate is priced by counting the string it actually
|
|
8
|
+
* materializes with `countTokens` — the same counter that priced the full
|
|
9
|
+
* text — so the cost the selector checks against the budget is the cost of
|
|
10
|
+
* the text that would be sent. A compression ratio is never a cost: a summary
|
|
11
|
+
* that returns its source unchanged saves nothing, and a representation whose
|
|
12
|
+
* materialized text is not cheaper than the full text is not offered at all.
|
|
13
|
+
* Pass the counter that produced `item.tokenEstimate`; mixing an exact
|
|
14
|
+
* tokenizer for the full text with the heuristic for its alternatives makes
|
|
15
|
+
* the candidates incomparable.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveRepresentationCandidates(item, policy = DEFAULT_HEADROOM_QUALITY_POLICY, countTokens = heuristicTokenCount) {
|
|
5
18
|
const full = fullTextTokens(item);
|
|
6
19
|
const sourceRef = item.sourceRef;
|
|
7
20
|
const retrievable = sourceRef?.retrievable === true;
|
|
8
21
|
const candidates = [];
|
|
22
|
+
const offerIfCheaper = (candidate) => {
|
|
23
|
+
if (candidate.text !== item.text && candidate.estimatedTokens < full)
|
|
24
|
+
candidates.push(candidate);
|
|
25
|
+
};
|
|
9
26
|
candidates.push({
|
|
10
27
|
kind: "full",
|
|
11
28
|
text: item.text,
|
|
@@ -15,31 +32,32 @@ export function deriveRepresentationCandidates(item, policy = DEFAULT_HEADROOM_Q
|
|
|
15
32
|
});
|
|
16
33
|
if (retrievable && sourceRef) {
|
|
17
34
|
const pointerText = formatPointer(sourceRef);
|
|
18
|
-
|
|
35
|
+
offerIfCheaper({
|
|
19
36
|
kind: "pointer",
|
|
20
37
|
text: pointerText,
|
|
21
|
-
estimatedTokens:
|
|
38
|
+
estimatedTokens: countTokens(pointerText),
|
|
22
39
|
fidelity: "bounded",
|
|
23
40
|
sourceRef,
|
|
24
41
|
});
|
|
25
42
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
if (isSummaryEligible(item, full, policy)) {
|
|
44
|
+
const summaryText = summarizeText(item.text);
|
|
45
|
+
offerIfCheaper({
|
|
29
46
|
kind: "summary",
|
|
30
|
-
text:
|
|
31
|
-
estimatedTokens:
|
|
47
|
+
text: summaryText,
|
|
48
|
+
estimatedTokens: countTokens(summaryText),
|
|
32
49
|
fidelity: "lossy",
|
|
33
50
|
summaryHash: fnv1aHex(item.text),
|
|
34
51
|
});
|
|
35
52
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
if (full > policy.headroomThresholdTokens && retrievable) {
|
|
54
|
+
const headroomText = formatHeadroom(item.text, sourceRef);
|
|
55
|
+
// The shadow compressor's reversible payload is larger than the visible
|
|
56
|
+
// head, so its price keeps a floor above the counted head text.
|
|
57
|
+
offerIfCheaper({
|
|
40
58
|
kind: "headroom-compressed",
|
|
41
59
|
text: headroomText,
|
|
42
|
-
estimatedTokens:
|
|
60
|
+
estimatedTokens: Math.max(Math.ceil(full * 0.35) + 16, countTokens(headroomText)),
|
|
43
61
|
fidelity: "reversible",
|
|
44
62
|
sourceRef,
|
|
45
63
|
compressorId: "headroom-shadow",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget-headroom-candidates.js","sourceRoot":"","sources":["../../src/core/context-budget-headroom-candidates.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,+BAA+B,EAC/B,QAAQ,EACR,cAAc,EAEd,mBAAmB,GACnB,MAAM,oCAAoC,CAAC;AAE5C,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,mBAAmB,GAAG,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"context-budget-headroom-candidates.js","sourceRoot":"","sources":["../../src/core/context-budget-headroom-candidates.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype ContextBudgetItemV2,\n\ttype ContextRepresentationCandidateV2,\n\ttype ContextSourceRefV2,\n\tDEFAULT_HEADROOM_QUALITY_POLICY,\n\tfnv1aHex,\n\tfullTextTokens,\n\ttype HeadroomQualityPolicyV2,\n\theuristicTokenCount,\n} from \"./context-budget-headroom-types.ts\";\n\nconst SUMMARY_HEAD_CHARS = 160;\nconst HEADROOM_HEAD_CHARS = 120;\n\n/** Prices one materialized representation string; must be the counter that produced the item's full-text cost. */\nexport type RepresentationTokenCounter = (text: string) => number;\n\n/**\n * Derive the representations the selector may choose for one item.\n *\n * Every non-full candidate is priced by counting the string it actually\n * materializes with `countTokens` — the same counter that priced the full\n * text — so the cost the selector checks against the budget is the cost of\n * the text that would be sent. A compression ratio is never a cost: a summary\n * that returns its source unchanged saves nothing, and a representation whose\n * materialized text is not cheaper than the full text is not offered at all.\n * Pass the counter that produced `item.tokenEstimate`; mixing an exact\n * tokenizer for the full text with the heuristic for its alternatives makes\n * the candidates incomparable.\n */\nexport function deriveRepresentationCandidates(\n\titem: ContextBudgetItemV2,\n\tpolicy: HeadroomQualityPolicyV2 = DEFAULT_HEADROOM_QUALITY_POLICY,\n\tcountTokens: RepresentationTokenCounter = heuristicTokenCount,\n): readonly ContextRepresentationCandidateV2[] {\n\tconst full = fullTextTokens(item);\n\tconst sourceRef = item.sourceRef;\n\tconst retrievable = sourceRef?.retrievable === true;\n\tconst candidates: ContextRepresentationCandidateV2[] = [];\n\tconst offerIfCheaper = (candidate: ContextRepresentationCandidateV2): void => {\n\t\tif (candidate.text !== item.text && candidate.estimatedTokens < full) candidates.push(candidate);\n\t};\n\n\tcandidates.push({\n\t\tkind: \"full\",\n\t\ttext: item.text,\n\t\testimatedTokens: full,\n\t\tfidelity: item.tokenEstimate !== undefined ? \"exact\" : \"bounded\",\n\t\tsourceRef,\n\t});\n\n\tif (retrievable && sourceRef) {\n\t\tconst pointerText = formatPointer(sourceRef);\n\t\tofferIfCheaper({\n\t\t\tkind: \"pointer\",\n\t\t\ttext: pointerText,\n\t\t\testimatedTokens: countTokens(pointerText),\n\t\t\tfidelity: \"bounded\",\n\t\t\tsourceRef,\n\t\t});\n\t}\n\n\tif (isSummaryEligible(item, full, policy)) {\n\t\tconst summaryText = summarizeText(item.text);\n\t\tofferIfCheaper({\n\t\t\tkind: \"summary\",\n\t\t\ttext: summaryText,\n\t\t\testimatedTokens: countTokens(summaryText),\n\t\t\tfidelity: \"lossy\",\n\t\t\tsummaryHash: fnv1aHex(item.text),\n\t\t});\n\t}\n\n\tif (full > policy.headroomThresholdTokens && retrievable) {\n\t\tconst headroomText = formatHeadroom(item.text, sourceRef);\n\t\t// The shadow compressor's reversible payload is larger than the visible\n\t\t// head, so its price keeps a floor above the counted head text.\n\t\tofferIfCheaper({\n\t\t\tkind: \"headroom-compressed\",\n\t\t\ttext: headroomText,\n\t\t\testimatedTokens: Math.max(Math.ceil(full * 0.35) + 16, countTokens(headroomText)),\n\t\t\tfidelity: \"reversible\",\n\t\t\tsourceRef,\n\t\t\tcompressorId: \"headroom-shadow\",\n\t\t});\n\t}\n\n\tif (policy.allowOmit) {\n\t\tcandidates.push({\n\t\t\tkind: \"omit\",\n\t\t\ttext: \"\",\n\t\t\testimatedTokens: 0,\n\t\t\tfidelity: \"lossy\",\n\t\t\tsourceRef,\n\t\t});\n\t}\n\n\treturn candidates;\n}\n\nfunction isSummaryEligible(item: ContextBudgetItemV2, fullTokens: number, policy: HeadroomQualityPolicyV2): boolean {\n\treturn (\n\t\titem.tier === \"history\" ||\n\t\titem.tier === \"evidence\" ||\n\t\titem.tier === \"scratch\" ||\n\t\t(item.ageTurns ?? 0) >= policy.summaryMaxAgeTurns ||\n\t\tfullTokens > policy.headroomThresholdTokens\n\t);\n}\n\nfunction formatPointer(ref: ContextSourceRefV2): string {\n\tconst parts: string[] = [`uri=\"${escapeMetadataValue(ref.uri)}\"`];\n\tif (ref.symbol) parts.push(`symbol=\"${escapeMetadataValue(ref.symbol)}\"`);\n\tif (ref.range) parts.push(`lines=\"${ref.range.startLine}-${ref.range.endLine}\"`);\n\tparts.push(`hash=\"${escapeMetadataValue(ref.contentHash)}\"`);\n\treturn `<pointer ${parts.join(\" \")} />`;\n}\n\nfunction summarizeText(text: string): string {\n\tif (text.length <= SUMMARY_HEAD_CHARS) {\n\t\treturn text;\n\t}\n\treturn `${text.slice(0, SUMMARY_HEAD_CHARS)} …[summary]`;\n}\n\nfunction formatHeadroom(text: string, ref?: ContextSourceRefV2): string {\n\tconst head = text.slice(0, HEADROOM_HEAD_CHARS);\n\tconst hash = fnv1aHex(text);\n\tconst where = ref ? ` uri=\"${escapeMetadataValue(ref.uri)}\"` : \"\";\n\treturn `${head} …[headroom-compressed${where} hash=\"${hash}\"]`;\n}\n\nfunction escapeMetadataValue(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n"]}
|
|
@@ -48,7 +48,13 @@ export interface ContextBudgetItemV2 {
|
|
|
48
48
|
readonly representations?: readonly ContextRepresentationCandidateV2[];
|
|
49
49
|
}
|
|
50
50
|
export interface HeadroomQualityPolicyV2 {
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Never read by the candidate generator or the selector; the
|
|
53
|
+
* priority weight already prefers full text for high-priority items. Kept
|
|
54
|
+
* optional so existing configs still type-check. Flipping it changes no
|
|
55
|
+
* decision (see context-budget-quality-policy-semantics.test.ts).
|
|
56
|
+
*/
|
|
57
|
+
readonly preferFullForHighPriority?: boolean;
|
|
52
58
|
readonly preferPointerForRetrievable: boolean;
|
|
53
59
|
readonly summaryMaxAgeTurns: number;
|
|
54
60
|
readonly headroomThresholdTokens: number;
|