@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,837 @@
1
+ /**
2
+ * ObservationalMemoryEngine: core orchestrator for the observational memory
3
+ * system.
4
+ *
5
+ * Ties together the observer, reflector, and buffering coordinator. Manages
6
+ * the observation slot, handles activation and reflection, and exposes the
7
+ * state management API.
8
+ *
9
+ * The engine runs inside CompactionManager's transformContext hook. It does
10
+ * NOT directly call ContextManager.setSlot(); instead, it manages the slot
11
+ * content string and the integration layer handles writing it to the slot.
12
+ *
13
+ * References:
14
+ * - observational-memory-architecture.md
15
+ * - compaction-strategy.md
16
+ * - context-manager.md
17
+ */
18
+
19
+ import type { CompleteFn } from '../compaction.js';
20
+ import type { AgentMessage, AgentContext } from '../../context-manager.js';
21
+ import type {
22
+ ObservationalMemoryConfig,
23
+ ObservationalMemoryState,
24
+ ObservationChunk,
25
+ ObservationEvent,
26
+ ReflectionEvent,
27
+ ContinuationHint,
28
+ RecallConfig,
29
+ } from './types.js';
30
+ import {
31
+ OBSERVATIONAL_MEMORY_DEFAULTS,
32
+ OBSERVATION_CONTEXT_PREAMBLE,
33
+ OBSERVATION_RECALL_INSTRUCTIONS,
34
+ } from './constants.js';
35
+ import { runObserver } from './observer.js';
36
+ import { runReflector, computeEffectiveReflectionThreshold } from './reflector.js';
37
+ import { BufferingCoordinator } from './buffering.js';
38
+ import { estimateTokens } from '../../token-estimator.js';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Re-exports
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export { BufferingCoordinator } from './buffering.js';
45
+ export { runObserver } from './observer.js';
46
+ export { runReflector, computeEffectiveReflectionThreshold } from './reflector.js';
47
+ export { createRecallTool } from './recall-tool.js';
48
+
49
+ export type {
50
+ ObservationalMemoryConfig,
51
+ ObservationalMemoryState,
52
+ ObservationChunk,
53
+ ObservationEvent,
54
+ ReflectionEvent,
55
+ ContinuationHint,
56
+ RecallConfig,
57
+ RecallResult,
58
+ ObserverOutput,
59
+ ReflectorOutput,
60
+ BufferState,
61
+ } from './types.js';
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // ObservationalMemoryEngine
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Core orchestrator for the observational memory system.
69
+ *
70
+ * Coordinates the observer, reflector, and buffering coordinator to maintain
71
+ * a compressed observation log of the conversation. Called from
72
+ * CompactionManager during transformContext.
73
+ */
74
+ export class ObservationalMemoryEngine {
75
+ private config: Required<Omit<ObservationalMemoryConfig, 'observerInstruction' | 'reflectorInstruction' | 'recall'>> & {
76
+ observerInstruction?: string;
77
+ reflectorInstruction?: string;
78
+ recall?: RecallConfig;
79
+ };
80
+ private buffering: BufferingCoordinator;
81
+ private completeFn: CompleteFn | null = null;
82
+ private observations: string = '';
83
+ private continuationHint: ContinuationHint | null = null;
84
+ private observationTokenCount: number = 0;
85
+ private generationCount: number = 0;
86
+ private contextWindow: number = 0;
87
+ private utilityModelContextWindow: number = 0;
88
+ private slotIndex: number;
89
+ private logger: { warn: (msg: string) => void; info: (msg: string) => void } | null = null;
90
+
91
+ // Event handler arrays (method-level registration, multiple handlers)
92
+ private observationHandlers: Array<(event: ObservationEvent) => void> = [];
93
+ private reflectionHandlers: Array<(event: ReflectionEvent) => void> = [];
94
+
95
+ constructor(config: Partial<ObservationalMemoryConfig>, slotIndex: number) {
96
+ this.slotIndex = slotIndex;
97
+ this.buffering = new BufferingCoordinator();
98
+
99
+ // Merge with defaults. Optional fields (observerInstruction,
100
+ // reflectorInstruction, recall) are only included when provided.
101
+ this.config = {
102
+ ...OBSERVATIONAL_MEMORY_DEFAULTS,
103
+ ...config,
104
+ };
105
+ }
106
+
107
+ // -------------------------------------------------------------------------
108
+ // Configuration
109
+ // -------------------------------------------------------------------------
110
+
111
+ /**
112
+ * Set the LLM completion function (wired to utilityComplete on CortexAgent).
113
+ */
114
+ setCompleteFn(fn: CompleteFn): void {
115
+ this.completeFn = fn;
116
+ }
117
+
118
+ /**
119
+ * Update the context window size.
120
+ */
121
+ setContextWindow(contextWindow: number): void {
122
+ this.contextWindow = contextWindow;
123
+ }
124
+
125
+ /**
126
+ * Update the utility model context window (for clamps).
127
+ */
128
+ setUtilityModelContextWindow(utilityModelContextWindow: number): void {
129
+ this.utilityModelContextWindow = utilityModelContextWindow;
130
+ }
131
+
132
+ /**
133
+ * Set the logger.
134
+ */
135
+ setLogger(logger: { warn: (msg: string) => void; info: (msg: string) => void }): void {
136
+ this.logger = logger;
137
+ }
138
+
139
+ // -------------------------------------------------------------------------
140
+ // Event Registration
141
+ // -------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Register an observation event handler.
145
+ */
146
+ onObservation(handler: (event: ObservationEvent) => void): void {
147
+ this.observationHandlers.push(handler);
148
+ }
149
+
150
+ /**
151
+ * Register a reflection event handler.
152
+ */
153
+ onReflection(handler: (event: ReflectionEvent) => void): void {
154
+ this.reflectionHandlers.push(handler);
155
+ }
156
+
157
+ // -------------------------------------------------------------------------
158
+ // Core: applyInTransformContext
159
+ // -------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Core method called from CompactionManager during transformContext.
163
+ *
164
+ * Handles observation activation and reflection when context utilization
165
+ * exceeds the activation threshold. Updates the observation slot and
166
+ * trims observed messages from history.
167
+ *
168
+ * @param context - The AgentContext from transformContext
169
+ * @param utilization - Current total context utilization (0-1)
170
+ * @param slotCount - Number of slot messages at the start of the array
171
+ * @param getHistory - Get conversation history from the context (post-slot)
172
+ * @param setHistory - Set conversation history in the context (post-slot)
173
+ * @param getSourceHistory - Get the original transcript history (agent.state.messages post-slot)
174
+ * @param setSourceHistory - Replace the original transcript history
175
+ * @returns Modified context with updated observations and trimmed history
176
+ */
177
+ async applyInTransformContext(
178
+ context: AgentContext,
179
+ utilization: number,
180
+ slotCount: number,
181
+ getHistory: (ctx: AgentContext) => AgentMessage[],
182
+ setHistory: (ctx: AgentContext, history: AgentMessage[]) => AgentContext,
183
+ getSourceHistory: () => AgentMessage[],
184
+ setSourceHistory: (history: AgentMessage[]) => void,
185
+ ): Promise<AgentContext> {
186
+ if (utilization < this.config.activationThreshold) {
187
+ return context;
188
+ }
189
+
190
+ // --- Activation ---
191
+ const sourceHistory = getSourceHistory();
192
+ const watermark = this.buffering.getWatermark();
193
+ let compactedMessages: AgentMessage[] = [];
194
+ let newObservationText = '';
195
+ let activatedSync = false;
196
+
197
+ // Step 1: Activate completed buffer chunks
198
+ if (this.buffering.hasCompletedChunks()) {
199
+ const { chunks, watermark: chunkWatermark } = this.buffering.getCompletedChunks();
200
+ const merged = this.mergeChunks(chunks);
201
+ newObservationText = merged.observations;
202
+
203
+ if (merged.hint) {
204
+ this.continuationHint = merged.hint;
205
+ }
206
+
207
+ // Messages covered by completed chunks (from start of source to watermark)
208
+ compactedMessages = sourceHistory.slice(0, chunkWatermark);
209
+
210
+ // Remove observed messages from source, keep unbuffered tail
211
+ setSourceHistory(sourceHistory.slice(chunkWatermark));
212
+ this.buffering.commitActivation();
213
+ }
214
+
215
+ // Step 2: Check if still over threshold after chunk activation
216
+ // Recompute utilization after trimming. Account for the full slot
217
+ // overhead (preamble, XML wrappers, continuation hints) rather than
218
+ // just the raw observation text, since the slot is always larger.
219
+ const postChunkSource = getSourceHistory();
220
+ const trimmedMessageTokens = estimateTokens(
221
+ compactedMessages.map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content)).join('\n'),
222
+ );
223
+ // Estimate the actual slot size including overhead, not just the raw
224
+ // observation text. buildSlotContent() wraps observations in the
225
+ // preamble, <observations> tags, and optional continuation hints.
226
+ const slotOverheadTokens = newObservationText
227
+ ? estimateTokens(this.buildSlotContentForEstimate(newObservationText))
228
+ : 0;
229
+ // Net change: removed messages, added observation slot content
230
+ const netTokenReduction = trimmedMessageTokens - slotOverheadTokens;
231
+ const postChunkUtilization = utilization - (this.contextWindow > 0 ? netTokenReduction / this.contextWindow : 0);
232
+
233
+ if (postChunkUtilization >= this.config.activationThreshold && this.completeFn) {
234
+ // Force sync observer on remaining unbuffered messages
235
+ const unbufferedMessages = postChunkSource;
236
+
237
+ if (unbufferedMessages.length > 0) {
238
+ activatedSync = true;
239
+
240
+ const output = await runObserver(
241
+ this.completeFn,
242
+ unbufferedMessages,
243
+ this.observations || null,
244
+ this.buildObserverConfig(),
245
+ );
246
+
247
+ // Append sync observations
248
+ if (newObservationText) {
249
+ newObservationText += '\n\n' + output.observations;
250
+ } else {
251
+ newObservationText = output.observations;
252
+ }
253
+
254
+ if (output.currentTask || output.suggestedResponse) {
255
+ this.continuationHint = {
256
+ currentTask: output.currentTask ?? '',
257
+ suggestedResponse: output.suggestedResponse ?? '',
258
+ };
259
+ }
260
+
261
+ // All unbuffered messages are now observed
262
+ compactedMessages = [...compactedMessages, ...unbufferedMessages];
263
+ setSourceHistory([]);
264
+ this.buffering.setWatermark(0);
265
+ // Invalidate any in-flight observers that were processing messages
266
+ // we just observed synchronously
267
+ this.buffering.advanceEpoch();
268
+ }
269
+ }
270
+
271
+ // Step 3: Merge new observations into existing
272
+ if (newObservationText) {
273
+ if (this.observations) {
274
+ this.observations += '\n\n' + newObservationText;
275
+ } else {
276
+ this.observations = newObservationText;
277
+ }
278
+ this.observationTokenCount = estimateTokens(this.observations);
279
+ }
280
+
281
+ // Step 4: Fire observation event
282
+ if (compactedMessages.length > 0) {
283
+ this.fireObservationEvent({
284
+ compactedMessages,
285
+ observations: this.observations,
286
+ contextUtilization: utilization,
287
+ sync: activatedSync,
288
+ timestamp: new Date(),
289
+ });
290
+ }
291
+
292
+ // Step 5: Handle reflection (may replace this.observations with condensed version)
293
+ await this.handleReflection();
294
+
295
+ // Step 6: Build slot content AFTER reflection so it contains post-reflection
296
+ // observations. Previously this was captured before reflection, requiring
297
+ // an external patch in cortex-agent.ts to correct stale content.
298
+ const slotContent = this.buildSlotContent();
299
+
300
+ // Step 7: Rebuild context with updated observations and trimmed history
301
+ const updatedSourceHistory = getSourceHistory();
302
+
303
+ // Build new message array: slot region + observation slot + remaining source messages
304
+ const slotRegion = context.messages.slice(0, this.slotIndex);
305
+ const observationSlotMessage: AgentMessage = {
306
+ role: 'user',
307
+ content: slotContent,
308
+ timestamp: Date.now(),
309
+ };
310
+ // Messages after the slot region that are not part of the observation slot
311
+ const postSlotMessages = updatedSourceHistory;
312
+
313
+ const newMessages: AgentMessage[] = [
314
+ ...slotRegion,
315
+ observationSlotMessage,
316
+ ...context.messages.slice(this.slotIndex + 1, slotCount),
317
+ ...postSlotMessages,
318
+ ];
319
+
320
+ return setHistory(
321
+ { ...context, messages: newMessages },
322
+ postSlotMessages,
323
+ );
324
+ }
325
+
326
+ // -------------------------------------------------------------------------
327
+ // Turn-end handler
328
+ // -------------------------------------------------------------------------
329
+
330
+ /**
331
+ * Called at each turn_end event. Handles async buffer triggering.
332
+ *
333
+ * Computes the dynamic buffer interval and launches an async observer
334
+ * if enough unobserved tokens have accumulated.
335
+ *
336
+ * @param totalTokens - Total tokens from the last LLM response
337
+ * @param contextWindow - Current context window size
338
+ * @param messages - Current conversation messages (post-slot)
339
+ * @param slotCount - Number of slot messages
340
+ */
341
+ onTurnEnd(
342
+ totalTokens: number,
343
+ contextWindow: number,
344
+ messages: AgentMessage[],
345
+ slotCount: number,
346
+ ): void {
347
+ if (!this.completeFn || contextWindow <= 0) return;
348
+
349
+ const currentUtilization = totalTokens / contextWindow;
350
+ const tokensUntilActivation = (this.config.activationThreshold - currentUtilization) * contextWindow;
351
+
352
+ // Already past threshold, activation will handle it in transformContext
353
+ if (tokensUntilActivation <= 0) return;
354
+
355
+ const bufferInterval = this.buffering.computeBufferInterval(tokensUntilActivation, {
356
+ bufferTargetCycles: this.config.bufferTargetCycles,
357
+ bufferTokenCap: this.config.bufferTokenCap,
358
+ bufferMinTokens: this.config.bufferMinTokens,
359
+ utilityModelContextWindow: this.utilityModelContextWindow,
360
+ });
361
+
362
+ // Skip slot messages to avoid processing them as conversation content
363
+ const history = messages.slice(slotCount);
364
+
365
+ // Compute unobserved tokens (messages after buffer watermark)
366
+ const watermark = this.buffering.getWatermark();
367
+ const unobservedMessages = history.slice(watermark);
368
+ const unobservedTokens = unobservedMessages.reduce((sum, msg) => {
369
+ const content = typeof msg.content === 'string'
370
+ ? msg.content
371
+ : JSON.stringify(msg.content);
372
+ return sum + estimateTokens(content);
373
+ }, 0);
374
+
375
+ if (this.buffering.shouldBuffer(unobservedTokens, bufferInterval)) {
376
+ // Snapshot the unobserved messages
377
+ const snapshot = [...unobservedMessages];
378
+ const endIndex = watermark + unobservedMessages.length;
379
+
380
+ this.buffering.launchObserver(
381
+ this.completeFn,
382
+ snapshot,
383
+ endIndex,
384
+ this.observations || null,
385
+ this.buildObserverConfig(),
386
+ this.logger ?? undefined,
387
+ );
388
+ }
389
+ }
390
+
391
+ // -------------------------------------------------------------------------
392
+ // Slot content
393
+ // -------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Build the full observation slot content.
397
+ *
398
+ * Assembles the preamble, optional recall instructions, observation block,
399
+ * and optional continuation hints into a single string.
400
+ */
401
+ buildSlotContent(): string {
402
+ let content = OBSERVATION_CONTEXT_PREAMBLE;
403
+
404
+ if (this.config.recall) {
405
+ content += OBSERVATION_RECALL_INSTRUCTIONS;
406
+ }
407
+
408
+ content += '\n\n<observations>\n' + this.observations + '\n</observations>';
409
+
410
+ if (this.continuationHint) {
411
+ content += '\n\n<current-task>\n' + this.continuationHint.currentTask + '\n</current-task>';
412
+ content += '\n\n<suggested-response>\n' + this.continuationHint.suggestedResponse + '\n</suggested-response>';
413
+ }
414
+
415
+ return content;
416
+ }
417
+
418
+ /**
419
+ * Estimate the full slot content size for a given observation text.
420
+ *
421
+ * Used by the post-chunk utilization estimate to account for slot
422
+ * overhead (preamble, XML wrappers, continuation hints) rather than
423
+ * just the raw observation text.
424
+ */
425
+ private buildSlotContentForEstimate(observationText: string): string {
426
+ let content = OBSERVATION_CONTEXT_PREAMBLE;
427
+
428
+ if (this.config.recall) {
429
+ content += OBSERVATION_RECALL_INSTRUCTIONS;
430
+ }
431
+
432
+ content += '\n\n<observations>\n' + observationText + '\n</observations>';
433
+
434
+ if (this.continuationHint) {
435
+ content += '\n\n<current-task>\n' + this.continuationHint.currentTask + '\n</current-task>';
436
+ content += '\n\n<suggested-response>\n' + this.continuationHint.suggestedResponse + '\n</suggested-response>';
437
+ }
438
+
439
+ return content;
440
+ }
441
+
442
+ // -------------------------------------------------------------------------
443
+ // Manual trigger
444
+ // -------------------------------------------------------------------------
445
+
446
+ /**
447
+ * Force a synchronous observation cycle.
448
+ *
449
+ * Used by consumers after critical corrections to ensure the observation
450
+ * log captures the correction immediately.
451
+ *
452
+ * @param messages - The full message array (may include slot messages)
453
+ * @param slotCount - Number of slot messages to skip
454
+ */
455
+ async triggerObservation(
456
+ messages: AgentMessage[],
457
+ slotCount: number,
458
+ ): Promise<void> {
459
+ const history = messages.slice(slotCount);
460
+ if (!this.completeFn || history.length === 0) return;
461
+
462
+ const output = await runObserver(
463
+ this.completeFn,
464
+ history,
465
+ this.observations || null,
466
+ this.buildObserverConfig(),
467
+ );
468
+
469
+ // Merge observations
470
+ if (this.observations) {
471
+ this.observations += '\n\n' + output.observations;
472
+ } else {
473
+ this.observations = output.observations;
474
+ }
475
+
476
+ if (output.currentTask || output.suggestedResponse) {
477
+ this.continuationHint = {
478
+ currentTask: output.currentTask ?? '',
479
+ suggestedResponse: output.suggestedResponse ?? '',
480
+ };
481
+ }
482
+
483
+ this.observationTokenCount = estimateTokens(this.observations);
484
+
485
+ // Fire observation event
486
+ this.fireObservationEvent({
487
+ compactedMessages: history,
488
+ observations: this.observations,
489
+ contextUtilization: 0, // manual trigger, utilization unknown
490
+ sync: true,
491
+ timestamp: new Date(),
492
+ });
493
+ }
494
+
495
+ // -------------------------------------------------------------------------
496
+ // Kickstart buffer (session resumption)
497
+ // -------------------------------------------------------------------------
498
+
499
+ /**
500
+ * Kick off an initial async buffer on unobserved messages.
501
+ * Called during session resumption for a head start before the first prompt().
502
+ */
503
+ kickstartBuffer(messages: AgentMessage[], slotCount: number): void {
504
+ if (!this.completeFn) return;
505
+ const history = messages.slice(slotCount);
506
+ if (history.length === 0) return;
507
+ const watermark = this.buffering.getWatermark();
508
+ const unobserved = history.slice(watermark);
509
+ if (unobserved.length === 0) return;
510
+ const unobservedTokens = unobserved.reduce(
511
+ (sum, m) => sum + estimateTokens(typeof m.content === 'string' ? m.content : JSON.stringify(m.content)),
512
+ 0,
513
+ );
514
+ if (unobservedTokens < this.config.bufferMinTokens) return;
515
+ // Launch async observer on the unobserved messages (non-blocking)
516
+ this.buffering.launchObserver(
517
+ this.completeFn,
518
+ unobserved,
519
+ watermark + unobserved.length,
520
+ this.observations || null,
521
+ this.buildObserverConfig(),
522
+ this.logger ?? undefined,
523
+ );
524
+ }
525
+
526
+ // -------------------------------------------------------------------------
527
+ // State management
528
+ // -------------------------------------------------------------------------
529
+
530
+ /**
531
+ * Returns the current state for session persistence.
532
+ */
533
+ getState(): ObservationalMemoryState {
534
+ return {
535
+ observations: this.observations,
536
+ continuationHint: this.continuationHint,
537
+ observationTokenCount: this.observationTokenCount,
538
+ generationCount: this.generationCount,
539
+ bufferedChunks: this.buffering.getState().chunks,
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Restore state from a previous session.
545
+ *
546
+ * Sets observations, continuation hint, token count, and generation count.
547
+ * Restores buffering state. If a completeFn is available and observations
548
+ * exist, updates the slot content. Kicks off an initial async buffer on
549
+ * unobserved messages as a non-blocking head start.
550
+ */
551
+ restoreState(state: ObservationalMemoryState): void {
552
+ this.observations = state.observations;
553
+ this.continuationHint = state.continuationHint;
554
+ this.observationTokenCount = state.observationTokenCount;
555
+ this.generationCount = state.generationCount;
556
+
557
+ // Discard buffered chunks from the previous session. Chunks represent
558
+ // observations that completed async but were never activated (merged
559
+ // into this.observations + messages trimmed). Restoring them with
560
+ // watermark=0 would merge their observations without trimming any
561
+ // messages, duplicating context. The observer will re-observe
562
+ // unobserved messages naturally on the next buffer cycle.
563
+ this.buffering.restoreState({
564
+ chunks: [],
565
+ watermark: 0,
566
+ });
567
+ }
568
+
569
+ /**
570
+ * Returns the current slot content string.
571
+ */
572
+ getSlotContent(): string {
573
+ return this.buildSlotContent();
574
+ }
575
+
576
+ /**
577
+ * Returns just the observation text.
578
+ */
579
+ getObservations(): string {
580
+ return this.observations;
581
+ }
582
+
583
+ /**
584
+ * Token count of activated observations only.
585
+ */
586
+ getObservationTokenCount(): number {
587
+ return this.observationTokenCount;
588
+ }
589
+
590
+
591
+ /**
592
+ * Whether the observer or reflector is currently running in the background.
593
+ */
594
+ isProcessing(): boolean {
595
+ return this.buffering.isObserverInFlight() || this.buffering.isReflectorInFlight();
596
+ }
597
+
598
+ /**
599
+ * Whether the observer specifically is in-flight.
600
+ */
601
+ isObserverInFlight(): boolean {
602
+ return this.buffering.isObserverInFlight();
603
+ }
604
+
605
+ /**
606
+ * Whether the reflector specifically is in-flight.
607
+ */
608
+ isReflectorInFlight(): boolean {
609
+ return this.buffering.isReflectorInFlight();
610
+ }
611
+
612
+ /**
613
+ * Abort all in-flight operations. Delegates to buffering.abort().
614
+ */
615
+ abort(): void {
616
+ this.buffering.abort();
617
+ }
618
+
619
+ /**
620
+ * Whether recall is configured.
621
+ */
622
+ hasRecall(): boolean {
623
+ return this.config.recall !== undefined;
624
+ }
625
+
626
+ /**
627
+ * Get the recall config if provided.
628
+ */
629
+ getRecallConfig(): RecallConfig | undefined {
630
+ return this.config.recall;
631
+ }
632
+
633
+ // -------------------------------------------------------------------------
634
+ // Private: Reflection handling
635
+ // -------------------------------------------------------------------------
636
+
637
+ /**
638
+ * Check and handle reflection after observation activation.
639
+ *
640
+ * Determines whether reflection should run (sync, async, or none) based
641
+ * on the current observation token count relative to the effective
642
+ * reflection threshold.
643
+ */
644
+ private async handleReflection(): Promise<void> {
645
+ if (!this.completeFn) return;
646
+
647
+ const effectiveThreshold = computeEffectiveReflectionThreshold(
648
+ this.contextWindow,
649
+ this.config.reflectionThreshold,
650
+ this.utilityModelContextWindow,
651
+ );
652
+
653
+ const reflectionAction = this.buffering.shouldReflect(
654
+ this.observationTokenCount,
655
+ effectiveThreshold,
656
+ this.config.reflectionBufferActivation,
657
+ );
658
+
659
+ if (reflectionAction === 'none') return;
660
+
661
+ if (reflectionAction === 'sync') {
662
+ const previousObservations = this.observations;
663
+
664
+ // Check for a ready buffered reflection first
665
+ if (this.buffering.hasBufferedReflection()) {
666
+ const buffered = this.buffering.consumeBufferedReflection();
667
+ if (buffered) {
668
+ this.observations = buffered.observations;
669
+ this.observationTokenCount = estimateTokens(this.observations);
670
+ this.generationCount++;
671
+
672
+ this.fireReflectionEvent({
673
+ previousObservations,
674
+ newObservations: this.observations,
675
+ generationCount: this.generationCount,
676
+ compressionLevel: buffered.compressionLevel,
677
+ timestamp: new Date(),
678
+ });
679
+ return;
680
+ }
681
+ }
682
+
683
+ // No buffered reflection, run synchronously
684
+ const output = await runReflector(
685
+ this.completeFn,
686
+ this.observations,
687
+ this.buildReflectorConfig(effectiveThreshold),
688
+ );
689
+
690
+ this.observations = output.observations;
691
+ this.observationTokenCount = estimateTokens(this.observations);
692
+ this.generationCount++;
693
+
694
+ this.fireReflectionEvent({
695
+ previousObservations,
696
+ newObservations: this.observations,
697
+ generationCount: this.generationCount,
698
+ compressionLevel: output.compressionLevel,
699
+ timestamp: new Date(),
700
+ });
701
+ return;
702
+ }
703
+
704
+ if (reflectionAction === 'async') {
705
+ // Launch async reflector
706
+ const effectiveThresholdForReflector = computeEffectiveReflectionThreshold(
707
+ this.contextWindow,
708
+ this.config.reflectionThreshold,
709
+ this.utilityModelContextWindow,
710
+ );
711
+
712
+ this.buffering.launchReflector(
713
+ this.completeFn,
714
+ this.observations,
715
+ this.buildReflectorConfig(effectiveThresholdForReflector),
716
+ this.logger ?? undefined,
717
+ );
718
+ }
719
+ }
720
+
721
+ // -------------------------------------------------------------------------
722
+ // Private: Config builders (exactOptionalPropertyTypes safe)
723
+ // -------------------------------------------------------------------------
724
+
725
+ /**
726
+ * Build the observer config object without assigning undefined to optional
727
+ * properties (exactOptionalPropertyTypes is enabled).
728
+ */
729
+ private buildObserverConfig(): { previousObserverTokens: number; observerInstruction?: string } {
730
+ const config: { previousObserverTokens: number; observerInstruction?: string } = {
731
+ previousObserverTokens: this.config.previousObserverTokens,
732
+ };
733
+ if (this.config.observerInstruction !== undefined) {
734
+ config.observerInstruction = this.config.observerInstruction;
735
+ }
736
+ return config;
737
+ }
738
+
739
+ /**
740
+ * Build the reflector config object without assigning undefined to optional
741
+ * properties (exactOptionalPropertyTypes is enabled).
742
+ */
743
+ private buildReflectorConfig(threshold: number): { reflectionThreshold: number; reflectorInstruction?: string } {
744
+ const config: { reflectionThreshold: number; reflectorInstruction?: string } = {
745
+ reflectionThreshold: threshold,
746
+ };
747
+ if (this.config.reflectorInstruction !== undefined) {
748
+ config.reflectorInstruction = this.config.reflectorInstruction;
749
+ }
750
+ return config;
751
+ }
752
+
753
+ // -------------------------------------------------------------------------
754
+ // Private: Chunk merging
755
+ // -------------------------------------------------------------------------
756
+
757
+ /**
758
+ * Merge buffered observation chunks into a single observation text.
759
+ *
760
+ * Concatenates all chunk observation texts with double newlines. Uses
761
+ * the latest chunk's currentTask and suggestedResponse (latest wins).
762
+ */
763
+ private mergeChunks(
764
+ chunks: ObservationChunk[],
765
+ ): { observations: string; hint: ContinuationHint | null } {
766
+ const observationParts: string[] = [];
767
+
768
+ // Find the latest chunk that produced at least one meaningful hint field.
769
+ // Each observer only sees its own batch of messages, so the latest chunk
770
+ // reflects the most recent state of the conversation. We do NOT mix fields
771
+ // across chunks: that would risk injecting a stale currentTask from an
772
+ // earlier chunk when the conversation has since moved on to something else.
773
+ //
774
+ // The parser already rejects placeholder-only content, so an observer that
775
+ // echoed the prompt template without filling it in will have undefined
776
+ // hint fields (not empty strings).
777
+ let latestHintChunk: ObservationChunk | null = null;
778
+
779
+ for (const chunk of chunks) {
780
+ observationParts.push(chunk.observations);
781
+ if (chunk.currentTask || chunk.suggestedResponse) {
782
+ latestHintChunk = chunk;
783
+ }
784
+ }
785
+
786
+ // If no chunk produced hints, preserve the existing hint from prior
787
+ // activation cycles. That hint is still more relevant than nothing, since
788
+ // it came from the observer that ran at the end of the previous cycle
789
+ // (itself more recent than anything older in this cycle).
790
+ const hint: ContinuationHint | null = latestHintChunk
791
+ ? {
792
+ currentTask: latestHintChunk.currentTask ?? '',
793
+ suggestedResponse: latestHintChunk.suggestedResponse ?? '',
794
+ }
795
+ : this.continuationHint;
796
+
797
+ return {
798
+ observations: observationParts.join('\n\n'),
799
+ hint,
800
+ };
801
+ }
802
+
803
+ // -------------------------------------------------------------------------
804
+ // Private: Event firing
805
+ // -------------------------------------------------------------------------
806
+
807
+ /**
808
+ * Fire all observation handlers. Each handler is individually try/catch
809
+ * wrapped to prevent one handler from breaking others.
810
+ */
811
+ private fireObservationEvent(event: ObservationEvent): void {
812
+ for (const handler of this.observationHandlers) {
813
+ try {
814
+ handler(event);
815
+ } catch (err) {
816
+ const message = err instanceof Error ? err.message : String(err);
817
+ this.logger?.warn(`Observation handler threw: ${message}`);
818
+ }
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Fire all reflection handlers. Each handler is individually try/catch
824
+ * wrapped to prevent one handler from breaking others.
825
+ */
826
+ private fireReflectionEvent(event: ReflectionEvent): void {
827
+ for (const handler of this.reflectionHandlers) {
828
+ try {
829
+ handler(event);
830
+ } catch (err) {
831
+ const message = err instanceof Error ? err.message : String(err);
832
+ this.logger?.warn(`Reflection handler threw: ${message}`);
833
+ }
834
+ }
835
+ }
836
+ }
837
+