@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,199 @@
1
+ /**
2
+ * Error classifier for LLM and network errors.
3
+ *
4
+ * Maps error strings to actionable categories using regex pattern matching.
5
+ * Follows the same pattern pi-ai uses for context overflow detection,
6
+ * extended to cover authentication, rate limits, server errors, and network errors.
7
+ *
8
+ * The classifier is a pure function. It does not throw, does not modify state.
9
+ * It takes an error (or error string) and returns a classification.
10
+ *
11
+ * Reference: error-recovery.md
12
+ */
13
+
14
+ import type { ClassifiedError, ErrorCategory, ErrorSeverity } from './types.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Pattern definitions per category (checked in priority order)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const AUTHENTICATION_PATTERNS: RegExp[] = [
21
+ /invalid.api.key/i,
22
+ /unauthorized/i,
23
+ /not.logged.in/i,
24
+ /authentication.required/i,
25
+ /expired.*token/i,
26
+ /invalid.*credentials/i,
27
+ /api.key.*invalid/i,
28
+ /permission.denied.*key/i,
29
+ /Could not resolve API key/i,
30
+ ];
31
+
32
+ const RATE_LIMIT_PATTERNS: RegExp[] = [
33
+ /rate.limit/i,
34
+ /too.many.requests/i,
35
+ /\b429\b/,
36
+ /rate_limit_exceeded/i,
37
+ /throttl/i,
38
+ /request.limit.reached/i,
39
+ /quota.exceeded/i,
40
+ ];
41
+
42
+ // Full context overflow detection delegates to pi-ai's isContextOverflow() when available.
43
+ // These minimal patterns serve as a fallback when pi-ai is not installed.
44
+ const CONTEXT_OVERFLOW_PATTERNS: RegExp[] = [
45
+ /context.*overflow/i,
46
+ /too.many.tokens/i,
47
+ /token.limit/i,
48
+ /prompt.is.too.long/i,
49
+ ];
50
+
51
+ const SERVER_ERROR_PATTERNS: RegExp[] = [
52
+ /internal.server.error/i,
53
+ /\b500\b/,
54
+ /\b502\b.*bad.gateway/i,
55
+ /\b503\b.*service.unavailable/i,
56
+ /\b504\b.*gateway.timeout/i,
57
+ /server.*error/i,
58
+ /overloaded/i,
59
+ ];
60
+
61
+ const NETWORK_PATTERNS: RegExp[] = [
62
+ /ECONNREFUSED/,
63
+ /ENOTFOUND/,
64
+ /ETIMEDOUT/,
65
+ /ECONNRESET/,
66
+ /network.*error/i,
67
+ /fetch.failed/i,
68
+ /socket.hang.up/i,
69
+ /DNS.*resolution/i,
70
+ ];
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Severity and action mappings
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const SEVERITY_MAP: Record<ErrorCategory, ErrorSeverity> = {
77
+ authentication: 'fatal',
78
+ rate_limit: 'retry',
79
+ context_overflow: 'recoverable',
80
+ server_error: 'retry',
81
+ network: 'retry',
82
+ cancelled: 'recoverable',
83
+ unknown: 'recoverable',
84
+ };
85
+
86
+ const SUGGESTED_ACTIONS: Record<ErrorCategory, string | undefined> = {
87
+ authentication: 'Check your API key or re-authenticate in Settings.',
88
+ rate_limit: 'Rate limit hit. The next tick will be delayed.',
89
+ context_overflow: 'Context window exceeded. Compaction will run.',
90
+ server_error: 'The provider is experiencing issues. Retrying.',
91
+ network: 'Network error. Check your connection.',
92
+ cancelled: undefined,
93
+ unknown: undefined,
94
+ };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Pattern matching helpers
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function matchesAny(message: string, patterns: RegExp[]): boolean {
101
+ return patterns.some((pattern) => pattern.test(message));
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Public API
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Options for the error classifier.
110
+ */
111
+ export interface ClassifyErrorOptions {
112
+ /**
113
+ * The model's context window size in tokens.
114
+ * Used for context overflow detection when delegating to pi-ai.
115
+ */
116
+ contextWindow?: number;
117
+
118
+ /**
119
+ * Whether the agent was aborted (user or system cancellation).
120
+ * When true, the error is immediately classified as 'cancelled'.
121
+ * The caller checks agent.state or AbortSignal.aborted and passes this flag;
122
+ * the classifier itself remains pure.
123
+ */
124
+ wasAborted?: boolean;
125
+ }
126
+
127
+ /**
128
+ * Classify an error into an actionable category.
129
+ *
130
+ * Checks error strings against regex patterns in priority order (first match wins):
131
+ * 1. Cancelled (if wasAborted is true)
132
+ * 2. Authentication (9 patterns)
133
+ * 3. Rate limit (7 patterns)
134
+ * 4. Context overflow (4 fallback patterns; delegates to pi-ai isContextOverflow when available)
135
+ * 5. Server error (7 patterns)
136
+ * 6. Network (8 patterns)
137
+ * 7. Unknown (catch-all)
138
+ *
139
+ * @param error - The error to classify (Error object or string)
140
+ * @param options - Optional classification options
141
+ * @returns A ClassifiedError with category, severity, original message, and suggested action
142
+ */
143
+ export function classifyError(
144
+ error: Error | string,
145
+ options?: ClassifyErrorOptions,
146
+ ): ClassifiedError {
147
+ const message = typeof error === 'string' ? error : error.message;
148
+
149
+ // 1. Cancelled (highest priority if wasAborted flag is set)
150
+ if (options?.wasAborted) {
151
+ return buildResult('cancelled', message);
152
+ }
153
+
154
+ // 2. Authentication
155
+ if (matchesAny(message, AUTHENTICATION_PATTERNS)) {
156
+ return buildResult('authentication', message);
157
+ }
158
+
159
+ // 3. Rate limit
160
+ if (matchesAny(message, RATE_LIMIT_PATTERNS)) {
161
+ return buildResult('rate_limit', message);
162
+ }
163
+
164
+ // 4. Context overflow
165
+ // Uses built-in patterns. In Phase 1B, this will also delegate to
166
+ // pi-ai's isContextOverflow() when available.
167
+ if (matchesAny(message, CONTEXT_OVERFLOW_PATTERNS)) {
168
+ return buildResult('context_overflow', message);
169
+ }
170
+
171
+ // 5. Server error
172
+ if (matchesAny(message, SERVER_ERROR_PATTERNS)) {
173
+ return buildResult('server_error', message);
174
+ }
175
+
176
+ // 6. Network
177
+ if (matchesAny(message, NETWORK_PATTERNS)) {
178
+ return buildResult('network', message);
179
+ }
180
+
181
+ // 7. Unknown (catch-all)
182
+ return buildResult('unknown', message);
183
+ }
184
+
185
+ /**
186
+ * Build a ClassifiedError from a category and original message.
187
+ */
188
+ function buildResult(category: ErrorCategory, originalMessage: string): ClassifiedError {
189
+ const action = SUGGESTED_ACTIONS[category];
190
+ const result: ClassifiedError = {
191
+ category,
192
+ severity: SEVERITY_MAP[category],
193
+ originalMessage,
194
+ };
195
+ if (action !== undefined) {
196
+ result.suggestedAction = action;
197
+ }
198
+ return result;
199
+ }
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Event bridge: maps pi-agent-core events to normalized consumer events.
3
+ *
4
+ * Pi-agent-core emits 10 events across 4 scopes (agent, turn, message, tool).
5
+ * The event bridge normalizes these into a consumer-facing event stream for
6
+ * logging, monitoring, and lifecycle hooks.
7
+ *
8
+ * Key mappings:
9
+ * agent_start -> loop_start
10
+ * agent_end -> loop_end (onLoopComplete fires here)
11
+ * turn_start -> turn_start
12
+ * turn_end -> turn_end + AgentTextOutput (parse working tags)
13
+ * message_start -> response_start
14
+ * message_update -> response_chunk
15
+ * message_end -> response_end
16
+ * tool_execution_start -> tool_call_start
17
+ * tool_execution_update -> tool_call_update
18
+ * tool_execution_end -> tool_call_end
19
+ *
20
+ * Child event forwarding:
21
+ * forwardFrom(childBridge, childTaskId) subscribes to a child agent's
22
+ * event bridge and re-emits events on this bridge with childTaskId set.
23
+ * Consumers use event.childTaskId to distinguish parent vs child events.
24
+ *
25
+ * Reference: cortex-architecture.md (Event Bridge section)
26
+ */
27
+
28
+ import type {
29
+ AgentTextOutput,
30
+ CortexLogger,
31
+ CortexUsage,
32
+ ToolCallStartPayload,
33
+ ToolCallUpdatePayload,
34
+ ToolCallEndPayload,
35
+ ToolContentDetails,
36
+ } from './types.js';
37
+ import { NOOP_LOGGER } from './noop-logger.js';
38
+ import { parseWorkingTags } from './working-tags.js';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Normalized event types emitted to consumers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export type CortexEventType =
45
+ | 'loop_start'
46
+ | 'loop_end'
47
+ | 'turn_start'
48
+ | 'turn_end'
49
+ | 'response_start'
50
+ | 'response_chunk'
51
+ | 'response_end'
52
+ | 'tool_call_start'
53
+ | 'tool_call_update'
54
+ | 'tool_call_end';
55
+
56
+ /**
57
+ * Normalized event data emitted by the event bridge.
58
+ */
59
+ export interface CortexEvent {
60
+ type: CortexEventType;
61
+ /** The original pi-agent-core event data (opaque to the bridge). */
62
+ data?: unknown;
63
+ /** Parsed text output, present only for turn_end events. */
64
+ textOutput?: AgentTextOutput;
65
+ /**
66
+ * Typed payload for tool events (tool_call_start, tool_call_update, tool_call_end).
67
+ * Provides typed access to tool event data without casting `data`.
68
+ */
69
+ payload?: ToolCallStartPayload | ToolCallUpdatePayload | ToolCallEndPayload;
70
+ /**
71
+ * Extracted usage data from the LLM response, present on turn_end events.
72
+ * Centralizes extraction from pi-ai's AssistantMessage.usage structure so
73
+ * subscribers (BudgetGuard, CortexAgent, consumers) read typed data instead
74
+ * of parsing the opaque `data` field themselves.
75
+ */
76
+ usage?: CortexUsage;
77
+ /**
78
+ * Present when this event originates from a child (sub-agent) event bridge.
79
+ * The value is the sub-agent's task ID, allowing consumers to route events
80
+ * to the correct UI component. Absent for parent agent events.
81
+ */
82
+ childTaskId?: string;
83
+ }
84
+
85
+ /**
86
+ * Callback type for event listeners.
87
+ */
88
+ export type CortexEventListener = (event: CortexEvent) => void;
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Pi-agent-core event types (minimal contract, no runtime dependency)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export type PiEventType =
95
+ | 'agent_start'
96
+ | 'agent_end'
97
+ | 'turn_start'
98
+ | 'turn_end'
99
+ | 'message_start'
100
+ | 'message_update'
101
+ | 'message_end'
102
+ | 'tool_execution_start'
103
+ | 'tool_execution_update'
104
+ | 'tool_execution_end';
105
+
106
+ export interface PiEvent {
107
+ type: PiEventType;
108
+ [key: string]: unknown;
109
+ }
110
+
111
+ /**
112
+ * Minimal interface for pi-agent-core's Agent.subscribe().
113
+ * Returns an unsubscribe function.
114
+ */
115
+ export interface PiEventSource {
116
+ subscribe(handler: (event: PiEvent) => void): () => void;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Event type mapping
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const PI_TO_CORTEX_MAP: Partial<Record<PiEventType, CortexEventType>> = {
124
+ agent_start: 'loop_start',
125
+ agent_end: 'loop_end',
126
+ turn_start: 'turn_start',
127
+ turn_end: 'turn_end',
128
+ message_start: 'response_start',
129
+ message_update: 'response_chunk',
130
+ message_end: 'response_end',
131
+ tool_execution_start: 'tool_call_start',
132
+ tool_execution_update: 'tool_call_update',
133
+ tool_execution_end: 'tool_call_end',
134
+ };
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // EventBridge
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export class EventBridge {
141
+ private readonly listeners = new Map<CortexEventType, Set<CortexEventListener>>();
142
+ private readonly allListeners = new Set<CortexEventListener>();
143
+ private unsubscribeFromPi: (() => void) | null = null;
144
+ private workingTagsEnabled: boolean;
145
+ private readonly logger: CortexLogger;
146
+
147
+ /**
148
+ * Create an EventBridge.
149
+ *
150
+ * @param workingTagsEnabled - Whether to parse working tags on turn_end
151
+ * @param logger - Optional logger for diagnostics (defaults to silent no-op)
152
+ */
153
+ constructor(workingTagsEnabled = true, logger?: CortexLogger) {
154
+ this.workingTagsEnabled = workingTagsEnabled;
155
+ this.logger = logger ?? NOOP_LOGGER;
156
+ }
157
+
158
+ /**
159
+ * Wire the bridge to a pi-agent-core Agent's event stream.
160
+ * Stores the unsubscribe function for cleanup.
161
+ *
162
+ * @param source - The pi-agent-core Agent (or any PiEventSource)
163
+ */
164
+ wire(source: PiEventSource): void {
165
+ // Clean up previous wiring if any
166
+ this.unwire();
167
+
168
+ this.unsubscribeFromPi = source.subscribe((piEvent: PiEvent) => {
169
+ this.handlePiEvent(piEvent);
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Disconnect from the pi-agent-core event stream.
175
+ */
176
+ unwire(): void {
177
+ if (this.unsubscribeFromPi) {
178
+ this.unsubscribeFromPi();
179
+ this.unsubscribeFromPi = null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Register a listener for a specific event type.
185
+ *
186
+ * @param type - The event type to listen for
187
+ * @param listener - The callback function
188
+ * @returns An unsubscribe function
189
+ */
190
+ on(type: CortexEventType, listener: CortexEventListener): () => void {
191
+ let typeListeners = this.listeners.get(type);
192
+ if (!typeListeners) {
193
+ typeListeners = new Set();
194
+ this.listeners.set(type, typeListeners);
195
+ }
196
+ typeListeners.add(listener);
197
+
198
+ return () => {
199
+ typeListeners!.delete(listener);
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Register a listener for all event types.
205
+ *
206
+ * @param listener - The callback function
207
+ * @returns An unsubscribe function
208
+ */
209
+ onAll(listener: CortexEventListener): () => void {
210
+ this.allListeners.add(listener);
211
+ return () => {
212
+ this.allListeners.delete(listener);
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Forward all events from a child agent's event bridge onto this bridge.
218
+ *
219
+ * Each forwarded event gets `childTaskId` set so consumers can distinguish
220
+ * parent events from child events. Returns an unsubscribe function that
221
+ * stops forwarding (call when the child agent completes or is destroyed).
222
+ *
223
+ * @param childBridge - The child agent's EventBridge
224
+ * @param childTaskId - The sub-agent task ID to tag forwarded events with
225
+ * @returns An unsubscribe function
226
+ */
227
+ forwardFrom(childBridge: EventBridge, childTaskId: string): () => void {
228
+ return childBridge.onAll((event) => {
229
+ this.emit({
230
+ ...event,
231
+ childTaskId,
232
+ });
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Update whether working tags parsing is enabled.
238
+ */
239
+ setWorkingTagsEnabled(enabled: boolean): void {
240
+ this.workingTagsEnabled = enabled;
241
+ }
242
+
243
+ /**
244
+ * Clean up all listeners and disconnect from the pi-agent-core event stream.
245
+ */
246
+ destroy(): void {
247
+ this.unwire();
248
+ this.listeners.clear();
249
+ this.allListeners.clear();
250
+ }
251
+
252
+ /**
253
+ * Handle a pi-agent-core event by mapping and emitting to consumers.
254
+ */
255
+ private handlePiEvent(piEvent: PiEvent): void {
256
+ const cortexType = PI_TO_CORTEX_MAP[piEvent.type];
257
+ if (!cortexType) {
258
+ return;
259
+ }
260
+
261
+ const cortexEvent: CortexEvent = {
262
+ type: cortexType,
263
+ data: piEvent,
264
+ };
265
+
266
+ // Populate typed payload for tool events
267
+ const payload = this.extractToolPayload(cortexType, piEvent);
268
+ if (payload) {
269
+ cortexEvent.payload = payload;
270
+ }
271
+
272
+ // For turn_end, extract typed usage and parse working tags
273
+ if (cortexType === 'turn_end') {
274
+ const usage = this.extractUsage(piEvent);
275
+ if (usage) {
276
+ cortexEvent.usage = usage;
277
+ }
278
+
279
+ if (this.workingTagsEnabled) {
280
+ const text = this.extractTurnText(piEvent);
281
+ if (text) {
282
+ cortexEvent.textOutput = parseWorkingTags(text);
283
+ }
284
+ }
285
+ }
286
+
287
+ this.emit(cortexEvent);
288
+ }
289
+
290
+ /**
291
+ * Extract a typed payload from a pi-agent-core tool event.
292
+ * Returns undefined for non-tool events.
293
+ */
294
+ private extractToolPayload(
295
+ cortexType: CortexEventType,
296
+ piEvent: PiEvent,
297
+ ): CortexEvent['payload'] {
298
+ if (cortexType === 'tool_call_start') {
299
+ return {
300
+ toolCallId: String(piEvent['toolCallId'] ?? piEvent['id'] ?? ''),
301
+ toolName: String(piEvent['toolName'] ?? piEvent['name'] ?? 'unknown'),
302
+ args: (piEvent['args'] ?? piEvent['input'] ?? {}) as Record<string, unknown>,
303
+ } satisfies ToolCallStartPayload;
304
+ }
305
+
306
+ if (cortexType === 'tool_call_update') {
307
+ const partialResult = piEvent['partialResult'] as ToolContentDetails<unknown> | undefined;
308
+ return {
309
+ toolCallId: String(piEvent['toolCallId'] ?? piEvent['id'] ?? ''),
310
+ toolName: String(piEvent['toolName'] ?? piEvent['name'] ?? 'unknown'),
311
+ args: (piEvent['args'] ?? piEvent['input'] ?? {}) as Record<string, unknown>,
312
+ partialResult: partialResult ?? { content: [], details: {} },
313
+ } satisfies ToolCallUpdatePayload;
314
+ }
315
+
316
+ if (cortexType === 'tool_call_end') {
317
+ const result = piEvent['result'] as ToolContentDetails<unknown> | undefined;
318
+ const isError = Boolean(piEvent['isError']);
319
+ const explicitError = piEvent['error'];
320
+ const payload: ToolCallEndPayload = {
321
+ toolCallId: String(piEvent['toolCallId'] ?? piEvent['id'] ?? ''),
322
+ toolName: String(piEvent['toolName'] ?? piEvent['name'] ?? 'unknown'),
323
+ result: result ?? { content: [], details: {} },
324
+ durationMs: Number(piEvent['durationMs'] ?? piEvent['duration'] ?? 0),
325
+ isError,
326
+ };
327
+ if (isError) {
328
+ // Extract error text from multiple possible sources:
329
+ // 1. Explicit error string field
330
+ // 2. Error object with message
331
+ // 3. Result content text (pi-agent-core puts error details here)
332
+ // 4. Fallback
333
+ let errorText: string | undefined;
334
+ if (typeof explicitError === 'string') {
335
+ errorText = explicitError;
336
+ } else if (explicitError instanceof Error) {
337
+ errorText = explicitError.message;
338
+ } else if (typeof explicitError === 'object' && explicitError !== null && 'message' in (explicitError as Record<string, unknown>)) {
339
+ errorText = String((explicitError as Record<string, unknown>)['message']);
340
+ }
341
+
342
+ // If no explicit error, extract from result content
343
+ if (!errorText && result?.content) {
344
+ const textParts = result.content
345
+ .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
346
+ .map(c => c.text);
347
+ if (textParts.length > 0) {
348
+ errorText = textParts.join('\n');
349
+ }
350
+ }
351
+
352
+ payload.error = errorText ?? 'unknown error';
353
+ }
354
+ return payload;
355
+ }
356
+
357
+ return undefined;
358
+ }
359
+
360
+ /**
361
+ * Extract the text content from a turn_end event.
362
+ * Pi-agent-core's turn_end event carries the assistant message for that turn.
363
+ */
364
+ private extractTurnText(piEvent: PiEvent): string | null {
365
+ // The turn_end event from pi-agent-core carries the assistant message.
366
+ // The structure varies, so we try multiple access patterns.
367
+
368
+ // Pattern 1: Direct text property
369
+ if (typeof piEvent['text'] === 'string') {
370
+ return piEvent['text'];
371
+ }
372
+
373
+ // Pattern 2: message.content as string
374
+ const message = piEvent['message'] as Record<string, unknown> | undefined;
375
+ if (message && typeof message['content'] === 'string') {
376
+ return message['content'];
377
+ }
378
+
379
+ // Pattern 3: message.content as array with text parts
380
+ if (message && Array.isArray(message['content'])) {
381
+ const textParts = (message['content'] as Array<{ type: string; text?: string }>)
382
+ .filter((part) => part.type === 'text' && typeof part.text === 'string')
383
+ .map((part) => part.text!);
384
+ if (textParts.length > 0) {
385
+ return textParts.join('');
386
+ }
387
+ }
388
+
389
+ // Pattern 4: result.content
390
+ const result = piEvent['result'] as Record<string, unknown> | undefined;
391
+ if (result && typeof result['content'] === 'string') {
392
+ return result['content'];
393
+ }
394
+
395
+ // Pattern 5: content on the content parts of the result
396
+ if (result && Array.isArray(result['content'])) {
397
+ const textParts = (result['content'] as Array<{ type: string; text?: string }>)
398
+ .filter((part) => part.type === 'text' && typeof part.text === 'string')
399
+ .map((part) => part.text!);
400
+ if (textParts.length > 0) {
401
+ return textParts.join('');
402
+ }
403
+ }
404
+
405
+ return null;
406
+ }
407
+
408
+ /**
409
+ * Extract typed CortexUsage from a turn_end event.
410
+ *
411
+ * Pi-ai's AssistantMessage carries usage at message.usage with a nested
412
+ * cost object. This method navigates the opaque event data once so all
413
+ * subscribers receive clean, typed usage without duplicating extraction.
414
+ */
415
+ private extractUsage(piEvent: PiEvent): CortexUsage | null {
416
+ // Pattern 1: message.usage (pi-ai AssistantMessage, the primary path)
417
+ const message = piEvent['message'] as Record<string, unknown> | undefined;
418
+ if (message) {
419
+ const usage = this.buildUsageFromObject(message['usage']);
420
+ if (usage) {
421
+ if (typeof message['model'] === 'string') {
422
+ usage.model = message['model'];
423
+ }
424
+ return usage;
425
+ }
426
+ }
427
+
428
+ // Pattern 2: Direct usage property on the event
429
+ const directUsage = this.buildUsageFromObject(piEvent['usage']);
430
+ if (directUsage) return directUsage;
431
+
432
+ // Pattern 3: result.usage
433
+ const result = piEvent['result'] as Record<string, unknown> | undefined;
434
+ if (result) {
435
+ const resultUsage = this.buildUsageFromObject(result['usage']);
436
+ if (resultUsage) return resultUsage;
437
+ }
438
+
439
+ return null;
440
+ }
441
+
442
+ /**
443
+ * Build a CortexUsage from a raw usage-shaped object.
444
+ * Returns null if the object is not a valid usage structure.
445
+ */
446
+ private buildUsageFromObject(raw: unknown): CortexUsage | null {
447
+ if (!raw || typeof raw !== 'object') return null;
448
+
449
+ const u = raw as Record<string, unknown>;
450
+ const input = typeof u['input'] === 'number' ? u['input'] : 0;
451
+ const output = typeof u['output'] === 'number' ? u['output'] : 0;
452
+ const cacheRead = typeof u['cacheRead'] === 'number' ? u['cacheRead'] : 0;
453
+ const cacheWrite = typeof u['cacheWrite'] === 'number' ? u['cacheWrite'] : 0;
454
+ const totalTokens = typeof u['totalTokens'] === 'number' ? u['totalTokens'] : input + output;
455
+
456
+ // At least one non-zero field to consider this a valid usage object
457
+ if (input === 0 && output === 0 && cacheRead === 0 && totalTokens === 0) return null;
458
+
459
+ let cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
460
+ const costObj = u['cost'];
461
+ if (costObj && typeof costObj === 'object') {
462
+ const c = costObj as Record<string, unknown>;
463
+ cost = {
464
+ input: typeof c['input'] === 'number' ? c['input'] : 0,
465
+ output: typeof c['output'] === 'number' ? c['output'] : 0,
466
+ cacheRead: typeof c['cacheRead'] === 'number' ? c['cacheRead'] : 0,
467
+ cacheWrite: typeof c['cacheWrite'] === 'number' ? c['cacheWrite'] : 0,
468
+ total: typeof c['total'] === 'number' ? c['total'] : 0,
469
+ };
470
+ }
471
+
472
+ return { input, output, cacheRead, cacheWrite, totalTokens, cost };
473
+ }
474
+
475
+ /**
476
+ * Emit a normalized event to all matching listeners.
477
+ * Each listener is wrapped in try/catch so a throwing listener
478
+ * does not prevent subsequent listeners from receiving the event.
479
+ */
480
+ private emit(event: CortexEvent): void {
481
+ // Notify type-specific listeners
482
+ const typeListeners = this.listeners.get(event.type);
483
+ if (typeListeners) {
484
+ for (const listener of typeListeners) {
485
+ try {
486
+ listener(event);
487
+ } catch (err) {
488
+ this.logger.error('[EventBridge] listener threw', {
489
+ eventType: event.type,
490
+ error: err instanceof Error ? err.message : String(err),
491
+ });
492
+ }
493
+ }
494
+ }
495
+
496
+ // Notify catch-all listeners
497
+ for (const listener of this.allListeners) {
498
+ try {
499
+ listener(event);
500
+ } catch (err) {
501
+ this.logger.error('[EventBridge] catch-all listener threw', {
502
+ eventType: event.type,
503
+ error: err instanceof Error ? err.message : String(err),
504
+ });
505
+ }
506
+ }
507
+ }
508
+ }