@caupulican/pi-adaptative 0.80.97 → 0.80.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/core/agent-session.d.ts +46 -5
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +385 -17
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/autonomy/envelope-enforcement.d.ts +17 -0
  7. package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -0
  8. package/dist/core/autonomy/envelope-enforcement.js +80 -0
  9. package/dist/core/autonomy/envelope-enforcement.js.map +1 -0
  10. package/dist/core/autonomy/foreground-envelope.d.ts +22 -0
  11. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -0
  12. package/dist/core/autonomy/foreground-envelope.js +65 -0
  13. package/dist/core/autonomy/foreground-envelope.js.map +1 -0
  14. package/dist/core/autonomy/status.d.ts +11 -0
  15. package/dist/core/autonomy/status.d.ts.map +1 -1
  16. package/dist/core/autonomy/status.js.map +1 -1
  17. package/dist/core/context/brain-curator.d.ts +7 -0
  18. package/dist/core/context/brain-curator.d.ts.map +1 -1
  19. package/dist/core/context/brain-curator.js +6 -0
  20. package/dist/core/context/brain-curator.js.map +1 -1
  21. package/dist/core/context/context-composition.d.ts.map +1 -1
  22. package/dist/core/context/context-composition.js +1 -1
  23. package/dist/core/context/context-composition.js.map +1 -1
  24. package/dist/core/delegation/session-worker-result.d.ts +8 -2
  25. package/dist/core/delegation/session-worker-result.d.ts.map +1 -1
  26. package/dist/core/delegation/session-worker-result.js +18 -1
  27. package/dist/core/delegation/session-worker-result.js.map +1 -1
  28. package/dist/core/delegation/worker-actions.d.ts +50 -0
  29. package/dist/core/delegation/worker-actions.d.ts.map +1 -0
  30. package/dist/core/delegation/worker-actions.js +70 -0
  31. package/dist/core/delegation/worker-actions.js.map +1 -0
  32. package/dist/core/delegation/worker-runner.d.ts +9 -0
  33. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-runner.js +38 -4
  35. package/dist/core/delegation/worker-runner.js.map +1 -1
  36. package/dist/core/learning/observation-store.d.ts +20 -0
  37. package/dist/core/learning/observation-store.d.ts.map +1 -0
  38. package/dist/core/learning/observation-store.js +101 -0
  39. package/dist/core/learning/observation-store.js.map +1 -0
  40. package/dist/core/model-capability.d.ts +19 -0
  41. package/dist/core/model-capability.d.ts.map +1 -1
  42. package/dist/core/model-capability.js +19 -0
  43. package/dist/core/model-capability.js.map +1 -1
  44. package/dist/core/model-router/executor-route.d.ts +8 -0
  45. package/dist/core/model-router/executor-route.d.ts.map +1 -0
  46. package/dist/core/model-router/executor-route.js +33 -0
  47. package/dist/core/model-router/executor-route.js.map +1 -0
  48. package/dist/core/model-router/tool-escalation.d.ts +2 -0
  49. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  50. package/dist/core/model-router/tool-escalation.js +6 -0
  51. package/dist/core/model-router/tool-escalation.js.map +1 -1
  52. package/dist/core/research/research-runner.d.ts +8 -1
  53. package/dist/core/research/research-runner.d.ts.map +1 -1
  54. package/dist/core/research/research-runner.js +13 -1
  55. package/dist/core/research/research-runner.js.map +1 -1
  56. package/dist/core/research/workspace-collector.d.ts +25 -0
  57. package/dist/core/research/workspace-collector.d.ts.map +1 -0
  58. package/dist/core/research/workspace-collector.js +286 -0
  59. package/dist/core/research/workspace-collector.js.map +1 -0
  60. package/dist/core/settings-manager.d.ts +5 -0
  61. package/dist/core/settings-manager.d.ts.map +1 -1
  62. package/dist/core/settings-manager.js +8 -0
  63. package/dist/core/settings-manager.js.map +1 -1
  64. package/dist/modes/interactive/components/fitness-role-selector.d.ts +1 -1
  65. package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/fitness-role-selector.js +5 -0
  67. package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
  68. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +20 -0
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode.js +9 -0
  73. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  75. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  76. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  77. package/examples/extensions/sandbox/package-lock.json +2 -2
  78. package/examples/extensions/sandbox/package.json +1 -1
  79. package/examples/extensions/with-deps/package-lock.json +2 -2
  80. package/examples/extensions/with-deps/package.json +1 -1
  81. package/npm-shrinkwrap.json +12 -12
  82. package/package.json +4 -4
@@ -0,0 +1,17 @@
1
+ import type { CapabilityEnvelope } from "./contracts.ts";
2
+ export declare function extractPathArguments(params: unknown): string[];
3
+ /**
4
+ * Deny wins over allow; an empty/absent allow list means "no positive scope restriction"
5
+ * (only denies apply) — mirroring the resource-profile filter semantics.
6
+ */
7
+ export declare function isPathWithinEnvelope(envelope: CapabilityEnvelope, rawPath: string, cwd: string): boolean;
8
+ export interface EnvelopeScopedTool {
9
+ name: string;
10
+ execute: (...args: unknown[]) => unknown;
11
+ }
12
+ /**
13
+ * Wrap a tool so every path-bearing argument is scope-checked when it RUNS. The wrapped tool is
14
+ * shape-identical; params are conventionally the second execute argument (toolCallId, params, …).
15
+ */
16
+ export declare function wrapToolWithEnvelopeScope<T extends EnvelopeScopedTool>(tool: T, envelope: CapabilityEnvelope, cwd: string): T;
17
+ //# sourceMappingURL=envelope-enforcement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-enforcement.d.ts","sourceRoot":"","sources":["../../../src/core/autonomy/envelope-enforcement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAczD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAiB9D;AAUD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQxG;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;CACzC;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,kBAAkB,EACrE,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,EAAE,MAAM,GACT,CAAC,CAsBH","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport type { CapabilityEnvelope } from \"./contracts.ts\";\n\n/**\n * Tool-level envelope enforcement (G2 prerequisite for code-writing workers): the capability\n * envelope's `allowedPaths`/`deniedPaths` were previously VALIDATION-ONLY — recorded on the\n * envelope but never checked when a tool actually ran. This module wraps tools so path-bearing\n * arguments are checked AT EXECUTION TIME, structurally refusing out-of-scope paths the same way\n * a failed script can never look like success: the refusal is an isError result with a stable\n * outcome code, never a silent no-op.\n */\n\nconst PATH_ARGUMENT_KEYS = [\"path\", \"file_path\", \"filePath\", \"cwd\", \"directory\", \"dir\", \"target\"] as const;\nconst PATH_LIST_ARGUMENT_KEYS = [\"paths\", \"files\"] as const;\n\nexport function extractPathArguments(params: unknown): string[] {\n\tif (!params || typeof params !== \"object\") return [];\n\tconst record = params as Record<string, unknown>;\n\tconst found: string[] = [];\n\tfor (const key of PATH_ARGUMENT_KEYS) {\n\t\tconst value = record[key];\n\t\tif (typeof value === \"string\" && value.length > 0) found.push(value);\n\t}\n\tfor (const key of PATH_LIST_ARGUMENT_KEYS) {\n\t\tconst value = record[key];\n\t\tif (Array.isArray(value)) {\n\t\t\tfor (const entry of value) {\n\t\t\t\tif (typeof entry === \"string\" && entry.length > 0) found.push(entry);\n\t\t\t}\n\t\t}\n\t}\n\treturn found;\n}\n\nfunction isWithinRoot(target: string, root: string): boolean {\n\tconst relativePath = relative(root, target);\n\treturn (\n\t\trelativePath === \"\" ||\n\t\t(!relativePath.startsWith(`..${sep}`) && relativePath !== \"..\" && !isAbsolute(relativePath))\n\t);\n}\n\n/**\n * Deny wins over allow; an empty/absent allow list means \"no positive scope restriction\"\n * (only denies apply) — mirroring the resource-profile filter semantics.\n */\nexport function isPathWithinEnvelope(envelope: CapabilityEnvelope, rawPath: string, cwd: string): boolean {\n\tconst target = resolve(cwd, rawPath);\n\tfor (const denied of envelope.deniedPaths ?? []) {\n\t\tif (isWithinRoot(target, resolve(cwd, denied))) return false;\n\t}\n\tconst allowed = envelope.allowedPaths ?? [];\n\tif (allowed.length === 0) return true;\n\treturn allowed.some((root) => isWithinRoot(target, resolve(cwd, root)));\n}\n\nexport interface EnvelopeScopedTool {\n\tname: string;\n\texecute: (...args: unknown[]) => unknown;\n}\n\n/**\n * Wrap a tool so every path-bearing argument is scope-checked when it RUNS. The wrapped tool is\n * shape-identical; params are conventionally the second execute argument (toolCallId, params, …).\n */\nexport function wrapToolWithEnvelopeScope<T extends EnvelopeScopedTool>(\n\ttool: T,\n\tenvelope: CapabilityEnvelope,\n\tcwd: string,\n): T {\n\treturn {\n\t\t...tool,\n\t\texecute: (...args: unknown[]) => {\n\t\t\tconst params = args[1];\n\t\t\tfor (const rawPath of extractPathArguments(params)) {\n\t\t\t\tif (!isPathWithinEnvelope(envelope, rawPath, cwd)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `envelope_path_denied: \"${rawPath}\" is outside envelope ${envelope.id}'s path scope. The tool was NOT run.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { outcome: \"envelope_path_denied\", tool: tool.name, path: rawPath, envelopeId: envelope.id },\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn tool.execute(...args);\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,80 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ /**
3
+ * Tool-level envelope enforcement (G2 prerequisite for code-writing workers): the capability
4
+ * envelope's `allowedPaths`/`deniedPaths` were previously VALIDATION-ONLY — recorded on the
5
+ * envelope but never checked when a tool actually ran. This module wraps tools so path-bearing
6
+ * arguments are checked AT EXECUTION TIME, structurally refusing out-of-scope paths the same way
7
+ * a failed script can never look like success: the refusal is an isError result with a stable
8
+ * outcome code, never a silent no-op.
9
+ */
10
+ const PATH_ARGUMENT_KEYS = ["path", "file_path", "filePath", "cwd", "directory", "dir", "target"];
11
+ const PATH_LIST_ARGUMENT_KEYS = ["paths", "files"];
12
+ export function extractPathArguments(params) {
13
+ if (!params || typeof params !== "object")
14
+ return [];
15
+ const record = params;
16
+ const found = [];
17
+ for (const key of PATH_ARGUMENT_KEYS) {
18
+ const value = record[key];
19
+ if (typeof value === "string" && value.length > 0)
20
+ found.push(value);
21
+ }
22
+ for (const key of PATH_LIST_ARGUMENT_KEYS) {
23
+ const value = record[key];
24
+ if (Array.isArray(value)) {
25
+ for (const entry of value) {
26
+ if (typeof entry === "string" && entry.length > 0)
27
+ found.push(entry);
28
+ }
29
+ }
30
+ }
31
+ return found;
32
+ }
33
+ function isWithinRoot(target, root) {
34
+ const relativePath = relative(root, target);
35
+ return (relativePath === "" ||
36
+ (!relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !isAbsolute(relativePath)));
37
+ }
38
+ /**
39
+ * Deny wins over allow; an empty/absent allow list means "no positive scope restriction"
40
+ * (only denies apply) — mirroring the resource-profile filter semantics.
41
+ */
42
+ export function isPathWithinEnvelope(envelope, rawPath, cwd) {
43
+ const target = resolve(cwd, rawPath);
44
+ for (const denied of envelope.deniedPaths ?? []) {
45
+ if (isWithinRoot(target, resolve(cwd, denied)))
46
+ return false;
47
+ }
48
+ const allowed = envelope.allowedPaths ?? [];
49
+ if (allowed.length === 0)
50
+ return true;
51
+ return allowed.some((root) => isWithinRoot(target, resolve(cwd, root)));
52
+ }
53
+ /**
54
+ * Wrap a tool so every path-bearing argument is scope-checked when it RUNS. The wrapped tool is
55
+ * shape-identical; params are conventionally the second execute argument (toolCallId, params, …).
56
+ */
57
+ export function wrapToolWithEnvelopeScope(tool, envelope, cwd) {
58
+ return {
59
+ ...tool,
60
+ execute: (...args) => {
61
+ const params = args[1];
62
+ for (const rawPath of extractPathArguments(params)) {
63
+ if (!isPathWithinEnvelope(envelope, rawPath, cwd)) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `envelope_path_denied: "${rawPath}" is outside envelope ${envelope.id}'s path scope. The tool was NOT run.`,
69
+ },
70
+ ],
71
+ details: { outcome: "envelope_path_denied", tool: tool.name, path: rawPath, envelopeId: envelope.id },
72
+ isError: true,
73
+ };
74
+ }
75
+ }
76
+ return tool.execute(...args);
77
+ },
78
+ };
79
+ }
80
+ //# sourceMappingURL=envelope-enforcement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-enforcement.js","sourceRoot":"","sources":["../../../src/core/autonomy/envelope-enforcement.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport type { CapabilityEnvelope } from \"./contracts.ts\";\n\n/**\n * Tool-level envelope enforcement (G2 prerequisite for code-writing workers): the capability\n * envelope's `allowedPaths`/`deniedPaths` were previously VALIDATION-ONLY — recorded on the\n * envelope but never checked when a tool actually ran. This module wraps tools so path-bearing\n * arguments are checked AT EXECUTION TIME, structurally refusing out-of-scope paths the same way\n * a failed script can never look like success: the refusal is an isError result with a stable\n * outcome code, never a silent no-op.\n */\n\nconst PATH_ARGUMENT_KEYS = [\"path\", \"file_path\", \"filePath\", \"cwd\", \"directory\", \"dir\", \"target\"] as const;\nconst PATH_LIST_ARGUMENT_KEYS = [\"paths\", \"files\"] as const;\n\nexport function extractPathArguments(params: unknown): string[] {\n\tif (!params || typeof params !== \"object\") return [];\n\tconst record = params as Record<string, unknown>;\n\tconst found: string[] = [];\n\tfor (const key of PATH_ARGUMENT_KEYS) {\n\t\tconst value = record[key];\n\t\tif (typeof value === \"string\" && value.length > 0) found.push(value);\n\t}\n\tfor (const key of PATH_LIST_ARGUMENT_KEYS) {\n\t\tconst value = record[key];\n\t\tif (Array.isArray(value)) {\n\t\t\tfor (const entry of value) {\n\t\t\t\tif (typeof entry === \"string\" && entry.length > 0) found.push(entry);\n\t\t\t}\n\t\t}\n\t}\n\treturn found;\n}\n\nfunction isWithinRoot(target: string, root: string): boolean {\n\tconst relativePath = relative(root, target);\n\treturn (\n\t\trelativePath === \"\" ||\n\t\t(!relativePath.startsWith(`..${sep}`) && relativePath !== \"..\" && !isAbsolute(relativePath))\n\t);\n}\n\n/**\n * Deny wins over allow; an empty/absent allow list means \"no positive scope restriction\"\n * (only denies apply) — mirroring the resource-profile filter semantics.\n */\nexport function isPathWithinEnvelope(envelope: CapabilityEnvelope, rawPath: string, cwd: string): boolean {\n\tconst target = resolve(cwd, rawPath);\n\tfor (const denied of envelope.deniedPaths ?? []) {\n\t\tif (isWithinRoot(target, resolve(cwd, denied))) return false;\n\t}\n\tconst allowed = envelope.allowedPaths ?? [];\n\tif (allowed.length === 0) return true;\n\treturn allowed.some((root) => isWithinRoot(target, resolve(cwd, root)));\n}\n\nexport interface EnvelopeScopedTool {\n\tname: string;\n\texecute: (...args: unknown[]) => unknown;\n}\n\n/**\n * Wrap a tool so every path-bearing argument is scope-checked when it RUNS. The wrapped tool is\n * shape-identical; params are conventionally the second execute argument (toolCallId, params, …).\n */\nexport function wrapToolWithEnvelopeScope<T extends EnvelopeScopedTool>(\n\ttool: T,\n\tenvelope: CapabilityEnvelope,\n\tcwd: string,\n): T {\n\treturn {\n\t\t...tool,\n\t\texecute: (...args: unknown[]) => {\n\t\t\tconst params = args[1];\n\t\t\tfor (const rawPath of extractPathArguments(params)) {\n\t\t\t\tif (!isPathWithinEnvelope(envelope, rawPath, cwd)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `envelope_path_denied: \"${rawPath}\" is outside envelope ${envelope.id}'s path scope. The tool was NOT run.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { outcome: \"envelope_path_denied\", tool: tool.name, path: rawPath, envelopeId: envelope.id },\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn tool.execute(...args);\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { CapabilityEnvelope } from "./contracts.ts";
2
+ /**
3
+ * Build the auto-constructed foreground {@link CapabilityEnvelope} for a single prompt turn.
4
+ *
5
+ * Pure and deterministic. `capabilities` are derived from the active tool names via the explicit
6
+ * {@link TOOL_CAPABILITY_MAP} (deduplicated, first-seen order; unknown tools omitted).
7
+ * `allowedTools` mirrors the active tool names, `allowedPaths` scopes to the working directory, and
8
+ * `maxEstimatedUsd` is set only when a positive per-turn ceiling is supplied.
9
+ */
10
+ export declare function buildForegroundEnvelope(args: {
11
+ turnIndex: number;
12
+ activeToolNames: readonly string[];
13
+ cwd: string;
14
+ maxTurnUsd?: number;
15
+ }): CapabilityEnvelope;
16
+ /**
17
+ * One bounded plain-text line describing a foreground envelope, for the /context dashboard.
18
+ * Lists capability names (bounded by the small {@link CapabilityName} union) and the tool COUNT
19
+ * (never the full tool list) so the line stays short regardless of how many tools are active.
20
+ */
21
+ export declare function formatForegroundEnvelopeObservation(envelope: CapabilityEnvelope): string;
22
+ //# sourceMappingURL=foreground-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreground-envelope.d.ts","sourceRoot":"","sources":["../../../src/core/autonomy/foreground-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,gBAAgB,CAAC;AA0BzE;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,kBAAkB,CAuBrB;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAKxF","sourcesContent":["import type { CapabilityEnvelope, CapabilityName } from \"./contracts.ts\";\n\n/**\n * G7: explicit tool-name -> capability mapping for foreground turns.\n *\n * Background lanes carry hand-authored {@link CapabilityEnvelope}s; foreground turns have none, so\n * {@link buildForegroundEnvelope} derives one per turn purely for VISIBILITY (observe-only this\n * round -- the foreground envelope is NOT enforced). Any tool not in this table contributes NO\n * capability: unknown capabilities are omitted, never guessed. Keys are matched against the\n * lowercased active tool name. Every value below is a real member of the {@link CapabilityName}\n * union in contracts.ts.\n */\nconst TOOL_CAPABILITY_MAP: Readonly<Record<string, CapabilityName>> = {\n\tread: \"read_files\",\n\tgrep: \"read_files\",\n\tfind: \"read_files\",\n\tls: \"read_files\",\n\tedit: \"write_files\",\n\twrite: \"write_files\",\n\tbash: \"run_shell\",\n\trun_toolkit_script: \"run_shell\",\n\tdelegate: \"delegate\",\n\tgoal: \"memory_write\",\n\tmemory: \"memory_write\",\n};\n\n/**\n * Build the auto-constructed foreground {@link CapabilityEnvelope} for a single prompt turn.\n *\n * Pure and deterministic. `capabilities` are derived from the active tool names via the explicit\n * {@link TOOL_CAPABILITY_MAP} (deduplicated, first-seen order; unknown tools omitted).\n * `allowedTools` mirrors the active tool names, `allowedPaths` scopes to the working directory, and\n * `maxEstimatedUsd` is set only when a positive per-turn ceiling is supplied.\n */\nexport function buildForegroundEnvelope(args: {\n\tturnIndex: number;\n\tactiveToolNames: readonly string[];\n\tcwd: string;\n\tmaxTurnUsd?: number;\n}): CapabilityEnvelope {\n\tconst { turnIndex, activeToolNames, cwd, maxTurnUsd } = args;\n\n\tconst capabilities: CapabilityName[] = [];\n\tconst seen = new Set<CapabilityName>();\n\tfor (const toolName of activeToolNames) {\n\t\tconst capability = TOOL_CAPABILITY_MAP[toolName.toLowerCase()];\n\t\tif (capability !== undefined && !seen.has(capability)) {\n\t\t\tseen.add(capability);\n\t\t\tcapabilities.push(capability);\n\t\t}\n\t}\n\n\tconst envelope: CapabilityEnvelope = {\n\t\tid: `foreground-turn-${turnIndex}`,\n\t\tcapabilities,\n\t\tallowedTools: [...activeToolNames],\n\t\tallowedPaths: [cwd],\n\t};\n\tif (typeof maxTurnUsd === \"number\" && maxTurnUsd > 0) {\n\t\tenvelope.maxEstimatedUsd = maxTurnUsd;\n\t}\n\treturn envelope;\n}\n\n/**\n * One bounded plain-text line describing a foreground envelope, for the /context dashboard.\n * Lists capability names (bounded by the small {@link CapabilityName} union) and the tool COUNT\n * (never the full tool list) so the line stays short regardless of how many tools are active.\n */\nexport function formatForegroundEnvelopeObservation(envelope: CapabilityEnvelope): string {\n\tconst capabilityNames = envelope.capabilities.length > 0 ? envelope.capabilities.join(\", \") : \"none\";\n\tconst toolCount = envelope.allowedTools?.length ?? 0;\n\tconst pathScope = envelope.allowedPaths?.[0] ?? \"(unscoped)\";\n\treturn `foreground envelope: ${envelope.capabilities.length} capability(ies) [${capabilityNames}], ${toolCount} tool(s), path scope ${pathScope}`;\n}\n"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * G7: explicit tool-name -> capability mapping for foreground turns.
3
+ *
4
+ * Background lanes carry hand-authored {@link CapabilityEnvelope}s; foreground turns have none, so
5
+ * {@link buildForegroundEnvelope} derives one per turn purely for VISIBILITY (observe-only this
6
+ * round -- the foreground envelope is NOT enforced). Any tool not in this table contributes NO
7
+ * capability: unknown capabilities are omitted, never guessed. Keys are matched against the
8
+ * lowercased active tool name. Every value below is a real member of the {@link CapabilityName}
9
+ * union in contracts.ts.
10
+ */
11
+ const TOOL_CAPABILITY_MAP = {
12
+ read: "read_files",
13
+ grep: "read_files",
14
+ find: "read_files",
15
+ ls: "read_files",
16
+ edit: "write_files",
17
+ write: "write_files",
18
+ bash: "run_shell",
19
+ run_toolkit_script: "run_shell",
20
+ delegate: "delegate",
21
+ goal: "memory_write",
22
+ memory: "memory_write",
23
+ };
24
+ /**
25
+ * Build the auto-constructed foreground {@link CapabilityEnvelope} for a single prompt turn.
26
+ *
27
+ * Pure and deterministic. `capabilities` are derived from the active tool names via the explicit
28
+ * {@link TOOL_CAPABILITY_MAP} (deduplicated, first-seen order; unknown tools omitted).
29
+ * `allowedTools` mirrors the active tool names, `allowedPaths` scopes to the working directory, and
30
+ * `maxEstimatedUsd` is set only when a positive per-turn ceiling is supplied.
31
+ */
32
+ export function buildForegroundEnvelope(args) {
33
+ const { turnIndex, activeToolNames, cwd, maxTurnUsd } = args;
34
+ const capabilities = [];
35
+ const seen = new Set();
36
+ for (const toolName of activeToolNames) {
37
+ const capability = TOOL_CAPABILITY_MAP[toolName.toLowerCase()];
38
+ if (capability !== undefined && !seen.has(capability)) {
39
+ seen.add(capability);
40
+ capabilities.push(capability);
41
+ }
42
+ }
43
+ const envelope = {
44
+ id: `foreground-turn-${turnIndex}`,
45
+ capabilities,
46
+ allowedTools: [...activeToolNames],
47
+ allowedPaths: [cwd],
48
+ };
49
+ if (typeof maxTurnUsd === "number" && maxTurnUsd > 0) {
50
+ envelope.maxEstimatedUsd = maxTurnUsd;
51
+ }
52
+ return envelope;
53
+ }
54
+ /**
55
+ * One bounded plain-text line describing a foreground envelope, for the /context dashboard.
56
+ * Lists capability names (bounded by the small {@link CapabilityName} union) and the tool COUNT
57
+ * (never the full tool list) so the line stays short regardless of how many tools are active.
58
+ */
59
+ export function formatForegroundEnvelopeObservation(envelope) {
60
+ const capabilityNames = envelope.capabilities.length > 0 ? envelope.capabilities.join(", ") : "none";
61
+ const toolCount = envelope.allowedTools?.length ?? 0;
62
+ const pathScope = envelope.allowedPaths?.[0] ?? "(unscoped)";
63
+ return `foreground envelope: ${envelope.capabilities.length} capability(ies) [${capabilityNames}], ${toolCount} tool(s), path scope ${pathScope}`;
64
+ }
65
+ //# sourceMappingURL=foreground-envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreground-envelope.js","sourceRoot":"","sources":["../../../src/core/autonomy/foreground-envelope.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,mBAAmB,GAA6C;IACrE,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,EAAE,EAAE,YAAY;IAChB,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,aAAa;IACpB,IAAI,EAAE,WAAW;IACjB,kBAAkB,EAAE,WAAW;IAC/B,QAAQ,EAAE,UAAU;IACpB,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,cAAc;CACtB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAKvC,EAAsB;IACtB,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAE7D,MAAM,YAAY,GAAqB,EAAE,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACxC,MAAM,UAAU,GAAG,mBAAmB,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/D,IAAI,UAAU,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACrB,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/B,CAAC;IACF,CAAC;IAED,MAAM,QAAQ,GAAuB;QACpC,EAAE,EAAE,mBAAmB,SAAS,EAAE;QAClC,YAAY;QACZ,YAAY,EAAE,CAAC,GAAG,eAAe,CAAC;QAClC,YAAY,EAAE,CAAC,GAAG,CAAC;KACnB,CAAC;IACF,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACtD,QAAQ,CAAC,eAAe,GAAG,UAAU,CAAC;IACvC,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;;;GAIG;AACH,MAAM,UAAU,mCAAmC,CAAC,QAA4B,EAAU;IACzF,MAAM,eAAe,GAAG,QAAQ,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACrG,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC;IAC7D,OAAO,wBAAwB,QAAQ,CAAC,YAAY,CAAC,MAAM,qBAAqB,eAAe,MAAM,SAAS,wBAAwB,SAAS,EAAE,CAAC;AAAA,CAClJ","sourcesContent":["import type { CapabilityEnvelope, CapabilityName } from \"./contracts.ts\";\n\n/**\n * G7: explicit tool-name -> capability mapping for foreground turns.\n *\n * Background lanes carry hand-authored {@link CapabilityEnvelope}s; foreground turns have none, so\n * {@link buildForegroundEnvelope} derives one per turn purely for VISIBILITY (observe-only this\n * round -- the foreground envelope is NOT enforced). Any tool not in this table contributes NO\n * capability: unknown capabilities are omitted, never guessed. Keys are matched against the\n * lowercased active tool name. Every value below is a real member of the {@link CapabilityName}\n * union in contracts.ts.\n */\nconst TOOL_CAPABILITY_MAP: Readonly<Record<string, CapabilityName>> = {\n\tread: \"read_files\",\n\tgrep: \"read_files\",\n\tfind: \"read_files\",\n\tls: \"read_files\",\n\tedit: \"write_files\",\n\twrite: \"write_files\",\n\tbash: \"run_shell\",\n\trun_toolkit_script: \"run_shell\",\n\tdelegate: \"delegate\",\n\tgoal: \"memory_write\",\n\tmemory: \"memory_write\",\n};\n\n/**\n * Build the auto-constructed foreground {@link CapabilityEnvelope} for a single prompt turn.\n *\n * Pure and deterministic. `capabilities` are derived from the active tool names via the explicit\n * {@link TOOL_CAPABILITY_MAP} (deduplicated, first-seen order; unknown tools omitted).\n * `allowedTools` mirrors the active tool names, `allowedPaths` scopes to the working directory, and\n * `maxEstimatedUsd` is set only when a positive per-turn ceiling is supplied.\n */\nexport function buildForegroundEnvelope(args: {\n\tturnIndex: number;\n\tactiveToolNames: readonly string[];\n\tcwd: string;\n\tmaxTurnUsd?: number;\n}): CapabilityEnvelope {\n\tconst { turnIndex, activeToolNames, cwd, maxTurnUsd } = args;\n\n\tconst capabilities: CapabilityName[] = [];\n\tconst seen = new Set<CapabilityName>();\n\tfor (const toolName of activeToolNames) {\n\t\tconst capability = TOOL_CAPABILITY_MAP[toolName.toLowerCase()];\n\t\tif (capability !== undefined && !seen.has(capability)) {\n\t\t\tseen.add(capability);\n\t\t\tcapabilities.push(capability);\n\t\t}\n\t}\n\n\tconst envelope: CapabilityEnvelope = {\n\t\tid: `foreground-turn-${turnIndex}`,\n\t\tcapabilities,\n\t\tallowedTools: [...activeToolNames],\n\t\tallowedPaths: [cwd],\n\t};\n\tif (typeof maxTurnUsd === \"number\" && maxTurnUsd > 0) {\n\t\tenvelope.maxEstimatedUsd = maxTurnUsd;\n\t}\n\treturn envelope;\n}\n\n/**\n * One bounded plain-text line describing a foreground envelope, for the /context dashboard.\n * Lists capability names (bounded by the small {@link CapabilityName} union) and the tool COUNT\n * (never the full tool list) so the line stays short regardless of how many tools are active.\n */\nexport function formatForegroundEnvelopeObservation(envelope: CapabilityEnvelope): string {\n\tconst capabilityNames = envelope.capabilities.length > 0 ? envelope.capabilities.join(\", \") : \"none\";\n\tconst toolCount = envelope.allowedTools?.length ?? 0;\n\tconst pathScope = envelope.allowedPaths?.[0] ?? \"(unscoped)\";\n\treturn `foreground envelope: ${envelope.capabilities.length} capability(ies) [${capabilityNames}], ${toolCount} tool(s), path scope ${pathScope}`;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { GateOutcomeKind } from "./contracts.ts";
1
2
  export interface AutonomyStatusSnapshot {
2
3
  latestRoute?: {
3
4
  tier: string;
@@ -20,6 +21,16 @@ export interface AutonomyStatusSnapshot {
20
21
  };
21
22
  activeLaneCount?: number;
22
23
  }
24
+ /**
25
+ * One bounded entry in AgentSession's gate-outcome history (G8). Codes/ids only — never the gate's
26
+ * human-facing `message`. The most recent entry is the tail; `at` is an ISO timestamp.
27
+ */
28
+ export interface GateOutcomeHistoryEntry {
29
+ outcome: GateOutcomeKind;
30
+ gate: string;
31
+ reasonCode: string;
32
+ at: string;
33
+ }
23
34
  export interface DiagnosticEntry {
24
35
  title: string;
25
36
  summary?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/core/autonomy/status.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACtC,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChG,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,0BAA0B;IAC1C,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CACnC;AA+CD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAuCzE;AAsBD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,0BAA0B,GAAG,MAAM,CAalF","sourcesContent":["export interface AutonomyStatusSnapshot {\n\tlatestRoute?: { tier: string; reasonCode: string; risk?: string };\n\tlatestGate?: { outcome: string; gate: string; reasonCode: string };\n\tcurrentCostUsd?: number;\n\tdailyCostUsd?: number;\n\tspawnedCostUsd?: number;\n\tactiveGoal?: { goalId: string; status: string; openRequirements?: number; stallTurns?: number };\n\tactiveLaneCount?: number;\n}\n\nexport interface DiagnosticEntry {\n\ttitle: string;\n\tsummary?: string;\n\treasonCode?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface AutonomyDiagnosticSnapshot {\n\troutes?: readonly DiagnosticEntry[];\n\tgates?: readonly DiagnosticEntry[];\n\tcosts?: readonly DiagnosticEntry[];\n\tresearch?: readonly DiagnosticEntry[];\n\tdelegation?: readonly DiagnosticEntry[];\n\tlearning?: readonly DiagnosticEntry[];\n\tgoals?: readonly DiagnosticEntry[];\n}\n\nconst REDACTED = \"[REDACTED]\";\nconst CIRCULAR = \"[Circular]\";\nconst MAX_STRING_LENGTH = 200;\nconst SENSITIVE_KEYS = [\"token\", \"secret\", \"key\", \"credential\", \"password\", \"authorization\"];\nconst SENSITIVE_VALUE_REGEX = /bearer\\s+[\\w\\-._]+|api[-_]?key[-_]?[\\w\\-._]+|sk-[\\w\\-._]+/i;\n\nfunction formatCost(value: number): string {\n\treturn Number.isFinite(value) ? `$${value.toFixed(4)}` : \"$0.0000\";\n}\n\nfunction redactAndTruncateString(value: string): string {\n\tif (SENSITIVE_VALUE_REGEX.test(value)) return REDACTED;\n\tif (value.length <= MAX_STRING_LENGTH) return value;\n\treturn `${value.slice(0, MAX_STRING_LENGTH - 1)}…`;\n}\n\nfunction isSensitiveKey(key: string): boolean {\n\tconst lowerKey = key.toLowerCase();\n\treturn SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey));\n}\n\nfunction sanitizeMetadataValue(value: unknown, seen: WeakSet<object>): unknown {\n\tif (typeof value === \"string\") return redactAndTruncateString(value);\n\tif (typeof value !== \"object\" || value === null) return value;\n\tif (seen.has(value)) return CIRCULAR;\n\tif (Array.isArray(value)) {\n\t\tseen.add(value);\n\t\treturn value.map((item) => sanitizeMetadataValue(item, seen));\n\t}\n\treturn sanitizeMetadata(value as Record<string, unknown>, seen);\n}\n\nfunction sanitizeMetadata(\n\tobj: Record<string, unknown>,\n\tseen: WeakSet<object> = new WeakSet(),\n): Record<string, unknown> {\n\tif (seen.has(obj)) return { value: CIRCULAR };\n\tseen.add(obj);\n\tconst result: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tresult[key] = isSensitiveKey(key) ? REDACTED : sanitizeMetadataValue(value, seen);\n\t}\n\treturn result;\n}\n\nexport function formatAutonomyStatus(args: AutonomyStatusSnapshot): string {\n\tconst parts: string[] = [];\n\n\tif (args.latestRoute) {\n\t\tconst risk = args.latestRoute.risk ? ` (${redactAndTruncateString(args.latestRoute.risk)})` : \"\";\n\t\tparts.push(\n\t\t\t`Route: ${redactAndTruncateString(args.latestRoute.tier)}${risk} - ${redactAndTruncateString(args.latestRoute.reasonCode)}`,\n\t\t);\n\t}\n\n\tif (args.latestGate) {\n\t\tparts.push(\n\t\t\t`Gate: ${redactAndTruncateString(args.latestGate.gate)} = ${redactAndTruncateString(args.latestGate.outcome)} (${redactAndTruncateString(args.latestGate.reasonCode)})`,\n\t\t);\n\t}\n\n\tconst costs: string[] = [];\n\tif (args.currentCostUsd !== undefined) costs.push(`current: ${formatCost(args.currentCostUsd)}`);\n\tif (args.dailyCostUsd !== undefined) costs.push(`daily: ${formatCost(args.dailyCostUsd)}`);\n\tif (args.spawnedCostUsd !== undefined) costs.push(`spawned: ${formatCost(args.spawnedCostUsd)}`);\n\tif (costs.length > 0) {\n\t\tparts.push(`Costs: ${costs.join(\", \")}`);\n\t}\n\n\tif (args.activeGoal) {\n\t\tconst goal = args.activeGoal;\n\t\tconst requirements = goal.openRequirements !== undefined ? `, open reqs: ${goal.openRequirements}` : \"\";\n\t\tconst stalls = goal.stallTurns !== undefined ? `, stalls: ${goal.stallTurns}` : \"\";\n\t\tparts.push(\n\t\t\t`Goal [${redactAndTruncateString(goal.goalId)}]: ${redactAndTruncateString(goal.status)}${requirements}${stalls}`,\n\t\t);\n\t}\n\n\tif (args.activeLaneCount !== undefined) {\n\t\tparts.push(`Lanes: ${args.activeLaneCount}`);\n\t}\n\n\tif (parts.length === 0) return \"Autonomy: idle\";\n\treturn parts.join(\" | \");\n}\n\nfunction formatDiagnosticSection(name: string, entries?: readonly DiagnosticEntry[]): string | undefined {\n\tif (!entries || entries.length === 0) return undefined;\n\n\tconst lines: string[] = [`--- ${name} ---`];\n\tfor (const entry of entries) {\n\t\tlet header = `- ${redactAndTruncateString(entry.title)}`;\n\t\tif (entry.reasonCode) header += ` [${redactAndTruncateString(entry.reasonCode)}]`;\n\t\tlines.push(header);\n\n\t\tif (entry.summary) {\n\t\t\tlines.push(` Summary: ${redactAndTruncateString(entry.summary)}`);\n\t\t}\n\n\t\tif (entry.metadata) {\n\t\t\tlines.push(` Metadata: ${JSON.stringify(sanitizeMetadata(entry.metadata))}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function formatAutonomyDiagnostics(args: AutonomyDiagnosticSnapshot): string {\n\tconst sections = [\n\t\tformatDiagnosticSection(\"Routes\", args.routes),\n\t\tformatDiagnosticSection(\"Gates\", args.gates),\n\t\tformatDiagnosticSection(\"Costs\", args.costs),\n\t\tformatDiagnosticSection(\"Research\", args.research),\n\t\tformatDiagnosticSection(\"Delegation\", args.delegation),\n\t\tformatDiagnosticSection(\"Learning\", args.learning),\n\t\tformatDiagnosticSection(\"Goals\", args.goals),\n\t].filter((section): section is string => Boolean(section));\n\n\tif (sections.length === 0) return \"No diagnostics available.\";\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/core/autonomy/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,sBAAsB;IACtC,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChG,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACvC,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,0BAA0B;IAC1C,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CACnC;AA+CD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAuCzE;AAsBD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,0BAA0B,GAAG,MAAM,CAalF","sourcesContent":["import type { GateOutcomeKind } from \"./contracts.ts\";\n\nexport interface AutonomyStatusSnapshot {\n\tlatestRoute?: { tier: string; reasonCode: string; risk?: string };\n\tlatestGate?: { outcome: string; gate: string; reasonCode: string };\n\tcurrentCostUsd?: number;\n\tdailyCostUsd?: number;\n\tspawnedCostUsd?: number;\n\tactiveGoal?: { goalId: string; status: string; openRequirements?: number; stallTurns?: number };\n\tactiveLaneCount?: number;\n}\n\n/**\n * One bounded entry in AgentSession's gate-outcome history (G8). Codes/ids only — never the gate's\n * human-facing `message`. The most recent entry is the tail; `at` is an ISO timestamp.\n */\nexport interface GateOutcomeHistoryEntry {\n\toutcome: GateOutcomeKind;\n\tgate: string;\n\treasonCode: string;\n\tat: string;\n}\n\nexport interface DiagnosticEntry {\n\ttitle: string;\n\tsummary?: string;\n\treasonCode?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface AutonomyDiagnosticSnapshot {\n\troutes?: readonly DiagnosticEntry[];\n\tgates?: readonly DiagnosticEntry[];\n\tcosts?: readonly DiagnosticEntry[];\n\tresearch?: readonly DiagnosticEntry[];\n\tdelegation?: readonly DiagnosticEntry[];\n\tlearning?: readonly DiagnosticEntry[];\n\tgoals?: readonly DiagnosticEntry[];\n}\n\nconst REDACTED = \"[REDACTED]\";\nconst CIRCULAR = \"[Circular]\";\nconst MAX_STRING_LENGTH = 200;\nconst SENSITIVE_KEYS = [\"token\", \"secret\", \"key\", \"credential\", \"password\", \"authorization\"];\nconst SENSITIVE_VALUE_REGEX = /bearer\\s+[\\w\\-._]+|api[-_]?key[-_]?[\\w\\-._]+|sk-[\\w\\-._]+/i;\n\nfunction formatCost(value: number): string {\n\treturn Number.isFinite(value) ? `$${value.toFixed(4)}` : \"$0.0000\";\n}\n\nfunction redactAndTruncateString(value: string): string {\n\tif (SENSITIVE_VALUE_REGEX.test(value)) return REDACTED;\n\tif (value.length <= MAX_STRING_LENGTH) return value;\n\treturn `${value.slice(0, MAX_STRING_LENGTH - 1)}…`;\n}\n\nfunction isSensitiveKey(key: string): boolean {\n\tconst lowerKey = key.toLowerCase();\n\treturn SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey));\n}\n\nfunction sanitizeMetadataValue(value: unknown, seen: WeakSet<object>): unknown {\n\tif (typeof value === \"string\") return redactAndTruncateString(value);\n\tif (typeof value !== \"object\" || value === null) return value;\n\tif (seen.has(value)) return CIRCULAR;\n\tif (Array.isArray(value)) {\n\t\tseen.add(value);\n\t\treturn value.map((item) => sanitizeMetadataValue(item, seen));\n\t}\n\treturn sanitizeMetadata(value as Record<string, unknown>, seen);\n}\n\nfunction sanitizeMetadata(\n\tobj: Record<string, unknown>,\n\tseen: WeakSet<object> = new WeakSet(),\n): Record<string, unknown> {\n\tif (seen.has(obj)) return { value: CIRCULAR };\n\tseen.add(obj);\n\tconst result: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tresult[key] = isSensitiveKey(key) ? REDACTED : sanitizeMetadataValue(value, seen);\n\t}\n\treturn result;\n}\n\nexport function formatAutonomyStatus(args: AutonomyStatusSnapshot): string {\n\tconst parts: string[] = [];\n\n\tif (args.latestRoute) {\n\t\tconst risk = args.latestRoute.risk ? ` (${redactAndTruncateString(args.latestRoute.risk)})` : \"\";\n\t\tparts.push(\n\t\t\t`Route: ${redactAndTruncateString(args.latestRoute.tier)}${risk} - ${redactAndTruncateString(args.latestRoute.reasonCode)}`,\n\t\t);\n\t}\n\n\tif (args.latestGate) {\n\t\tparts.push(\n\t\t\t`Gate: ${redactAndTruncateString(args.latestGate.gate)} = ${redactAndTruncateString(args.latestGate.outcome)} (${redactAndTruncateString(args.latestGate.reasonCode)})`,\n\t\t);\n\t}\n\n\tconst costs: string[] = [];\n\tif (args.currentCostUsd !== undefined) costs.push(`current: ${formatCost(args.currentCostUsd)}`);\n\tif (args.dailyCostUsd !== undefined) costs.push(`daily: ${formatCost(args.dailyCostUsd)}`);\n\tif (args.spawnedCostUsd !== undefined) costs.push(`spawned: ${formatCost(args.spawnedCostUsd)}`);\n\tif (costs.length > 0) {\n\t\tparts.push(`Costs: ${costs.join(\", \")}`);\n\t}\n\n\tif (args.activeGoal) {\n\t\tconst goal = args.activeGoal;\n\t\tconst requirements = goal.openRequirements !== undefined ? `, open reqs: ${goal.openRequirements}` : \"\";\n\t\tconst stalls = goal.stallTurns !== undefined ? `, stalls: ${goal.stallTurns}` : \"\";\n\t\tparts.push(\n\t\t\t`Goal [${redactAndTruncateString(goal.goalId)}]: ${redactAndTruncateString(goal.status)}${requirements}${stalls}`,\n\t\t);\n\t}\n\n\tif (args.activeLaneCount !== undefined) {\n\t\tparts.push(`Lanes: ${args.activeLaneCount}`);\n\t}\n\n\tif (parts.length === 0) return \"Autonomy: idle\";\n\treturn parts.join(\" | \");\n}\n\nfunction formatDiagnosticSection(name: string, entries?: readonly DiagnosticEntry[]): string | undefined {\n\tif (!entries || entries.length === 0) return undefined;\n\n\tconst lines: string[] = [`--- ${name} ---`];\n\tfor (const entry of entries) {\n\t\tlet header = `- ${redactAndTruncateString(entry.title)}`;\n\t\tif (entry.reasonCode) header += ` [${redactAndTruncateString(entry.reasonCode)}]`;\n\t\tlines.push(header);\n\n\t\tif (entry.summary) {\n\t\t\tlines.push(` Summary: ${redactAndTruncateString(entry.summary)}`);\n\t\t}\n\n\t\tif (entry.metadata) {\n\t\t\tlines.push(` Metadata: ${JSON.stringify(sanitizeMetadata(entry.metadata))}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function formatAutonomyDiagnostics(args: AutonomyDiagnosticSnapshot): string {\n\tconst sections = [\n\t\tformatDiagnosticSection(\"Routes\", args.routes),\n\t\tformatDiagnosticSection(\"Gates\", args.gates),\n\t\tformatDiagnosticSection(\"Costs\", args.costs),\n\t\tformatDiagnosticSection(\"Research\", args.research),\n\t\tformatDiagnosticSection(\"Delegation\", args.delegation),\n\t\tformatDiagnosticSection(\"Learning\", args.learning),\n\t\tformatDiagnosticSection(\"Goals\", args.goals),\n\t].filter((section): section is string => Boolean(section));\n\n\tif (sections.length === 0) return \"No diagnostics available.\";\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/core/autonomy/status.ts"],"names":[],"mappings":"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","sourcesContent":["export interface AutonomyStatusSnapshot {\n\tlatestRoute?: { tier: string; reasonCode: string; risk?: string };\n\tlatestGate?: { outcome: string; gate: string; reasonCode: string };\n\tcurrentCostUsd?: number;\n\tdailyCostUsd?: number;\n\tspawnedCostUsd?: number;\n\tactiveGoal?: { goalId: string; status: string; openRequirements?: number; stallTurns?: number };\n\tactiveLaneCount?: number;\n}\n\nexport interface DiagnosticEntry {\n\ttitle: string;\n\tsummary?: string;\n\treasonCode?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface AutonomyDiagnosticSnapshot {\n\troutes?: readonly DiagnosticEntry[];\n\tgates?: readonly DiagnosticEntry[];\n\tcosts?: readonly DiagnosticEntry[];\n\tresearch?: readonly DiagnosticEntry[];\n\tdelegation?: readonly DiagnosticEntry[];\n\tlearning?: readonly DiagnosticEntry[];\n\tgoals?: readonly DiagnosticEntry[];\n}\n\nconst REDACTED = \"[REDACTED]\";\nconst CIRCULAR = \"[Circular]\";\nconst MAX_STRING_LENGTH = 200;\nconst SENSITIVE_KEYS = [\"token\", \"secret\", \"key\", \"credential\", \"password\", \"authorization\"];\nconst SENSITIVE_VALUE_REGEX = /bearer\\s+[\\w\\-._]+|api[-_]?key[-_]?[\\w\\-._]+|sk-[\\w\\-._]+/i;\n\nfunction formatCost(value: number): string {\n\treturn Number.isFinite(value) ? `$${value.toFixed(4)}` : \"$0.0000\";\n}\n\nfunction redactAndTruncateString(value: string): string {\n\tif (SENSITIVE_VALUE_REGEX.test(value)) return REDACTED;\n\tif (value.length <= MAX_STRING_LENGTH) return value;\n\treturn `${value.slice(0, MAX_STRING_LENGTH - 1)}…`;\n}\n\nfunction isSensitiveKey(key: string): boolean {\n\tconst lowerKey = key.toLowerCase();\n\treturn SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey));\n}\n\nfunction sanitizeMetadataValue(value: unknown, seen: WeakSet<object>): unknown {\n\tif (typeof value === \"string\") return redactAndTruncateString(value);\n\tif (typeof value !== \"object\" || value === null) return value;\n\tif (seen.has(value)) return CIRCULAR;\n\tif (Array.isArray(value)) {\n\t\tseen.add(value);\n\t\treturn value.map((item) => sanitizeMetadataValue(item, seen));\n\t}\n\treturn sanitizeMetadata(value as Record<string, unknown>, seen);\n}\n\nfunction sanitizeMetadata(\n\tobj: Record<string, unknown>,\n\tseen: WeakSet<object> = new WeakSet(),\n): Record<string, unknown> {\n\tif (seen.has(obj)) return { value: CIRCULAR };\n\tseen.add(obj);\n\tconst result: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tresult[key] = isSensitiveKey(key) ? REDACTED : sanitizeMetadataValue(value, seen);\n\t}\n\treturn result;\n}\n\nexport function formatAutonomyStatus(args: AutonomyStatusSnapshot): string {\n\tconst parts: string[] = [];\n\n\tif (args.latestRoute) {\n\t\tconst risk = args.latestRoute.risk ? ` (${redactAndTruncateString(args.latestRoute.risk)})` : \"\";\n\t\tparts.push(\n\t\t\t`Route: ${redactAndTruncateString(args.latestRoute.tier)}${risk} - ${redactAndTruncateString(args.latestRoute.reasonCode)}`,\n\t\t);\n\t}\n\n\tif (args.latestGate) {\n\t\tparts.push(\n\t\t\t`Gate: ${redactAndTruncateString(args.latestGate.gate)} = ${redactAndTruncateString(args.latestGate.outcome)} (${redactAndTruncateString(args.latestGate.reasonCode)})`,\n\t\t);\n\t}\n\n\tconst costs: string[] = [];\n\tif (args.currentCostUsd !== undefined) costs.push(`current: ${formatCost(args.currentCostUsd)}`);\n\tif (args.dailyCostUsd !== undefined) costs.push(`daily: ${formatCost(args.dailyCostUsd)}`);\n\tif (args.spawnedCostUsd !== undefined) costs.push(`spawned: ${formatCost(args.spawnedCostUsd)}`);\n\tif (costs.length > 0) {\n\t\tparts.push(`Costs: ${costs.join(\", \")}`);\n\t}\n\n\tif (args.activeGoal) {\n\t\tconst goal = args.activeGoal;\n\t\tconst requirements = goal.openRequirements !== undefined ? `, open reqs: ${goal.openRequirements}` : \"\";\n\t\tconst stalls = goal.stallTurns !== undefined ? `, stalls: ${goal.stallTurns}` : \"\";\n\t\tparts.push(\n\t\t\t`Goal [${redactAndTruncateString(goal.goalId)}]: ${redactAndTruncateString(goal.status)}${requirements}${stalls}`,\n\t\t);\n\t}\n\n\tif (args.activeLaneCount !== undefined) {\n\t\tparts.push(`Lanes: ${args.activeLaneCount}`);\n\t}\n\n\tif (parts.length === 0) return \"Autonomy: idle\";\n\treturn parts.join(\" | \");\n}\n\nfunction formatDiagnosticSection(name: string, entries?: readonly DiagnosticEntry[]): string | undefined {\n\tif (!entries || entries.length === 0) return undefined;\n\n\tconst lines: string[] = [`--- ${name} ---`];\n\tfor (const entry of entries) {\n\t\tlet header = `- ${redactAndTruncateString(entry.title)}`;\n\t\tif (entry.reasonCode) header += ` [${redactAndTruncateString(entry.reasonCode)}]`;\n\t\tlines.push(header);\n\n\t\tif (entry.summary) {\n\t\t\tlines.push(` Summary: ${redactAndTruncateString(entry.summary)}`);\n\t\t}\n\n\t\tif (entry.metadata) {\n\t\t\tlines.push(` Metadata: ${JSON.stringify(sanitizeMetadata(entry.metadata))}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function formatAutonomyDiagnostics(args: AutonomyDiagnosticSnapshot): string {\n\tconst sections = [\n\t\tformatDiagnosticSection(\"Routes\", args.routes),\n\t\tformatDiagnosticSection(\"Gates\", args.gates),\n\t\tformatDiagnosticSection(\"Costs\", args.costs),\n\t\tformatDiagnosticSection(\"Research\", args.research),\n\t\tformatDiagnosticSection(\"Delegation\", args.delegation),\n\t\tformatDiagnosticSection(\"Learning\", args.learning),\n\t\tformatDiagnosticSection(\"Goals\", args.goals),\n\t].filter((section): section is string => Boolean(section));\n\n\tif (sections.length === 0) return \"No diagnostics available.\";\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/core/autonomy/status.ts"],"names":[],"mappings":"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","sourcesContent":["import type { GateOutcomeKind } from \"./contracts.ts\";\n\nexport interface AutonomyStatusSnapshot {\n\tlatestRoute?: { tier: string; reasonCode: string; risk?: string };\n\tlatestGate?: { outcome: string; gate: string; reasonCode: string };\n\tcurrentCostUsd?: number;\n\tdailyCostUsd?: number;\n\tspawnedCostUsd?: number;\n\tactiveGoal?: { goalId: string; status: string; openRequirements?: number; stallTurns?: number };\n\tactiveLaneCount?: number;\n}\n\n/**\n * One bounded entry in AgentSession's gate-outcome history (G8). Codes/ids only — never the gate's\n * human-facing `message`. The most recent entry is the tail; `at` is an ISO timestamp.\n */\nexport interface GateOutcomeHistoryEntry {\n\toutcome: GateOutcomeKind;\n\tgate: string;\n\treasonCode: string;\n\tat: string;\n}\n\nexport interface DiagnosticEntry {\n\ttitle: string;\n\tsummary?: string;\n\treasonCode?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface AutonomyDiagnosticSnapshot {\n\troutes?: readonly DiagnosticEntry[];\n\tgates?: readonly DiagnosticEntry[];\n\tcosts?: readonly DiagnosticEntry[];\n\tresearch?: readonly DiagnosticEntry[];\n\tdelegation?: readonly DiagnosticEntry[];\n\tlearning?: readonly DiagnosticEntry[];\n\tgoals?: readonly DiagnosticEntry[];\n}\n\nconst REDACTED = \"[REDACTED]\";\nconst CIRCULAR = \"[Circular]\";\nconst MAX_STRING_LENGTH = 200;\nconst SENSITIVE_KEYS = [\"token\", \"secret\", \"key\", \"credential\", \"password\", \"authorization\"];\nconst SENSITIVE_VALUE_REGEX = /bearer\\s+[\\w\\-._]+|api[-_]?key[-_]?[\\w\\-._]+|sk-[\\w\\-._]+/i;\n\nfunction formatCost(value: number): string {\n\treturn Number.isFinite(value) ? `$${value.toFixed(4)}` : \"$0.0000\";\n}\n\nfunction redactAndTruncateString(value: string): string {\n\tif (SENSITIVE_VALUE_REGEX.test(value)) return REDACTED;\n\tif (value.length <= MAX_STRING_LENGTH) return value;\n\treturn `${value.slice(0, MAX_STRING_LENGTH - 1)}…`;\n}\n\nfunction isSensitiveKey(key: string): boolean {\n\tconst lowerKey = key.toLowerCase();\n\treturn SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey));\n}\n\nfunction sanitizeMetadataValue(value: unknown, seen: WeakSet<object>): unknown {\n\tif (typeof value === \"string\") return redactAndTruncateString(value);\n\tif (typeof value !== \"object\" || value === null) return value;\n\tif (seen.has(value)) return CIRCULAR;\n\tif (Array.isArray(value)) {\n\t\tseen.add(value);\n\t\treturn value.map((item) => sanitizeMetadataValue(item, seen));\n\t}\n\treturn sanitizeMetadata(value as Record<string, unknown>, seen);\n}\n\nfunction sanitizeMetadata(\n\tobj: Record<string, unknown>,\n\tseen: WeakSet<object> = new WeakSet(),\n): Record<string, unknown> {\n\tif (seen.has(obj)) return { value: CIRCULAR };\n\tseen.add(obj);\n\tconst result: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tresult[key] = isSensitiveKey(key) ? REDACTED : sanitizeMetadataValue(value, seen);\n\t}\n\treturn result;\n}\n\nexport function formatAutonomyStatus(args: AutonomyStatusSnapshot): string {\n\tconst parts: string[] = [];\n\n\tif (args.latestRoute) {\n\t\tconst risk = args.latestRoute.risk ? ` (${redactAndTruncateString(args.latestRoute.risk)})` : \"\";\n\t\tparts.push(\n\t\t\t`Route: ${redactAndTruncateString(args.latestRoute.tier)}${risk} - ${redactAndTruncateString(args.latestRoute.reasonCode)}`,\n\t\t);\n\t}\n\n\tif (args.latestGate) {\n\t\tparts.push(\n\t\t\t`Gate: ${redactAndTruncateString(args.latestGate.gate)} = ${redactAndTruncateString(args.latestGate.outcome)} (${redactAndTruncateString(args.latestGate.reasonCode)})`,\n\t\t);\n\t}\n\n\tconst costs: string[] = [];\n\tif (args.currentCostUsd !== undefined) costs.push(`current: ${formatCost(args.currentCostUsd)}`);\n\tif (args.dailyCostUsd !== undefined) costs.push(`daily: ${formatCost(args.dailyCostUsd)}`);\n\tif (args.spawnedCostUsd !== undefined) costs.push(`spawned: ${formatCost(args.spawnedCostUsd)}`);\n\tif (costs.length > 0) {\n\t\tparts.push(`Costs: ${costs.join(\", \")}`);\n\t}\n\n\tif (args.activeGoal) {\n\t\tconst goal = args.activeGoal;\n\t\tconst requirements = goal.openRequirements !== undefined ? `, open reqs: ${goal.openRequirements}` : \"\";\n\t\tconst stalls = goal.stallTurns !== undefined ? `, stalls: ${goal.stallTurns}` : \"\";\n\t\tparts.push(\n\t\t\t`Goal [${redactAndTruncateString(goal.goalId)}]: ${redactAndTruncateString(goal.status)}${requirements}${stalls}`,\n\t\t);\n\t}\n\n\tif (args.activeLaneCount !== undefined) {\n\t\tparts.push(`Lanes: ${args.activeLaneCount}`);\n\t}\n\n\tif (parts.length === 0) return \"Autonomy: idle\";\n\treturn parts.join(\" | \");\n}\n\nfunction formatDiagnosticSection(name: string, entries?: readonly DiagnosticEntry[]): string | undefined {\n\tif (!entries || entries.length === 0) return undefined;\n\n\tconst lines: string[] = [`--- ${name} ---`];\n\tfor (const entry of entries) {\n\t\tlet header = `- ${redactAndTruncateString(entry.title)}`;\n\t\tif (entry.reasonCode) header += ` [${redactAndTruncateString(entry.reasonCode)}]`;\n\t\tlines.push(header);\n\n\t\tif (entry.summary) {\n\t\t\tlines.push(` Summary: ${redactAndTruncateString(entry.summary)}`);\n\t\t}\n\n\t\tif (entry.metadata) {\n\t\t\tlines.push(` Metadata: ${JSON.stringify(sanitizeMetadata(entry.metadata))}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function formatAutonomyDiagnostics(args: AutonomyDiagnosticSnapshot): string {\n\tconst sections = [\n\t\tformatDiagnosticSection(\"Routes\", args.routes),\n\t\tformatDiagnosticSection(\"Gates\", args.gates),\n\t\tformatDiagnosticSection(\"Costs\", args.costs),\n\t\tformatDiagnosticSection(\"Research\", args.research),\n\t\tformatDiagnosticSection(\"Delegation\", args.delegation),\n\t\tformatDiagnosticSection(\"Learning\", args.learning),\n\t\tformatDiagnosticSection(\"Goals\", args.goals),\n\t].filter((section): section is string => Boolean(section));\n\n\tif (sections.length === 0) return \"No diagnostics available.\";\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
@@ -55,6 +55,10 @@ export interface CurationTelemetrySnapshot {
55
55
  jobsRun: number;
56
56
  parseFailures: number;
57
57
  droppedJobs: number;
58
+ /** Times a computed digest was actually RENDERED into a GC stub on a real turn — the
59
+ * pays-for-itself proxy: every serve is packed content the frontier model got a semantic
60
+ * handle on without re-running tools. */
61
+ digestsServed: number;
58
62
  /** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */
59
63
  localChars: number;
60
64
  queued: number;
@@ -82,9 +86,12 @@ export declare class BrainCurator {
82
86
  private _parseFailures;
83
87
  private _droppedJobs;
84
88
  private _localChars;
89
+ private _digestsServed;
85
90
  private _draining;
86
91
  enqueue(job: CurationJob): void;
87
92
  getDigest(key: string): string | undefined;
93
+ /** Callers report when a digest was rendered into a real (sent) prompt stub. */
94
+ noteDigestServed(): void;
88
95
  getRelevance(key: string): {
89
96
  relevant: boolean;
90
97
  confidence: number;
@@ -1 +1 @@
1
- {"version":3,"file":"brain-curator.d.ts","sourceRoot":"","sources":["../../../src/core/context/brain-curator.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\n\n/**\n * Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):\n * a SIDECAR curator that consumes reports the context pipeline already produces and feeds back\n * small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte\n * identically when a result is absent (missing digest -> today's stub; missing relevance ->\n * today's enforcement decision). The curator itself is provider-free — the completion executor is\n * injected per drain, so it works against any registered local model and faux providers in tests.\n *\n * Memory bounds are explicit: the queue and result map are both capped, and drops are counted in\n * telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's\n * content hash, relevance by the audit item id), so re-enqueueing the same work is free.\n */\n\nexport const CURATION_DIGEST_SYSTEM_PROMPT = [\n\t\"You digest tool-output chunks for a coding agent's context curator. You never solve the task.\",\n\t\"Given a chunk, respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<one or two sentences, max 200 characters, keeping exact identifiers>\"}',\n\t\"Keep exact file paths, symbol names, error codes, and version strings verbatim.\",\n].join(\"\\n\");\n\nexport const CURATION_RELEVANCE_SYSTEM_PROMPT = [\n\t\"You judge whether a stale tool output is still relevant to the user's current goal.\",\n\t\"You never solve the task. Respond with STRICT JSON only - no prose:\",\n\t'{\"relevant\":true|false,\"confidence\":<0..1>}',\n\t\"relevant=false means the chunk is about something the current goal no longer needs.\",\n\t\"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.\",\n].join(\"\\n\");\n\nexport const CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT = [\n\t\"You pre-digest a chunk of an agent conversation for compaction. You never continue the conversation.\",\n\t\"Extract ONLY durable facts: decisions made, file paths and symbols touched, errors and their causes,\",\n\t\"user requirements, and outcomes. Respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<bullet-style summary, max 700 characters, exact identifiers verbatim>\"}',\n].join(\"\\n\");\n\nexport function parseCompactionChunkDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim();\n\tif (trimmed.length === 0 || trimmed.length > 800) return undefined;\n\treturn trimmed;\n}\n\nexport interface PreDigestResult {\n\ttext: string;\n\ttotalChunks: number;\n\tdigested: number;\n\tfailed: number;\n}\n\nconst PRE_DIGEST_CHUNK_CHARS = 24_000;\nconst PRE_DIGEST_KEEP_RECENT_CHARS = 16_000;\nconst PRE_DIGEST_CHUNK_WALL_CLOCK_MS = 25_000;\n\n/**\n * Compaction pre-digest (design surface 3): shrink the conversation text sent to the frontier\n * summarizer by digesting OLD chunks locally, keeping the recent tail verbatim. Chunk digestion\n * is mechanical extraction — the frontier model still writes the summary. Partial assist, never\n * partial loss: any chunk whose digest fails (parse/timeout) passes through verbatim.\n */\nexport async function preDigestConversationText(args: {\n\ttext: string;\n\tcomplete: CurationComplete;\n\tsignal?: AbortSignal;\n\tchunkChars?: number;\n\tkeepRecentChars?: number;\n}): Promise<PreDigestResult> {\n\tconst chunkChars = args.chunkChars ?? PRE_DIGEST_CHUNK_CHARS;\n\tconst keepRecentChars = args.keepRecentChars ?? PRE_DIGEST_KEEP_RECENT_CHARS;\n\tif (args.text.length <= chunkChars + keepRecentChars) {\n\t\treturn { text: args.text, totalChunks: 0, digested: 0, failed: 0 };\n\t}\n\tconst cut = args.text.length - keepRecentChars;\n\tconst prefix = args.text.slice(0, cut);\n\tconst tail = args.text.slice(cut);\n\tconst chunks: string[] = [];\n\tfor (let offset = 0; offset < prefix.length; offset += chunkChars) {\n\t\tchunks.push(prefix.slice(offset, offset + chunkChars));\n\t}\n\tlet digested = 0;\n\tlet failed = 0;\n\tconst parts: string[] = [];\n\tfor (const [index, chunk] of chunks.entries()) {\n\t\tif (args.signal?.aborted) {\n\t\t\tparts.push(chunk);\n\t\t\tfailed++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst bounded = await runBoundedCompletion({\n\t\t\tmaxWallClockMs: PRE_DIGEST_CHUNK_WALL_CLOCK_MS,\n\t\t\tsignal: args.signal,\n\t\t\texecute: (signal) =>\n\t\t\t\targs.complete({ systemPrompt: CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT, userPrompt: chunk, signal }),\n\t\t});\n\t\tconst digest =\n\t\t\tbounded.completion && !bounded.failure ? parseCompactionChunkDigest(bounded.completion.text) : undefined;\n\t\tif (digest !== undefined) {\n\t\t\tdigested++;\n\t\t\tparts.push(`[locally pre-digested chunk ${index + 1}/${chunks.length} (${chunk.length} chars):]\\n${digest}`);\n\t\t} else {\n\t\t\tfailed++;\n\t\t\tparts.push(chunk);\n\t\t}\n\t}\n\treturn { text: `${parts.join(\"\\n\\n\")}${tail}`, totalChunks: chunks.length, digested, failed };\n}\n\nexport interface CurationJob {\n\tkind: \"stub_digest\" | \"relevance\";\n\t/** Idempotency key: digest jobs use the GC record's content hash, relevance jobs the item id. */\n\tkey: string;\n\t/** Bounded chunk the local model must actually be able to process (sliced on enqueue). */\n\tcontent: string;\n\t/** Relevance jobs only: the goal/intent line the chunk is judged against. */\n\tgoal?: string;\n}\n\nexport interface CurationResult {\n\tkey: string;\n\tkind: CurationJob[\"kind\"];\n\tok: boolean;\n\tdigest?: string;\n\trelevant?: boolean;\n\tconfidence?: number;\n\tms: number;\n}\n\nexport interface CurationTelemetrySnapshot {\n\tjobsRun: number;\n\tparseFailures: number;\n\tdroppedJobs: number;\n\t/** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */\n\tlocalChars: number;\n\tqueued: number;\n\tresultsHeld: number;\n}\n\nexport type CurationComplete = (input: {\n\tsystemPrompt: string;\n\tuserPrompt: string;\n\tsignal?: AbortSignal;\n}) => Promise<{ text: string; costUsd: number; stopReason: string }>;\n\nconst MAX_QUEUE = 32;\nconst MAX_RESULTS = 200;\nconst MAX_JOB_CONTENT_CHARS = 8_000;\nconst DIGEST_MAX_WALL_CLOCK_MS = 20_000;\nconst RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;\nexport const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;\n\nfunction extractJsonObject(text: string): unknown | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(candidate);\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed;\n\t\t} catch {\n\t\t\t// try next candidate\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function parseCurationDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim().replace(/\\s+/g, \" \");\n\tif (trimmed.length === 0 || trimmed.length > 240) return undefined;\n\treturn trimmed;\n}\n\nexport function parseCurationRelevance(text: string): { relevant: boolean; confidence: number } | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst record = parsed as { relevant?: unknown; confidence?: unknown };\n\tif (typeof record.relevant !== \"boolean\") return undefined;\n\tconst confidence =\n\t\ttypeof record.confidence === \"number\" && Number.isFinite(record.confidence)\n\t\t\t? Math.max(0, Math.min(1, record.confidence))\n\t\t\t: 0;\n\treturn { relevant: record.relevant, confidence };\n}\n\nexport class BrainCurator {\n\tprivate readonly _queue = new Map<string, CurationJob>();\n\tprivate readonly _results = new Map<string, CurationResult>();\n\tprivate _jobsRun = 0;\n\tprivate _parseFailures = 0;\n\tprivate _droppedJobs = 0;\n\tprivate _localChars = 0;\n\tprivate _draining = false;\n\n\tenqueue(job: CurationJob): void {\n\t\tif (this._results.has(job.key) || this._queue.has(job.key)) return;\n\t\tif (this._queue.size >= MAX_QUEUE) {\n\t\t\t// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.\n\t\t\tconst oldest = this._queue.keys().next().value;\n\t\t\tif (oldest !== undefined) this._queue.delete(oldest);\n\t\t\tthis._droppedJobs++;\n\t\t}\n\t\tthis._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });\n\t}\n\n\tgetDigest(key: string): string | undefined {\n\t\tconst result = this._results.get(key);\n\t\treturn result?.ok && result.kind === \"stub_digest\" ? result.digest : undefined;\n\t}\n\n\tgetRelevance(key: string): { relevant: boolean; confidence: number } | undefined {\n\t\tconst result = this._results.get(key);\n\t\tif (!result?.ok || result.kind !== \"relevance\" || result.relevant === undefined) return undefined;\n\t\treturn { relevant: result.relevant, confidence: result.confidence ?? 0 };\n\t}\n\n\thasWork(): boolean {\n\t\treturn this._queue.size > 0;\n\t}\n\n\tget isDraining(): boolean {\n\t\treturn this._draining;\n\t}\n\n\ttelemetry(): CurationTelemetrySnapshot {\n\t\treturn {\n\t\t\tjobsRun: this._jobsRun,\n\t\t\tparseFailures: this._parseFailures,\n\t\t\tdroppedJobs: this._droppedJobs,\n\t\t\tlocalChars: this._localChars,\n\t\t\tqueued: this._queue.size,\n\t\t\tresultsHeld: this._results.size,\n\t\t};\n\t}\n\n\t/**\n\t * Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:\n\t * a concurrent drain call returns [] immediately rather than double-running jobs. Every call\n\t * is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is\n\t * not retried forever) and counted in telemetry.\n\t */\n\tasync drain(args: {\n\t\tcomplete: CurationComplete;\n\t\tmaxJobs: number;\n\t\tsignal?: AbortSignal;\n\t\tnow?: () => number;\n\t}): Promise<CurationResult[]> {\n\t\tif (this._draining) return [];\n\t\tthis._draining = true;\n\t\tconst now = args.now ?? Date.now;\n\t\tconst completed: CurationResult[] = [];\n\t\ttry {\n\t\t\tconst jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (args.signal?.aborted) break;\n\t\t\t\tthis._queue.delete(job.key);\n\t\t\t\tconst started = now();\n\t\t\t\tconst bounded = await runBoundedCompletion({\n\t\t\t\t\tmaxWallClockMs: job.kind === \"stub_digest\" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,\n\t\t\t\t\tsignal: args.signal,\n\t\t\t\t\texecute: (signal) =>\n\t\t\t\t\t\targs.complete({\n\t\t\t\t\t\t\tsystemPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,\n\t\t\t\t\t\t\tuserPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\"\n\t\t\t\t\t\t\t\t\t? job.content\n\t\t\t\t\t\t\t\t\t: `Current goal: ${job.goal ?? \"(unknown)\"}\\n\\nStale chunk:\\n${job.content}`,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tconst ms = now() - started;\n\t\t\t\tthis._jobsRun++;\n\t\t\t\tthis._localChars += job.content.length;\n\t\t\t\tlet result: CurationResult = { key: job.key, kind: job.kind, ok: false, ms };\n\t\t\t\tif (bounded.completion && !bounded.failure) {\n\t\t\t\t\tif (job.kind === \"stub_digest\") {\n\t\t\t\t\t\tconst digest = parseCurationDigest(bounded.completion.text);\n\t\t\t\t\t\tresult = digest !== undefined ? { ...result, ok: true, digest } : result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst relevance = parseCurationRelevance(bounded.completion.text);\n\t\t\t\t\t\tresult =\n\t\t\t\t\t\t\trelevance !== undefined\n\t\t\t\t\t\t\t\t? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }\n\t\t\t\t\t\t\t\t: result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!result.ok) this._parseFailures++;\n\t\t\t\tthis._storeResult(result);\n\t\t\t\tcompleted.push(result);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._draining = false;\n\t\t}\n\t\treturn completed;\n\t}\n\n\tprivate _storeResult(result: CurationResult): void {\n\t\tif (this._results.size >= MAX_RESULTS) {\n\t\t\tconst oldest = this._results.keys().next().value;\n\t\t\tif (oldest !== undefined) this._results.delete(oldest);\n\t\t}\n\t\tthis._results.set(result.key, result);\n\t}\n}\n"]}
1
+ {"version":3,"file":"brain-curator.d.ts","sourceRoot":"","sources":["../../../src/core/context/brain-curator.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\n\n/**\n * Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):\n * a SIDECAR curator that consumes reports the context pipeline already produces and feeds back\n * small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte\n * identically when a result is absent (missing digest -> today's stub; missing relevance ->\n * today's enforcement decision). The curator itself is provider-free — the completion executor is\n * injected per drain, so it works against any registered local model and faux providers in tests.\n *\n * Memory bounds are explicit: the queue and result map are both capped, and drops are counted in\n * telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's\n * content hash, relevance by the audit item id), so re-enqueueing the same work is free.\n */\n\nexport const CURATION_DIGEST_SYSTEM_PROMPT = [\n\t\"You digest tool-output chunks for a coding agent's context curator. You never solve the task.\",\n\t\"Given a chunk, respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<one or two sentences, max 200 characters, keeping exact identifiers>\"}',\n\t\"Keep exact file paths, symbol names, error codes, and version strings verbatim.\",\n].join(\"\\n\");\n\nexport const CURATION_RELEVANCE_SYSTEM_PROMPT = [\n\t\"You judge whether a stale tool output is still relevant to the user's current goal.\",\n\t\"You never solve the task. Respond with STRICT JSON only - no prose:\",\n\t'{\"relevant\":true|false,\"confidence\":<0..1>}',\n\t\"relevant=false means the chunk is about something the current goal no longer needs.\",\n\t\"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.\",\n].join(\"\\n\");\n\nexport const CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT = [\n\t\"You pre-digest a chunk of an agent conversation for compaction. You never continue the conversation.\",\n\t\"Extract ONLY durable facts: decisions made, file paths and symbols touched, errors and their causes,\",\n\t\"user requirements, and outcomes. Respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<bullet-style summary, max 700 characters, exact identifiers verbatim>\"}',\n].join(\"\\n\");\n\nexport function parseCompactionChunkDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim();\n\tif (trimmed.length === 0 || trimmed.length > 800) return undefined;\n\treturn trimmed;\n}\n\nexport interface PreDigestResult {\n\ttext: string;\n\ttotalChunks: number;\n\tdigested: number;\n\tfailed: number;\n}\n\nconst PRE_DIGEST_CHUNK_CHARS = 24_000;\nconst PRE_DIGEST_KEEP_RECENT_CHARS = 16_000;\nconst PRE_DIGEST_CHUNK_WALL_CLOCK_MS = 25_000;\n\n/**\n * Compaction pre-digest (design surface 3): shrink the conversation text sent to the frontier\n * summarizer by digesting OLD chunks locally, keeping the recent tail verbatim. Chunk digestion\n * is mechanical extraction — the frontier model still writes the summary. Partial assist, never\n * partial loss: any chunk whose digest fails (parse/timeout) passes through verbatim.\n */\nexport async function preDigestConversationText(args: {\n\ttext: string;\n\tcomplete: CurationComplete;\n\tsignal?: AbortSignal;\n\tchunkChars?: number;\n\tkeepRecentChars?: number;\n}): Promise<PreDigestResult> {\n\tconst chunkChars = args.chunkChars ?? PRE_DIGEST_CHUNK_CHARS;\n\tconst keepRecentChars = args.keepRecentChars ?? PRE_DIGEST_KEEP_RECENT_CHARS;\n\tif (args.text.length <= chunkChars + keepRecentChars) {\n\t\treturn { text: args.text, totalChunks: 0, digested: 0, failed: 0 };\n\t}\n\tconst cut = args.text.length - keepRecentChars;\n\tconst prefix = args.text.slice(0, cut);\n\tconst tail = args.text.slice(cut);\n\tconst chunks: string[] = [];\n\tfor (let offset = 0; offset < prefix.length; offset += chunkChars) {\n\t\tchunks.push(prefix.slice(offset, offset + chunkChars));\n\t}\n\tlet digested = 0;\n\tlet failed = 0;\n\tconst parts: string[] = [];\n\tfor (const [index, chunk] of chunks.entries()) {\n\t\tif (args.signal?.aborted) {\n\t\t\tparts.push(chunk);\n\t\t\tfailed++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst bounded = await runBoundedCompletion({\n\t\t\tmaxWallClockMs: PRE_DIGEST_CHUNK_WALL_CLOCK_MS,\n\t\t\tsignal: args.signal,\n\t\t\texecute: (signal) =>\n\t\t\t\targs.complete({ systemPrompt: CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT, userPrompt: chunk, signal }),\n\t\t});\n\t\tconst digest =\n\t\t\tbounded.completion && !bounded.failure ? parseCompactionChunkDigest(bounded.completion.text) : undefined;\n\t\tif (digest !== undefined) {\n\t\t\tdigested++;\n\t\t\tparts.push(`[locally pre-digested chunk ${index + 1}/${chunks.length} (${chunk.length} chars):]\\n${digest}`);\n\t\t} else {\n\t\t\tfailed++;\n\t\t\tparts.push(chunk);\n\t\t}\n\t}\n\treturn { text: `${parts.join(\"\\n\\n\")}${tail}`, totalChunks: chunks.length, digested, failed };\n}\n\nexport interface CurationJob {\n\tkind: \"stub_digest\" | \"relevance\";\n\t/** Idempotency key: digest jobs use the GC record's content hash, relevance jobs the item id. */\n\tkey: string;\n\t/** Bounded chunk the local model must actually be able to process (sliced on enqueue). */\n\tcontent: string;\n\t/** Relevance jobs only: the goal/intent line the chunk is judged against. */\n\tgoal?: string;\n}\n\nexport interface CurationResult {\n\tkey: string;\n\tkind: CurationJob[\"kind\"];\n\tok: boolean;\n\tdigest?: string;\n\trelevant?: boolean;\n\tconfidence?: number;\n\tms: number;\n}\n\nexport interface CurationTelemetrySnapshot {\n\tjobsRun: number;\n\tparseFailures: number;\n\tdroppedJobs: number;\n\t/** Times a computed digest was actually RENDERED into a GC stub on a real turn — the\n\t * pays-for-itself proxy: every serve is packed content the frontier model got a semantic\n\t * handle on without re-running tools. */\n\tdigestsServed: number;\n\t/** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */\n\tlocalChars: number;\n\tqueued: number;\n\tresultsHeld: number;\n}\n\nexport type CurationComplete = (input: {\n\tsystemPrompt: string;\n\tuserPrompt: string;\n\tsignal?: AbortSignal;\n}) => Promise<{ text: string; costUsd: number; stopReason: string }>;\n\nconst MAX_QUEUE = 32;\nconst MAX_RESULTS = 200;\nconst MAX_JOB_CONTENT_CHARS = 8_000;\nconst DIGEST_MAX_WALL_CLOCK_MS = 20_000;\nconst RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;\nexport const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;\n\nfunction extractJsonObject(text: string): unknown | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(candidate);\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed;\n\t\t} catch {\n\t\t\t// try next candidate\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function parseCurationDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim().replace(/\\s+/g, \" \");\n\tif (trimmed.length === 0 || trimmed.length > 240) return undefined;\n\treturn trimmed;\n}\n\nexport function parseCurationRelevance(text: string): { relevant: boolean; confidence: number } | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst record = parsed as { relevant?: unknown; confidence?: unknown };\n\tif (typeof record.relevant !== \"boolean\") return undefined;\n\tconst confidence =\n\t\ttypeof record.confidence === \"number\" && Number.isFinite(record.confidence)\n\t\t\t? Math.max(0, Math.min(1, record.confidence))\n\t\t\t: 0;\n\treturn { relevant: record.relevant, confidence };\n}\n\nexport class BrainCurator {\n\tprivate readonly _queue = new Map<string, CurationJob>();\n\tprivate readonly _results = new Map<string, CurationResult>();\n\tprivate _jobsRun = 0;\n\tprivate _parseFailures = 0;\n\tprivate _droppedJobs = 0;\n\tprivate _localChars = 0;\n\tprivate _digestsServed = 0;\n\tprivate _draining = false;\n\n\tenqueue(job: CurationJob): void {\n\t\tif (this._results.has(job.key) || this._queue.has(job.key)) return;\n\t\tif (this._queue.size >= MAX_QUEUE) {\n\t\t\t// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.\n\t\t\tconst oldest = this._queue.keys().next().value;\n\t\t\tif (oldest !== undefined) this._queue.delete(oldest);\n\t\t\tthis._droppedJobs++;\n\t\t}\n\t\tthis._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });\n\t}\n\n\tgetDigest(key: string): string | undefined {\n\t\tconst result = this._results.get(key);\n\t\treturn result?.ok && result.kind === \"stub_digest\" ? result.digest : undefined;\n\t}\n\n\t/** Callers report when a digest was rendered into a real (sent) prompt stub. */\n\tnoteDigestServed(): void {\n\t\tthis._digestsServed++;\n\t}\n\n\tgetRelevance(key: string): { relevant: boolean; confidence: number } | undefined {\n\t\tconst result = this._results.get(key);\n\t\tif (!result?.ok || result.kind !== \"relevance\" || result.relevant === undefined) return undefined;\n\t\treturn { relevant: result.relevant, confidence: result.confidence ?? 0 };\n\t}\n\n\thasWork(): boolean {\n\t\treturn this._queue.size > 0;\n\t}\n\n\tget isDraining(): boolean {\n\t\treturn this._draining;\n\t}\n\n\ttelemetry(): CurationTelemetrySnapshot {\n\t\treturn {\n\t\t\tjobsRun: this._jobsRun,\n\t\t\tparseFailures: this._parseFailures,\n\t\t\tdroppedJobs: this._droppedJobs,\n\t\t\tdigestsServed: this._digestsServed,\n\t\t\tlocalChars: this._localChars,\n\t\t\tqueued: this._queue.size,\n\t\t\tresultsHeld: this._results.size,\n\t\t};\n\t}\n\n\t/**\n\t * Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:\n\t * a concurrent drain call returns [] immediately rather than double-running jobs. Every call\n\t * is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is\n\t * not retried forever) and counted in telemetry.\n\t */\n\tasync drain(args: {\n\t\tcomplete: CurationComplete;\n\t\tmaxJobs: number;\n\t\tsignal?: AbortSignal;\n\t\tnow?: () => number;\n\t}): Promise<CurationResult[]> {\n\t\tif (this._draining) return [];\n\t\tthis._draining = true;\n\t\tconst now = args.now ?? Date.now;\n\t\tconst completed: CurationResult[] = [];\n\t\ttry {\n\t\t\tconst jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (args.signal?.aborted) break;\n\t\t\t\tthis._queue.delete(job.key);\n\t\t\t\tconst started = now();\n\t\t\t\tconst bounded = await runBoundedCompletion({\n\t\t\t\t\tmaxWallClockMs: job.kind === \"stub_digest\" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,\n\t\t\t\t\tsignal: args.signal,\n\t\t\t\t\texecute: (signal) =>\n\t\t\t\t\t\targs.complete({\n\t\t\t\t\t\t\tsystemPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,\n\t\t\t\t\t\t\tuserPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\"\n\t\t\t\t\t\t\t\t\t? job.content\n\t\t\t\t\t\t\t\t\t: `Current goal: ${job.goal ?? \"(unknown)\"}\\n\\nStale chunk:\\n${job.content}`,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tconst ms = now() - started;\n\t\t\t\tthis._jobsRun++;\n\t\t\t\tthis._localChars += job.content.length;\n\t\t\t\tlet result: CurationResult = { key: job.key, kind: job.kind, ok: false, ms };\n\t\t\t\tif (bounded.completion && !bounded.failure) {\n\t\t\t\t\tif (job.kind === \"stub_digest\") {\n\t\t\t\t\t\tconst digest = parseCurationDigest(bounded.completion.text);\n\t\t\t\t\t\tresult = digest !== undefined ? { ...result, ok: true, digest } : result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst relevance = parseCurationRelevance(bounded.completion.text);\n\t\t\t\t\t\tresult =\n\t\t\t\t\t\t\trelevance !== undefined\n\t\t\t\t\t\t\t\t? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }\n\t\t\t\t\t\t\t\t: result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!result.ok) this._parseFailures++;\n\t\t\t\tthis._storeResult(result);\n\t\t\t\tcompleted.push(result);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._draining = false;\n\t\t}\n\t\treturn completed;\n\t}\n\n\tprivate _storeResult(result: CurationResult): void {\n\t\tif (this._results.size >= MAX_RESULTS) {\n\t\t\tconst oldest = this._results.keys().next().value;\n\t\t\tif (oldest !== undefined) this._results.delete(oldest);\n\t\t}\n\t\tthis._results.set(result.key, result);\n\t}\n}\n"]}
@@ -149,6 +149,7 @@ export class BrainCurator {
149
149
  _parseFailures = 0;
150
150
  _droppedJobs = 0;
151
151
  _localChars = 0;
152
+ _digestsServed = 0;
152
153
  _draining = false;
153
154
  enqueue(job) {
154
155
  if (this._results.has(job.key) || this._queue.has(job.key))
@@ -166,6 +167,10 @@ export class BrainCurator {
166
167
  const result = this._results.get(key);
167
168
  return result?.ok && result.kind === "stub_digest" ? result.digest : undefined;
168
169
  }
170
+ /** Callers report when a digest was rendered into a real (sent) prompt stub. */
171
+ noteDigestServed() {
172
+ this._digestsServed++;
173
+ }
169
174
  getRelevance(key) {
170
175
  const result = this._results.get(key);
171
176
  if (!result?.ok || result.kind !== "relevance" || result.relevant === undefined)
@@ -183,6 +188,7 @@ export class BrainCurator {
183
188
  jobsRun: this._jobsRun,
184
189
  parseFailures: this._parseFailures,
185
190
  droppedJobs: this._droppedJobs,
191
+ digestsServed: this._digestsServed,
186
192
  localChars: this._localChars,
187
193
  queued: this._queue.size,
188
194
  resultsHeld: this._results.size,
@@ -1 +1 @@
1
- {"version":3,"file":"brain-curator.js","sourceRoot":"","sources":["../../../src/core/context/brain-curator.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\n\n/**\n * Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):\n * a SIDECAR curator that consumes reports the context pipeline already produces and feeds back\n * small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte\n * identically when a result is absent (missing digest -> today's stub; missing relevance ->\n * today's enforcement decision). The curator itself is provider-free — the completion executor is\n * injected per drain, so it works against any registered local model and faux providers in tests.\n *\n * Memory bounds are explicit: the queue and result map are both capped, and drops are counted in\n * telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's\n * content hash, relevance by the audit item id), so re-enqueueing the same work is free.\n */\n\nexport const CURATION_DIGEST_SYSTEM_PROMPT = [\n\t\"You digest tool-output chunks for a coding agent's context curator. You never solve the task.\",\n\t\"Given a chunk, respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<one or two sentences, max 200 characters, keeping exact identifiers>\"}',\n\t\"Keep exact file paths, symbol names, error codes, and version strings verbatim.\",\n].join(\"\\n\");\n\nexport const CURATION_RELEVANCE_SYSTEM_PROMPT = [\n\t\"You judge whether a stale tool output is still relevant to the user's current goal.\",\n\t\"You never solve the task. Respond with STRICT JSON only - no prose:\",\n\t'{\"relevant\":true|false,\"confidence\":<0..1>}',\n\t\"relevant=false means the chunk is about something the current goal no longer needs.\",\n\t\"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.\",\n].join(\"\\n\");\n\nexport const CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT = [\n\t\"You pre-digest a chunk of an agent conversation for compaction. You never continue the conversation.\",\n\t\"Extract ONLY durable facts: decisions made, file paths and symbols touched, errors and their causes,\",\n\t\"user requirements, and outcomes. Respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<bullet-style summary, max 700 characters, exact identifiers verbatim>\"}',\n].join(\"\\n\");\n\nexport function parseCompactionChunkDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim();\n\tif (trimmed.length === 0 || trimmed.length > 800) return undefined;\n\treturn trimmed;\n}\n\nexport interface PreDigestResult {\n\ttext: string;\n\ttotalChunks: number;\n\tdigested: number;\n\tfailed: number;\n}\n\nconst PRE_DIGEST_CHUNK_CHARS = 24_000;\nconst PRE_DIGEST_KEEP_RECENT_CHARS = 16_000;\nconst PRE_DIGEST_CHUNK_WALL_CLOCK_MS = 25_000;\n\n/**\n * Compaction pre-digest (design surface 3): shrink the conversation text sent to the frontier\n * summarizer by digesting OLD chunks locally, keeping the recent tail verbatim. Chunk digestion\n * is mechanical extraction — the frontier model still writes the summary. Partial assist, never\n * partial loss: any chunk whose digest fails (parse/timeout) passes through verbatim.\n */\nexport async function preDigestConversationText(args: {\n\ttext: string;\n\tcomplete: CurationComplete;\n\tsignal?: AbortSignal;\n\tchunkChars?: number;\n\tkeepRecentChars?: number;\n}): Promise<PreDigestResult> {\n\tconst chunkChars = args.chunkChars ?? PRE_DIGEST_CHUNK_CHARS;\n\tconst keepRecentChars = args.keepRecentChars ?? PRE_DIGEST_KEEP_RECENT_CHARS;\n\tif (args.text.length <= chunkChars + keepRecentChars) {\n\t\treturn { text: args.text, totalChunks: 0, digested: 0, failed: 0 };\n\t}\n\tconst cut = args.text.length - keepRecentChars;\n\tconst prefix = args.text.slice(0, cut);\n\tconst tail = args.text.slice(cut);\n\tconst chunks: string[] = [];\n\tfor (let offset = 0; offset < prefix.length; offset += chunkChars) {\n\t\tchunks.push(prefix.slice(offset, offset + chunkChars));\n\t}\n\tlet digested = 0;\n\tlet failed = 0;\n\tconst parts: string[] = [];\n\tfor (const [index, chunk] of chunks.entries()) {\n\t\tif (args.signal?.aborted) {\n\t\t\tparts.push(chunk);\n\t\t\tfailed++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst bounded = await runBoundedCompletion({\n\t\t\tmaxWallClockMs: PRE_DIGEST_CHUNK_WALL_CLOCK_MS,\n\t\t\tsignal: args.signal,\n\t\t\texecute: (signal) =>\n\t\t\t\targs.complete({ systemPrompt: CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT, userPrompt: chunk, signal }),\n\t\t});\n\t\tconst digest =\n\t\t\tbounded.completion && !bounded.failure ? parseCompactionChunkDigest(bounded.completion.text) : undefined;\n\t\tif (digest !== undefined) {\n\t\t\tdigested++;\n\t\t\tparts.push(`[locally pre-digested chunk ${index + 1}/${chunks.length} (${chunk.length} chars):]\\n${digest}`);\n\t\t} else {\n\t\t\tfailed++;\n\t\t\tparts.push(chunk);\n\t\t}\n\t}\n\treturn { text: `${parts.join(\"\\n\\n\")}${tail}`, totalChunks: chunks.length, digested, failed };\n}\n\nexport interface CurationJob {\n\tkind: \"stub_digest\" | \"relevance\";\n\t/** Idempotency key: digest jobs use the GC record's content hash, relevance jobs the item id. */\n\tkey: string;\n\t/** Bounded chunk the local model must actually be able to process (sliced on enqueue). */\n\tcontent: string;\n\t/** Relevance jobs only: the goal/intent line the chunk is judged against. */\n\tgoal?: string;\n}\n\nexport interface CurationResult {\n\tkey: string;\n\tkind: CurationJob[\"kind\"];\n\tok: boolean;\n\tdigest?: string;\n\trelevant?: boolean;\n\tconfidence?: number;\n\tms: number;\n}\n\nexport interface CurationTelemetrySnapshot {\n\tjobsRun: number;\n\tparseFailures: number;\n\tdroppedJobs: number;\n\t/** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */\n\tlocalChars: number;\n\tqueued: number;\n\tresultsHeld: number;\n}\n\nexport type CurationComplete = (input: {\n\tsystemPrompt: string;\n\tuserPrompt: string;\n\tsignal?: AbortSignal;\n}) => Promise<{ text: string; costUsd: number; stopReason: string }>;\n\nconst MAX_QUEUE = 32;\nconst MAX_RESULTS = 200;\nconst MAX_JOB_CONTENT_CHARS = 8_000;\nconst DIGEST_MAX_WALL_CLOCK_MS = 20_000;\nconst RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;\nexport const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;\n\nfunction extractJsonObject(text: string): unknown | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(candidate);\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed;\n\t\t} catch {\n\t\t\t// try next candidate\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function parseCurationDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim().replace(/\\s+/g, \" \");\n\tif (trimmed.length === 0 || trimmed.length > 240) return undefined;\n\treturn trimmed;\n}\n\nexport function parseCurationRelevance(text: string): { relevant: boolean; confidence: number } | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst record = parsed as { relevant?: unknown; confidence?: unknown };\n\tif (typeof record.relevant !== \"boolean\") return undefined;\n\tconst confidence =\n\t\ttypeof record.confidence === \"number\" && Number.isFinite(record.confidence)\n\t\t\t? Math.max(0, Math.min(1, record.confidence))\n\t\t\t: 0;\n\treturn { relevant: record.relevant, confidence };\n}\n\nexport class BrainCurator {\n\tprivate readonly _queue = new Map<string, CurationJob>();\n\tprivate readonly _results = new Map<string, CurationResult>();\n\tprivate _jobsRun = 0;\n\tprivate _parseFailures = 0;\n\tprivate _droppedJobs = 0;\n\tprivate _localChars = 0;\n\tprivate _draining = false;\n\n\tenqueue(job: CurationJob): void {\n\t\tif (this._results.has(job.key) || this._queue.has(job.key)) return;\n\t\tif (this._queue.size >= MAX_QUEUE) {\n\t\t\t// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.\n\t\t\tconst oldest = this._queue.keys().next().value;\n\t\t\tif (oldest !== undefined) this._queue.delete(oldest);\n\t\t\tthis._droppedJobs++;\n\t\t}\n\t\tthis._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });\n\t}\n\n\tgetDigest(key: string): string | undefined {\n\t\tconst result = this._results.get(key);\n\t\treturn result?.ok && result.kind === \"stub_digest\" ? result.digest : undefined;\n\t}\n\n\tgetRelevance(key: string): { relevant: boolean; confidence: number } | undefined {\n\t\tconst result = this._results.get(key);\n\t\tif (!result?.ok || result.kind !== \"relevance\" || result.relevant === undefined) return undefined;\n\t\treturn { relevant: result.relevant, confidence: result.confidence ?? 0 };\n\t}\n\n\thasWork(): boolean {\n\t\treturn this._queue.size > 0;\n\t}\n\n\tget isDraining(): boolean {\n\t\treturn this._draining;\n\t}\n\n\ttelemetry(): CurationTelemetrySnapshot {\n\t\treturn {\n\t\t\tjobsRun: this._jobsRun,\n\t\t\tparseFailures: this._parseFailures,\n\t\t\tdroppedJobs: this._droppedJobs,\n\t\t\tlocalChars: this._localChars,\n\t\t\tqueued: this._queue.size,\n\t\t\tresultsHeld: this._results.size,\n\t\t};\n\t}\n\n\t/**\n\t * Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:\n\t * a concurrent drain call returns [] immediately rather than double-running jobs. Every call\n\t * is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is\n\t * not retried forever) and counted in telemetry.\n\t */\n\tasync drain(args: {\n\t\tcomplete: CurationComplete;\n\t\tmaxJobs: number;\n\t\tsignal?: AbortSignal;\n\t\tnow?: () => number;\n\t}): Promise<CurationResult[]> {\n\t\tif (this._draining) return [];\n\t\tthis._draining = true;\n\t\tconst now = args.now ?? Date.now;\n\t\tconst completed: CurationResult[] = [];\n\t\ttry {\n\t\t\tconst jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (args.signal?.aborted) break;\n\t\t\t\tthis._queue.delete(job.key);\n\t\t\t\tconst started = now();\n\t\t\t\tconst bounded = await runBoundedCompletion({\n\t\t\t\t\tmaxWallClockMs: job.kind === \"stub_digest\" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,\n\t\t\t\t\tsignal: args.signal,\n\t\t\t\t\texecute: (signal) =>\n\t\t\t\t\t\targs.complete({\n\t\t\t\t\t\t\tsystemPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,\n\t\t\t\t\t\t\tuserPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\"\n\t\t\t\t\t\t\t\t\t? job.content\n\t\t\t\t\t\t\t\t\t: `Current goal: ${job.goal ?? \"(unknown)\"}\\n\\nStale chunk:\\n${job.content}`,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tconst ms = now() - started;\n\t\t\t\tthis._jobsRun++;\n\t\t\t\tthis._localChars += job.content.length;\n\t\t\t\tlet result: CurationResult = { key: job.key, kind: job.kind, ok: false, ms };\n\t\t\t\tif (bounded.completion && !bounded.failure) {\n\t\t\t\t\tif (job.kind === \"stub_digest\") {\n\t\t\t\t\t\tconst digest = parseCurationDigest(bounded.completion.text);\n\t\t\t\t\t\tresult = digest !== undefined ? { ...result, ok: true, digest } : result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst relevance = parseCurationRelevance(bounded.completion.text);\n\t\t\t\t\t\tresult =\n\t\t\t\t\t\t\trelevance !== undefined\n\t\t\t\t\t\t\t\t? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }\n\t\t\t\t\t\t\t\t: result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!result.ok) this._parseFailures++;\n\t\t\t\tthis._storeResult(result);\n\t\t\t\tcompleted.push(result);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._draining = false;\n\t\t}\n\t\treturn completed;\n\t}\n\n\tprivate _storeResult(result: CurationResult): void {\n\t\tif (this._results.size >= MAX_RESULTS) {\n\t\t\tconst oldest = this._results.keys().next().value;\n\t\t\tif (oldest !== undefined) this._results.delete(oldest);\n\t\t}\n\t\tthis._results.set(result.key, result);\n\t}\n}\n"]}
1
+ {"version":3,"file":"brain-curator.js","sourceRoot":"","sources":["../../../src/core/context/brain-curator.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\n\n/**\n * Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):\n * a SIDECAR curator that consumes reports the context pipeline already produces and feeds back\n * small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte\n * identically when a result is absent (missing digest -> today's stub; missing relevance ->\n * today's enforcement decision). The curator itself is provider-free — the completion executor is\n * injected per drain, so it works against any registered local model and faux providers in tests.\n *\n * Memory bounds are explicit: the queue and result map are both capped, and drops are counted in\n * telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's\n * content hash, relevance by the audit item id), so re-enqueueing the same work is free.\n */\n\nexport const CURATION_DIGEST_SYSTEM_PROMPT = [\n\t\"You digest tool-output chunks for a coding agent's context curator. You never solve the task.\",\n\t\"Given a chunk, respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<one or two sentences, max 200 characters, keeping exact identifiers>\"}',\n\t\"Keep exact file paths, symbol names, error codes, and version strings verbatim.\",\n].join(\"\\n\");\n\nexport const CURATION_RELEVANCE_SYSTEM_PROMPT = [\n\t\"You judge whether a stale tool output is still relevant to the user's current goal.\",\n\t\"You never solve the task. Respond with STRICT JSON only - no prose:\",\n\t'{\"relevant\":true|false,\"confidence\":<0..1>}',\n\t\"relevant=false means the chunk is about something the current goal no longer needs.\",\n\t\"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.\",\n].join(\"\\n\");\n\nexport const CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT = [\n\t\"You pre-digest a chunk of an agent conversation for compaction. You never continue the conversation.\",\n\t\"Extract ONLY durable facts: decisions made, file paths and symbols touched, errors and their causes,\",\n\t\"user requirements, and outcomes. Respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<bullet-style summary, max 700 characters, exact identifiers verbatim>\"}',\n].join(\"\\n\");\n\nexport function parseCompactionChunkDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim();\n\tif (trimmed.length === 0 || trimmed.length > 800) return undefined;\n\treturn trimmed;\n}\n\nexport interface PreDigestResult {\n\ttext: string;\n\ttotalChunks: number;\n\tdigested: number;\n\tfailed: number;\n}\n\nconst PRE_DIGEST_CHUNK_CHARS = 24_000;\nconst PRE_DIGEST_KEEP_RECENT_CHARS = 16_000;\nconst PRE_DIGEST_CHUNK_WALL_CLOCK_MS = 25_000;\n\n/**\n * Compaction pre-digest (design surface 3): shrink the conversation text sent to the frontier\n * summarizer by digesting OLD chunks locally, keeping the recent tail verbatim. Chunk digestion\n * is mechanical extraction — the frontier model still writes the summary. Partial assist, never\n * partial loss: any chunk whose digest fails (parse/timeout) passes through verbatim.\n */\nexport async function preDigestConversationText(args: {\n\ttext: string;\n\tcomplete: CurationComplete;\n\tsignal?: AbortSignal;\n\tchunkChars?: number;\n\tkeepRecentChars?: number;\n}): Promise<PreDigestResult> {\n\tconst chunkChars = args.chunkChars ?? PRE_DIGEST_CHUNK_CHARS;\n\tconst keepRecentChars = args.keepRecentChars ?? PRE_DIGEST_KEEP_RECENT_CHARS;\n\tif (args.text.length <= chunkChars + keepRecentChars) {\n\t\treturn { text: args.text, totalChunks: 0, digested: 0, failed: 0 };\n\t}\n\tconst cut = args.text.length - keepRecentChars;\n\tconst prefix = args.text.slice(0, cut);\n\tconst tail = args.text.slice(cut);\n\tconst chunks: string[] = [];\n\tfor (let offset = 0; offset < prefix.length; offset += chunkChars) {\n\t\tchunks.push(prefix.slice(offset, offset + chunkChars));\n\t}\n\tlet digested = 0;\n\tlet failed = 0;\n\tconst parts: string[] = [];\n\tfor (const [index, chunk] of chunks.entries()) {\n\t\tif (args.signal?.aborted) {\n\t\t\tparts.push(chunk);\n\t\t\tfailed++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst bounded = await runBoundedCompletion({\n\t\t\tmaxWallClockMs: PRE_DIGEST_CHUNK_WALL_CLOCK_MS,\n\t\t\tsignal: args.signal,\n\t\t\texecute: (signal) =>\n\t\t\t\targs.complete({ systemPrompt: CURATION_COMPACTION_DIGEST_SYSTEM_PROMPT, userPrompt: chunk, signal }),\n\t\t});\n\t\tconst digest =\n\t\t\tbounded.completion && !bounded.failure ? parseCompactionChunkDigest(bounded.completion.text) : undefined;\n\t\tif (digest !== undefined) {\n\t\t\tdigested++;\n\t\t\tparts.push(`[locally pre-digested chunk ${index + 1}/${chunks.length} (${chunk.length} chars):]\\n${digest}`);\n\t\t} else {\n\t\t\tfailed++;\n\t\t\tparts.push(chunk);\n\t\t}\n\t}\n\treturn { text: `${parts.join(\"\\n\\n\")}${tail}`, totalChunks: chunks.length, digested, failed };\n}\n\nexport interface CurationJob {\n\tkind: \"stub_digest\" | \"relevance\";\n\t/** Idempotency key: digest jobs use the GC record's content hash, relevance jobs the item id. */\n\tkey: string;\n\t/** Bounded chunk the local model must actually be able to process (sliced on enqueue). */\n\tcontent: string;\n\t/** Relevance jobs only: the goal/intent line the chunk is judged against. */\n\tgoal?: string;\n}\n\nexport interface CurationResult {\n\tkey: string;\n\tkind: CurationJob[\"kind\"];\n\tok: boolean;\n\tdigest?: string;\n\trelevant?: boolean;\n\tconfidence?: number;\n\tms: number;\n}\n\nexport interface CurationTelemetrySnapshot {\n\tjobsRun: number;\n\tparseFailures: number;\n\tdroppedJobs: number;\n\t/** Times a computed digest was actually RENDERED into a GC stub on a real turn — the\n\t * pays-for-itself proxy: every serve is packed content the frontier model got a semantic\n\t * handle on without re-running tools. */\n\tdigestsServed: number;\n\t/** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */\n\tlocalChars: number;\n\tqueued: number;\n\tresultsHeld: number;\n}\n\nexport type CurationComplete = (input: {\n\tsystemPrompt: string;\n\tuserPrompt: string;\n\tsignal?: AbortSignal;\n}) => Promise<{ text: string; costUsd: number; stopReason: string }>;\n\nconst MAX_QUEUE = 32;\nconst MAX_RESULTS = 200;\nconst MAX_JOB_CONTENT_CHARS = 8_000;\nconst DIGEST_MAX_WALL_CLOCK_MS = 20_000;\nconst RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;\nexport const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;\n\nfunction extractJsonObject(text: string): unknown | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(candidate);\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed;\n\t\t} catch {\n\t\t\t// try next candidate\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function parseCurationDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim().replace(/\\s+/g, \" \");\n\tif (trimmed.length === 0 || trimmed.length > 240) return undefined;\n\treturn trimmed;\n}\n\nexport function parseCurationRelevance(text: string): { relevant: boolean; confidence: number } | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst record = parsed as { relevant?: unknown; confidence?: unknown };\n\tif (typeof record.relevant !== \"boolean\") return undefined;\n\tconst confidence =\n\t\ttypeof record.confidence === \"number\" && Number.isFinite(record.confidence)\n\t\t\t? Math.max(0, Math.min(1, record.confidence))\n\t\t\t: 0;\n\treturn { relevant: record.relevant, confidence };\n}\n\nexport class BrainCurator {\n\tprivate readonly _queue = new Map<string, CurationJob>();\n\tprivate readonly _results = new Map<string, CurationResult>();\n\tprivate _jobsRun = 0;\n\tprivate _parseFailures = 0;\n\tprivate _droppedJobs = 0;\n\tprivate _localChars = 0;\n\tprivate _digestsServed = 0;\n\tprivate _draining = false;\n\n\tenqueue(job: CurationJob): void {\n\t\tif (this._results.has(job.key) || this._queue.has(job.key)) return;\n\t\tif (this._queue.size >= MAX_QUEUE) {\n\t\t\t// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.\n\t\t\tconst oldest = this._queue.keys().next().value;\n\t\t\tif (oldest !== undefined) this._queue.delete(oldest);\n\t\t\tthis._droppedJobs++;\n\t\t}\n\t\tthis._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });\n\t}\n\n\tgetDigest(key: string): string | undefined {\n\t\tconst result = this._results.get(key);\n\t\treturn result?.ok && result.kind === \"stub_digest\" ? result.digest : undefined;\n\t}\n\n\t/** Callers report when a digest was rendered into a real (sent) prompt stub. */\n\tnoteDigestServed(): void {\n\t\tthis._digestsServed++;\n\t}\n\n\tgetRelevance(key: string): { relevant: boolean; confidence: number } | undefined {\n\t\tconst result = this._results.get(key);\n\t\tif (!result?.ok || result.kind !== \"relevance\" || result.relevant === undefined) return undefined;\n\t\treturn { relevant: result.relevant, confidence: result.confidence ?? 0 };\n\t}\n\n\thasWork(): boolean {\n\t\treturn this._queue.size > 0;\n\t}\n\n\tget isDraining(): boolean {\n\t\treturn this._draining;\n\t}\n\n\ttelemetry(): CurationTelemetrySnapshot {\n\t\treturn {\n\t\t\tjobsRun: this._jobsRun,\n\t\t\tparseFailures: this._parseFailures,\n\t\t\tdroppedJobs: this._droppedJobs,\n\t\t\tdigestsServed: this._digestsServed,\n\t\t\tlocalChars: this._localChars,\n\t\t\tqueued: this._queue.size,\n\t\t\tresultsHeld: this._results.size,\n\t\t};\n\t}\n\n\t/**\n\t * Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:\n\t * a concurrent drain call returns [] immediately rather than double-running jobs. Every call\n\t * is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is\n\t * not retried forever) and counted in telemetry.\n\t */\n\tasync drain(args: {\n\t\tcomplete: CurationComplete;\n\t\tmaxJobs: number;\n\t\tsignal?: AbortSignal;\n\t\tnow?: () => number;\n\t}): Promise<CurationResult[]> {\n\t\tif (this._draining) return [];\n\t\tthis._draining = true;\n\t\tconst now = args.now ?? Date.now;\n\t\tconst completed: CurationResult[] = [];\n\t\ttry {\n\t\t\tconst jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (args.signal?.aborted) break;\n\t\t\t\tthis._queue.delete(job.key);\n\t\t\t\tconst started = now();\n\t\t\t\tconst bounded = await runBoundedCompletion({\n\t\t\t\t\tmaxWallClockMs: job.kind === \"stub_digest\" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,\n\t\t\t\t\tsignal: args.signal,\n\t\t\t\t\texecute: (signal) =>\n\t\t\t\t\t\targs.complete({\n\t\t\t\t\t\t\tsystemPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,\n\t\t\t\t\t\t\tuserPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\"\n\t\t\t\t\t\t\t\t\t? job.content\n\t\t\t\t\t\t\t\t\t: `Current goal: ${job.goal ?? \"(unknown)\"}\\n\\nStale chunk:\\n${job.content}`,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tconst ms = now() - started;\n\t\t\t\tthis._jobsRun++;\n\t\t\t\tthis._localChars += job.content.length;\n\t\t\t\tlet result: CurationResult = { key: job.key, kind: job.kind, ok: false, ms };\n\t\t\t\tif (bounded.completion && !bounded.failure) {\n\t\t\t\t\tif (job.kind === \"stub_digest\") {\n\t\t\t\t\t\tconst digest = parseCurationDigest(bounded.completion.text);\n\t\t\t\t\t\tresult = digest !== undefined ? { ...result, ok: true, digest } : result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst relevance = parseCurationRelevance(bounded.completion.text);\n\t\t\t\t\t\tresult =\n\t\t\t\t\t\t\trelevance !== undefined\n\t\t\t\t\t\t\t\t? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }\n\t\t\t\t\t\t\t\t: result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!result.ok) this._parseFailures++;\n\t\t\t\tthis._storeResult(result);\n\t\t\t\tcompleted.push(result);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._draining = false;\n\t\t}\n\t\treturn completed;\n\t}\n\n\tprivate _storeResult(result: CurationResult): void {\n\t\tif (this._results.size >= MAX_RESULTS) {\n\t\t\tconst oldest = this._results.keys().next().value;\n\t\t\tif (oldest !== undefined) this._results.delete(oldest);\n\t\t}\n\t\tthis._results.set(result.key, result);\n\t}\n}\n"]}