@caupulican/pi-adaptative 0.80.88 → 0.80.89

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 (53) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/core/agent-session.d.ts +35 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +262 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/context/brain-curator.d.ts +88 -0
  7. package/dist/core/context/brain-curator.d.ts.map +1 -0
  8. package/dist/core/context/brain-curator.js +192 -0
  9. package/dist/core/context/brain-curator.js.map +1 -0
  10. package/dist/core/context/context-composition.d.ts +122 -0
  11. package/dist/core/context/context-composition.d.ts.map +1 -0
  12. package/dist/core/context/context-composition.js +163 -0
  13. package/dist/core/context/context-composition.js.map +1 -0
  14. package/dist/core/context/context-prompt-enforcement.d.ts +13 -0
  15. package/dist/core/context/context-prompt-enforcement.d.ts.map +1 -1
  16. package/dist/core/context/context-prompt-enforcement.js +17 -2
  17. package/dist/core/context/context-prompt-enforcement.js.map +1 -1
  18. package/dist/core/context-gc.d.ts +13 -0
  19. package/dist/core/context-gc.d.ts.map +1 -1
  20. package/dist/core/context-gc.js +6 -0
  21. package/dist/core/context-gc.js.map +1 -1
  22. package/dist/core/research/model-fitness.d.ts +3 -0
  23. package/dist/core/research/model-fitness.d.ts.map +1 -1
  24. package/dist/core/research/model-fitness.js +54 -3
  25. package/dist/core/research/model-fitness.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +13 -0
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js +19 -0
  29. package/dist/core/settings-manager.js.map +1 -1
  30. package/dist/core/slash-commands.d.ts.map +1 -1
  31. package/dist/core/slash-commands.js +6 -1
  32. package/dist/core/slash-commands.js.map +1 -1
  33. package/dist/modes/interactive/components/fitness-role-selector.d.ts +13 -0
  34. package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -0
  35. package/dist/modes/interactive/components/fitness-role-selector.js +65 -0
  36. package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -0
  37. package/dist/modes/interactive/components/settings-selector.d.ts +4 -1
  38. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  39. package/dist/modes/interactive/components/settings-selector.js +84 -0
  40. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  42. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  43. package/dist/modes/interactive/interactive-mode.js +91 -0
  44. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  45. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  46. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  47. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  48. package/examples/extensions/sandbox/package-lock.json +2 -2
  49. package/examples/extensions/sandbox/package.json +1 -1
  50. package/examples/extensions/with-deps/package-lock.json +2 -2
  51. package/examples/extensions/with-deps/package.json +1 -1
  52. package/npm-shrinkwrap.json +12 -12
  53. package/package.json +4 -4
@@ -0,0 +1,163 @@
1
+ import { estimateTokens } from "../compaction/compaction.js";
2
+ function estimateTextTokens(text) {
3
+ return Math.ceil(text.length / 4);
4
+ }
5
+ function messageText(message) {
6
+ const content = message.content;
7
+ if (typeof content === "string")
8
+ return content;
9
+ if (!Array.isArray(content))
10
+ return "";
11
+ return content
12
+ .filter((part) => part.type === "text")
13
+ .map((part) => part.text)
14
+ .join("\n");
15
+ }
16
+ function classifyMessage(message) {
17
+ const details = message.details;
18
+ if (details?.contextGc?.packed === true)
19
+ return "gc-packed stub";
20
+ if (details?.promptPolicy?.enforced === true)
21
+ return "policy stub";
22
+ if (message.role === "custom") {
23
+ const customType = message.customType ?? "";
24
+ if (customType === "memory_context" || messageText(message).includes("<memory_context")) {
25
+ return "memory recall page";
26
+ }
27
+ return `custom (${customType || "unknown"})`;
28
+ }
29
+ if (message.role === "toolResult")
30
+ return `toolResult (${message.toolName ?? "?"})`;
31
+ return message.role;
32
+ }
33
+ export function buildContextCompositionReport(input) {
34
+ const systemPromptTokens = estimateTextTokens(input.systemPrompt);
35
+ const tools = input.tools
36
+ .map((tool) => ({
37
+ name: tool.name,
38
+ schemaTokens: estimateTextTokens(JSON.stringify({ name: tool.name, description: tool.description ?? "", parameters: tool.parameters ?? {} })),
39
+ source: tool.source ?? "built-in",
40
+ }))
41
+ .sort((a, b) => b.schemaTokens - a.schemaTokens);
42
+ const toolSchemaTokens = tools.reduce((sum, tool) => sum + tool.schemaTokens, 0);
43
+ const toolTokensByName = new Map(tools.map((tool) => [tool.name, tool.schemaTokens]));
44
+ const extensions = input.extensions
45
+ .map((extension) => ({
46
+ name: extension.name,
47
+ path: extension.path,
48
+ toolCount: extension.toolNames.length,
49
+ commandCount: extension.commandCount,
50
+ activeToolSchemaTokens: extension.toolNames.reduce((sum, toolName) => sum + (toolTokensByName.get(toolName) ?? 0), 0),
51
+ }))
52
+ .sort((a, b) => b.activeToolSchemaTokens - a.activeToolSchemaTokens);
53
+ const classes = new Map();
54
+ let messageTokens = 0;
55
+ for (const message of input.messages) {
56
+ const label = classifyMessage(message);
57
+ const tokens = estimateTokens(message);
58
+ messageTokens += tokens;
59
+ const row = classes.get(label) ?? { label, count: 0, tokens: 0 };
60
+ row.count++;
61
+ row.tokens += tokens;
62
+ classes.set(label, row);
63
+ }
64
+ const messageClasses = [...classes.values()].sort((a, b) => b.tokens - a.tokens);
65
+ const adjustments = input.adjustments ?? { memoryEvidenceTokens: 0, enforcementSavedTokens: 0 };
66
+ const estimatedRequestTokens = Math.max(0, systemPromptTokens +
67
+ toolSchemaTokens +
68
+ messageTokens +
69
+ adjustments.memoryEvidenceTokens -
70
+ adjustments.enforcementSavedTokens);
71
+ const observations = [];
72
+ const heaviestTool = tools[0];
73
+ if (heaviestTool && toolSchemaTokens > 0 && heaviestTool.schemaTokens > Math.max(500, toolSchemaTokens * 0.3)) {
74
+ observations.push(`tool "${heaviestTool.name}" alone is ~${heaviestTool.schemaTokens} tokens of schema on EVERY request — trim its description/schema if you own it`);
75
+ }
76
+ const recall = messageClasses.find((row) => row.label === "memory recall page");
77
+ if (recall && recall.tokens > 1500) {
78
+ observations.push(`${recall.count} memory recall page(s) hold ~${recall.tokens} tokens — verify context GC is packing stale ones (gc packed: ${input.gc?.packedCount ?? 0})`);
79
+ }
80
+ if (input.contextWindow && systemPromptTokens + toolSchemaTokens > input.contextWindow * 0.35) {
81
+ observations.push(`fixed per-request overhead (system+tools) is ~${Math.round(((systemPromptTokens + toolSchemaTokens) / input.contextWindow) * 100)}% of the context window before any conversation`);
82
+ }
83
+ if (input.providerReportedTokens !== null) {
84
+ const delta = input.providerReportedTokens - estimatedRequestTokens;
85
+ if (Math.abs(delta) > Math.max(2000, estimatedRequestTokens * 0.25)) {
86
+ observations.push(`provider-reported context (${input.providerReportedTokens}) differs from the estimate by ${delta > 0 ? "+" : ""}${delta} tokens — treat estimates as directional`);
87
+ }
88
+ }
89
+ if (input.curation?.enabled && input.curation.lastSkipReason) {
90
+ observations.push(`curation is enabled but idle: ${input.curation.lastSkipReason}`);
91
+ }
92
+ return {
93
+ systemPromptTokens,
94
+ systemPromptChars: input.systemPrompt.length,
95
+ toolSchemaTokens,
96
+ tools,
97
+ extensions,
98
+ messageClasses,
99
+ messageTokens,
100
+ messageCount: input.messages.length,
101
+ estimatedRequestTokens,
102
+ providerReportedTokens: input.providerReportedTokens,
103
+ contextWindow: input.contextWindow,
104
+ gc: input.gc ?? null,
105
+ enforcement: input.enforcement ?? null,
106
+ curation: input.curation ?? null,
107
+ spawned: input.spawned ?? null,
108
+ adjustments,
109
+ observations,
110
+ };
111
+ }
112
+ /** Bounded plain-text dashboard (interactive `/context` command and tests). */
113
+ export function formatContextCompositionDashboard(report, maxToolRows = 10) {
114
+ const pct = (tokens) => report.contextWindow ? ` (${((tokens / report.contextWindow) * 100).toFixed(1)}% of window)` : "";
115
+ const lines = [
116
+ "Context composition — what rides on EVERY request",
117
+ `estimated request total: ~${report.estimatedRequestTokens} tokens${pct(report.estimatedRequestTokens)}${report.providerReportedTokens !== null ? ` · provider-reported: ${report.providerReportedTokens}` : ""}`,
118
+ "",
119
+ `system prompt: ~${report.systemPromptTokens} tokens (${report.systemPromptChars} chars)`,
120
+ `tool schemas: ~${report.toolSchemaTokens} tokens across ${report.tools.length} active tool(s)`,
121
+ ];
122
+ for (const tool of report.tools.slice(0, maxToolRows)) {
123
+ lines.push(` - ${tool.name}: ~${tool.schemaTokens} tok [${tool.source}]`);
124
+ }
125
+ if (report.tools.length > maxToolRows) {
126
+ const rest = report.tools.slice(maxToolRows).reduce((sum, tool) => sum + tool.schemaTokens, 0);
127
+ lines.push(` - (+${report.tools.length - maxToolRows} more: ~${rest} tok)`);
128
+ }
129
+ if (report.extensions.length > 0) {
130
+ lines.push("", "extensions:");
131
+ for (const extension of report.extensions.slice(0, 8)) {
132
+ lines.push(` - ${extension.name}: ${extension.toolCount} tool(s), ${extension.commandCount} command(s), ~${extension.activeToolSchemaTokens} tok of active schemas`);
133
+ }
134
+ }
135
+ lines.push("", `session messages: ${report.messageCount} row(s), ~${report.messageTokens} tokens`);
136
+ if (report.adjustments.memoryEvidenceTokens > 0 || report.adjustments.enforcementSavedTokens > 0) {
137
+ lines.push(`send-time adjustments: +${report.adjustments.memoryEvidenceTokens} memory evidence, -${report.adjustments.enforcementSavedTokens} policy stubs (applied when the request is built)`);
138
+ }
139
+ for (const row of report.messageClasses.slice(0, 10)) {
140
+ lines.push(` - ${row.label}: ${row.count} row(s), ~${row.tokens} tok`);
141
+ }
142
+ if (report.gc) {
143
+ lines.push("", `context GC: ${report.gc.packedCount} row(s) packed, ~${report.gc.savedTokens} tokens saved this pass`);
144
+ }
145
+ if (report.enforcement) {
146
+ lines.push(`prompt policy: ${report.enforcement.enforcedCount} stub(s) this turn (${report.enforcement.advisoryEvictions} via brain advisory)`);
147
+ }
148
+ if (report.curation) {
149
+ const t = report.curation.telemetry;
150
+ lines.push(`brain curation: ${report.curation.enabled ? "enabled" : "disabled"} — ${t.jobsRun} job(s) run, ${t.parseFailures} parse failure(s), ${t.queued} queued, ~${Math.ceil(t.localChars / 4)} tokens processed locally${report.curation.lastSkipReason ? ` · last skip: ${report.curation.lastSkipReason}` : ""}`);
151
+ }
152
+ if (report.spawned && report.spawned.reports > 0) {
153
+ lines.push(`spawned/background spend (NOT in this context): ${report.spawned.reports} report(s), $${report.spawned.cost.toFixed(4)}`);
154
+ }
155
+ if (report.observations.length > 0) {
156
+ lines.push("", "observations:");
157
+ for (const observation of report.observations.slice(0, 5)) {
158
+ lines.push(` ! ${observation}`);
159
+ }
160
+ }
161
+ return lines.join("\n");
162
+ }
163
+ //# sourceMappingURL=context-composition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-composition.js","sourceRoot":"","sources":["../../../src/core/context/context-composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport { estimateTokens } from \"../compaction/compaction.ts\";\nimport type { CurationTelemetrySnapshot } from \"./brain-curator.ts\";\n\n/**\n * Context composition dashboard (user-facing): decomposes EVERYTHING that rides along on every\n * request — system prompt, active tool schemas, extension contributions, injected blocks\n * (memory recall pages, evidence blocks), and the session messages themselves (raw vs. GC-packed\n * vs. policy-stubbed) — so a user integrating their own tools/extensions can see exactly what\n * each addition costs per request and where cleaning is (or is not) working.\n *\n * Honesty contract: everything here is an ESTIMATE (chars/4) EXCEPT `providerReportedTokens`,\n * which is what the provider actually billed. The dashboard always shows both and the delta —\n * the delta is the measure of how much the estimates can be trusted, never hidden.\n *\n * Known exclusions (named, not hidden): extension `context` handlers may rewrite messages at\n * send time in ways this view cannot see. The memory evidence block and enforcement stubbing\n * are ALSO send-time-only, but those are modeled explicitly via `adjustments`.\n */\n\nexport interface ToolCompositionRow {\n\tname: string;\n\t/** Estimated tokens for the tool's name+description+schema as sent to the provider. */\n\tschemaTokens: number;\n\tsource: \"built-in\" | \"extension\";\n}\n\nexport interface ExtensionCompositionRow {\n\tname: string;\n\tpath: string;\n\ttoolCount: number;\n\tcommandCount: number;\n\t/** Estimated schema tokens of this extension's ACTIVE tools (its per-request cost). */\n\tactiveToolSchemaTokens: number;\n}\n\nexport interface MessageClassRow {\n\tlabel: string;\n\tcount: number;\n\ttokens: number;\n}\n\nexport interface ContextCompositionReport {\n\t/** Estimated tokens of the system prompt sent on every request. */\n\tsystemPromptTokens: number;\n\tsystemPromptChars: number;\n\t/** Estimated tokens of ALL active tool schemas sent on every request. */\n\ttoolSchemaTokens: number;\n\ttools: ToolCompositionRow[];\n\textensions: ExtensionCompositionRow[];\n\t/** Session message classes (raw/user/assistant/stubs/recall pages), heaviest first. */\n\tmessageClasses: MessageClassRow[];\n\tmessageTokens: number;\n\tmessageCount: number;\n\t/** Estimated total sent per request: system prompt + tool schemas + messages. */\n\testimatedRequestTokens: number;\n\t/** What the provider actually reported for the current context, when known. */\n\tproviderReportedTokens: number | null;\n\tcontextWindow: number | null;\n\tgc: { packedCount: number; savedTokens: number } | null;\n\tenforcement: { enforcedCount: number; advisoryEvictions: number } | null;\n\tcuration: { enabled: boolean; telemetry: CurationTelemetrySnapshot; lastSkipReason?: string } | null;\n\t/** Background/side-channel spend that does NOT ride in this context but bills the account. */\n\tspawned: { cost: number; reports: number } | null;\n\t/** Send-time-only deltas folded into estimatedRequestTokens: +evidence block, -policy stubs. */\n\tadjustments: { memoryEvidenceTokens: number; enforcementSavedTokens: number };\n\t/** Actionable, bounded observations derived from the numbers above. */\n\tobservations: string[];\n}\n\nexport interface BuildContextCompositionInput {\n\tsystemPrompt: string;\n\ttools: Array<{ name: string; description?: string; parameters?: unknown; source?: \"built-in\" | \"extension\" }>;\n\textensions: Array<{\n\t\tname: string;\n\t\tpath: string;\n\t\ttoolNames: string[];\n\t\tcommandCount: number;\n\t}>;\n\tmessages: AgentMessage[];\n\tproviderReportedTokens: number | null;\n\tcontextWindow: number | null;\n\tgc?: { packedCount: number; savedTokens: number };\n\tenforcement?: { enforcedCount: number; advisoryEvictions: number };\n\tcuration?: { enabled: boolean; telemetry: CurationTelemetrySnapshot; lastSkipReason?: string };\n\tspawned?: { cost: number; reports: number };\n\tadjustments?: { memoryEvidenceTokens: number; enforcementSavedTokens: number };\n}\n\nfunction estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction messageText(message: AgentMessage): string {\n\tconst content = (message as { content?: unknown }).content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((part): part is { type: \"text\"; text: string } => (part as { type?: string }).type === \"text\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\");\n}\n\nfunction classifyMessage(message: AgentMessage): string {\n\tconst details = (\n\t\tmessage as { details?: { contextGc?: { packed?: unknown }; promptPolicy?: { enforced?: unknown } } }\n\t).details;\n\tif (details?.contextGc?.packed === true) return \"gc-packed stub\";\n\tif (details?.promptPolicy?.enforced === true) return \"policy stub\";\n\tif (message.role === \"custom\") {\n\t\tconst customType = (message as { customType?: string }).customType ?? \"\";\n\t\tif (customType === \"memory_context\" || messageText(message).includes(\"<memory_context\")) {\n\t\t\treturn \"memory recall page\";\n\t\t}\n\t\treturn `custom (${customType || \"unknown\"})`;\n\t}\n\tif (message.role === \"toolResult\") return `toolResult (${(message as { toolName?: string }).toolName ?? \"?\"})`;\n\treturn message.role;\n}\n\nexport function buildContextCompositionReport(input: BuildContextCompositionInput): ContextCompositionReport {\n\tconst systemPromptTokens = estimateTextTokens(input.systemPrompt);\n\n\tconst tools: ToolCompositionRow[] = input.tools\n\t\t.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tschemaTokens: estimateTextTokens(\n\t\t\t\tJSON.stringify({ name: tool.name, description: tool.description ?? \"\", parameters: tool.parameters ?? {} }),\n\t\t\t),\n\t\t\tsource: tool.source ?? (\"built-in\" as const),\n\t\t}))\n\t\t.sort((a, b) => b.schemaTokens - a.schemaTokens);\n\tconst toolSchemaTokens = tools.reduce((sum, tool) => sum + tool.schemaTokens, 0);\n\tconst toolTokensByName = new Map(tools.map((tool) => [tool.name, tool.schemaTokens]));\n\n\tconst extensions: ExtensionCompositionRow[] = input.extensions\n\t\t.map((extension) => ({\n\t\t\tname: extension.name,\n\t\t\tpath: extension.path,\n\t\t\ttoolCount: extension.toolNames.length,\n\t\t\tcommandCount: extension.commandCount,\n\t\t\tactiveToolSchemaTokens: extension.toolNames.reduce(\n\t\t\t\t(sum, toolName) => sum + (toolTokensByName.get(toolName) ?? 0),\n\t\t\t\t0,\n\t\t\t),\n\t\t}))\n\t\t.sort((a, b) => b.activeToolSchemaTokens - a.activeToolSchemaTokens);\n\n\tconst classes = new Map<string, MessageClassRow>();\n\tlet messageTokens = 0;\n\tfor (const message of input.messages) {\n\t\tconst label = classifyMessage(message);\n\t\tconst tokens = estimateTokens(message);\n\t\tmessageTokens += tokens;\n\t\tconst row = classes.get(label) ?? { label, count: 0, tokens: 0 };\n\t\trow.count++;\n\t\trow.tokens += tokens;\n\t\tclasses.set(label, row);\n\t}\n\tconst messageClasses = [...classes.values()].sort((a, b) => b.tokens - a.tokens);\n\n\tconst adjustments = input.adjustments ?? { memoryEvidenceTokens: 0, enforcementSavedTokens: 0 };\n\tconst estimatedRequestTokens = Math.max(\n\t\t0,\n\t\tsystemPromptTokens +\n\t\t\ttoolSchemaTokens +\n\t\t\tmessageTokens +\n\t\t\tadjustments.memoryEvidenceTokens -\n\t\t\tadjustments.enforcementSavedTokens,\n\t);\n\n\tconst observations: string[] = [];\n\tconst heaviestTool = tools[0];\n\tif (heaviestTool && toolSchemaTokens > 0 && heaviestTool.schemaTokens > Math.max(500, toolSchemaTokens * 0.3)) {\n\t\tobservations.push(\n\t\t\t`tool \"${heaviestTool.name}\" alone is ~${heaviestTool.schemaTokens} tokens of schema on EVERY request — trim its description/schema if you own it`,\n\t\t);\n\t}\n\tconst recall = messageClasses.find((row) => row.label === \"memory recall page\");\n\tif (recall && recall.tokens > 1500) {\n\t\tobservations.push(\n\t\t\t`${recall.count} memory recall page(s) hold ~${recall.tokens} tokens — verify context GC is packing stale ones (gc packed: ${input.gc?.packedCount ?? 0})`,\n\t\t);\n\t}\n\tif (input.contextWindow && systemPromptTokens + toolSchemaTokens > input.contextWindow * 0.35) {\n\t\tobservations.push(\n\t\t\t`fixed per-request overhead (system+tools) is ~${Math.round(((systemPromptTokens + toolSchemaTokens) / input.contextWindow) * 100)}% of the context window before any conversation`,\n\t\t);\n\t}\n\tif (input.providerReportedTokens !== null) {\n\t\tconst delta = input.providerReportedTokens - estimatedRequestTokens;\n\t\tif (Math.abs(delta) > Math.max(2000, estimatedRequestTokens * 0.25)) {\n\t\t\tobservations.push(\n\t\t\t\t`provider-reported context (${input.providerReportedTokens}) differs from the estimate by ${delta > 0 ? \"+\" : \"\"}${delta} tokens — treat estimates as directional`,\n\t\t\t);\n\t\t}\n\t}\n\tif (input.curation?.enabled && input.curation.lastSkipReason) {\n\t\tobservations.push(`curation is enabled but idle: ${input.curation.lastSkipReason}`);\n\t}\n\n\treturn {\n\t\tsystemPromptTokens,\n\t\tsystemPromptChars: input.systemPrompt.length,\n\t\ttoolSchemaTokens,\n\t\ttools,\n\t\textensions,\n\t\tmessageClasses,\n\t\tmessageTokens,\n\t\tmessageCount: input.messages.length,\n\t\testimatedRequestTokens,\n\t\tproviderReportedTokens: input.providerReportedTokens,\n\t\tcontextWindow: input.contextWindow,\n\t\tgc: input.gc ?? null,\n\t\tenforcement: input.enforcement ?? null,\n\t\tcuration: input.curation ?? null,\n\t\tspawned: input.spawned ?? null,\n\t\tadjustments,\n\t\tobservations,\n\t};\n}\n\n/** Bounded plain-text dashboard (interactive `/context` command and tests). */\nexport function formatContextCompositionDashboard(report: ContextCompositionReport, maxToolRows = 10): string {\n\tconst pct = (tokens: number) =>\n\t\treport.contextWindow ? ` (${((tokens / report.contextWindow) * 100).toFixed(1)}% of window)` : \"\";\n\tconst lines: string[] = [\n\t\t\"Context composition — what rides on EVERY request\",\n\t\t`estimated request total: ~${report.estimatedRequestTokens} tokens${pct(report.estimatedRequestTokens)}${\n\t\t\treport.providerReportedTokens !== null ? ` · provider-reported: ${report.providerReportedTokens}` : \"\"\n\t\t}`,\n\t\t\"\",\n\t\t`system prompt: ~${report.systemPromptTokens} tokens (${report.systemPromptChars} chars)`,\n\t\t`tool schemas: ~${report.toolSchemaTokens} tokens across ${report.tools.length} active tool(s)`,\n\t];\n\tfor (const tool of report.tools.slice(0, maxToolRows)) {\n\t\tlines.push(` - ${tool.name}: ~${tool.schemaTokens} tok [${tool.source}]`);\n\t}\n\tif (report.tools.length > maxToolRows) {\n\t\tconst rest = report.tools.slice(maxToolRows).reduce((sum, tool) => sum + tool.schemaTokens, 0);\n\t\tlines.push(` - (+${report.tools.length - maxToolRows} more: ~${rest} tok)`);\n\t}\n\tif (report.extensions.length > 0) {\n\t\tlines.push(\"\", \"extensions:\");\n\t\tfor (const extension of report.extensions.slice(0, 8)) {\n\t\t\tlines.push(\n\t\t\t\t` - ${extension.name}: ${extension.toolCount} tool(s), ${extension.commandCount} command(s), ~${extension.activeToolSchemaTokens} tok of active schemas`,\n\t\t\t);\n\t\t}\n\t}\n\tlines.push(\"\", `session messages: ${report.messageCount} row(s), ~${report.messageTokens} tokens`);\n\tif (report.adjustments.memoryEvidenceTokens > 0 || report.adjustments.enforcementSavedTokens > 0) {\n\t\tlines.push(\n\t\t\t`send-time adjustments: +${report.adjustments.memoryEvidenceTokens} memory evidence, -${report.adjustments.enforcementSavedTokens} policy stubs (applied when the request is built)`,\n\t\t);\n\t}\n\tfor (const row of report.messageClasses.slice(0, 10)) {\n\t\tlines.push(` - ${row.label}: ${row.count} row(s), ~${row.tokens} tok`);\n\t}\n\tif (report.gc) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`context GC: ${report.gc.packedCount} row(s) packed, ~${report.gc.savedTokens} tokens saved this pass`,\n\t\t);\n\t}\n\tif (report.enforcement) {\n\t\tlines.push(\n\t\t\t`prompt policy: ${report.enforcement.enforcedCount} stub(s) this turn (${report.enforcement.advisoryEvictions} via brain advisory)`,\n\t\t);\n\t}\n\tif (report.curation) {\n\t\tconst t = report.curation.telemetry;\n\t\tlines.push(\n\t\t\t`brain curation: ${report.curation.enabled ? \"enabled\" : \"disabled\"} — ${t.jobsRun} job(s) run, ${t.parseFailures} parse failure(s), ${t.queued} queued, ~${Math.ceil(t.localChars / 4)} tokens processed locally${\n\t\t\t\treport.curation.lastSkipReason ? ` · last skip: ${report.curation.lastSkipReason}` : \"\"\n\t\t\t}`,\n\t\t);\n\t}\n\tif (report.spawned && report.spawned.reports > 0) {\n\t\tlines.push(\n\t\t\t`spawned/background spend (NOT in this context): ${report.spawned.reports} report(s), $${report.spawned.cost.toFixed(4)}`,\n\t\t);\n\t}\n\tif (report.observations.length > 0) {\n\t\tlines.push(\"\", \"observations:\");\n\t\tfor (const observation of report.observations.slice(0, 5)) {\n\t\t\tlines.push(` ! ${observation}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -42,6 +42,17 @@ export interface ContextPromptEnforcementSettings {
42
42
  * `AgentSession.getActiveToolNames().includes("artifact_retrieve")`), never assume it.
43
43
  */
44
44
  retrievalToolAvailable: boolean;
45
+ /**
46
+ * Brain-curator relevance lookup (runtime fact, like `retrievalToolAvailable`; never
47
+ * persisted). ASYMMETRIC by design: an explicit high-confidence irrelevance verdict may
48
+ * evict an otherwise-eligible item from within the recent window (never past the absolute
49
+ * floor), but an advisory can never keep an item the policy wants gone, never stub a
50
+ * hard-constraint-protected item, and its absence is byte-for-byte today's behavior.
51
+ */
52
+ brainRelevance?: (itemId: string) => {
53
+ relevant: boolean;
54
+ confidence: number;
55
+ } | undefined;
45
56
  }
46
57
  export type PromptEnforcementSkipReason = "message_mismatch" | "within_recent_window" | "errored_tool_result" | "already_stubbed_or_packed" | "not_artifact_backed" | "retrieval_tool_unavailable" | "hard_constraint_rejected" | "missing_artifact_id" | "below_min_chars";
47
58
  export interface PromptEnforcementItemReport {
@@ -53,6 +64,8 @@ export interface PromptEnforcementItemReport {
53
64
  artifactId?: string;
54
65
  originalChars?: number;
55
66
  skipReason?: PromptEnforcementSkipReason;
67
+ /** Set when a brain-curator irrelevance verdict allowed eviction inside the recent window. */
68
+ advisory?: "brain_irrelevant";
56
69
  }
57
70
  export interface PromptEnforcementReport {
58
71
  turnIndex: number;
@@ -1 +1 @@
1
- {"version":3,"file":"context-prompt-enforcement.d.ts","sourceRoot":"","sources":["../../../src/core/context/context-prompt-enforcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,gCAAgC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,sBAAsB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,2BAA2B,GACpC,kBAAkB,GAClB,sBAAsB,GACtB,qBAAqB,GACrB,2BAA2B,GAC3B,qBAAqB,GACrB,4BAA4B,GAC5B,0BAA0B,GAC1B,qBAAqB,GACrB,iBAAiB,CAAC;AAErB,MAAM,WAAW,2BAA2B;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,2BAA2B,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,uBAAuB,CAAC;CAChC;AA0CD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,YAAY,EAAE,EACxB,YAAY,EAAE,wBAAwB,EACtC,QAAQ,EAAE,gCAAgC,GACxC,yBAAyB,CAkF3B","sourcesContent":["/**\n * First enforcement pilot for the context-policy layer (opt-in, default disabled). Unlike\n * context-audit.ts/context-prompt-policy.ts (both strictly observe-only), this module CAN\n * change the provider-visible message array -- but only ever via stub-in-place on\n * artifact-backed tool_output results, never by removing a message or breaking\n * assistant/toolResult pairing. It never touches the transcript, never releases/reclaims\n * artifact references, and never writes a new artifact -- it only replaces the visible\n * text of an already artifact-backed message with a bounded pointer to the existing\n * artifact, retrievable via the `artifact_retrieve` tool.\n *\n * Eligibility for stubbing is deliberately conservative (see `enforcePromptPolicy`): the\n * setting must be enabled, the item must be outside the recent-message safety window, not\n * an errored tool result, not already stubbed by this module or already packed by legacy\n * context-gc this turn, must have a resolvable artifact id, the `artifact_retrieve` tool\n * must actually be active this turn, and must clear `hardConstraints.dropFromPrompt` (see\n * below for why that specific action, not `pack_to_artifact`).\n *\n * Why `dropFromPrompt`, not `packToArtifact`: this operation does not create a new\n * artifact -- it reuses the ref an earlier `pack_to_artifact` capture already produced (see\n * tool-output-artifacts.md's \"measure -> digest/preview/artifact -> prompt item\" pipeline).\n * `drop_from_prompt` requires an existing retrieval path and is exactly the operation being\n * performed (evicting raw content from the live prompt in favor of that existing path);\n * `pack_to_artifact` is the distinct first-capture operation, which we never invoke here.\n *\n * Why `retrievalToolAvailable` is checked separately from `hasAvailableRetrievalPath`: the\n * latter only proves the artifact still exists in the store; it says nothing about whether\n * the model can currently act on the stub's instruction to call `artifact_retrieve`.\n * `artifact_retrieve` is a companion affordance (auto-activated alongside grep/find, not a\n * default/global tool -- see agent-session.ts's companion-activation enforcement), so active\n * tools can differ turn to turn. Stubbing content with an unactionable pointer would be\n * strictly worse than leaving the raw content in place.\n */\n\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport type { PromptPolicyShadowReport } from \"./context-prompt-policy.ts\";\n\nexport interface ContextPromptEnforcementSettings {\n\tenabled: boolean;\n\tpreserveRecentMessages: number;\n\tminChars: number;\n\t/**\n\t * Whether the `artifact_retrieve` tool is actually active this turn -- a runtime fact,\n\t * not a persisted setting. Callers must derive this from the live active-tool set (e.g.\n\t * `AgentSession.getActiveToolNames().includes(\"artifact_retrieve\")`), never assume it.\n\t */\n\tretrievalToolAvailable: boolean;\n}\n\nexport type PromptEnforcementSkipReason =\n\t| \"message_mismatch\"\n\t| \"within_recent_window\"\n\t| \"errored_tool_result\"\n\t| \"already_stubbed_or_packed\"\n\t| \"not_artifact_backed\"\n\t| \"retrieval_tool_unavailable\"\n\t| \"hard_constraint_rejected\"\n\t| \"missing_artifact_id\"\n\t| \"below_min_chars\";\n\nexport interface PromptEnforcementItemReport {\n\titemId: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\tenforced: boolean;\n\taction?: \"artifact_stub\";\n\tartifactId?: string;\n\toriginalChars?: number;\n\tskipReason?: PromptEnforcementSkipReason;\n}\n\nexport interface PromptEnforcementReport {\n\tturnIndex: number;\n\titems: PromptEnforcementItemReport[];\n}\n\nexport interface EnforcePromptPolicyResult {\n\tmessages: AgentMessage[];\n\treport: PromptEnforcementReport;\n}\n\nfunction extractDetailsArtifactId(details: unknown): string | undefined {\n\tif (typeof details !== \"object\" || details === null) return undefined;\n\tconst artifactId = (details as { artifactId?: unknown }).artifactId;\n\treturn typeof artifactId === \"string\" ? artifactId : undefined;\n}\n\n/** True if legacy context-gc already packed this message this turn, or this module already stubbed it. */\nfunction isAlreadyStubbedOrPacked(details: unknown): boolean {\n\tif (typeof details !== \"object\" || details === null) return false;\n\tconst record = details as { promptPolicy?: { enforced?: unknown }; contextGc?: { packed?: unknown } };\n\treturn record.promptPolicy?.enforced === true || record.contextGc?.packed === true;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\") parts.push(part.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction buildStubText(toolName: string, originalChars: number, artifactId: string): string {\n\treturn `[content replaced by prompt-policy: originally ${originalChars} chars from a stale ${toolName} tool result. Retrieve the full output with artifact_retrieve using artifactId \"${artifactId}\".]`;\n}\n\nfunction skip(\n\titem: { itemId: string; toolCallId: string; messageIndex: number },\n\tskipReason: PromptEnforcementSkipReason,\n\textra?: { artifactId?: string; originalChars?: number },\n): PromptEnforcementItemReport {\n\treturn {\n\t\titemId: item.itemId,\n\t\ttoolCallId: item.toolCallId,\n\t\tmessageIndex: item.messageIndex,\n\t\tenforced: false,\n\t\tskipReason,\n\t\t...extra,\n\t};\n}\n\n/**\n * Apply the first enforcement pilot to `messages` (expected to be the provider-visible\n * array after existing context-gc has already run). Returns a new array only when at least\n * one item was actually stubbed; otherwise returns the same `messages` reference unchanged\n * (in particular, always true when `settings.enabled` is false). Never mutates `messages`\n * or any message object within it -- every stubbed entry is a fresh object.\n */\nexport function enforcePromptPolicy(\n\tmessages: AgentMessage[],\n\tshadowReport: PromptPolicyShadowReport,\n\tsettings: ContextPromptEnforcementSettings,\n): EnforcePromptPolicyResult {\n\tif (!settings.enabled) {\n\t\treturn { messages, report: { turnIndex: shadowReport.turnIndex, items: [] } };\n\t}\n\n\tconst recentCutoffIndex = Math.max(0, messages.length - settings.preserveRecentMessages);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\tconst items: PromptEnforcementItemReport[] = [];\n\n\tfor (const planItem of shadowReport.items) {\n\t\tconst message = messages[planItem.messageIndex];\n\t\tif (!message || message.role !== \"toolResult\" || message.toolCallId !== planItem.toolCallId) {\n\t\t\titems.push(skip(planItem, \"message_mismatch\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.messageIndex >= recentCutoffIndex) {\n\t\t\titems.push(skip(planItem, \"within_recent_window\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.isError) {\n\t\t\titems.push(skip(planItem, \"errored_tool_result\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (isAlreadyStubbedOrPacked(message.details)) {\n\t\t\titems.push(skip(planItem, \"already_stubbed_or_packed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!planItem.hasAvailableRetrievalPath) {\n\t\t\titems.push(skip(planItem, \"not_artifact_backed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!settings.retrievalToolAvailable) {\n\t\t\titems.push(skip(planItem, \"retrieval_tool_unavailable\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.hardConstraints.dropFromPrompt.length > 0) {\n\t\t\titems.push(skip(planItem, \"hard_constraint_rejected\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst artifactId = extractDetailsArtifactId(message.details);\n\t\tif (!artifactId) {\n\t\t\titems.push(skip(planItem, \"missing_artifact_id\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst originalChars = toolResultText(message).length;\n\t\tif (originalChars < settings.minChars) {\n\t\t\titems.push(skip(planItem, \"below_min_chars\", { artifactId, originalChars }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existingDetails = typeof message.details === \"object\" && message.details !== null ? message.details : {};\n\t\tnextMessages[planItem.messageIndex] = {\n\t\t\t...message,\n\t\t\tcontent: [{ type: \"text\", text: buildStubText(message.toolName, originalChars, artifactId) }],\n\t\t\tdetails: {\n\t\t\t\t...existingDetails,\n\t\t\t\tpromptPolicy: {\n\t\t\t\t\tenforced: true,\n\t\t\t\t\taction: \"artifact_stub\",\n\t\t\t\t\tartifactId,\n\t\t\t\t\toriginalChars,\n\t\t\t\t\treason: \"stale_artifact_backed_tool_output\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tchanged = true;\n\t\titems.push({\n\t\t\titemId: planItem.itemId,\n\t\t\ttoolCallId: planItem.toolCallId,\n\t\t\tmessageIndex: planItem.messageIndex,\n\t\t\tenforced: true,\n\t\t\taction: \"artifact_stub\",\n\t\t\tartifactId,\n\t\t\toriginalChars,\n\t\t});\n\t}\n\n\treturn {\n\t\tmessages: changed ? nextMessages : messages,\n\t\treport: { turnIndex: shadowReport.turnIndex, items },\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-prompt-enforcement.d.ts","sourceRoot":"","sources":["../../../src/core/context/context-prompt-enforcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,gCAAgC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,sBAAsB,EAAE,OAAO,CAAC;IAChC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAC3F;AAED,MAAM,MAAM,2BAA2B,GACpC,kBAAkB,GAClB,sBAAsB,GACtB,qBAAqB,GACrB,2BAA2B,GAC3B,qBAAqB,GACrB,4BAA4B,GAC5B,0BAA0B,GAC1B,qBAAqB,GACrB,iBAAiB,CAAC;AAErB,MAAM,WAAW,2BAA2B;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,2BAA2B,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,uBAAuB,CAAC;CAChC;AA4CD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,YAAY,EAAE,EACxB,YAAY,EAAE,wBAAwB,EACtC,QAAQ,EAAE,gCAAgC,GACxC,yBAAyB,CA+F3B","sourcesContent":["/**\n * First enforcement pilot for the context-policy layer (opt-in, default disabled). Unlike\n * context-audit.ts/context-prompt-policy.ts (both strictly observe-only), this module CAN\n * change the provider-visible message array -- but only ever via stub-in-place on\n * artifact-backed tool_output results, never by removing a message or breaking\n * assistant/toolResult pairing. It never touches the transcript, never releases/reclaims\n * artifact references, and never writes a new artifact -- it only replaces the visible\n * text of an already artifact-backed message with a bounded pointer to the existing\n * artifact, retrievable via the `artifact_retrieve` tool.\n *\n * Eligibility for stubbing is deliberately conservative (see `enforcePromptPolicy`): the\n * setting must be enabled, the item must be outside the recent-message safety window, not\n * an errored tool result, not already stubbed by this module or already packed by legacy\n * context-gc this turn, must have a resolvable artifact id, the `artifact_retrieve` tool\n * must actually be active this turn, and must clear `hardConstraints.dropFromPrompt` (see\n * below for why that specific action, not `pack_to_artifact`).\n *\n * Why `dropFromPrompt`, not `packToArtifact`: this operation does not create a new\n * artifact -- it reuses the ref an earlier `pack_to_artifact` capture already produced (see\n * tool-output-artifacts.md's \"measure -> digest/preview/artifact -> prompt item\" pipeline).\n * `drop_from_prompt` requires an existing retrieval path and is exactly the operation being\n * performed (evicting raw content from the live prompt in favor of that existing path);\n * `pack_to_artifact` is the distinct first-capture operation, which we never invoke here.\n *\n * Why `retrievalToolAvailable` is checked separately from `hasAvailableRetrievalPath`: the\n * latter only proves the artifact still exists in the store; it says nothing about whether\n * the model can currently act on the stub's instruction to call `artifact_retrieve`.\n * `artifact_retrieve` is a companion affordance (auto-activated alongside grep/find, not a\n * default/global tool -- see agent-session.ts's companion-activation enforcement), so active\n * tools can differ turn to turn. Stubbing content with an unactionable pointer would be\n * strictly worse than leaving the raw content in place.\n */\n\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport { CURATION_RELEVANCE_MIN_CONFIDENCE } from \"./brain-curator.ts\";\nimport type { PromptPolicyShadowReport } from \"./context-prompt-policy.ts\";\n\nexport interface ContextPromptEnforcementSettings {\n\tenabled: boolean;\n\tpreserveRecentMessages: number;\n\tminChars: number;\n\t/**\n\t * Whether the `artifact_retrieve` tool is actually active this turn -- a runtime fact,\n\t * not a persisted setting. Callers must derive this from the live active-tool set (e.g.\n\t * `AgentSession.getActiveToolNames().includes(\"artifact_retrieve\")`), never assume it.\n\t */\n\tretrievalToolAvailable: boolean;\n\t/**\n\t * Brain-curator relevance lookup (runtime fact, like `retrievalToolAvailable`; never\n\t * persisted). ASYMMETRIC by design: an explicit high-confidence irrelevance verdict may\n\t * evict an otherwise-eligible item from within the recent window (never past the absolute\n\t * floor), but an advisory can never keep an item the policy wants gone, never stub a\n\t * hard-constraint-protected item, and its absence is byte-for-byte today's behavior.\n\t */\n\tbrainRelevance?: (itemId: string) => { relevant: boolean; confidence: number } | undefined;\n}\n\nexport type PromptEnforcementSkipReason =\n\t| \"message_mismatch\"\n\t| \"within_recent_window\"\n\t| \"errored_tool_result\"\n\t| \"already_stubbed_or_packed\"\n\t| \"not_artifact_backed\"\n\t| \"retrieval_tool_unavailable\"\n\t| \"hard_constraint_rejected\"\n\t| \"missing_artifact_id\"\n\t| \"below_min_chars\";\n\nexport interface PromptEnforcementItemReport {\n\titemId: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\tenforced: boolean;\n\taction?: \"artifact_stub\";\n\tartifactId?: string;\n\toriginalChars?: number;\n\tskipReason?: PromptEnforcementSkipReason;\n\t/** Set when a brain-curator irrelevance verdict allowed eviction inside the recent window. */\n\tadvisory?: \"brain_irrelevant\";\n}\n\nexport interface PromptEnforcementReport {\n\tturnIndex: number;\n\titems: PromptEnforcementItemReport[];\n}\n\nexport interface EnforcePromptPolicyResult {\n\tmessages: AgentMessage[];\n\treport: PromptEnforcementReport;\n}\n\nconst ENFORCEMENT_ABSOLUTE_RECENT_FLOOR = 4;\n\nfunction extractDetailsArtifactId(details: unknown): string | undefined {\n\tif (typeof details !== \"object\" || details === null) return undefined;\n\tconst artifactId = (details as { artifactId?: unknown }).artifactId;\n\treturn typeof artifactId === \"string\" ? artifactId : undefined;\n}\n\n/** True if legacy context-gc already packed this message this turn, or this module already stubbed it. */\nfunction isAlreadyStubbedOrPacked(details: unknown): boolean {\n\tif (typeof details !== \"object\" || details === null) return false;\n\tconst record = details as { promptPolicy?: { enforced?: unknown }; contextGc?: { packed?: unknown } };\n\treturn record.promptPolicy?.enforced === true || record.contextGc?.packed === true;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\") parts.push(part.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction buildStubText(toolName: string, originalChars: number, artifactId: string): string {\n\treturn `[content replaced by prompt-policy: originally ${originalChars} chars from a stale ${toolName} tool result. Retrieve the full output with artifact_retrieve using artifactId \"${artifactId}\".]`;\n}\n\nfunction skip(\n\titem: { itemId: string; toolCallId: string; messageIndex: number },\n\tskipReason: PromptEnforcementSkipReason,\n\textra?: { artifactId?: string; originalChars?: number },\n): PromptEnforcementItemReport {\n\treturn {\n\t\titemId: item.itemId,\n\t\ttoolCallId: item.toolCallId,\n\t\tmessageIndex: item.messageIndex,\n\t\tenforced: false,\n\t\tskipReason,\n\t\t...extra,\n\t};\n}\n\n/**\n * Apply the first enforcement pilot to `messages` (expected to be the provider-visible\n * array after existing context-gc has already run). Returns a new array only when at least\n * one item was actually stubbed; otherwise returns the same `messages` reference unchanged\n * (in particular, always true when `settings.enabled` is false). Never mutates `messages`\n * or any message object within it -- every stubbed entry is a fresh object.\n */\nexport function enforcePromptPolicy(\n\tmessages: AgentMessage[],\n\tshadowReport: PromptPolicyShadowReport,\n\tsettings: ContextPromptEnforcementSettings,\n): EnforcePromptPolicyResult {\n\tif (!settings.enabled) {\n\t\treturn { messages, report: { turnIndex: shadowReport.turnIndex, items: [] } };\n\t}\n\n\tconst recentCutoffIndex = Math.max(0, messages.length - settings.preserveRecentMessages);\n\t// Advisory evictions may reach inside the recent window but NEVER past this absolute floor:\n\t// the last few messages are what the model is actively reasoning over.\n\tconst absoluteFloorIndex = Math.max(0, messages.length - ENFORCEMENT_ABSOLUTE_RECENT_FLOOR);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\tconst items: PromptEnforcementItemReport[] = [];\n\n\tfor (const planItem of shadowReport.items) {\n\t\tconst message = messages[planItem.messageIndex];\n\t\tif (!message || message.role !== \"toolResult\" || message.toolCallId !== planItem.toolCallId) {\n\t\t\titems.push(skip(planItem, \"message_mismatch\"));\n\t\t\tcontinue;\n\t\t}\n\t\tlet advisoryEviction = false;\n\t\tif (planItem.messageIndex >= recentCutoffIndex) {\n\t\t\tconst advisory = settings.brainRelevance?.(planItem.itemId);\n\t\t\tadvisoryEviction =\n\t\t\t\tadvisory !== undefined &&\n\t\t\t\t!advisory.relevant &&\n\t\t\t\tadvisory.confidence >= CURATION_RELEVANCE_MIN_CONFIDENCE &&\n\t\t\t\tplanItem.messageIndex < absoluteFloorIndex;\n\t\t\tif (!advisoryEviction) {\n\t\t\t\titems.push(skip(planItem, \"within_recent_window\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (message.isError) {\n\t\t\titems.push(skip(planItem, \"errored_tool_result\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (isAlreadyStubbedOrPacked(message.details)) {\n\t\t\titems.push(skip(planItem, \"already_stubbed_or_packed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!planItem.hasAvailableRetrievalPath) {\n\t\t\titems.push(skip(planItem, \"not_artifact_backed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!settings.retrievalToolAvailable) {\n\t\t\titems.push(skip(planItem, \"retrieval_tool_unavailable\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.hardConstraints.dropFromPrompt.length > 0) {\n\t\t\titems.push(skip(planItem, \"hard_constraint_rejected\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst artifactId = extractDetailsArtifactId(message.details);\n\t\tif (!artifactId) {\n\t\t\titems.push(skip(planItem, \"missing_artifact_id\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst originalChars = toolResultText(message).length;\n\t\tif (originalChars < settings.minChars) {\n\t\t\titems.push(skip(planItem, \"below_min_chars\", { artifactId, originalChars }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existingDetails = typeof message.details === \"object\" && message.details !== null ? message.details : {};\n\t\tnextMessages[planItem.messageIndex] = {\n\t\t\t...message,\n\t\t\tcontent: [{ type: \"text\", text: buildStubText(message.toolName, originalChars, artifactId) }],\n\t\t\tdetails: {\n\t\t\t\t...existingDetails,\n\t\t\t\tpromptPolicy: {\n\t\t\t\t\tenforced: true,\n\t\t\t\t\taction: \"artifact_stub\",\n\t\t\t\t\tartifactId,\n\t\t\t\t\toriginalChars,\n\t\t\t\t\treason: \"stale_artifact_backed_tool_output\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tchanged = true;\n\t\titems.push({\n\t\t\titemId: planItem.itemId,\n\t\t\ttoolCallId: planItem.toolCallId,\n\t\t\tmessageIndex: planItem.messageIndex,\n\t\t\tenforced: true,\n\t\t\taction: \"artifact_stub\",\n\t\t\tartifactId,\n\t\t\toriginalChars,\n\t\t\t...(advisoryEviction ? { advisory: \"brain_irrelevant\" as const } : {}),\n\t\t});\n\t}\n\n\treturn {\n\t\tmessages: changed ? nextMessages : messages,\n\t\treport: { turnIndex: shadowReport.turnIndex, items },\n\t};\n}\n"]}
@@ -30,6 +30,8 @@
30
30
  * tools can differ turn to turn. Stubbing content with an unactionable pointer would be
31
31
  * strictly worse than leaving the raw content in place.
32
32
  */
33
+ import { CURATION_RELEVANCE_MIN_CONFIDENCE } from "./brain-curator.js";
34
+ const ENFORCEMENT_ABSOLUTE_RECENT_FLOOR = 4;
33
35
  function extractDetailsArtifactId(details) {
34
36
  if (typeof details !== "object" || details === null)
35
37
  return undefined;
@@ -76,6 +78,9 @@ export function enforcePromptPolicy(messages, shadowReport, settings) {
76
78
  return { messages, report: { turnIndex: shadowReport.turnIndex, items: [] } };
77
79
  }
78
80
  const recentCutoffIndex = Math.max(0, messages.length - settings.preserveRecentMessages);
81
+ // Advisory evictions may reach inside the recent window but NEVER past this absolute floor:
82
+ // the last few messages are what the model is actively reasoning over.
83
+ const absoluteFloorIndex = Math.max(0, messages.length - ENFORCEMENT_ABSOLUTE_RECENT_FLOOR);
79
84
  const nextMessages = messages.slice();
80
85
  let changed = false;
81
86
  const items = [];
@@ -85,9 +90,18 @@ export function enforcePromptPolicy(messages, shadowReport, settings) {
85
90
  items.push(skip(planItem, "message_mismatch"));
86
91
  continue;
87
92
  }
93
+ let advisoryEviction = false;
88
94
  if (planItem.messageIndex >= recentCutoffIndex) {
89
- items.push(skip(planItem, "within_recent_window"));
90
- continue;
95
+ const advisory = settings.brainRelevance?.(planItem.itemId);
96
+ advisoryEviction =
97
+ advisory !== undefined &&
98
+ !advisory.relevant &&
99
+ advisory.confidence >= CURATION_RELEVANCE_MIN_CONFIDENCE &&
100
+ planItem.messageIndex < absoluteFloorIndex;
101
+ if (!advisoryEviction) {
102
+ items.push(skip(planItem, "within_recent_window"));
103
+ continue;
104
+ }
91
105
  }
92
106
  if (message.isError) {
93
107
  items.push(skip(planItem, "errored_tool_result"));
@@ -143,6 +157,7 @@ export function enforcePromptPolicy(messages, shadowReport, settings) {
143
157
  action: "artifact_stub",
144
158
  artifactId,
145
159
  originalChars,
160
+ ...(advisoryEviction ? { advisory: "brain_irrelevant" } : {}),
146
161
  });
147
162
  }
148
163
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"context-prompt-enforcement.js","sourceRoot":"","sources":["../../../src/core/context/context-prompt-enforcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First enforcement pilot for the context-policy layer (opt-in, default disabled). Unlike\n * context-audit.ts/context-prompt-policy.ts (both strictly observe-only), this module CAN\n * change the provider-visible message array -- but only ever via stub-in-place on\n * artifact-backed tool_output results, never by removing a message or breaking\n * assistant/toolResult pairing. It never touches the transcript, never releases/reclaims\n * artifact references, and never writes a new artifact -- it only replaces the visible\n * text of an already artifact-backed message with a bounded pointer to the existing\n * artifact, retrievable via the `artifact_retrieve` tool.\n *\n * Eligibility for stubbing is deliberately conservative (see `enforcePromptPolicy`): the\n * setting must be enabled, the item must be outside the recent-message safety window, not\n * an errored tool result, not already stubbed by this module or already packed by legacy\n * context-gc this turn, must have a resolvable artifact id, the `artifact_retrieve` tool\n * must actually be active this turn, and must clear `hardConstraints.dropFromPrompt` (see\n * below for why that specific action, not `pack_to_artifact`).\n *\n * Why `dropFromPrompt`, not `packToArtifact`: this operation does not create a new\n * artifact -- it reuses the ref an earlier `pack_to_artifact` capture already produced (see\n * tool-output-artifacts.md's \"measure -> digest/preview/artifact -> prompt item\" pipeline).\n * `drop_from_prompt` requires an existing retrieval path and is exactly the operation being\n * performed (evicting raw content from the live prompt in favor of that existing path);\n * `pack_to_artifact` is the distinct first-capture operation, which we never invoke here.\n *\n * Why `retrievalToolAvailable` is checked separately from `hasAvailableRetrievalPath`: the\n * latter only proves the artifact still exists in the store; it says nothing about whether\n * the model can currently act on the stub's instruction to call `artifact_retrieve`.\n * `artifact_retrieve` is a companion affordance (auto-activated alongside grep/find, not a\n * default/global tool -- see agent-session.ts's companion-activation enforcement), so active\n * tools can differ turn to turn. Stubbing content with an unactionable pointer would be\n * strictly worse than leaving the raw content in place.\n */\n\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport type { PromptPolicyShadowReport } from \"./context-prompt-policy.ts\";\n\nexport interface ContextPromptEnforcementSettings {\n\tenabled: boolean;\n\tpreserveRecentMessages: number;\n\tminChars: number;\n\t/**\n\t * Whether the `artifact_retrieve` tool is actually active this turn -- a runtime fact,\n\t * not a persisted setting. Callers must derive this from the live active-tool set (e.g.\n\t * `AgentSession.getActiveToolNames().includes(\"artifact_retrieve\")`), never assume it.\n\t */\n\tretrievalToolAvailable: boolean;\n}\n\nexport type PromptEnforcementSkipReason =\n\t| \"message_mismatch\"\n\t| \"within_recent_window\"\n\t| \"errored_tool_result\"\n\t| \"already_stubbed_or_packed\"\n\t| \"not_artifact_backed\"\n\t| \"retrieval_tool_unavailable\"\n\t| \"hard_constraint_rejected\"\n\t| \"missing_artifact_id\"\n\t| \"below_min_chars\";\n\nexport interface PromptEnforcementItemReport {\n\titemId: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\tenforced: boolean;\n\taction?: \"artifact_stub\";\n\tartifactId?: string;\n\toriginalChars?: number;\n\tskipReason?: PromptEnforcementSkipReason;\n}\n\nexport interface PromptEnforcementReport {\n\tturnIndex: number;\n\titems: PromptEnforcementItemReport[];\n}\n\nexport interface EnforcePromptPolicyResult {\n\tmessages: AgentMessage[];\n\treport: PromptEnforcementReport;\n}\n\nfunction extractDetailsArtifactId(details: unknown): string | undefined {\n\tif (typeof details !== \"object\" || details === null) return undefined;\n\tconst artifactId = (details as { artifactId?: unknown }).artifactId;\n\treturn typeof artifactId === \"string\" ? artifactId : undefined;\n}\n\n/** True if legacy context-gc already packed this message this turn, or this module already stubbed it. */\nfunction isAlreadyStubbedOrPacked(details: unknown): boolean {\n\tif (typeof details !== \"object\" || details === null) return false;\n\tconst record = details as { promptPolicy?: { enforced?: unknown }; contextGc?: { packed?: unknown } };\n\treturn record.promptPolicy?.enforced === true || record.contextGc?.packed === true;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\") parts.push(part.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction buildStubText(toolName: string, originalChars: number, artifactId: string): string {\n\treturn `[content replaced by prompt-policy: originally ${originalChars} chars from a stale ${toolName} tool result. Retrieve the full output with artifact_retrieve using artifactId \"${artifactId}\".]`;\n}\n\nfunction skip(\n\titem: { itemId: string; toolCallId: string; messageIndex: number },\n\tskipReason: PromptEnforcementSkipReason,\n\textra?: { artifactId?: string; originalChars?: number },\n): PromptEnforcementItemReport {\n\treturn {\n\t\titemId: item.itemId,\n\t\ttoolCallId: item.toolCallId,\n\t\tmessageIndex: item.messageIndex,\n\t\tenforced: false,\n\t\tskipReason,\n\t\t...extra,\n\t};\n}\n\n/**\n * Apply the first enforcement pilot to `messages` (expected to be the provider-visible\n * array after existing context-gc has already run). Returns a new array only when at least\n * one item was actually stubbed; otherwise returns the same `messages` reference unchanged\n * (in particular, always true when `settings.enabled` is false). Never mutates `messages`\n * or any message object within it -- every stubbed entry is a fresh object.\n */\nexport function enforcePromptPolicy(\n\tmessages: AgentMessage[],\n\tshadowReport: PromptPolicyShadowReport,\n\tsettings: ContextPromptEnforcementSettings,\n): EnforcePromptPolicyResult {\n\tif (!settings.enabled) {\n\t\treturn { messages, report: { turnIndex: shadowReport.turnIndex, items: [] } };\n\t}\n\n\tconst recentCutoffIndex = Math.max(0, messages.length - settings.preserveRecentMessages);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\tconst items: PromptEnforcementItemReport[] = [];\n\n\tfor (const planItem of shadowReport.items) {\n\t\tconst message = messages[planItem.messageIndex];\n\t\tif (!message || message.role !== \"toolResult\" || message.toolCallId !== planItem.toolCallId) {\n\t\t\titems.push(skip(planItem, \"message_mismatch\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.messageIndex >= recentCutoffIndex) {\n\t\t\titems.push(skip(planItem, \"within_recent_window\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (message.isError) {\n\t\t\titems.push(skip(planItem, \"errored_tool_result\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (isAlreadyStubbedOrPacked(message.details)) {\n\t\t\titems.push(skip(planItem, \"already_stubbed_or_packed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!planItem.hasAvailableRetrievalPath) {\n\t\t\titems.push(skip(planItem, \"not_artifact_backed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!settings.retrievalToolAvailable) {\n\t\t\titems.push(skip(planItem, \"retrieval_tool_unavailable\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.hardConstraints.dropFromPrompt.length > 0) {\n\t\t\titems.push(skip(planItem, \"hard_constraint_rejected\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst artifactId = extractDetailsArtifactId(message.details);\n\t\tif (!artifactId) {\n\t\t\titems.push(skip(planItem, \"missing_artifact_id\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst originalChars = toolResultText(message).length;\n\t\tif (originalChars < settings.minChars) {\n\t\t\titems.push(skip(planItem, \"below_min_chars\", { artifactId, originalChars }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existingDetails = typeof message.details === \"object\" && message.details !== null ? message.details : {};\n\t\tnextMessages[planItem.messageIndex] = {\n\t\t\t...message,\n\t\t\tcontent: [{ type: \"text\", text: buildStubText(message.toolName, originalChars, artifactId) }],\n\t\t\tdetails: {\n\t\t\t\t...existingDetails,\n\t\t\t\tpromptPolicy: {\n\t\t\t\t\tenforced: true,\n\t\t\t\t\taction: \"artifact_stub\",\n\t\t\t\t\tartifactId,\n\t\t\t\t\toriginalChars,\n\t\t\t\t\treason: \"stale_artifact_backed_tool_output\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tchanged = true;\n\t\titems.push({\n\t\t\titemId: planItem.itemId,\n\t\t\ttoolCallId: planItem.toolCallId,\n\t\t\tmessageIndex: planItem.messageIndex,\n\t\t\tenforced: true,\n\t\t\taction: \"artifact_stub\",\n\t\t\tartifactId,\n\t\t\toriginalChars,\n\t\t});\n\t}\n\n\treturn {\n\t\tmessages: changed ? nextMessages : messages,\n\t\treport: { turnIndex: shadowReport.turnIndex, items },\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-prompt-enforcement.js","sourceRoot":"","sources":["../../../src/core/context/context-prompt-enforcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First enforcement pilot for the context-policy layer (opt-in, default disabled). Unlike\n * context-audit.ts/context-prompt-policy.ts (both strictly observe-only), this module CAN\n * change the provider-visible message array -- but only ever via stub-in-place on\n * artifact-backed tool_output results, never by removing a message or breaking\n * assistant/toolResult pairing. It never touches the transcript, never releases/reclaims\n * artifact references, and never writes a new artifact -- it only replaces the visible\n * text of an already artifact-backed message with a bounded pointer to the existing\n * artifact, retrievable via the `artifact_retrieve` tool.\n *\n * Eligibility for stubbing is deliberately conservative (see `enforcePromptPolicy`): the\n * setting must be enabled, the item must be outside the recent-message safety window, not\n * an errored tool result, not already stubbed by this module or already packed by legacy\n * context-gc this turn, must have a resolvable artifact id, the `artifact_retrieve` tool\n * must actually be active this turn, and must clear `hardConstraints.dropFromPrompt` (see\n * below for why that specific action, not `pack_to_artifact`).\n *\n * Why `dropFromPrompt`, not `packToArtifact`: this operation does not create a new\n * artifact -- it reuses the ref an earlier `pack_to_artifact` capture already produced (see\n * tool-output-artifacts.md's \"measure -> digest/preview/artifact -> prompt item\" pipeline).\n * `drop_from_prompt` requires an existing retrieval path and is exactly the operation being\n * performed (evicting raw content from the live prompt in favor of that existing path);\n * `pack_to_artifact` is the distinct first-capture operation, which we never invoke here.\n *\n * Why `retrievalToolAvailable` is checked separately from `hasAvailableRetrievalPath`: the\n * latter only proves the artifact still exists in the store; it says nothing about whether\n * the model can currently act on the stub's instruction to call `artifact_retrieve`.\n * `artifact_retrieve` is a companion affordance (auto-activated alongside grep/find, not a\n * default/global tool -- see agent-session.ts's companion-activation enforcement), so active\n * tools can differ turn to turn. Stubbing content with an unactionable pointer would be\n * strictly worse than leaving the raw content in place.\n */\n\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport { CURATION_RELEVANCE_MIN_CONFIDENCE } from \"./brain-curator.ts\";\nimport type { PromptPolicyShadowReport } from \"./context-prompt-policy.ts\";\n\nexport interface ContextPromptEnforcementSettings {\n\tenabled: boolean;\n\tpreserveRecentMessages: number;\n\tminChars: number;\n\t/**\n\t * Whether the `artifact_retrieve` tool is actually active this turn -- a runtime fact,\n\t * not a persisted setting. Callers must derive this from the live active-tool set (e.g.\n\t * `AgentSession.getActiveToolNames().includes(\"artifact_retrieve\")`), never assume it.\n\t */\n\tretrievalToolAvailable: boolean;\n\t/**\n\t * Brain-curator relevance lookup (runtime fact, like `retrievalToolAvailable`; never\n\t * persisted). ASYMMETRIC by design: an explicit high-confidence irrelevance verdict may\n\t * evict an otherwise-eligible item from within the recent window (never past the absolute\n\t * floor), but an advisory can never keep an item the policy wants gone, never stub a\n\t * hard-constraint-protected item, and its absence is byte-for-byte today's behavior.\n\t */\n\tbrainRelevance?: (itemId: string) => { relevant: boolean; confidence: number } | undefined;\n}\n\nexport type PromptEnforcementSkipReason =\n\t| \"message_mismatch\"\n\t| \"within_recent_window\"\n\t| \"errored_tool_result\"\n\t| \"already_stubbed_or_packed\"\n\t| \"not_artifact_backed\"\n\t| \"retrieval_tool_unavailable\"\n\t| \"hard_constraint_rejected\"\n\t| \"missing_artifact_id\"\n\t| \"below_min_chars\";\n\nexport interface PromptEnforcementItemReport {\n\titemId: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\tenforced: boolean;\n\taction?: \"artifact_stub\";\n\tartifactId?: string;\n\toriginalChars?: number;\n\tskipReason?: PromptEnforcementSkipReason;\n\t/** Set when a brain-curator irrelevance verdict allowed eviction inside the recent window. */\n\tadvisory?: \"brain_irrelevant\";\n}\n\nexport interface PromptEnforcementReport {\n\tturnIndex: number;\n\titems: PromptEnforcementItemReport[];\n}\n\nexport interface EnforcePromptPolicyResult {\n\tmessages: AgentMessage[];\n\treport: PromptEnforcementReport;\n}\n\nconst ENFORCEMENT_ABSOLUTE_RECENT_FLOOR = 4;\n\nfunction extractDetailsArtifactId(details: unknown): string | undefined {\n\tif (typeof details !== \"object\" || details === null) return undefined;\n\tconst artifactId = (details as { artifactId?: unknown }).artifactId;\n\treturn typeof artifactId === \"string\" ? artifactId : undefined;\n}\n\n/** True if legacy context-gc already packed this message this turn, or this module already stubbed it. */\nfunction isAlreadyStubbedOrPacked(details: unknown): boolean {\n\tif (typeof details !== \"object\" || details === null) return false;\n\tconst record = details as { promptPolicy?: { enforced?: unknown }; contextGc?: { packed?: unknown } };\n\treturn record.promptPolicy?.enforced === true || record.contextGc?.packed === true;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\") parts.push(part.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction buildStubText(toolName: string, originalChars: number, artifactId: string): string {\n\treturn `[content replaced by prompt-policy: originally ${originalChars} chars from a stale ${toolName} tool result. Retrieve the full output with artifact_retrieve using artifactId \"${artifactId}\".]`;\n}\n\nfunction skip(\n\titem: { itemId: string; toolCallId: string; messageIndex: number },\n\tskipReason: PromptEnforcementSkipReason,\n\textra?: { artifactId?: string; originalChars?: number },\n): PromptEnforcementItemReport {\n\treturn {\n\t\titemId: item.itemId,\n\t\ttoolCallId: item.toolCallId,\n\t\tmessageIndex: item.messageIndex,\n\t\tenforced: false,\n\t\tskipReason,\n\t\t...extra,\n\t};\n}\n\n/**\n * Apply the first enforcement pilot to `messages` (expected to be the provider-visible\n * array after existing context-gc has already run). Returns a new array only when at least\n * one item was actually stubbed; otherwise returns the same `messages` reference unchanged\n * (in particular, always true when `settings.enabled` is false). Never mutates `messages`\n * or any message object within it -- every stubbed entry is a fresh object.\n */\nexport function enforcePromptPolicy(\n\tmessages: AgentMessage[],\n\tshadowReport: PromptPolicyShadowReport,\n\tsettings: ContextPromptEnforcementSettings,\n): EnforcePromptPolicyResult {\n\tif (!settings.enabled) {\n\t\treturn { messages, report: { turnIndex: shadowReport.turnIndex, items: [] } };\n\t}\n\n\tconst recentCutoffIndex = Math.max(0, messages.length - settings.preserveRecentMessages);\n\t// Advisory evictions may reach inside the recent window but NEVER past this absolute floor:\n\t// the last few messages are what the model is actively reasoning over.\n\tconst absoluteFloorIndex = Math.max(0, messages.length - ENFORCEMENT_ABSOLUTE_RECENT_FLOOR);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\tconst items: PromptEnforcementItemReport[] = [];\n\n\tfor (const planItem of shadowReport.items) {\n\t\tconst message = messages[planItem.messageIndex];\n\t\tif (!message || message.role !== \"toolResult\" || message.toolCallId !== planItem.toolCallId) {\n\t\t\titems.push(skip(planItem, \"message_mismatch\"));\n\t\t\tcontinue;\n\t\t}\n\t\tlet advisoryEviction = false;\n\t\tif (planItem.messageIndex >= recentCutoffIndex) {\n\t\t\tconst advisory = settings.brainRelevance?.(planItem.itemId);\n\t\t\tadvisoryEviction =\n\t\t\t\tadvisory !== undefined &&\n\t\t\t\t!advisory.relevant &&\n\t\t\t\tadvisory.confidence >= CURATION_RELEVANCE_MIN_CONFIDENCE &&\n\t\t\t\tplanItem.messageIndex < absoluteFloorIndex;\n\t\t\tif (!advisoryEviction) {\n\t\t\t\titems.push(skip(planItem, \"within_recent_window\"));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (message.isError) {\n\t\t\titems.push(skip(planItem, \"errored_tool_result\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (isAlreadyStubbedOrPacked(message.details)) {\n\t\t\titems.push(skip(planItem, \"already_stubbed_or_packed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!planItem.hasAvailableRetrievalPath) {\n\t\t\titems.push(skip(planItem, \"not_artifact_backed\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!settings.retrievalToolAvailable) {\n\t\t\titems.push(skip(planItem, \"retrieval_tool_unavailable\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (planItem.hardConstraints.dropFromPrompt.length > 0) {\n\t\t\titems.push(skip(planItem, \"hard_constraint_rejected\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst artifactId = extractDetailsArtifactId(message.details);\n\t\tif (!artifactId) {\n\t\t\titems.push(skip(planItem, \"missing_artifact_id\"));\n\t\t\tcontinue;\n\t\t}\n\t\tconst originalChars = toolResultText(message).length;\n\t\tif (originalChars < settings.minChars) {\n\t\t\titems.push(skip(planItem, \"below_min_chars\", { artifactId, originalChars }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existingDetails = typeof message.details === \"object\" && message.details !== null ? message.details : {};\n\t\tnextMessages[planItem.messageIndex] = {\n\t\t\t...message,\n\t\t\tcontent: [{ type: \"text\", text: buildStubText(message.toolName, originalChars, artifactId) }],\n\t\t\tdetails: {\n\t\t\t\t...existingDetails,\n\t\t\t\tpromptPolicy: {\n\t\t\t\t\tenforced: true,\n\t\t\t\t\taction: \"artifact_stub\",\n\t\t\t\t\tartifactId,\n\t\t\t\t\toriginalChars,\n\t\t\t\t\treason: \"stale_artifact_backed_tool_output\",\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tchanged = true;\n\t\titems.push({\n\t\t\titemId: planItem.itemId,\n\t\t\ttoolCallId: planItem.toolCallId,\n\t\t\tmessageIndex: planItem.messageIndex,\n\t\t\tenforced: true,\n\t\t\taction: \"artifact_stub\",\n\t\t\tartifactId,\n\t\t\toriginalChars,\n\t\t\t...(advisoryEviction ? { advisory: \"brain_irrelevant\" as const } : {}),\n\t\t});\n\t}\n\n\treturn {\n\t\tmessages: changed ? nextMessages : messages,\n\t\treport: { turnIndex: shadowReport.turnIndex, items },\n\t};\n}\n"]}
@@ -22,10 +22,20 @@ export interface ContextGcSettings {
22
22
  export interface NormalizedContextGcSettings extends Omit<Required<ContextGcSettings>, "semanticMemory"> {
23
23
  semanticMemory: Required<SemanticMemoryGcSettings>;
24
24
  }
25
+ /**
26
+ * Brain-curation hooks (both optional; absent hooks are byte-for-byte today's behavior).
27
+ * `resolveDigest` is a pure lookup keyed by the record's content hash; `onPacked` lets the
28
+ * caller enqueue digest work with the exact original text at the moment it is packed.
29
+ */
30
+ export interface ContextGcCurationHooks {
31
+ resolveDigest?: (digestKey: string) => string | undefined;
32
+ onPacked?: (record: ContextGcPackedRecord, originalText: string) => void;
33
+ }
25
34
  export interface ContextGcOptions extends NormalizedContextGcSettings {
26
35
  cwd: string;
27
36
  storageDir?: string;
28
37
  writePayloads?: boolean;
38
+ curation?: ContextGcCurationHooks;
29
39
  }
30
40
  export interface ContextGcPackedRecord {
31
41
  toolName: string;
@@ -39,6 +49,8 @@ export interface ContextGcPackedRecord {
39
49
  path?: string;
40
50
  command?: string;
41
51
  key?: string;
52
+ /** Brain-curator semantic digest of the packed content (model-generated; advisory only). */
53
+ digest?: string;
42
54
  }
43
55
  export interface ContextGcReport {
44
56
  enabled: boolean;
@@ -58,5 +70,6 @@ export declare function applyContextGc(messages: AgentMessage[], rawSettings: Co
58
70
  cwd?: string;
59
71
  storageDir?: string;
60
72
  writePayloads?: boolean;
73
+ curation?: ContextGcCurationHooks;
61
74
  }): ContextGcResult;
62
75
  //# sourceMappingURL=context-gc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAK9D,MAAM,WAAW,wBAAwB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,sFAAsF;IACtF,cAAc,CAAC,EAAE,wBAAwB,CAAC;CAC1C;AAED,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;IACvG,cAAc,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,gBAAiB,SAAQ,2BAA2B;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,uBAAuB,CAAC;IAC1E,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC;CACxB;AAuBD,eAAO,MAAM,2BAA2B,EAAE,2BA4BzC,CAAC;AA6CF,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,2BAA2B,CAE9F;AAkND,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,YAAY,EAAE,EACxB,WAAW,EAAE,iBAAiB,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,eAAe,CA6GjB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport { normalizePath } from \"../utils/paths.ts\";\nimport { estimateTokens } from \"./compaction/compaction.ts\";\n\nexport interface SemanticMemoryGcSettings {\n\tenabled?: boolean;\n\t/** Number of newest Automata/Mind injected pages to preserve verbatim. */\n\tpreserveRecentPages?: number;\n\t/** Minimum provider-visible text chars before a stale semantic memory page is packed. */\n\tminChars?: number;\n\t/** Markers that identify deterministic Automata/Mind context pages. */\n\tmarkers?: string[];\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean;\n\t/** Number of most recent AgentMessage rows to preserve verbatim. */\n\tpreserveRecentMessages?: number;\n\t/** Minimum provider-visible text chars before a stale tool result is packed. */\n\tminToolResultChars?: number;\n\t/** Tool names eligible for stale result packing. */\n\ttools?: string[];\n\t/** Provider-context control for deterministic Automata/Mind semantic memory pages. */\n\tsemanticMemory?: SemanticMemoryGcSettings;\n}\n\nexport interface NormalizedContextGcSettings extends Omit<Required<ContextGcSettings>, \"semanticMemory\"> {\n\tsemanticMemory: Required<SemanticMemoryGcSettings>;\n}\n\nexport interface ContextGcOptions extends NormalizedContextGcSettings {\n\tcwd: string;\n\tstorageDir?: string;\n\twritePayloads?: boolean;\n}\n\nexport interface ContextGcPackedRecord {\n\ttoolName: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\treason: \"superseded-read\" | \"stale-tool-result\" | \"stale-semantic-memory\";\n\toriginalChars: number;\n\toriginalTokens: number;\n\tpackedTokens: number;\n\tstoragePath?: string;\n\tpath?: string;\n\tcommand?: string;\n\tkey?: string;\n}\n\nexport interface ContextGcReport {\n\tenabled: boolean;\n\tpackedCount: number;\n\toriginalTokens: number;\n\tpackedTokens: number;\n\tsavedTokens: number;\n\trecords: ContextGcPackedRecord[];\n}\n\nexport interface ContextGcResult {\n\tmessages: AgentMessage[];\n\treport: ContextGcReport;\n}\n\nconst DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS: Required<SemanticMemoryGcSettings> = {\n\tenabled: true,\n\tpreserveRecentPages: 1,\n\tminChars: 900,\n\tmarkers: [\n\t\t// Generic memory-subsystem recall page marker (brand-free). Provider-specific markers are\n\t\t// merged in dynamically at runtime via MemoryManager.getContextMarkers().\n\t\t\"<memory_context\",\n\t\t// Pre-existing provider-specific markers (to be generalized to provider-declared markers).\n\t\t\"<automata_context\",\n\t\t\"<automata_response\",\n\t\t\"<automata_query\",\n\t\t\"<automata_fetch\",\n\t\t\"<memory_lifecycle_audit\",\n\t\t\"<memory_lifecycle_purge\",\n\t\t\"<automata_doctor\",\n\t\t\"<automata_optimizer\",\n\t\t\"<automata_mesh\",\n\t],\n};\n\nexport const DEFAULT_CONTEXT_GC_SETTINGS: NormalizedContextGcSettings = {\n\tenabled: true,\n\tpreserveRecentMessages: 8,\n\tminToolResultChars: 1200,\n\ttools: [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"rg\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"skill_open\",\n\t\t\"automata_graph_status\",\n\t\t\"automata_graph_search\",\n\t\t\"automata_graph_query\",\n\t\t\"automata_graph_neighbors\",\n\t\t\"automata_graph_path\",\n\t\t\"automata_graph_pointer_pack\",\n\t\t\"learning_query_memory\",\n\t\t\"subagent\",\n\t\t\"task_steps\",\n\t\t\"task_background\",\n\t\t\"task_goal\",\n\t\t\"run_ledger\",\n\t\t\"context_headroom_retrieve\",\n\t\t\"headroom_retrieve\",\n\t],\n\tsemanticMemory: DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS,\n};\n\ntype ToolCallMeta = {\n\tid: string;\n\tname: string;\n\targs: Record<string, unknown>;\n\tmessageIndex: number;\n};\n\nfunction cap(text: string, limit = 220): string {\n\tconst compact = text.replace(/\\s+/g, \" \").trim();\n\treturn compact.length > limit ? `${compact.slice(0, Math.max(0, limit - 1))}…` : compact;\n}\n\nfunction normalizeSemanticMemoryGcSettings(settings?: SemanticMemoryGcSettings): Required<SemanticMemoryGcSettings> {\n\treturn {\n\t\tenabled: settings?.enabled ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.enabled,\n\t\tpreserveRecentPages: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.preserveRecentPages ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.preserveRecentPages),\n\t\t),\n\t\tminChars: Math.max(0, Math.floor(settings?.minChars ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.minChars)),\n\t\tmarkers:\n\t\t\tsettings?.markers && settings.markers.length > 0\n\t\t\t\t? settings.markers\n\t\t\t\t: DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.markers,\n\t};\n}\n\nfunction normalizeContextGcSettings(settings?: ContextGcSettings): NormalizedContextGcSettings {\n\treturn {\n\t\tenabled: settings?.enabled ?? DEFAULT_CONTEXT_GC_SETTINGS.enabled,\n\t\tpreserveRecentMessages: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.preserveRecentMessages ?? DEFAULT_CONTEXT_GC_SETTINGS.preserveRecentMessages),\n\t\t),\n\t\tminToolResultChars: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.minToolResultChars ?? DEFAULT_CONTEXT_GC_SETTINGS.minToolResultChars),\n\t\t),\n\t\ttools: settings?.tools && settings.tools.length > 0 ? settings.tools : DEFAULT_CONTEXT_GC_SETTINGS.tools,\n\t\tsemanticMemory: normalizeSemanticMemoryGcSettings(settings?.semanticMemory),\n\t};\n}\n\nexport function getContextGcSettings(settings?: ContextGcSettings): NormalizedContextGcSettings {\n\treturn normalizeContextGcSettings(settings);\n}\n\nfunction textContentParts(content: unknown): string[] | undefined {\n\tif (typeof content === \"string\") return [content];\n\tif (!Array.isArray(content)) return undefined;\n\tconst parts: string[] = [];\n\tfor (const part of content) {\n\t\tif (typeof part !== \"object\" || part === null) return undefined;\n\t\tconst typed = part as { type?: string; text?: string; mimeType?: string };\n\t\tif (typed.type === \"text\" && typeof typed.text === \"string\") parts.push(typed.text);\n\t\telse if (typed.type === \"image\") return undefined;\n\t\telse return undefined;\n\t}\n\treturn parts;\n}\n\nfunction contentText(content: unknown): string | undefined {\n\tif (typeof content === \"string\") return content;\n\treturn textContentParts(content)?.join(\"\\n\");\n}\n\nfunction toolResultParts(message: ToolResultMessage): string[] {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\" && part.text) parts.push(part.text);\n\t\telse if (part.type === \"image\") parts.push(`[image ${part.mimeType}]`);\n\t}\n\treturn parts;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\treturn toolResultParts(message).join(\"\\n\");\n}\n\nfunction smallStringSlice(value: string, start?: number, end?: number): string {\n\tconst sliced = value.slice(start, end);\n\treturn sliced ? ` ${sliced}`.slice(1) : \"\";\n}\n\nfunction joinedPartsContainMarker(parts: string[], marker: string): boolean {\n\tif (marker.length === 0) return true;\n\tconst tailLength = marker.length - 1;\n\tlet tail = \"\";\n\tlet first = true;\n\tfor (const part of parts) {\n\t\tif (part.includes(marker)) return true;\n\t\tif (!first && `${tail}\\n${smallStringSlice(part, 0, tailLength)}`.includes(marker)) return true;\n\t\tif (tailLength === 0) tail = \"\";\n\t\telse if (part.length >= tailLength) tail = smallStringSlice(part, -tailLength);\n\t\telse tail = `${tail}${first ? \"\" : \"\\n\"}${part}`.slice(-tailLength);\n\t\tfirst = false;\n\t}\n\treturn false;\n}\n\nfunction joinedPartsContainAnyMarker(parts: string[], markers: readonly string[]): boolean {\n\treturn markers.some((marker) => joinedPartsContainMarker(parts, marker));\n}\n\nfunction isSemanticMemoryCustomMessage(message: AgentMessage): boolean {\n\tif (message.role !== \"custom\") return false;\n\tconst customType = String((message as { customType?: unknown }).customType ?? \"\").toLowerCase();\n\treturn customType.includes(\"automata\") || customType.includes(\"memory\") || customType.includes(\"mind\");\n}\n\nfunction agentMessageText(message: AgentMessage): string | undefined {\n\tif (message.role === \"toolResult\") return toolResultText(message);\n\tif (isSemanticMemoryCustomMessage(message)) return contentText((message as { content?: unknown }).content);\n\treturn undefined;\n}\n\nfunction semanticMessageHasMarker(message: AgentMessage, settings: Required<SemanticMemoryGcSettings>): boolean {\n\tif (message.role === \"toolResult\") return joinedPartsContainAnyMarker(toolResultParts(message), settings.markers);\n\tif (isSemanticMemoryCustomMessage(message)) {\n\t\tconst parts = textContentParts((message as { content?: unknown }).content);\n\t\treturn parts ? joinedPartsContainAnyMarker(parts, settings.markers) : false;\n\t}\n\treturn false;\n}\n\ninterface ContextGcPlan {\n\tcalls: Map<string, ToolCallMeta>;\n\tlatestReadByPath: Map<string, string>;\n\tsemanticIndexes: number[];\n}\n\nfunction normalizeToolPath(cwd: string, value: unknown): string | undefined {\n\tif (typeof value !== \"string\" || value.trim() === \"\") return undefined;\n\tconst path = value.trim();\n\treturn normalizePath(isAbsolute(path) ? path : resolve(cwd, path));\n}\n\nfunction collectContextGcPlan(\n\tmessages: AgentMessage[],\n\tcwd: string,\n\tsemanticSettings: Required<SemanticMemoryGcSettings>,\n): ContextGcPlan {\n\tconst calls = new Map<string, ToolCallMeta>();\n\tconst readResultCallIds: string[] = [];\n\tconst semanticIndexes: number[] = [];\n\n\tfor (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {\n\t\tconst message = messages[messageIndex];\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type !== \"toolCall\") continue;\n\t\t\t\tcalls.set(part.id, {\n\t\t\t\t\tid: part.id,\n\t\t\t\t\tname: part.name,\n\t\t\t\t\targs: part.arguments ?? {},\n\t\t\t\t\tmessageIndex,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (message.role === \"toolResult\" && message.toolName === \"read\") {\n\t\t\treadResultCallIds.push(message.toolCallId);\n\t\t}\n\n\t\tif (semanticSettings.enabled && semanticMessageHasMarker(message, semanticSettings)) {\n\t\t\tsemanticIndexes.push(messageIndex);\n\t\t}\n\t}\n\n\tconst latestReadByPath = new Map<string, string>();\n\tfor (const toolCallId of readResultCallIds) {\n\t\tconst call = calls.get(toolCallId);\n\t\tconst path = normalizeToolPath(cwd, call?.args.path);\n\t\tif (path) latestReadByPath.set(path, toolCallId);\n\t}\n\n\treturn { calls, latestReadByPath, semanticIndexes };\n}\n\nfunction storagePathFor(storageDir: string | undefined, key: string): string | undefined {\n\tif (!storageDir || !isAbsolute(storageDir)) return undefined;\n\treturn resolve(storageDir, `${key}.txt`);\n}\n\nfunction maybeStoreOriginal(options: ContextGcOptions, key: string, original: string): string | undefined {\n\tconst path = storagePathFor(options.storageDir, key);\n\tif (!path || !options.writePayloads) return path;\n\ttry {\n\t\tmkdirSync(options.storageDir!, { recursive: true });\n\t\tif (!existsSync(path)) writeFileSync(path, original, \"utf8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn path;\n}\n\nfunction reasonText(record: ContextGcPackedRecord): string {\n\tif (record.reason === \"superseded-read\") return \"older read snapshot superseded by a later read of the same file\";\n\tif (record.reason === \"stale-semantic-memory\") {\n\t\treturn \"older Automata/Mind semantic context page outside the semantic-memory freshness window\";\n\t}\n\treturn \"stale bulky tool output outside the recent context window\";\n}\n\nfunction buildSummary(record: ContextGcPackedRecord): string {\n\tconst semantic = record.reason === \"stale-semantic-memory\";\n\tconst lines = [\n\t\tsemantic ? \"[Semantic GC packed stale Automata/Mind context page]\" : \"[Context GC packed stale tool result]\",\n\t\tsemantic ? undefined : `tool: ${record.toolName}`,\n\t\trecord.path ? `path: ${record.path}` : undefined,\n\t\trecord.command ? `command: ${cap(record.command)}` : undefined,\n\t\t`reason: ${reasonText(record)}`,\n\t\t`original: ${record.originalChars} chars (~${record.originalTokens} tokens)`,\n\t\trecord.storagePath\n\t\t\t? `exact old provider-visible text stored at: ${record.storagePath}`\n\t\t\t: \"exact old provider-visible text retained in the session log, not inline in provider context\",\n\t\tsemantic\n\t\t\t? \"If this memory context matters, query Automata/Mind again with the same topic/filter or fetch the drawer pointers from the stored page.\"\n\t\t\t: record.path\n\t\t\t\t? \"For current file contents, use the read tool on the path above. For the exact old output, read the stored payload path if present.\"\n\t\t\t\t: \"If this exact old output matters, retrieve/read the stored payload path if present or rerun the tool command.\",\n\t\t\"Do not rely on this summary as the original content.\",\n\t].filter((line): line is string => line !== undefined);\n\treturn lines.join(\"\\n\");\n}\n\nfunction gcDetails(message: { details?: unknown }, record: ContextGcPackedRecord): Record<string, unknown> {\n\treturn {\n\t\t...(typeof message.details === \"object\" && message.details !== null ? message.details : {}),\n\t\tcontextGc: {\n\t\t\tpacked: true,\n\t\t\toriginalChars: record.originalChars,\n\t\t\toriginalTokens: record.originalTokens,\n\t\t\tstoragePath: record.storagePath,\n\t\t\treason: record.reason,\n\t\t},\n\t};\n}\n\nfunction makePackedToolResult(message: ToolResultMessage, record: ContextGcPackedRecord): ToolResultMessage {\n\tconst summary = buildSummary(record);\n\treturn {\n\t\t...message,\n\t\tcontent: [{ type: \"text\", text: summary }],\n\t\tdetails: gcDetails(message, record),\n\t};\n}\n\nfunction makePackedSemanticMemoryMessage(message: AgentMessage, record: ContextGcPackedRecord): AgentMessage {\n\tconst summary = buildSummary(record);\n\treturn {\n\t\t...(message as unknown as Record<string, unknown>),\n\t\tcontent: [{ type: \"text\", text: summary }],\n\t\tdetails: gcDetails(message as { details?: unknown }, record),\n\t} as AgentMessage;\n}\n\nexport function applyContextGc(\n\tmessages: AgentMessage[],\n\trawSettings: ContextGcSettings & { cwd?: string; storageDir?: string; writePayloads?: boolean },\n): ContextGcResult {\n\tconst settings = normalizeContextGcSettings(rawSettings);\n\tconst baseReport: ContextGcReport = {\n\t\tenabled: settings.enabled,\n\t\tpackedCount: 0,\n\t\toriginalTokens: 0,\n\t\tpackedTokens: 0,\n\t\tsavedTokens: 0,\n\t\trecords: [],\n\t};\n\tif (!settings.enabled) return { messages, report: baseReport };\n\n\tconst options: ContextGcOptions = {\n\t\t...settings,\n\t\tcwd: rawSettings.cwd ?? process.cwd(),\n\t\tstorageDir: rawSettings.storageDir,\n\t\twritePayloads: rawSettings.writePayloads ?? true,\n\t};\n\tconst eligibleTools = new Set(options.tools);\n\tconst plan = collectContextGcPlan(messages, options.cwd, options.semanticMemory);\n\tconst recentStart = Math.max(0, messages.length - options.preserveRecentMessages);\n\tconst semanticIndexSet = new Set(plan.semanticIndexes);\n\tconst preservedSemanticIndexes = new Set(\n\t\toptions.semanticMemory.preserveRecentPages > 0\n\t\t\t? plan.semanticIndexes.slice(-options.semanticMemory.preserveRecentPages)\n\t\t\t: [],\n\t);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\n\tfor (let index = 0; index < messages.length; index++) {\n\t\tconst message = messages[index];\n\t\tif (semanticIndexSet.has(index) && !preservedSemanticIndexes.has(index) && index < recentStart) {\n\t\t\tconst originalText = agentMessageText(message);\n\t\t\tif (originalText && originalText.length >= options.semanticMemory.minChars) {\n\t\t\t\tconst originalTokens = estimateTokens(message);\n\t\t\t\tconst key = createHash(\"sha256\")\n\t\t\t\t\t.update(`semantic-memory\\0${index}\\0${originalText}`)\n\t\t\t\t\t.digest(\"hex\")\n\t\t\t\t\t.slice(0, 24);\n\t\t\t\tconst storagePath = maybeStoreOriginal(options, key, originalText);\n\t\t\t\tconst record: ContextGcPackedRecord = {\n\t\t\t\t\ttoolName: \"automata-mind\",\n\t\t\t\t\ttoolCallId: `semantic-${index}`,\n\t\t\t\t\tmessageIndex: index,\n\t\t\t\t\treason: \"stale-semantic-memory\",\n\t\t\t\t\toriginalChars: originalText.length,\n\t\t\t\t\toriginalTokens,\n\t\t\t\t\tpackedTokens: 0,\n\t\t\t\t\tstoragePath,\n\t\t\t\t\tkey,\n\t\t\t\t};\n\t\t\t\tconst packed = makePackedSemanticMemoryMessage(message, record);\n\t\t\t\trecord.packedTokens = estimateTokens(packed);\n\t\t\t\tnextMessages[index] = packed;\n\t\t\t\tbaseReport.records.push(record);\n\t\t\t\tbaseReport.originalTokens += record.originalTokens;\n\t\t\t\tbaseReport.packedTokens += record.packedTokens;\n\t\t\t\tchanged = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (message.role !== \"toolResult\") continue;\n\t\tif (!eligibleTools.has(message.toolName)) continue;\n\t\tif (index >= recentStart) continue;\n\n\t\tconst originalText = toolResultText(message);\n\t\tif (originalText.length < options.minToolResultChars) continue;\n\n\t\tconst call = plan.calls.get(message.toolCallId);\n\t\tconst path = normalizeToolPath(options.cwd, call?.args.path);\n\t\tlet reason: ContextGcPackedRecord[\"reason\"] = \"stale-tool-result\";\n\t\tif (message.toolName === \"read\" && path) {\n\t\t\tif (plan.latestReadByPath.get(path) === message.toolCallId) continue;\n\t\t\treason = \"superseded-read\";\n\t\t}\n\n\t\tconst originalTokens = estimateTokens(message);\n\t\tconst key = createHash(\"sha256\")\n\t\t\t.update(`${message.toolName}\\0${message.toolCallId}\\0${originalText}`)\n\t\t\t.digest(\"hex\")\n\t\t\t.slice(0, 24);\n\t\tconst storagePath = maybeStoreOriginal(options, key, originalText);\n\t\tconst record: ContextGcPackedRecord = {\n\t\t\ttoolName: message.toolName,\n\t\t\ttoolCallId: message.toolCallId,\n\t\t\tmessageIndex: index,\n\t\t\treason,\n\t\t\toriginalChars: originalText.length,\n\t\t\toriginalTokens,\n\t\t\tpackedTokens: 0,\n\t\t\tstoragePath,\n\t\t\tpath,\n\t\t\tcommand: typeof call?.args.command === \"string\" ? call.args.command : undefined,\n\t\t\tkey,\n\t\t};\n\t\tconst packed = makePackedToolResult(message, record);\n\t\trecord.packedTokens = estimateTokens(packed);\n\t\tnextMessages[index] = packed as AgentMessage;\n\t\tbaseReport.records.push(record);\n\t\tbaseReport.originalTokens += record.originalTokens;\n\t\tbaseReport.packedTokens += record.packedTokens;\n\t\tchanged = true;\n\t}\n\n\tbaseReport.packedCount = baseReport.records.length;\n\tbaseReport.savedTokens = Math.max(0, baseReport.originalTokens - baseReport.packedTokens);\n\treturn { messages: changed ? nextMessages : messages, report: baseReport };\n}\n"]}
1
+ {"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAK9D,MAAM,WAAW,wBAAwB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,sFAAsF;IACtF,cAAc,CAAC,EAAE,wBAAwB,CAAC;CAC1C;AAED,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;IACvG,cAAc,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC;CACnD;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CACzE;AAED,MAAM,WAAW,gBAAiB,SAAQ,2BAA2B;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,uBAAuB,CAAC;IAC1E,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC;CACxB;AAuBD,eAAO,MAAM,2BAA2B,EAAE,2BA4BzC,CAAC;AA6CF,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,2BAA2B,CAE9F;AAmND,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,YAAY,EAAE,EACxB,WAAW,EAAE,iBAAiB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACC,eAAe,CAkHjB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@caupulican/pi-agent-core\";\nimport type { ToolResultMessage } from \"@caupulican/pi-ai\";\nimport { normalizePath } from \"../utils/paths.ts\";\nimport { estimateTokens } from \"./compaction/compaction.ts\";\n\nexport interface SemanticMemoryGcSettings {\n\tenabled?: boolean;\n\t/** Number of newest Automata/Mind injected pages to preserve verbatim. */\n\tpreserveRecentPages?: number;\n\t/** Minimum provider-visible text chars before a stale semantic memory page is packed. */\n\tminChars?: number;\n\t/** Markers that identify deterministic Automata/Mind context pages. */\n\tmarkers?: string[];\n}\n\nexport interface ContextGcSettings {\n\tenabled?: boolean;\n\t/** Number of most recent AgentMessage rows to preserve verbatim. */\n\tpreserveRecentMessages?: number;\n\t/** Minimum provider-visible text chars before a stale tool result is packed. */\n\tminToolResultChars?: number;\n\t/** Tool names eligible for stale result packing. */\n\ttools?: string[];\n\t/** Provider-context control for deterministic Automata/Mind semantic memory pages. */\n\tsemanticMemory?: SemanticMemoryGcSettings;\n}\n\nexport interface NormalizedContextGcSettings extends Omit<Required<ContextGcSettings>, \"semanticMemory\"> {\n\tsemanticMemory: Required<SemanticMemoryGcSettings>;\n}\n\n/**\n * Brain-curation hooks (both optional; absent hooks are byte-for-byte today's behavior).\n * `resolveDigest` is a pure lookup keyed by the record's content hash; `onPacked` lets the\n * caller enqueue digest work with the exact original text at the moment it is packed.\n */\nexport interface ContextGcCurationHooks {\n\tresolveDigest?: (digestKey: string) => string | undefined;\n\tonPacked?: (record: ContextGcPackedRecord, originalText: string) => void;\n}\n\nexport interface ContextGcOptions extends NormalizedContextGcSettings {\n\tcwd: string;\n\tstorageDir?: string;\n\twritePayloads?: boolean;\n\tcuration?: ContextGcCurationHooks;\n}\n\nexport interface ContextGcPackedRecord {\n\ttoolName: string;\n\ttoolCallId: string;\n\tmessageIndex: number;\n\treason: \"superseded-read\" | \"stale-tool-result\" | \"stale-semantic-memory\";\n\toriginalChars: number;\n\toriginalTokens: number;\n\tpackedTokens: number;\n\tstoragePath?: string;\n\tpath?: string;\n\tcommand?: string;\n\tkey?: string;\n\t/** Brain-curator semantic digest of the packed content (model-generated; advisory only). */\n\tdigest?: string;\n}\n\nexport interface ContextGcReport {\n\tenabled: boolean;\n\tpackedCount: number;\n\toriginalTokens: number;\n\tpackedTokens: number;\n\tsavedTokens: number;\n\trecords: ContextGcPackedRecord[];\n}\n\nexport interface ContextGcResult {\n\tmessages: AgentMessage[];\n\treport: ContextGcReport;\n}\n\nconst DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS: Required<SemanticMemoryGcSettings> = {\n\tenabled: true,\n\tpreserveRecentPages: 1,\n\tminChars: 900,\n\tmarkers: [\n\t\t// Generic memory-subsystem recall page marker (brand-free). Provider-specific markers are\n\t\t// merged in dynamically at runtime via MemoryManager.getContextMarkers().\n\t\t\"<memory_context\",\n\t\t// Pre-existing provider-specific markers (to be generalized to provider-declared markers).\n\t\t\"<automata_context\",\n\t\t\"<automata_response\",\n\t\t\"<automata_query\",\n\t\t\"<automata_fetch\",\n\t\t\"<memory_lifecycle_audit\",\n\t\t\"<memory_lifecycle_purge\",\n\t\t\"<automata_doctor\",\n\t\t\"<automata_optimizer\",\n\t\t\"<automata_mesh\",\n\t],\n};\n\nexport const DEFAULT_CONTEXT_GC_SETTINGS: NormalizedContextGcSettings = {\n\tenabled: true,\n\tpreserveRecentMessages: 8,\n\tminToolResultChars: 1200,\n\ttools: [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"rg\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"skill_open\",\n\t\t\"automata_graph_status\",\n\t\t\"automata_graph_search\",\n\t\t\"automata_graph_query\",\n\t\t\"automata_graph_neighbors\",\n\t\t\"automata_graph_path\",\n\t\t\"automata_graph_pointer_pack\",\n\t\t\"learning_query_memory\",\n\t\t\"subagent\",\n\t\t\"task_steps\",\n\t\t\"task_background\",\n\t\t\"task_goal\",\n\t\t\"run_ledger\",\n\t\t\"context_headroom_retrieve\",\n\t\t\"headroom_retrieve\",\n\t],\n\tsemanticMemory: DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS,\n};\n\ntype ToolCallMeta = {\n\tid: string;\n\tname: string;\n\targs: Record<string, unknown>;\n\tmessageIndex: number;\n};\n\nfunction cap(text: string, limit = 220): string {\n\tconst compact = text.replace(/\\s+/g, \" \").trim();\n\treturn compact.length > limit ? `${compact.slice(0, Math.max(0, limit - 1))}…` : compact;\n}\n\nfunction normalizeSemanticMemoryGcSettings(settings?: SemanticMemoryGcSettings): Required<SemanticMemoryGcSettings> {\n\treturn {\n\t\tenabled: settings?.enabled ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.enabled,\n\t\tpreserveRecentPages: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.preserveRecentPages ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.preserveRecentPages),\n\t\t),\n\t\tminChars: Math.max(0, Math.floor(settings?.minChars ?? DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.minChars)),\n\t\tmarkers:\n\t\t\tsettings?.markers && settings.markers.length > 0\n\t\t\t\t? settings.markers\n\t\t\t\t: DEFAULT_SEMANTIC_MEMORY_GC_SETTINGS.markers,\n\t};\n}\n\nfunction normalizeContextGcSettings(settings?: ContextGcSettings): NormalizedContextGcSettings {\n\treturn {\n\t\tenabled: settings?.enabled ?? DEFAULT_CONTEXT_GC_SETTINGS.enabled,\n\t\tpreserveRecentMessages: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.preserveRecentMessages ?? DEFAULT_CONTEXT_GC_SETTINGS.preserveRecentMessages),\n\t\t),\n\t\tminToolResultChars: Math.max(\n\t\t\t0,\n\t\t\tMath.floor(settings?.minToolResultChars ?? DEFAULT_CONTEXT_GC_SETTINGS.minToolResultChars),\n\t\t),\n\t\ttools: settings?.tools && settings.tools.length > 0 ? settings.tools : DEFAULT_CONTEXT_GC_SETTINGS.tools,\n\t\tsemanticMemory: normalizeSemanticMemoryGcSettings(settings?.semanticMemory),\n\t};\n}\n\nexport function getContextGcSettings(settings?: ContextGcSettings): NormalizedContextGcSettings {\n\treturn normalizeContextGcSettings(settings);\n}\n\nfunction textContentParts(content: unknown): string[] | undefined {\n\tif (typeof content === \"string\") return [content];\n\tif (!Array.isArray(content)) return undefined;\n\tconst parts: string[] = [];\n\tfor (const part of content) {\n\t\tif (typeof part !== \"object\" || part === null) return undefined;\n\t\tconst typed = part as { type?: string; text?: string; mimeType?: string };\n\t\tif (typed.type === \"text\" && typeof typed.text === \"string\") parts.push(typed.text);\n\t\telse if (typed.type === \"image\") return undefined;\n\t\telse return undefined;\n\t}\n\treturn parts;\n}\n\nfunction contentText(content: unknown): string | undefined {\n\tif (typeof content === \"string\") return content;\n\treturn textContentParts(content)?.join(\"\\n\");\n}\n\nfunction toolResultParts(message: ToolResultMessage): string[] {\n\tconst parts: string[] = [];\n\tfor (const part of message.content) {\n\t\tif (part.type === \"text\" && part.text) parts.push(part.text);\n\t\telse if (part.type === \"image\") parts.push(`[image ${part.mimeType}]`);\n\t}\n\treturn parts;\n}\n\nfunction toolResultText(message: ToolResultMessage): string {\n\treturn toolResultParts(message).join(\"\\n\");\n}\n\nfunction smallStringSlice(value: string, start?: number, end?: number): string {\n\tconst sliced = value.slice(start, end);\n\treturn sliced ? ` ${sliced}`.slice(1) : \"\";\n}\n\nfunction joinedPartsContainMarker(parts: string[], marker: string): boolean {\n\tif (marker.length === 0) return true;\n\tconst tailLength = marker.length - 1;\n\tlet tail = \"\";\n\tlet first = true;\n\tfor (const part of parts) {\n\t\tif (part.includes(marker)) return true;\n\t\tif (!first && `${tail}\\n${smallStringSlice(part, 0, tailLength)}`.includes(marker)) return true;\n\t\tif (tailLength === 0) tail = \"\";\n\t\telse if (part.length >= tailLength) tail = smallStringSlice(part, -tailLength);\n\t\telse tail = `${tail}${first ? \"\" : \"\\n\"}${part}`.slice(-tailLength);\n\t\tfirst = false;\n\t}\n\treturn false;\n}\n\nfunction joinedPartsContainAnyMarker(parts: string[], markers: readonly string[]): boolean {\n\treturn markers.some((marker) => joinedPartsContainMarker(parts, marker));\n}\n\nfunction isSemanticMemoryCustomMessage(message: AgentMessage): boolean {\n\tif (message.role !== \"custom\") return false;\n\tconst customType = String((message as { customType?: unknown }).customType ?? \"\").toLowerCase();\n\treturn customType.includes(\"automata\") || customType.includes(\"memory\") || customType.includes(\"mind\");\n}\n\nfunction agentMessageText(message: AgentMessage): string | undefined {\n\tif (message.role === \"toolResult\") return toolResultText(message);\n\tif (isSemanticMemoryCustomMessage(message)) return contentText((message as { content?: unknown }).content);\n\treturn undefined;\n}\n\nfunction semanticMessageHasMarker(message: AgentMessage, settings: Required<SemanticMemoryGcSettings>): boolean {\n\tif (message.role === \"toolResult\") return joinedPartsContainAnyMarker(toolResultParts(message), settings.markers);\n\tif (isSemanticMemoryCustomMessage(message)) {\n\t\tconst parts = textContentParts((message as { content?: unknown }).content);\n\t\treturn parts ? joinedPartsContainAnyMarker(parts, settings.markers) : false;\n\t}\n\treturn false;\n}\n\ninterface ContextGcPlan {\n\tcalls: Map<string, ToolCallMeta>;\n\tlatestReadByPath: Map<string, string>;\n\tsemanticIndexes: number[];\n}\n\nfunction normalizeToolPath(cwd: string, value: unknown): string | undefined {\n\tif (typeof value !== \"string\" || value.trim() === \"\") return undefined;\n\tconst path = value.trim();\n\treturn normalizePath(isAbsolute(path) ? path : resolve(cwd, path));\n}\n\nfunction collectContextGcPlan(\n\tmessages: AgentMessage[],\n\tcwd: string,\n\tsemanticSettings: Required<SemanticMemoryGcSettings>,\n): ContextGcPlan {\n\tconst calls = new Map<string, ToolCallMeta>();\n\tconst readResultCallIds: string[] = [];\n\tconst semanticIndexes: number[] = [];\n\n\tfor (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {\n\t\tconst message = messages[messageIndex];\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type !== \"toolCall\") continue;\n\t\t\t\tcalls.set(part.id, {\n\t\t\t\t\tid: part.id,\n\t\t\t\t\tname: part.name,\n\t\t\t\t\targs: part.arguments ?? {},\n\t\t\t\t\tmessageIndex,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (message.role === \"toolResult\" && message.toolName === \"read\") {\n\t\t\treadResultCallIds.push(message.toolCallId);\n\t\t}\n\n\t\tif (semanticSettings.enabled && semanticMessageHasMarker(message, semanticSettings)) {\n\t\t\tsemanticIndexes.push(messageIndex);\n\t\t}\n\t}\n\n\tconst latestReadByPath = new Map<string, string>();\n\tfor (const toolCallId of readResultCallIds) {\n\t\tconst call = calls.get(toolCallId);\n\t\tconst path = normalizeToolPath(cwd, call?.args.path);\n\t\tif (path) latestReadByPath.set(path, toolCallId);\n\t}\n\n\treturn { calls, latestReadByPath, semanticIndexes };\n}\n\nfunction storagePathFor(storageDir: string | undefined, key: string): string | undefined {\n\tif (!storageDir || !isAbsolute(storageDir)) return undefined;\n\treturn resolve(storageDir, `${key}.txt`);\n}\n\nfunction maybeStoreOriginal(options: ContextGcOptions, key: string, original: string): string | undefined {\n\tconst path = storagePathFor(options.storageDir, key);\n\tif (!path || !options.writePayloads) return path;\n\ttry {\n\t\tmkdirSync(options.storageDir!, { recursive: true });\n\t\tif (!existsSync(path)) writeFileSync(path, original, \"utf8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn path;\n}\n\nfunction reasonText(record: ContextGcPackedRecord): string {\n\tif (record.reason === \"superseded-read\") return \"older read snapshot superseded by a later read of the same file\";\n\tif (record.reason === \"stale-semantic-memory\") {\n\t\treturn \"older Automata/Mind semantic context page outside the semantic-memory freshness window\";\n\t}\n\treturn \"stale bulky tool output outside the recent context window\";\n}\n\nfunction buildSummary(record: ContextGcPackedRecord): string {\n\tconst semantic = record.reason === \"stale-semantic-memory\";\n\tconst lines = [\n\t\tsemantic ? \"[Semantic GC packed stale Automata/Mind context page]\" : \"[Context GC packed stale tool result]\",\n\t\tsemantic ? undefined : `tool: ${record.toolName}`,\n\t\trecord.path ? `path: ${record.path}` : undefined,\n\t\trecord.command ? `command: ${cap(record.command)}` : undefined,\n\t\t`reason: ${reasonText(record)}`,\n\t\t`original: ${record.originalChars} chars (~${record.originalTokens} tokens)`,\n\t\trecord.digest ? `summary (auto-digest, machine paraphrase, not authoritative): ${record.digest}` : undefined,\n\t\trecord.storagePath\n\t\t\t? `exact old provider-visible text stored at: ${record.storagePath}`\n\t\t\t: \"exact old provider-visible text retained in the session log, not inline in provider context\",\n\t\tsemantic\n\t\t\t? \"If this memory context matters, query Automata/Mind again with the same topic/filter or fetch the drawer pointers from the stored page.\"\n\t\t\t: record.path\n\t\t\t\t? \"For current file contents, use the read tool on the path above. For the exact old output, read the stored payload path if present.\"\n\t\t\t\t: \"If this exact old output matters, retrieve/read the stored payload path if present or rerun the tool command.\",\n\t\t\"Do not rely on this summary as the original content.\",\n\t].filter((line): line is string => line !== undefined);\n\treturn lines.join(\"\\n\");\n}\n\nfunction gcDetails(message: { details?: unknown }, record: ContextGcPackedRecord): Record<string, unknown> {\n\treturn {\n\t\t...(typeof message.details === \"object\" && message.details !== null ? message.details : {}),\n\t\tcontextGc: {\n\t\t\tpacked: true,\n\t\t\toriginalChars: record.originalChars,\n\t\t\toriginalTokens: record.originalTokens,\n\t\t\tstoragePath: record.storagePath,\n\t\t\treason: record.reason,\n\t\t},\n\t};\n}\n\nfunction makePackedToolResult(message: ToolResultMessage, record: ContextGcPackedRecord): ToolResultMessage {\n\tconst summary = buildSummary(record);\n\treturn {\n\t\t...message,\n\t\tcontent: [{ type: \"text\", text: summary }],\n\t\tdetails: gcDetails(message, record),\n\t};\n}\n\nfunction makePackedSemanticMemoryMessage(message: AgentMessage, record: ContextGcPackedRecord): AgentMessage {\n\tconst summary = buildSummary(record);\n\treturn {\n\t\t...(message as unknown as Record<string, unknown>),\n\t\tcontent: [{ type: \"text\", text: summary }],\n\t\tdetails: gcDetails(message as { details?: unknown }, record),\n\t} as AgentMessage;\n}\n\nexport function applyContextGc(\n\tmessages: AgentMessage[],\n\trawSettings: ContextGcSettings & {\n\t\tcwd?: string;\n\t\tstorageDir?: string;\n\t\twritePayloads?: boolean;\n\t\tcuration?: ContextGcCurationHooks;\n\t},\n): ContextGcResult {\n\tconst settings = normalizeContextGcSettings(rawSettings);\n\tconst baseReport: ContextGcReport = {\n\t\tenabled: settings.enabled,\n\t\tpackedCount: 0,\n\t\toriginalTokens: 0,\n\t\tpackedTokens: 0,\n\t\tsavedTokens: 0,\n\t\trecords: [],\n\t};\n\tif (!settings.enabled) return { messages, report: baseReport };\n\n\tconst options: ContextGcOptions = {\n\t\t...settings,\n\t\tcwd: rawSettings.cwd ?? process.cwd(),\n\t\tstorageDir: rawSettings.storageDir,\n\t\twritePayloads: rawSettings.writePayloads ?? true,\n\t\tcuration: rawSettings.curation,\n\t};\n\tconst eligibleTools = new Set(options.tools);\n\tconst plan = collectContextGcPlan(messages, options.cwd, options.semanticMemory);\n\tconst recentStart = Math.max(0, messages.length - options.preserveRecentMessages);\n\tconst semanticIndexSet = new Set(plan.semanticIndexes);\n\tconst preservedSemanticIndexes = new Set(\n\t\toptions.semanticMemory.preserveRecentPages > 0\n\t\t\t? plan.semanticIndexes.slice(-options.semanticMemory.preserveRecentPages)\n\t\t\t: [],\n\t);\n\tconst nextMessages = messages.slice();\n\tlet changed = false;\n\n\tfor (let index = 0; index < messages.length; index++) {\n\t\tconst message = messages[index];\n\t\tif (semanticIndexSet.has(index) && !preservedSemanticIndexes.has(index) && index < recentStart) {\n\t\t\tconst originalText = agentMessageText(message);\n\t\t\tif (originalText && originalText.length >= options.semanticMemory.minChars) {\n\t\t\t\tconst originalTokens = estimateTokens(message);\n\t\t\t\tconst key = createHash(\"sha256\")\n\t\t\t\t\t.update(`semantic-memory\\0${index}\\0${originalText}`)\n\t\t\t\t\t.digest(\"hex\")\n\t\t\t\t\t.slice(0, 24);\n\t\t\t\tconst storagePath = maybeStoreOriginal(options, key, originalText);\n\t\t\t\tconst record: ContextGcPackedRecord = {\n\t\t\t\t\ttoolName: \"automata-mind\",\n\t\t\t\t\ttoolCallId: `semantic-${index}`,\n\t\t\t\t\tmessageIndex: index,\n\t\t\t\t\treason: \"stale-semantic-memory\",\n\t\t\t\t\toriginalChars: originalText.length,\n\t\t\t\t\toriginalTokens,\n\t\t\t\t\tpackedTokens: 0,\n\t\t\t\t\tstoragePath,\n\t\t\t\t\tkey,\n\t\t\t\t};\n\t\t\t\trecord.digest = options.curation?.resolveDigest?.(key);\n\t\t\t\toptions.curation?.onPacked?.(record, originalText);\n\t\t\t\tconst packed = makePackedSemanticMemoryMessage(message, record);\n\t\t\t\trecord.packedTokens = estimateTokens(packed);\n\t\t\t\tnextMessages[index] = packed;\n\t\t\t\tbaseReport.records.push(record);\n\t\t\t\tbaseReport.originalTokens += record.originalTokens;\n\t\t\t\tbaseReport.packedTokens += record.packedTokens;\n\t\t\t\tchanged = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (message.role !== \"toolResult\") continue;\n\t\tif (!eligibleTools.has(message.toolName)) continue;\n\t\tif (index >= recentStart) continue;\n\n\t\tconst originalText = toolResultText(message);\n\t\tif (originalText.length < options.minToolResultChars) continue;\n\n\t\tconst call = plan.calls.get(message.toolCallId);\n\t\tconst path = normalizeToolPath(options.cwd, call?.args.path);\n\t\tlet reason: ContextGcPackedRecord[\"reason\"] = \"stale-tool-result\";\n\t\tif (message.toolName === \"read\" && path) {\n\t\t\tif (plan.latestReadByPath.get(path) === message.toolCallId) continue;\n\t\t\treason = \"superseded-read\";\n\t\t}\n\n\t\tconst originalTokens = estimateTokens(message);\n\t\tconst key = createHash(\"sha256\")\n\t\t\t.update(`${message.toolName}\\0${message.toolCallId}\\0${originalText}`)\n\t\t\t.digest(\"hex\")\n\t\t\t.slice(0, 24);\n\t\tconst storagePath = maybeStoreOriginal(options, key, originalText);\n\t\tconst record: ContextGcPackedRecord = {\n\t\t\ttoolName: message.toolName,\n\t\t\ttoolCallId: message.toolCallId,\n\t\t\tmessageIndex: index,\n\t\t\treason,\n\t\t\toriginalChars: originalText.length,\n\t\t\toriginalTokens,\n\t\t\tpackedTokens: 0,\n\t\t\tstoragePath,\n\t\t\tpath,\n\t\t\tcommand: typeof call?.args.command === \"string\" ? call.args.command : undefined,\n\t\t\tkey,\n\t\t};\n\t\trecord.digest = options.curation?.resolveDigest?.(key);\n\t\toptions.curation?.onPacked?.(record, originalText);\n\t\tconst packed = makePackedToolResult(message, record);\n\t\trecord.packedTokens = estimateTokens(packed);\n\t\tnextMessages[index] = packed as AgentMessage;\n\t\tbaseReport.records.push(record);\n\t\tbaseReport.originalTokens += record.originalTokens;\n\t\tbaseReport.packedTokens += record.packedTokens;\n\t\tchanged = true;\n\t}\n\n\tbaseReport.packedCount = baseReport.records.length;\n\tbaseReport.savedTokens = Math.max(0, baseReport.originalTokens - baseReport.packedTokens);\n\treturn { messages: changed ? nextMessages : messages, report: baseReport };\n}\n"]}
@@ -241,6 +241,7 @@ function buildSummary(record) {
241
241
  record.command ? `command: ${cap(record.command)}` : undefined,
242
242
  `reason: ${reasonText(record)}`,
243
243
  `original: ${record.originalChars} chars (~${record.originalTokens} tokens)`,
244
+ record.digest ? `summary (auto-digest, machine paraphrase, not authoritative): ${record.digest}` : undefined,
244
245
  record.storagePath
245
246
  ? `exact old provider-visible text stored at: ${record.storagePath}`
246
247
  : "exact old provider-visible text retained in the session log, not inline in provider context",
@@ -298,6 +299,7 @@ export function applyContextGc(messages, rawSettings) {
298
299
  cwd: rawSettings.cwd ?? process.cwd(),
299
300
  storageDir: rawSettings.storageDir,
300
301
  writePayloads: rawSettings.writePayloads ?? true,
302
+ curation: rawSettings.curation,
301
303
  };
302
304
  const eligibleTools = new Set(options.tools);
303
305
  const plan = collectContextGcPlan(messages, options.cwd, options.semanticMemory);
@@ -330,6 +332,8 @@ export function applyContextGc(messages, rawSettings) {
330
332
  storagePath,
331
333
  key,
332
334
  };
335
+ record.digest = options.curation?.resolveDigest?.(key);
336
+ options.curation?.onPacked?.(record, originalText);
333
337
  const packed = makePackedSemanticMemoryMessage(message, record);
334
338
  record.packedTokens = estimateTokens(packed);
335
339
  nextMessages[index] = packed;
@@ -376,6 +380,8 @@ export function applyContextGc(messages, rawSettings) {
376
380
  command: typeof call?.args.command === "string" ? call.args.command : undefined,
377
381
  key,
378
382
  };
383
+ record.digest = options.curation?.resolveDigest?.(key);
384
+ options.curation?.onPacked?.(record, originalText);
379
385
  const packed = makePackedToolResult(message, record);
380
386
  record.packedTokens = estimateTokens(packed);
381
387
  nextMessages[index] = packed;