@animus-labs/cortex 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Layer 2: Conversation Summarization.
3
+ *
4
+ * Replaces older conversation history with an LLM-generated summary
5
+ * while preserving a tail of recent turns. Uses the primary model
6
+ * for summarization quality (the conversation history is structurally
7
+ * complex with interleaved tool calls and multi-turn reasoning).
8
+ *
9
+ * Fires at 70% of context window (configurable). Emits lifecycle
10
+ * events (onBeforeCompaction, onPostCompaction) for consumer
11
+ * coordination (e.g., observational memory flush).
12
+ *
13
+ * References:
14
+ * - compaction-strategy.md (Layer 2: Conversation Summarization)
15
+ * - phase-5-compaction.md (5.3)
16
+ */
17
+
18
+ import type { AgentMessage } from '../context-manager.js';
19
+ import type { CompactionConfig, CompactionResult, CompactionTarget } from '../types.js';
20
+ import { estimateTokens } from '../token-estimator.js';
21
+ import { extractTextContent, isToolUseMessage, isToolResultMessage } from './microcompaction.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Defaults
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export const COMPACTION_DEFAULTS: CompactionConfig = {
28
+ threshold: 0.70,
29
+ preserveRecentTurns: 6,
30
+ };
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Summarization prompt
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const DEFAULT_SUMMARIZATION_PROMPT = `Your task is to create a detailed summary of the conversation so far. This summary will replace the conversation history, so it must capture everything needed to continue work without losing context. A small tail of the most recent turns is preserved separately and does not need to be repeated.
37
+
38
+ Before writing your summary, analyze the conversation inside <analysis> tags. Walk through the conversation chronologically and note:
39
+ - Each user request and how it was addressed
40
+ - Key decisions and their rationale
41
+ - Tool calls made, what they returned, and any errors
42
+ - User feedback or corrections (especially when you were told to do something differently)
43
+ - What was being worked on most recently
44
+
45
+ The <analysis> block is a private scratchpad. Keep it concise (a line or two per point). Save all detail for the <summary> block.
46
+
47
+ Then write your summary inside <summary> tags with the following sections:
48
+
49
+ 1. Primary Request and Intent
50
+ Capture all user requests and intents in detail. Preserve the user's exact words for directives, preferences, and constraints.
51
+
52
+ 2. Key Technical Concepts
53
+ List all important technical concepts, technologies, and frameworks discussed.
54
+
55
+ 3. Files and Code Sections
56
+ Enumerate specific files and code sections examined, modified, or created. Include file paths and relevant code snippets. For each file, summarize why it was read or edited and what changed.
57
+
58
+ 4. Tool Call Outcomes
59
+ What tools were called, what they found, and what failed. Include specific file paths, function names, URLs, error messages, and return values. Pay special attention to tool results that informed later decisions.
60
+
61
+ 5. Errors and Fixes
62
+ List all errors encountered and how they were resolved. Include specific user feedback received, especially corrections or redirections.
63
+
64
+ 6. All User Messages
65
+ List ALL user messages that are not tool results. These are critical for understanding the user's feedback and changing intent. Preserve the user's exact words.
66
+
67
+ 7. Problem Solving
68
+ Document problems solved and any ongoing troubleshooting efforts.
69
+
70
+ 8. Pending Tasks
71
+ Outline any pending tasks that have been explicitly requested but not yet completed.
72
+
73
+ 9. Current Work
74
+ Describe precisely what was being worked on immediately before this summary. Include file names, code snippets, and the specific state of the work. This section is the most important for seamless continuation.
75
+
76
+ 10. Key Decisions (Cumulative)
77
+ If a previous compaction summary exists in the conversation, carry forward its Key Decisions section and append any new decisions from this cycle. This section grows across compactions to prevent progressive loss of important decisions.
78
+
79
+ 11. Optional Next Step
80
+ List the next step related to the most recent work, but ONLY if it is directly in line with the user's most recent explicit request. If the last task was concluded, do not suggest tangential work. Include direct quotes from the conversation showing exactly what task was in progress.
81
+
82
+ When preserving details, extract and retain exact values rather than paraphrasing:
83
+ - File paths, directory names, and line numbers
84
+ - URLs, API endpoints, and query parameters
85
+ - Function names, class names, variable names
86
+ - IDs, hashes, version numbers, and configuration values
87
+ - Error messages and status codes
88
+ - Specific quantities, dates, and thresholds
89
+
90
+ Be thorough. Err on the side of including information that would prevent duplicate work or repeated mistakes.`;
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Summary extraction
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Extract the <summary> content from the LLM's compaction output.
98
+ * The prompt asks for <analysis> (scratchpad) then <summary> (the actual summary).
99
+ * We strip the analysis and keep only the summary content.
100
+ * If no <summary> tags are found, return the full output (the model may
101
+ * have skipped the tags but still produced useful content).
102
+ */
103
+ export function extractSummaryContent(raw: string): string {
104
+ const match = raw.match(/<summary>([\s\S]*?)<\/summary>/);
105
+ if (match?.[1]) {
106
+ return match[1].trim();
107
+ }
108
+ // Fallback: strip <analysis> block if present, return the rest
109
+ const stripped = raw.replace(/<analysis>[\s\S]*?<\/analysis>/g, '').trim();
110
+ return stripped || raw.trim();
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Summarization
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Partition conversation history into compaction target and preserved tail.
119
+ *
120
+ * @param history - The full conversation history (post-slot region)
121
+ * @param preserveRecentTurns - Number of recent turns to preserve
122
+ * @returns [target, preserved] where target is summarized and preserved is kept verbatim
123
+ */
124
+ export function partitionHistory(
125
+ history: AgentMessage[],
126
+ preserveRecentTurns: number,
127
+ ): [AgentMessage[], AgentMessage[]] {
128
+ if (history.length <= preserveRecentTurns) {
129
+ return [[], history];
130
+ }
131
+
132
+ let splitPoint = history.length - preserveRecentTurns;
133
+
134
+ // Never split between a tool_use (assistant) and its tool_result (user).
135
+ // If the split lands on a tool_result whose preceding message is a tool_use,
136
+ // move the split back one so the entire pair goes into the preserved tail.
137
+ if (
138
+ splitPoint > 0 &&
139
+ splitPoint < history.length &&
140
+ isToolResultMessage(history[splitPoint]!) &&
141
+ isToolUseMessage(history[splitPoint - 1]!)
142
+ ) {
143
+ splitPoint -= 1;
144
+ }
145
+
146
+ // Guard: don't create an empty target from the adjustment
147
+ if (splitPoint <= 0) {
148
+ return [[], history];
149
+ }
150
+
151
+ return [history.slice(0, splitPoint), history.slice(splitPoint)];
152
+ }
153
+
154
+ /**
155
+ * Build the compaction summary message wrapping it in XML tags.
156
+ *
157
+ * @param summary - The LLM-generated summary text
158
+ * @param turnsCompacted - Number of turns that were summarized
159
+ * @returns A user-role message containing the tagged summary
160
+ */
161
+ export function buildSummaryMessage(
162
+ summary: string,
163
+ turnsCompacted: number,
164
+ ): AgentMessage {
165
+ const timestamp = new Date().toISOString();
166
+ const content = `<compaction-summary generated="${timestamp}" turns-summarized="${turnsCompacted}">\n${summary}\n</compaction-summary>`;
167
+ return { role: 'user', content, timestamp: Date.now() };
168
+ }
169
+
170
+ /**
171
+ * Format conversation turns for the summarization prompt.
172
+ * Extracts text content and labels each turn with role.
173
+ */
174
+ export function formatTurnsForSummarization(turns: AgentMessage[]): string {
175
+ // No per-turn truncation. The compaction target is already bounded by
176
+ // partitionHistory (everything minus the preserved tail), and the
177
+ // summarizer needs access to full turn content for high-quality
178
+ // compression. See compaction-strategy.md Layer 2.
179
+ return turns
180
+ .map((msg, i) => {
181
+ const text = extractTextContent(msg);
182
+ return `[Turn ${i + 1}] ${msg.role}:\n${text}`;
183
+ })
184
+ .join('\n\n---\n\n');
185
+ }
186
+
187
+ /**
188
+ * Type for the LLM completion function.
189
+ * Matches the signature of CortexAgent.directComplete().
190
+ */
191
+ export type CompleteFn = (context: {
192
+ systemPrompt: string;
193
+ messages: unknown[];
194
+ }) => Promise<string>;
195
+
196
+ /**
197
+ * Type for the consumer's onBeforeCompaction handler.
198
+ */
199
+ export type BeforeCompactionHandler = (target: CompactionTarget) => Promise<void>;
200
+
201
+ /**
202
+ * Type for the consumer's onPostCompaction handler.
203
+ */
204
+ export type PostCompactionHandler = (result: CompactionResult) => void;
205
+
206
+ /**
207
+ * Type for the consumer's onCompactionError handler.
208
+ */
209
+ export type CompactionErrorHandler = (error: Error) => void;
210
+
211
+ /**
212
+ * Run Layer 2 conversation summarization.
213
+ *
214
+ * Steps:
215
+ * 1. Partition history into target and preserved tail
216
+ * 2. Emit onBeforeCompaction (awaited)
217
+ * 3. Generate summary via LLM
218
+ * 4. Build new history: [summary message] + [preserved tail]
219
+ * 5. Emit onPostCompaction
220
+ *
221
+ * @param history - Current conversation history (post-slot region)
222
+ * @param config - Compaction configuration
223
+ * @param complete - LLM completion function
224
+ * @param handlers - Consumer lifecycle handlers
225
+ * @returns The new conversation history and compaction result
226
+ */
227
+ export async function runCompaction(
228
+ history: AgentMessage[],
229
+ config: CompactionConfig,
230
+ complete: CompleteFn,
231
+ handlers: {
232
+ onBeforeCompaction?: BeforeCompactionHandler[];
233
+ onPostCompaction?: PostCompactionHandler[];
234
+ onCompactionError?: CompactionErrorHandler[];
235
+ } = {},
236
+ /** Actual full-context token count (includes system prompt, slots, tools). When provided, used as tokensBefore instead of text-only heuristic. */
237
+ actualContextTokens?: number,
238
+ ): Promise<{ newHistory: AgentMessage[]; result: CompactionResult }> {
239
+ const [target, preserved] = partitionHistory(history, config.preserveRecentTurns);
240
+
241
+ if (target.length === 0) {
242
+ // Nothing to compact; not enough history
243
+ throw new Error('Not enough conversation history to compact');
244
+ }
245
+
246
+ // Compute text-only heuristic for history content.
247
+ const historyTextTokens = estimateTokens(
248
+ history.map(m => extractTextContent(m)).join('\n'),
249
+ );
250
+
251
+ // Use actual full-context token count when provided (includes system prompt,
252
+ // slots, tool definitions); fall back to text-only heuristic for backward compat.
253
+ const tokensBefore = actualContextTokens ?? historyTextTokens;
254
+
255
+ // Overhead = system prompt + slots + tool definitions (everything except history text).
256
+ // Used to compute tokensAfter on the same basis as tokensBefore.
257
+ const overhead = actualContextTokens ? Math.max(0, actualContextTokens - historyTextTokens) : 0;
258
+
259
+ // Build compaction target info for the event
260
+ const targetInfo: CompactionTarget = {
261
+ turnsToCompact: target.length,
262
+ estimatedTokens: estimateTokens(
263
+ target.map(m => extractTextContent(m)).join('\n'),
264
+ ),
265
+ };
266
+
267
+ // Emit onBeforeCompaction (awaited)
268
+ if (handlers.onBeforeCompaction) {
269
+ for (const handler of handlers.onBeforeCompaction) {
270
+ await handler(targetInfo);
271
+ }
272
+ }
273
+
274
+ // Generate summary via LLM
275
+ const prompt = config.customPrompt ?? DEFAULT_SUMMARIZATION_PROMPT;
276
+ const turnsText = formatTurnsForSummarization(target);
277
+
278
+ let summary: string;
279
+ try {
280
+ summary = await complete({
281
+ systemPrompt: prompt,
282
+ messages: [
283
+ {
284
+ role: 'user',
285
+ content: `Here are the conversation turns to summarize:\n\n${turnsText}`,
286
+ },
287
+ ],
288
+ });
289
+ } catch (err) {
290
+ const error = err instanceof Error ? err : new Error(String(err));
291
+ // Emit compaction error
292
+ if (handlers.onCompactionError) {
293
+ for (const handler of handlers.onCompactionError) {
294
+ try {
295
+ handler(error);
296
+ } catch {
297
+ // Swallow handler errors
298
+ }
299
+ }
300
+ }
301
+ throw error;
302
+ }
303
+
304
+ // Extract summary content from <summary> tags, stripping <analysis>
305
+ const parsedSummary = extractSummaryContent(summary);
306
+
307
+ // Build new history
308
+ const summaryMessage = buildSummaryMessage(parsedSummary, target.length);
309
+ const newHistory = [summaryMessage, ...preserved];
310
+
311
+ // Calculate result metrics. Include the same overhead (system prompt, slots,
312
+ // tool definitions) so tokensBefore and tokensAfter are on the same basis.
313
+ const newHistoryTextTokens = estimateTokens(
314
+ newHistory.map(m => extractTextContent(m)).join('\n'),
315
+ );
316
+ const tokensAfter = overhead + newHistoryTextTokens;
317
+ const summaryTokens = estimateTokens(parsedSummary);
318
+
319
+ // The oldest preserved turn's index in the original history.
320
+ // target.length is the split point: all turns before it were compacted.
321
+ const oldestPreservedIndex = target.length;
322
+
323
+ // Attempt to find a timestamp in the preserved messages; null if not found.
324
+ const oldestPreservedTimestamp = findOldestTimestamp(preserved);
325
+
326
+ const result: CompactionResult = {
327
+ tokensBefore,
328
+ tokensAfter,
329
+ turnsCompacted: target.length,
330
+ turnsPreserved: preserved.length,
331
+ summaryTokens,
332
+ oldestPreservedTimestamp,
333
+ oldestPreservedIndex,
334
+ summary: parsedSummary,
335
+ };
336
+
337
+ // Emit onPostCompaction
338
+ if (handlers.onPostCompaction) {
339
+ for (const handler of handlers.onPostCompaction) {
340
+ try {
341
+ handler(result);
342
+ } catch {
343
+ // Swallow handler errors
344
+ }
345
+ }
346
+ }
347
+
348
+ return { newHistory, result };
349
+ }
350
+
351
+ /**
352
+ * Attempt to find the oldest timestamp in a set of messages.
353
+ *
354
+ * Scans message content for ISO date patterns. Returns the first match
355
+ * or null if none found. This is a best-effort heuristic; the consumer
356
+ * should prefer `oldestPreservedIndex` from CompactionResult for
357
+ * reliable timestamp resolution via their own database.
358
+ */
359
+ function findOldestTimestamp(messages: AgentMessage[]): string | null {
360
+ const isoPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
361
+
362
+ for (const msg of messages) {
363
+ const text = extractTextContent(msg);
364
+ const match = isoPattern.exec(text);
365
+ if (match) {
366
+ return match[0];
367
+ }
368
+ }
369
+
370
+ // No ISO timestamp found in preserved messages. Return null rather
371
+ // than Date.now() so the consumer knows no timestamp was found and
372
+ // can fall back to oldestPreservedIndex for database-level resolution.
373
+ return null;
374
+ }
375
+
376
+ /**
377
+ * Check if compaction should trigger based on token count and threshold.
378
+ */
379
+ export function shouldCompact(
380
+ currentTokens: number,
381
+ contextWindow: number,
382
+ threshold: number,
383
+ ): boolean {
384
+ if (contextWindow <= 0) return false;
385
+ return (currentTokens / contextWindow) >= threshold;
386
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Layer 3: Emergency Truncation (Failsafe).
3
+ *
4
+ * Last-resort truncation when Layer 2 fails or context is still too large.
5
+ * Drops the oldest conversation turns purely mechanically (no LLM call).
6
+ * Preserves structural integrity: tool_use/tool_result pairs are dropped together.
7
+ *
8
+ * Triggers at 90% of context window (configurable), or reactively when
9
+ * the API returns a context overflow error.
10
+ *
11
+ * This layer also serves as a mid-loop safety valve: it fires inside
12
+ * transformContext during the agentic loop when estimated token count
13
+ * exceeds 90%. Mid-loop truncation does NOT emit onBeforeCompaction
14
+ * (no observational memory processing mid-loop).
15
+ *
16
+ * References:
17
+ * - compaction-strategy.md (Layer 3: Emergency Truncation)
18
+ * - phase-5-compaction.md (5.4)
19
+ */
20
+
21
+ import type { AgentMessage } from '../context-manager.js';
22
+ import type { FailsafeConfig } from '../types.js';
23
+ import { estimateTokens } from '../token-estimator.js';
24
+ import { isToolResultMessage, isToolUseMessage, extractTextContent } from './microcompaction.js';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Defaults
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export const FAILSAFE_DEFAULTS: FailsafeConfig = {
31
+ threshold: 0.90,
32
+ };
33
+
34
+ /**
35
+ * Minimum number of recent turns to preserve during emergency truncation.
36
+ * Fewer turns preserved than Layer 2 since this is a last resort.
37
+ */
38
+ const FAILSAFE_PRESERVE_TURNS = 3;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Truncation
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Result of an emergency truncation operation.
46
+ */
47
+ export interface FailsafeTruncationResult {
48
+ /** The truncated conversation history. */
49
+ newHistory: AgentMessage[];
50
+ /** Number of turns removed. */
51
+ turnsRemoved: number;
52
+ /** Estimated tokens after truncation. */
53
+ tokensAfter: number;
54
+ }
55
+
56
+ /**
57
+ * Find structural pairs in conversation history.
58
+ * A tool_use message and its corresponding tool_result form a pair.
59
+ * When dropping one, we must drop both.
60
+ *
61
+ * Returns indices that should be dropped together for each index.
62
+ * If a message at index i is part of a pair, pairMap[i] contains
63
+ * all indices in that pair.
64
+ */
65
+ function findStructuralPairs(history: AgentMessage[]): Map<number, number[]> {
66
+ const pairMap = new Map<number, number[]>();
67
+
68
+ for (let i = 0; i < history.length; i++) {
69
+ const msg = history[i]!;
70
+
71
+ if (isToolUseMessage(msg)) {
72
+ // Look for the corresponding tool_result in the next message
73
+ if (i + 1 < history.length && isToolResultMessage(history[i + 1]!)) {
74
+ const pair = [i, i + 1];
75
+ pairMap.set(i, pair);
76
+ pairMap.set(i + 1, pair);
77
+ }
78
+ }
79
+ }
80
+
81
+ return pairMap;
82
+ }
83
+
84
+ /**
85
+ * Perform emergency truncation on conversation history.
86
+ *
87
+ * Drops the oldest turns (preserving structural pairs) until the
88
+ * estimated token count drops below the threshold, or until only
89
+ * the preserved tail remains.
90
+ *
91
+ * @param history - Conversation history (post-slot region)
92
+ * @param contextWindow - Total context window size in tokens
93
+ * @param slotTokens - Estimated tokens used by slots
94
+ * @param threshold - Usage ratio threshold (default 0.90)
95
+ * @returns Truncation result with new history and metrics
96
+ */
97
+ export function emergencyTruncate(
98
+ history: AgentMessage[],
99
+ contextWindow: number,
100
+ slotTokens: number,
101
+ threshold: number = FAILSAFE_DEFAULTS.threshold,
102
+ ): FailsafeTruncationResult {
103
+ if (history.length === 0) {
104
+ return { newHistory: [], turnsRemoved: 0, tokensAfter: slotTokens };
105
+ }
106
+
107
+ const targetTokens = contextWindow * threshold;
108
+ const pairMap = findStructuralPairs(history);
109
+ const dropped = new Set<number>();
110
+
111
+ // Calculate initial token estimate
112
+ let currentTokens = slotTokens + estimateTokens(
113
+ history.map(m => extractTextContent(m)).join('\n'),
114
+ );
115
+
116
+ // Drop from the front, but respect the preserved tail
117
+ const preserveFrom = Math.max(0, history.length - FAILSAFE_PRESERVE_TURNS);
118
+ let i = 0;
119
+
120
+ while (currentTokens > targetTokens && i < preserveFrom) {
121
+ if (dropped.has(i)) {
122
+ i++;
123
+ continue;
124
+ }
125
+
126
+ // Get all indices that must be dropped together
127
+ const pair = pairMap.get(i);
128
+ const indicesToDrop = pair ?? [i];
129
+
130
+ // Check that none of the pair indices are in the preserved tail
131
+ const canDrop = indicesToDrop.every(idx => idx < preserveFrom);
132
+ if (!canDrop) {
133
+ i++;
134
+ continue;
135
+ }
136
+
137
+ // Drop the turn(s)
138
+ for (const idx of indicesToDrop) {
139
+ const msgTokens = estimateTokens(extractTextContent(history[idx]!));
140
+ currentTokens -= msgTokens;
141
+ dropped.add(idx);
142
+ }
143
+
144
+ i++;
145
+ }
146
+
147
+ // Build new history excluding dropped messages
148
+ const newHistory = history.filter((_, idx) => !dropped.has(idx));
149
+
150
+ return {
151
+ newHistory,
152
+ turnsRemoved: dropped.size,
153
+ tokensAfter: currentTokens,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Check if emergency truncation should fire based on token count.
159
+ */
160
+ export function shouldTruncate(
161
+ currentTokens: number,
162
+ contextWindow: number,
163
+ threshold: number = FAILSAFE_DEFAULTS.threshold,
164
+ ): boolean {
165
+ if (contextWindow <= 0) return false;
166
+ return (currentTokens / contextWindow) >= threshold;
167
+ }
168
+
169
+ /**
170
+ * Check if an error represents a context overflow.
171
+ * Matches common API error patterns from various providers.
172
+ */
173
+ export function isContextOverflow(error: Error): boolean {
174
+ const msg = error.message.toLowerCase();
175
+ return (
176
+ msg.includes('context_length_exceeded') ||
177
+ msg.includes('context window') ||
178
+ msg.includes('maximum context length') ||
179
+ msg.includes('token limit') ||
180
+ msg.includes('too many tokens') ||
181
+ msg.includes('request too large') ||
182
+ msg.includes('prompt is too long') ||
183
+ msg.includes('input too long')
184
+ );
185
+ }