@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- 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
|
+
}
|