@data-club/ai-hub-server 0.0.1
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/dist/auth/argon2.d.ts +72 -0
- package/dist/auth/argon2.d.ts.map +1 -0
- package/dist/auth/argon2.js +145 -0
- package/dist/auth/argon2.js.map +1 -0
- package/dist/auth/encryption.d.ts +52 -0
- package/dist/auth/encryption.d.ts.map +1 -0
- package/dist/auth/encryption.js +98 -0
- package/dist/auth/encryption.js.map +1 -0
- package/dist/auth/jwt.d.ts +133 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +147 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +101 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +161 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/passwordPolicy.d.ts +72 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +88 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/roleRules.d.ts +200 -0
- package/dist/auth/roleRules.d.ts.map +1 -0
- package/dist/auth/roleRules.js +365 -0
- package/dist/auth/roleRules.js.map +1 -0
- package/dist/auth/userView.d.ts +55 -0
- package/dist/auth/userView.d.ts.map +1 -0
- package/dist/auth/userView.js +77 -0
- package/dist/auth/userView.js.map +1 -0
- package/dist/bootstrap.d.ts +104 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +160 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/db/connection.d.ts +39 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +62 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +197 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1902 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +662 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/seed.d.ts +92 -0
- package/dist/db/seed.d.ts.map +1 -0
- package/dist/db/seed.js +160 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/handlers/fetch.d.ts +89 -0
- package/dist/handlers/fetch.d.ts.map +1 -0
- package/dist/handlers/fetch.js +173 -0
- package/dist/handlers/fetch.js.map +1 -0
- package/dist/handlers/nextjs.d.ts +40 -0
- package/dist/handlers/nextjs.d.ts.map +1 -0
- package/dist/handlers/nextjs.js +42 -0
- package/dist/handlers/nextjs.js.map +1 -0
- package/dist/handlers/responses.d.ts +46 -0
- package/dist/handlers/responses.d.ts.map +1 -0
- package/dist/handlers/responses.js +74 -0
- package/dist/handlers/responses.js.map +1 -0
- package/dist/handlers/routeTable.d.ts +21 -0
- package/dist/handlers/routeTable.d.ts.map +1 -0
- package/dist/handlers/routeTable.js +133 -0
- package/dist/handlers/routeTable.js.map +1 -0
- package/dist/handlers/router.d.ts +37 -0
- package/dist/handlers/router.d.ts.map +1 -0
- package/dist/handlers/router.js +82 -0
- package/dist/handlers/router.js.map +1 -0
- package/dist/handlers/sse.d.ts +38 -0
- package/dist/handlers/sse.d.ts.map +1 -0
- package/dist/handlers/sse.js +52 -0
- package/dist/handlers/sse.js.map +1 -0
- package/dist/handlers/types.d.ts +96 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +14 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/letta/client.d.ts +355 -0
- package/dist/letta/client.d.ts.map +1 -0
- package/dist/letta/client.js +529 -0
- package/dist/letta/client.js.map +1 -0
- package/dist/letta/eventTranslator.d.ts +166 -0
- package/dist/letta/eventTranslator.d.ts.map +1 -0
- package/dist/letta/eventTranslator.js +532 -0
- package/dist/letta/eventTranslator.js.map +1 -0
- package/dist/letta/index.d.ts +13 -0
- package/dist/letta/index.d.ts.map +1 -0
- package/dist/letta/index.js +12 -0
- package/dist/letta/index.js.map +1 -0
- package/dist/letta/modelSettings.d.ts +176 -0
- package/dist/letta/modelSettings.d.ts.map +1 -0
- package/dist/letta/modelSettings.js +314 -0
- package/dist/letta/modelSettings.js.map +1 -0
- package/dist/letta/promptWrapping.d.ts +141 -0
- package/dist/letta/promptWrapping.d.ts.map +1 -0
- package/dist/letta/promptWrapping.js +213 -0
- package/dist/letta/promptWrapping.js.map +1 -0
- package/dist/letta/sandbox.d.ts +100 -0
- package/dist/letta/sandbox.d.ts.map +1 -0
- package/dist/letta/sandbox.js +151 -0
- package/dist/letta/sandbox.js.map +1 -0
- package/dist/letta/settingsFileSeed.d.ts +146 -0
- package/dist/letta/settingsFileSeed.d.ts.map +1 -0
- package/dist/letta/settingsFileSeed.js +177 -0
- package/dist/letta/settingsFileSeed.js.map +1 -0
- package/dist/letta/subprocessThrottle.d.ts +151 -0
- package/dist/letta/subprocessThrottle.d.ts.map +1 -0
- package/dist/letta/subprocessThrottle.js +195 -0
- package/dist/letta/subprocessThrottle.js.map +1 -0
- package/dist/letta/tpmRecovery.d.ts +202 -0
- package/dist/letta/tpmRecovery.d.ts.map +1 -0
- package/dist/letta/tpmRecovery.js +314 -0
- package/dist/letta/tpmRecovery.js.map +1 -0
- package/dist/letta/types.d.ts +54 -0
- package/dist/letta/types.d.ts.map +1 -0
- package/dist/letta/types.js +16 -0
- package/dist/letta/types.js.map +1 -0
- package/dist/observability/logger.d.ts +48 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +81 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +92 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/routes/admin/agents.d.ts +63 -0
- package/dist/routes/admin/agents.d.ts.map +1 -0
- package/dist/routes/admin/agents.js +461 -0
- package/dist/routes/admin/agents.js.map +1 -0
- package/dist/routes/admin/rotatingMessages.d.ts +51 -0
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
- package/dist/routes/admin/rotatingMessages.js +292 -0
- package/dist/routes/admin/rotatingMessages.js.map +1 -0
- package/dist/routes/admin/sessions.d.ts +61 -0
- package/dist/routes/admin/sessions.d.ts.map +1 -0
- package/dist/routes/admin/sessions.js +232 -0
- package/dist/routes/admin/sessions.js.map +1 -0
- package/dist/routes/admin/settings.d.ts +110 -0
- package/dist/routes/admin/settings.d.ts.map +1 -0
- package/dist/routes/admin/settings.js +401 -0
- package/dist/routes/admin/settings.js.map +1 -0
- package/dist/routes/admin/users.d.ts +115 -0
- package/dist/routes/admin/users.d.ts.map +1 -0
- package/dist/routes/admin/users.js +374 -0
- package/dist/routes/admin/users.js.map +1 -0
- package/dist/routes/agents.d.ts +30 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +55 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +121 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +422 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/chat.d.ts +97 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +484 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/featureToggles.d.ts +34 -0
- package/dist/routes/featureToggles.d.ts.map +1 -0
- package/dist/routes/featureToggles.js +47 -0
- package/dist/routes/featureToggles.js.map +1 -0
- package/dist/routes/health.d.ts +24 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/metrics.d.ts +35 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +106 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/sessions.d.ts +161 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +1501 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stubs.d.ts +25 -0
- package/dist/routes/stubs.d.ts.map +1 -0
- package/dist/routes/stubs.js +25 -0
- package/dist/routes/stubs.js.map +1 -0
- package/dist/state/cron.d.ts +58 -0
- package/dist/state/cron.d.ts.map +1 -0
- package/dist/state/cron.js +160 -0
- package/dist/state/cron.js.map +1 -0
- package/dist/state/rotationRunner.d.ts +96 -0
- package/dist/state/rotationRunner.d.ts.map +1 -0
- package/dist/state/rotationRunner.js +246 -0
- package/dist/state/rotationRunner.js.map +1 -0
- package/dist/state/rotationScheduler.d.ts +76 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -0
- package/dist/state/rotationScheduler.js +124 -0
- package/dist/state/rotationScheduler.js.map +1 -0
- package/dist/state/turnRegistry.d.ts +95 -0
- package/dist/state/turnRegistry.d.ts.map +1 -0
- package/dist/state/turnRegistry.js +149 -0
- package/dist/state/turnRegistry.js.map +1 -0
- package/dist/state/turnRunner.d.ts +134 -0
- package/dist/state/turnRunner.d.ts.map +1 -0
- package/dist/state/turnRunner.js +476 -0
- package/dist/state/turnRunner.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/argon2.ts +152 -0
- package/src/auth/encryption.ts +110 -0
- package/src/auth/jwt.ts +217 -0
- package/src/auth/middleware.ts +250 -0
- package/src/auth/passwordPolicy.ts +99 -0
- package/src/auth/roleRules.ts +497 -0
- package/src/auth/userView.ts +88 -0
- package/src/bootstrap.ts +254 -0
- package/src/db/connection.ts +75 -0
- package/src/db/migrate.ts +212 -0
- package/src/db/schema.ts +750 -0
- package/src/db/seed.ts +236 -0
- package/src/handlers/fetch.ts +251 -0
- package/src/handlers/nextjs.ts +49 -0
- package/src/handlers/responses.ts +95 -0
- package/src/handlers/routeTable.ts +188 -0
- package/src/handlers/router.ts +96 -0
- package/src/handlers/sse.ts +55 -0
- package/src/handlers/types.ts +104 -0
- package/src/index.ts +60 -0
- package/src/letta/client.ts +860 -0
- package/src/letta/eventTranslator.ts +667 -0
- package/src/letta/index.ts +46 -0
- package/src/letta/lettaCliShim.mjs +40 -0
- package/src/letta/sandbox.ts +167 -0
- package/src/letta/settingsFileSeed.ts +254 -0
- package/src/letta/subprocessThrottle.ts +251 -0
- package/src/letta/types.ts +65 -0
- package/src/observability/logger.ts +103 -0
- package/src/observability/metrics.ts +114 -0
- package/src/routes/admin/agents.ts +574 -0
- package/src/routes/admin/rotatingMessages.ts +351 -0
- package/src/routes/admin/sessions.ts +283 -0
- package/src/routes/admin/settings.ts +463 -0
- package/src/routes/admin/users.ts +455 -0
- package/src/routes/agents.ts +66 -0
- package/src/routes/auth.ts +513 -0
- package/src/routes/chat.ts +534 -0
- package/src/routes/featureToggles.ts +52 -0
- package/src/routes/health.ts +44 -0
- package/src/routes/metrics.ts +111 -0
- package/src/routes/sessions.ts +1878 -0
- package/src/state/cron.ts +179 -0
- package/src/state/rotationRunner.ts +326 -0
- package/src/state/rotationScheduler.ts +164 -0
- package/src/state/turnRegistry.ts +210 -0
- package/src/state/turnRunner.ts +588 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function: Letta Code SDK message → ai-hub SSE events (§9).
|
|
3
|
+
*
|
|
4
|
+
* Input type: `SDKMessage` (Letta Code SDK's stdin/stdout JSON wire
|
|
5
|
+
* shape). Output type: ai-hub's stable SSE events from §9.
|
|
6
|
+
*
|
|
7
|
+
* The translator has **no I/O** — it takes one `SDKMessage` and the
|
|
8
|
+
* current translation state (running text length, open tool-call ids)
|
|
9
|
+
* and returns zero or more ai-hub event payloads + the next state.
|
|
10
|
+
* Persistence, `seq` assignment, and SSE writing are the caller's
|
|
11
|
+
* responsibility (see `state/turnRunner.ts`).
|
|
12
|
+
*
|
|
13
|
+
* ## SDK message variants we handle
|
|
14
|
+
*
|
|
15
|
+
* - `assistant` — streaming chunk of assistant text. Same `uuid` across
|
|
16
|
+
* chunks of one logical assistant message; different `uuid` signals a
|
|
17
|
+
* new logical step (multi-step turn). Emits `assistant_delta`.
|
|
18
|
+
* - `reasoning` — same as `assistant` but for the reasoning channel.
|
|
19
|
+
* Emits `reasoning_delta`. Phase → `model_reasoning`.
|
|
20
|
+
* - `tool_call` — incremental tool-call event. Same `toolCallId` across
|
|
21
|
+
* chunks; `rawArguments` carries one chunk of the partial JSON
|
|
22
|
+
* arguments. First sighting emits `tool_call_started`; subsequent
|
|
23
|
+
* sightings (or first sighting with a non-empty `rawArguments`) emit
|
|
24
|
+
* `tool_call_input_delta`. Phase → `model_tool_input`.
|
|
25
|
+
* - `tool_result` — one event per call. Emits `tool_call_dispatched`
|
|
26
|
+
* (if not already emitted for this id, with the assembled JSON input)
|
|
27
|
+
* then `tool_call_result` (success or error variant).
|
|
28
|
+
* - `result` — terminal. `success: true` → `assistant_message_complete`
|
|
29
|
+
* followed by `stream_end {done}`. `success: false` with
|
|
30
|
+
* `errorCode: "interrupted"` → `stream_end {cancelled}`. Any other
|
|
31
|
+
* failure → `error` + `stream_end {error}`.
|
|
32
|
+
* - `error` — non-terminal error inside the stream (rare; Letta Code
|
|
33
|
+
* surfaces most errors through the terminal `result` event). Emits
|
|
34
|
+
* `error` and does NOT change phase — the runner decides whether the
|
|
35
|
+
* turn fails based on subsequent events.
|
|
36
|
+
* - `retry` — Letta Code's internal retry notice (its own TPM
|
|
37
|
+
* recovery). We drop it; users don't need to see it in v1.x.
|
|
38
|
+
* - `init` — never appears on `stream()`; it's the return of
|
|
39
|
+
* `session.initialize()`. We don't process it here.
|
|
40
|
+
* - `stream_event` — only emitted when `--include-partial-messages` is
|
|
41
|
+
* set on the CLI. We don't enable that flag.
|
|
42
|
+
*
|
|
43
|
+
* @see SPECIFICATIONS.md §9 — wire protocol
|
|
44
|
+
* @see SPECIFICATIONS.md §10 — state machine
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type {
|
|
48
|
+
AIHubEvent,
|
|
49
|
+
AIHubEventState,
|
|
50
|
+
AIHubMessage,
|
|
51
|
+
RunningPhase,
|
|
52
|
+
TurnStatus,
|
|
53
|
+
} from "@data-club/ai-hub";
|
|
54
|
+
import type { SDKMessage } from "@letta-ai/letta-code-sdk";
|
|
55
|
+
|
|
56
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
57
|
+
/* Translator state */
|
|
58
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Carried across events for a single turn. The translator is otherwise
|
|
62
|
+
* pure — no module-level mutable state.
|
|
63
|
+
*
|
|
64
|
+
* The shape is preserved verbatim from the pre-migration translator so
|
|
65
|
+
* `state/turnRunner.ts` keeps its existing call sites unchanged. The
|
|
66
|
+
* `assistantText` / `reasoningText` accumulators are still the canonical
|
|
67
|
+
* assembled payloads (the SDK delivers deltas in `content`); the
|
|
68
|
+
* step-boundary detection now uses `uuid` change instead of Letta's
|
|
69
|
+
* `id` field on `assistant_message`.
|
|
70
|
+
*/
|
|
71
|
+
export interface TranslatorState {
|
|
72
|
+
/** Current `(status, phase)` pair. Pre-emission state. */
|
|
73
|
+
status: TurnStatus;
|
|
74
|
+
/** Current sub-phase or `null`. */
|
|
75
|
+
phase: RunningPhase | null;
|
|
76
|
+
/** Running concatenation of assistant deltas across the current step. */
|
|
77
|
+
assistantText: string;
|
|
78
|
+
/** Same idea for reasoning content (only populated when effort != none). */
|
|
79
|
+
reasoningText: string;
|
|
80
|
+
/**
|
|
81
|
+
* Tool-call ids we've already announced via `tool_call_started`. The
|
|
82
|
+
* translator only emits `started` on first sight; subsequent arrivals
|
|
83
|
+
* with the same id emit deltas.
|
|
84
|
+
*/
|
|
85
|
+
openToolCalls: Set<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Per-id accumulator for the tool-call's `rawArguments` chunks. Used
|
|
88
|
+
* to assemble the full JSON input we send on `tool_call_dispatched`.
|
|
89
|
+
*/
|
|
90
|
+
toolCallArguments: Map<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* Tool-call ids we've already emitted `tool_call_dispatched` for. The
|
|
93
|
+
* runner doesn't depend on per-call timing, just "dispatched yet or
|
|
94
|
+
* not"; we transition phase → `tool_executing` on first sighting of
|
|
95
|
+
* a `tool_result` for an undispatched call.
|
|
96
|
+
*/
|
|
97
|
+
dispatchedToolCalls: Set<string>;
|
|
98
|
+
/**
|
|
99
|
+
* Stable `uuid` of the assistant message currently being assembled,
|
|
100
|
+
* if any. Used by the step-boundary detector to spot when a new
|
|
101
|
+
* logical step starts (new uuid means new step).
|
|
102
|
+
*/
|
|
103
|
+
assistantMessageUuid: string | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Builds the per-turn initial state. */
|
|
107
|
+
export function initialTranslatorState(): TranslatorState {
|
|
108
|
+
return {
|
|
109
|
+
status: "running",
|
|
110
|
+
phase: "model_text",
|
|
111
|
+
assistantText: "",
|
|
112
|
+
reasoningText: "",
|
|
113
|
+
openToolCalls: new Set(),
|
|
114
|
+
toolCallArguments: new Map(),
|
|
115
|
+
dispatchedToolCalls: new Set(),
|
|
116
|
+
assistantMessageUuid: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Output of a single translation step. */
|
|
121
|
+
export interface TranslateStep {
|
|
122
|
+
/** Zero-or-more ai-hub event payloads (without `seq` — the runner assigns). */
|
|
123
|
+
events: ReadonlyArray<UnsequencedEvent>;
|
|
124
|
+
/** Next translator state. */
|
|
125
|
+
state: TranslatorState;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* An ai-hub event without the `seq` field. The runner assigns seq when
|
|
130
|
+
* it persists + writes the event. The `error` event additionally
|
|
131
|
+
* carries a required `request_id` that only the runner knows — we
|
|
132
|
+
* strip it from the translator surface so the per-variant translators
|
|
133
|
+
* stay pure (no need to thread the requestId through every call).
|
|
134
|
+
* `emit()` in the runner stamps it on every error event before it goes
|
|
135
|
+
* out on the wire.
|
|
136
|
+
*
|
|
137
|
+
* Uses a distributive conditional so each variant of `AIHubEvent` is
|
|
138
|
+
* mapped independently — without this, the default non-distributive
|
|
139
|
+
* `Omit` would collapse the union to just the keys common to every
|
|
140
|
+
* variant (which loses `data` on every variant except `heartbeat`).
|
|
141
|
+
*/
|
|
142
|
+
export type UnsequencedEvent = AIHubEvent extends infer T
|
|
143
|
+
? T extends AIHubEvent
|
|
144
|
+
? T extends { event: "error"; data: infer D }
|
|
145
|
+
? Omit<T, "seq" | "data"> & { data: Omit<D, "request_id"> }
|
|
146
|
+
: Omit<T, "seq">
|
|
147
|
+
: never
|
|
148
|
+
: never;
|
|
149
|
+
|
|
150
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
151
|
+
/* Entry point */
|
|
152
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Translates one SDK message into zero-or-more ai-hub events.
|
|
156
|
+
*
|
|
157
|
+
* Pure: no I/O, no module state, no clocks (the caller stamps
|
|
158
|
+
* timestamps).
|
|
159
|
+
*
|
|
160
|
+
* @param msg - the Letta Code SDK message.
|
|
161
|
+
* @param state - the translator state.
|
|
162
|
+
* @returns the events to emit + the next state.
|
|
163
|
+
*/
|
|
164
|
+
export function translateSdkMessage(
|
|
165
|
+
msg: SDKMessage,
|
|
166
|
+
state: TranslatorState,
|
|
167
|
+
): TranslateStep {
|
|
168
|
+
// Mid-stream step-boundary detection. A turn that runs in multiple
|
|
169
|
+
// steps (reasoning + tool + assistant text, then reasoning + tool +
|
|
170
|
+
// assistant text again …) produces multiple logical assistant
|
|
171
|
+
// messages within one streaming response. The boundary fires when
|
|
172
|
+
// we've already accumulated assistant text and the next event is
|
|
173
|
+
// either:
|
|
174
|
+
//
|
|
175
|
+
// - a `reasoning` chunk (new step's reasoning starting), or
|
|
176
|
+
// - a `tool_call` chunk (new step's first tool call), or
|
|
177
|
+
// - an `assistant` chunk with a different `uuid` than the one
|
|
178
|
+
// currently being assembled.
|
|
179
|
+
if (isStepBoundary(msg, state)) {
|
|
180
|
+
const boundaryEvents = finalizeAssistantMessageMidStream(state);
|
|
181
|
+
const fresh = resetForNewStep(state);
|
|
182
|
+
const inner = translateInner(msg, fresh);
|
|
183
|
+
return {
|
|
184
|
+
events: [...boundaryEvents, ...inner.events],
|
|
185
|
+
state: inner.state,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return translateInner(msg, state);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns true when `msg` signals that the current logical assistant
|
|
193
|
+
* step is done and a new one is starting. Triggers only when the
|
|
194
|
+
* translator already has accumulated assistant text — otherwise there's
|
|
195
|
+
* nothing to finalize and the message just enters the current step.
|
|
196
|
+
*/
|
|
197
|
+
function isStepBoundary(msg: SDKMessage, state: TranslatorState): boolean {
|
|
198
|
+
if (state.assistantText.length === 0) return false;
|
|
199
|
+
switch (msg.type) {
|
|
200
|
+
case "reasoning":
|
|
201
|
+
case "tool_call":
|
|
202
|
+
return true;
|
|
203
|
+
case "assistant":
|
|
204
|
+
return (
|
|
205
|
+
state.assistantMessageUuid !== null &&
|
|
206
|
+
msg.uuid !== state.assistantMessageUuid
|
|
207
|
+
);
|
|
208
|
+
default:
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resets the per-step accumulators for a new logical step. Carries
|
|
215
|
+
* `status`/`phase` through — the next event will set them via its
|
|
216
|
+
* variant translator.
|
|
217
|
+
*/
|
|
218
|
+
function resetForNewStep(state: TranslatorState): TranslatorState {
|
|
219
|
+
return {
|
|
220
|
+
...state,
|
|
221
|
+
assistantText: "",
|
|
222
|
+
reasoningText: "",
|
|
223
|
+
openToolCalls: new Set(),
|
|
224
|
+
toolCallArguments: new Map(),
|
|
225
|
+
dispatchedToolCalls: new Set(),
|
|
226
|
+
assistantMessageUuid: null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Mid-stream sibling of `finalizeAssistantMessage`. Emits the same
|
|
232
|
+
* `assistant_message_complete` for the accumulated step but snapshots
|
|
233
|
+
* the state as `{ status: "running", phase: <current> }` — the turn
|
|
234
|
+
* isn't done yet, only this logical step is. Returns empty when no
|
|
235
|
+
* assistant text was accumulated.
|
|
236
|
+
*/
|
|
237
|
+
function finalizeAssistantMessageMidStream(
|
|
238
|
+
state: TranslatorState,
|
|
239
|
+
): ReadonlyArray<UnsequencedEvent> {
|
|
240
|
+
if (state.assistantText.length === 0) return [];
|
|
241
|
+
const message: AIHubMessage = {
|
|
242
|
+
id: state.assistantMessageUuid ?? "",
|
|
243
|
+
role: "assistant",
|
|
244
|
+
content: state.assistantText,
|
|
245
|
+
created_at: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
event: "assistant_message_complete",
|
|
250
|
+
data: { message },
|
|
251
|
+
state: snapshot(state),
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The original per-variant switch — extracted so the entry-point can
|
|
257
|
+
* wrap it with the step-boundary check above. */
|
|
258
|
+
function translateInner(msg: SDKMessage, state: TranslatorState): TranslateStep {
|
|
259
|
+
switch (msg.type) {
|
|
260
|
+
case "assistant":
|
|
261
|
+
return translateAssistant(msg, state);
|
|
262
|
+
case "reasoning":
|
|
263
|
+
return translateReasoning(msg, state);
|
|
264
|
+
case "tool_call":
|
|
265
|
+
return translateToolCall(msg, state);
|
|
266
|
+
case "tool_result":
|
|
267
|
+
return translateToolResult(msg, state);
|
|
268
|
+
case "error":
|
|
269
|
+
return translateError(msg, state);
|
|
270
|
+
case "result":
|
|
271
|
+
case "init":
|
|
272
|
+
case "retry":
|
|
273
|
+
case "stream_event":
|
|
274
|
+
default:
|
|
275
|
+
// `result` is terminal — the runner detects it and calls
|
|
276
|
+
// `finalizeAssistantMessage` + `finalizeTerminal` itself. The
|
|
277
|
+
// others are either no-ops (`init` never appears in stream();
|
|
278
|
+
// `retry` is internal; `stream_event` requires a flag we don't
|
|
279
|
+
// set) or unknown future variants we pass through.
|
|
280
|
+
return { events: [], state };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ── per-variant translators ──────────────────────────────────────────────── */
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `assistant` → `assistant_delta`. Each chunk carries the **delta** text
|
|
288
|
+
* in `content` (not cumulative); we forward the delta verbatim and
|
|
289
|
+
* accumulate the full text in `assistantText` for the eventual
|
|
290
|
+
* `assistant_message_complete`.
|
|
291
|
+
*/
|
|
292
|
+
function translateAssistant(
|
|
293
|
+
msg: Extract<SDKMessage, { type: "assistant" }>,
|
|
294
|
+
state: TranslatorState,
|
|
295
|
+
): TranslateStep {
|
|
296
|
+
if (!msg.content || msg.content.length === 0) {
|
|
297
|
+
return { events: [], state };
|
|
298
|
+
}
|
|
299
|
+
const next: TranslatorState = {
|
|
300
|
+
...state,
|
|
301
|
+
status: "running",
|
|
302
|
+
phase: "model_text",
|
|
303
|
+
assistantText: state.assistantText + msg.content,
|
|
304
|
+
assistantMessageUuid: state.assistantMessageUuid ?? msg.uuid,
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
events: [
|
|
308
|
+
{
|
|
309
|
+
event: "assistant_delta",
|
|
310
|
+
data: { delta: msg.content },
|
|
311
|
+
state: snapshot(next),
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
state: next,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* `reasoning` → `reasoning_delta`. Phase transitions to
|
|
320
|
+
* `model_reasoning` while the block is in flight; the next non-
|
|
321
|
+
* reasoning message drives it back to `model_text`.
|
|
322
|
+
*/
|
|
323
|
+
function translateReasoning(
|
|
324
|
+
msg: Extract<SDKMessage, { type: "reasoning" }>,
|
|
325
|
+
state: TranslatorState,
|
|
326
|
+
): TranslateStep {
|
|
327
|
+
if (!msg.content || msg.content.length === 0) {
|
|
328
|
+
return { events: [], state };
|
|
329
|
+
}
|
|
330
|
+
const next: TranslatorState = {
|
|
331
|
+
...state,
|
|
332
|
+
status: "running",
|
|
333
|
+
phase: "model_reasoning",
|
|
334
|
+
reasoningText: state.reasoningText + msg.content,
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
events: [
|
|
338
|
+
{
|
|
339
|
+
event: "reasoning_delta",
|
|
340
|
+
data: { delta: msg.content },
|
|
341
|
+
state: snapshot(next),
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
state: next,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* `tool_call` → `tool_call_started` on first sighting of the id, then
|
|
350
|
+
* `tool_call_input_delta` for each chunk of `rawArguments`. The
|
|
351
|
+
* SDK delivers tool-call arguments in multiple chunks (e.g. 3 events
|
|
352
|
+
* for `{"path": "/etc/hostname"}`) sharing the same `toolCallId`.
|
|
353
|
+
*
|
|
354
|
+
* We accumulate `rawArguments` per id so `translateToolResult` can
|
|
355
|
+
* include the assembled parsed input in `tool_call_dispatched`.
|
|
356
|
+
*/
|
|
357
|
+
function translateToolCall(
|
|
358
|
+
msg: Extract<SDKMessage, { type: "tool_call" }>,
|
|
359
|
+
state: TranslatorState,
|
|
360
|
+
): TranslateStep {
|
|
361
|
+
const id = msg.toolCallId;
|
|
362
|
+
if (!id) return { events: [], state };
|
|
363
|
+
|
|
364
|
+
const events: UnsequencedEvent[] = [];
|
|
365
|
+
let next: TranslatorState = {
|
|
366
|
+
...state,
|
|
367
|
+
status: "running",
|
|
368
|
+
phase: "model_tool_input",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (!next.openToolCalls.has(id)) {
|
|
372
|
+
events.push({
|
|
373
|
+
event: "tool_call_started",
|
|
374
|
+
data: { tool_call_id: id, tool_name: msg.toolName },
|
|
375
|
+
state: snapshot(next),
|
|
376
|
+
});
|
|
377
|
+
const opened = new Set(next.openToolCalls);
|
|
378
|
+
opened.add(id);
|
|
379
|
+
next = { ...next, openToolCalls: opened };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Per-chunk rawArguments accumulation. The 1st event sometimes
|
|
383
|
+
// arrives with toolInput={raw:""} and no rawArguments; subsequent
|
|
384
|
+
// events carry chunks of the JSON args string.
|
|
385
|
+
const chunk = msg.rawArguments ?? "";
|
|
386
|
+
if (chunk.length > 0) {
|
|
387
|
+
const accumulated = (next.toolCallArguments.get(id) ?? "") + chunk;
|
|
388
|
+
const nextArgs = new Map(next.toolCallArguments);
|
|
389
|
+
nextArgs.set(id, accumulated);
|
|
390
|
+
next = { ...next, toolCallArguments: nextArgs };
|
|
391
|
+
|
|
392
|
+
events.push({
|
|
393
|
+
event: "tool_call_input_delta",
|
|
394
|
+
data: { tool_call_id: id, partial_json: chunk },
|
|
395
|
+
state: snapshot(next),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { events, state: next };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* `tool_result` → `tool_call_dispatched` (if not already) +
|
|
404
|
+
* `tool_call_result`.
|
|
405
|
+
*
|
|
406
|
+
* Phase transitions: `model_tool_input` → `tool_executing` (on
|
|
407
|
+
* dispatched) → `model_text` (after the result lands). Per §10.3 a
|
|
408
|
+
* tool error does NOT fail the turn — phase reverts to `model_text`
|
|
409
|
+
* either way.
|
|
410
|
+
*/
|
|
411
|
+
function translateToolResult(
|
|
412
|
+
msg: Extract<SDKMessage, { type: "tool_result" }>,
|
|
413
|
+
state: TranslatorState,
|
|
414
|
+
): TranslateStep {
|
|
415
|
+
const id = msg.toolCallId;
|
|
416
|
+
if (!id) return { events: [], state };
|
|
417
|
+
|
|
418
|
+
const events: UnsequencedEvent[] = [];
|
|
419
|
+
let next: TranslatorState = { ...state };
|
|
420
|
+
|
|
421
|
+
if (!next.dispatchedToolCalls.has(id)) {
|
|
422
|
+
const assembled = next.toolCallArguments.get(id);
|
|
423
|
+
events.push({
|
|
424
|
+
event: "tool_call_dispatched",
|
|
425
|
+
data: {
|
|
426
|
+
tool_call_id: id,
|
|
427
|
+
input: tryParseJson(assembled),
|
|
428
|
+
},
|
|
429
|
+
state: snapshot({ ...next, phase: "tool_executing" }),
|
|
430
|
+
});
|
|
431
|
+
const dispatched = new Set(next.dispatchedToolCalls);
|
|
432
|
+
dispatched.add(id);
|
|
433
|
+
next = {
|
|
434
|
+
...next,
|
|
435
|
+
phase: "tool_executing",
|
|
436
|
+
dispatchedToolCalls: dispatched,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (msg.isError) {
|
|
441
|
+
events.push({
|
|
442
|
+
event: "tool_call_result",
|
|
443
|
+
data: {
|
|
444
|
+
tool_call_id: id,
|
|
445
|
+
error: { code: "tool_error", message: msg.content ?? "" },
|
|
446
|
+
},
|
|
447
|
+
state: snapshot({ ...next, phase: "model_text" }),
|
|
448
|
+
});
|
|
449
|
+
} else {
|
|
450
|
+
events.push({
|
|
451
|
+
event: "tool_call_result",
|
|
452
|
+
data: { tool_call_id: id, output: msg.content ?? "" },
|
|
453
|
+
state: snapshot({ ...next, phase: "model_text" }),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
next = { ...next, phase: "model_text" };
|
|
458
|
+
return { events, state: next };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* `error` SDK message → `error` SSE event.
|
|
463
|
+
*
|
|
464
|
+
* Letta Code surfaces most errors through the terminal `result` event
|
|
465
|
+
* (see `finalizeTerminal`), but a few error conditions also produce a
|
|
466
|
+
* mid-stream `error` event with `stopReason` and an optional
|
|
467
|
+
* `apiError`. We pass them through as ai-hub `error` events; the
|
|
468
|
+
* runner decides whether the turn should fail (it typically does, but
|
|
469
|
+
* the terminal `result` event is the authoritative signal).
|
|
470
|
+
*/
|
|
471
|
+
function translateError(
|
|
472
|
+
msg: Extract<SDKMessage, { type: "error" }>,
|
|
473
|
+
state: TranslatorState,
|
|
474
|
+
): TranslateStep {
|
|
475
|
+
const code = classifyErrorCode(msg.errorCode, msg.stopReason);
|
|
476
|
+
return {
|
|
477
|
+
events: [
|
|
478
|
+
{
|
|
479
|
+
event: "error",
|
|
480
|
+
data: {
|
|
481
|
+
code,
|
|
482
|
+
message: msg.message ?? "Letta Code reported an error.",
|
|
483
|
+
},
|
|
484
|
+
state: snapshot(state),
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
state,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
492
|
+
/* Finalizers */
|
|
493
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Emits the closing `assistant_message_complete` event for a turn that
|
|
497
|
+
* accumulated assistant text. Called by the runner exactly once when
|
|
498
|
+
* the terminal `result` SDK message arrives — but only when the turn
|
|
499
|
+
* succeeded. For a cancelled / failed turn we still emit it (the user
|
|
500
|
+
* deserves to see whatever partial text was generated), but the
|
|
501
|
+
* snapshot reflects the terminal status.
|
|
502
|
+
*
|
|
503
|
+
* @param state - the translator state at end-of-stream.
|
|
504
|
+
* @param terminalStatus - the status to snapshot into the envelope.
|
|
505
|
+
* @returns the closing events (empty if no assistant text was produced).
|
|
506
|
+
*/
|
|
507
|
+
export function finalizeAssistantMessage(
|
|
508
|
+
state: TranslatorState,
|
|
509
|
+
terminalStatus: TurnStatus = "done",
|
|
510
|
+
): ReadonlyArray<UnsequencedEvent> {
|
|
511
|
+
if (state.assistantText.length === 0) return [];
|
|
512
|
+
const message: AIHubMessage = {
|
|
513
|
+
id: state.assistantMessageUuid ?? "",
|
|
514
|
+
role: "assistant",
|
|
515
|
+
content: state.assistantText,
|
|
516
|
+
created_at: new Date().toISOString(),
|
|
517
|
+
};
|
|
518
|
+
return [
|
|
519
|
+
{
|
|
520
|
+
event: "assistant_message_complete",
|
|
521
|
+
data: { message },
|
|
522
|
+
state: snapshot({ ...state, status: terminalStatus, phase: null }),
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Interprets the terminal SDK `result` message into the right closing
|
|
529
|
+
* sequence: `assistant_message_complete` (if any) + the final
|
|
530
|
+
* `stream_end` with the appropriate reason. Pure function — no I/O.
|
|
531
|
+
*
|
|
532
|
+
* Behavior matrix:
|
|
533
|
+
*
|
|
534
|
+
* | result.success | result.errorCode | Emitted closing events |
|
|
535
|
+
* |---|---|---|
|
|
536
|
+
* | true | — | assistant_message_complete + stream_end{done} |
|
|
537
|
+
* | false | "interrupted" | assistant_message_complete (with partial text, if any) + stream_end{cancelled} |
|
|
538
|
+
* | false | anything else | (assistant_message_complete if partial text) + error + stream_end{error} |
|
|
539
|
+
*
|
|
540
|
+
* @param msg - the terminal SDK result message.
|
|
541
|
+
* @param state - the translator state right before the result arrived.
|
|
542
|
+
* @returns the events to emit + the (terminal) state.
|
|
543
|
+
*/
|
|
544
|
+
export function finalizeTerminal(
|
|
545
|
+
msg: Extract<SDKMessage, { type: "result" }>,
|
|
546
|
+
state: TranslatorState,
|
|
547
|
+
): TranslateStep {
|
|
548
|
+
if (msg.success) {
|
|
549
|
+
const completion = finalizeAssistantMessage(state, "done");
|
|
550
|
+
const finalState: TranslatorState = { ...state, status: "done", phase: null };
|
|
551
|
+
return {
|
|
552
|
+
events: [
|
|
553
|
+
...completion,
|
|
554
|
+
{
|
|
555
|
+
event: "stream_end",
|
|
556
|
+
data: { reason: "done" },
|
|
557
|
+
state: snapshot(finalState),
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
state: finalState,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (msg.errorCode === "interrupted") {
|
|
565
|
+
const completion = finalizeAssistantMessage(state, "cancelled");
|
|
566
|
+
const finalState: TranslatorState = { ...state, status: "cancelled", phase: null };
|
|
567
|
+
return {
|
|
568
|
+
events: [
|
|
569
|
+
...completion,
|
|
570
|
+
{
|
|
571
|
+
event: "stream_end",
|
|
572
|
+
data: { reason: "cancelled" },
|
|
573
|
+
state: snapshot(finalState),
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
state: finalState,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Any other failure (llm_api_error, max_steps, protocol_error,
|
|
581
|
+
// stream_closed, …) is a hard fail. Still emit the partial assistant
|
|
582
|
+
// text if any was produced, then the error and stream_end.
|
|
583
|
+
const partial = finalizeAssistantMessage(state, "failed");
|
|
584
|
+
const finalState: TranslatorState = { ...state, status: "failed", phase: null };
|
|
585
|
+
const code = classifyErrorCode(msg.errorCode, msg.stopReason);
|
|
586
|
+
return {
|
|
587
|
+
events: [
|
|
588
|
+
...partial,
|
|
589
|
+
{
|
|
590
|
+
event: "error",
|
|
591
|
+
data: {
|
|
592
|
+
code,
|
|
593
|
+
message: msg.errorDetail ?? msg.error ?? "Letta Code reported a terminal error.",
|
|
594
|
+
},
|
|
595
|
+
state: snapshot(finalState),
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
event: "stream_end",
|
|
599
|
+
data: { reason: "error" },
|
|
600
|
+
state: snapshot(finalState),
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
state: finalState,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
608
|
+
/* Internal helpers */
|
|
609
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
610
|
+
|
|
611
|
+
/** Snapshots `(status, phase)` for the SSE event envelope. */
|
|
612
|
+
function snapshot(state: TranslatorState): AIHubEventState {
|
|
613
|
+
return { status: state.status, phase: state.phase };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Best-effort JSON parse. Returns `undefined` on failure so the
|
|
618
|
+
* `tool_call_dispatched.data.input` is null-shaped rather than
|
|
619
|
+
* throwing. If `raw` is `undefined` we also return `undefined` —
|
|
620
|
+
* happens when the tool was called with no args at all.
|
|
621
|
+
*/
|
|
622
|
+
function tryParseJson(raw: string | undefined): unknown {
|
|
623
|
+
if (raw === undefined || raw === "") return undefined;
|
|
624
|
+
try {
|
|
625
|
+
return JSON.parse(raw);
|
|
626
|
+
} catch {
|
|
627
|
+
return raw;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Maps the SDK's `errorCode` + `stopReason` strings onto ai-hub's
|
|
633
|
+
* stable `error.code` enum.
|
|
634
|
+
*
|
|
635
|
+
* SDK error codes we know about:
|
|
636
|
+
* - `approval_conflict` — non-fatal approval flow snag; treat as `internal`
|
|
637
|
+
* (ai-hub doesn't surface approvals to end-users in v1).
|
|
638
|
+
* - `protocol_error` — SDK ↔ CLI wire-shape mismatch; `internal`.
|
|
639
|
+
* - `llm_api_error` — the underlying LLM call failed; `provider_error`.
|
|
640
|
+
* - `max_steps` — agent hit its step ceiling; `internal` for now (a
|
|
641
|
+
* `max_steps` ai-hub code is a future v1.x enhancement).
|
|
642
|
+
* - `interrupted` — handled separately in `finalizeTerminal`.
|
|
643
|
+
* - `stream_closed` — SDK pump closed early; `internal`.
|
|
644
|
+
* - `error` (generic) — fall back to `stopReason` sniffing.
|
|
645
|
+
*
|
|
646
|
+
* The set of legitimate ai-hub `error.code` values lives in
|
|
647
|
+
* `@data-club/ai-hub` (§9.4). We keep `tpm_exhausted` reserved for
|
|
648
|
+
* future use (it is no longer emitted post-migration — the recovery
|
|
649
|
+
* loop is gone — but we keep the enum value so a future restore can
|
|
650
|
+
* use it without protocol-version churn).
|
|
651
|
+
*/
|
|
652
|
+
function classifyErrorCode(
|
|
653
|
+
errorCode: string | undefined,
|
|
654
|
+
stopReason: string | undefined,
|
|
655
|
+
): "tpm_exhausted" | "provider_error" | "internal" {
|
|
656
|
+
if (errorCode === "llm_api_error") return "provider_error";
|
|
657
|
+
if (errorCode === "approval_conflict" || errorCode === "approval_conflict_terminal") return "internal";
|
|
658
|
+
if (errorCode === "protocol_error" || errorCode === "stream_closed") return "internal";
|
|
659
|
+
if (errorCode === "max_steps") return "internal";
|
|
660
|
+
if (errorCode && errorCode !== "error" && errorCode !== "interrupted") return "internal";
|
|
661
|
+
// Fall back on stopReason heuristics. Future Letta Code versions may
|
|
662
|
+
// expose richer error codes; we'll extend the map here.
|
|
663
|
+
const l = (stopReason ?? "").toLowerCase();
|
|
664
|
+
if (l.includes("rate") || l.includes("limit") || l.includes("429")) return "tpm_exhausted";
|
|
665
|
+
if (l.includes("provider") || l.includes("upstream") || l.includes("llm")) return "provider_error";
|
|
666
|
+
return "internal";
|
|
667
|
+
}
|