@intx/inference 0.1.2 → 0.2.2

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 (97) hide show
  1. package/LICENSE +176 -0
  2. package/dist/actions.d.ts +16 -0
  3. package/dist/actions.js +200 -0
  4. package/dist/adapter.d.ts +38 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +68 -0
  7. package/dist/assembly.js +132 -0
  8. package/dist/audit-collector.d.ts +10 -0
  9. package/dist/audit-collector.js +139 -0
  10. package/dist/auth.d.ts +24 -0
  11. package/{src/auth.ts → dist/auth.js} +13 -19
  12. package/dist/authz-extension.d.ts +32 -0
  13. package/dist/authz-extension.js +100 -0
  14. package/dist/correlation.d.ts +25 -0
  15. package/dist/correlation.js +32 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +199 -0
  18. package/dist/director.d.ts +6 -0
  19. package/dist/director.js +56 -0
  20. package/dist/errors.d.ts +18 -0
  21. package/dist/errors.js +83 -0
  22. package/dist/gates.d.ts +27 -0
  23. package/dist/gates.js +80 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1319 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +21 -0
  28. package/dist/manifest.d.ts +31 -0
  29. package/dist/manifest.js +44 -0
  30. package/dist/providers/anthropic.d.ts +33 -0
  31. package/dist/providers/anthropic.js +670 -0
  32. package/dist/providers/google-genai-files.d.ts +48 -0
  33. package/dist/providers/google-genai-files.js +205 -0
  34. package/dist/providers/google-genai.d.ts +3 -0
  35. package/dist/providers/google-genai.js +1196 -0
  36. package/dist/providers/index.d.ts +38 -0
  37. package/dist/providers/index.js +56 -0
  38. package/dist/providers/openai.d.ts +3 -0
  39. package/dist/providers/openai.js +609 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +920 -0
  42. package/dist/retry-policy.d.ts +31 -0
  43. package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
  44. package/dist/sse.d.ts +1 -0
  45. package/dist/sse.js +63 -0
  46. package/dist/state.d.ts +23 -0
  47. package/dist/state.js +100 -0
  48. package/dist/tool-name.d.ts +6 -0
  49. package/dist/tool-name.js +110 -0
  50. package/dist/transform.d.ts +11 -0
  51. package/dist/transform.js +117 -0
  52. package/dist/transforms/index.d.ts +2 -0
  53. package/dist/transforms/index.js +1 -0
  54. package/dist/transforms/size-cap.d.ts +12 -0
  55. package/dist/transforms/size-cap.js +80 -0
  56. package/dist/turns.d.ts +21 -0
  57. package/dist/turns.js +135 -0
  58. package/package.json +21 -6
  59. package/src/actions.ts +0 -245
  60. package/src/adapter.ts +0 -57
  61. package/src/assembly.test.ts +0 -728
  62. package/src/assembly.ts +0 -250
  63. package/src/audit-collector.test.ts +0 -332
  64. package/src/audit-collector.ts +0 -172
  65. package/src/auth.test.ts +0 -117
  66. package/src/authz-extension.test.ts +0 -269
  67. package/src/authz-extension.ts +0 -145
  68. package/src/correlation.ts +0 -61
  69. package/src/default-director.test.ts +0 -314
  70. package/src/default-director.ts +0 -344
  71. package/src/director.ts +0 -87
  72. package/src/errors.test.ts +0 -133
  73. package/src/errors.ts +0 -115
  74. package/src/gates.ts +0 -128
  75. package/src/harness.test.ts +0 -655
  76. package/src/harness.ts +0 -1571
  77. package/src/index.ts +0 -76
  78. package/src/providers/anthropic.test.ts +0 -771
  79. package/src/providers/anthropic.ts +0 -810
  80. package/src/providers/google-genai-files.ts +0 -289
  81. package/src/providers/google-genai.ts +0 -1518
  82. package/src/providers/openai.ts +0 -719
  83. package/src/providers/registry.ts +0 -33
  84. package/src/reactor.test.ts +0 -3660
  85. package/src/reactor.ts +0 -1058
  86. package/src/scheduler.test.ts +0 -41
  87. package/src/sse.test.ts +0 -133
  88. package/src/sse.ts +0 -76
  89. package/src/state.ts +0 -135
  90. package/src/transform.test.ts +0 -207
  91. package/src/transform.ts +0 -159
  92. package/src/transforms/index.ts +0 -2
  93. package/src/transforms/size-cap.test.ts +0 -172
  94. package/src/transforms/size-cap.ts +0 -110
  95. package/src/turns.ts +0 -54
  96. package/tsconfig.json +0 -4
  97. package/tsconfig.tsbuildinfo +0 -1
@@ -1,344 +0,0 @@
1
- // Default conversational director — reference ReactorDirector implementation.
2
- //
3
- // Implements the decision table from INFERENCE.md § Director Decision Function:
4
- //
5
- // message.received → infer
6
- // inference.done (tools) → checkpoint + execute_tools
7
- // tool.done → checkpoint + infer (re-infer with tool results)
8
- // inference.done (no tools) → checkpoint + reply (connector sends the message)
9
- // inference.error → checkpoint + reply (error message to user)
10
- // abort → done
11
- // reactor.gate.cleared → checkpoint + infer (resume after gate)
12
- //
13
- // The director never throws. Inference errors are surfaced to the user as a
14
- // reply so the problem is visible, and the agent remains alive for retries.
15
-
16
- import { getLogger } from "@intx/log";
17
- import type {
18
- ReactorDirector,
19
- ReactorInboundEvent,
20
- ReactorState,
21
- ReactorCapabilities,
22
- ReactorAction,
23
- AssistantTurn,
24
- ToolCall,
25
- ToolDefinition,
26
- } from "@intx/types/runtime";
27
-
28
- const logger = getLogger(["interchange", "inference", "default-director"]);
29
-
30
- /**
31
- * Decision returned by an `afterInferenceDone` policy hook.
32
- *
33
- * continue — proceed with the director's normal post-inference logic
34
- * (tool extraction, reply, or wait per the existing flow).
35
- * abort — terminate the agent. Routes to `[checkpoint, reply, done]`
36
- * and the reactor shuts down. Stronger than the
37
- * `inference.error` branch, which only replies and stays
38
- * alive — `abort` is for "session is over, do not accept
39
- * further inputs."
40
- * halt — pause the current cycle without terminating. Routes to
41
- * `[checkpoint, reply, wait]`. Reactor stays alive waiting
42
- * for the next inbound event. There is no auto-resume; an
43
- * external event (mail, gate clearance, etc.) must reach
44
- * the reactor for the agent to make progress again.
45
- *
46
- * `reason` becomes the connector reply text verbatim — policy authors
47
- * choose what is safe to surface to the user. There is no separate
48
- * private-reason / user-message split today; add one if a real need
49
- * appears.
50
- */
51
- export type AfterInferenceDecision =
52
- | { type: "continue" }
53
- | { type: "abort"; reason: string }
54
- | { type: "halt"; reason: string };
55
-
56
- /**
57
- * Function shape for an after-inference-done policy hook.
58
- *
59
- * The hook fires only on `inference.done` (a successful cycle). Errored
60
- * cycles do not invoke it. `mode: "reactive"` does not change firing —
61
- * the hook gates the entire `inference.done` branch, including the
62
- * reactive-wait shortcut, so a budget check applies to reactive agents
63
- * the same way it does to conversational ones.
64
- *
65
- * The hook receives the post-cycle `ReactorState` (with `lastCycleSource`
66
- * and `lastCycleUsage` populated for the just-completed call) and the
67
- * assistant turn. Returns a decision (sync or async) that controls
68
- * whether the director continues, terminates the agent, or pauses the
69
- * cycle.
70
- *
71
- * Canonical use case: cost-aware gating. Read `state.lastCycleSource`
72
- * + `state.lastCycleUsage`, price the call against user-supplied rate
73
- * data, decide whether the budget is exhausted. Token caps, time caps,
74
- * wallet checks, and governance triggers fit the same shape; the
75
- * type stays policy-agnostic.
76
- *
77
- * "Downgrade to cheaper model" policies do NOT use this hook to return
78
- * a new source. Compose them via an external observer of
79
- * `lastCycleSource` / `lastCycleUsage` that calls `setSource` from
80
- * outside the director.
81
- *
82
- * The hook blocks the reactor's inference.done branch: keep its
83
- * latency low. The return type admits a Promise, but every await
84
- * inside the hook is wall-clock time the agent isn't making progress.
85
- * Small lookups (in-memory caches, fast DB reads) are fine; arbitrary
86
- * waits are not.
87
- *
88
- * Tool calls and `halt`: if the model emitted tool calls and the hook
89
- * returns `halt` (or `abort`), those tool calls are dropped — the
90
- * director never executes them. On resume, the model's next inference
91
- * sees an assistant turn with unanswered tool calls; depending on the
92
- * provider this is either a validation error or a confused model.
93
- * Policy authors that combine `halt` with tool-heavy agents need to
94
- * understand this.
95
- */
96
- export type AfterInferenceHook = (
97
- state: ReactorState,
98
- turn: AssistantTurn,
99
- ) => AfterInferenceDecision | Promise<AfterInferenceDecision>;
100
-
101
- export type DefaultDirectorPolicy = {
102
- /**
103
- * Controls the agent's behavior after inference completes.
104
- *
105
- * "conversational" (default) — The standard agentic loop. After tools
106
- * complete, re-infer so the model can reason about results, issue more
107
- * tool calls, or compose a reply. When inference produces text without
108
- * tool calls, send it as a connector reply.
109
- *
110
- * "reactive" — The agent acts on each message by executing tools, then
111
- * returns to the event loop to wait for the next inbound event. It does
112
- * not re-infer after tools complete and does not send connector replies.
113
- * Use this for agents that perform a single action per message.
114
- */
115
- mode?: "conversational" | "reactive";
116
-
117
- /**
118
- * Optional policy hook fired after every successful `inference.done`.
119
- * See `AfterInferenceHook` for the contract: firing boundary, return
120
- * shape, composition patterns, and policy-author caveats.
121
- *
122
- * If the hook throws or rejects, the director catches the error,
123
- * routes to `{ type: "abort", reason: "afterInferenceDone policy
124
- * threw: <message>" }`, and logs at error level. The director's
125
- * never-throws contract is preserved.
126
- */
127
- afterInferenceDone?: AfterInferenceHook;
128
- };
129
-
130
- function extractToolCalls(turn: AssistantTurn): ToolCall[] {
131
- const calls: ToolCall[] = [];
132
- for (const block of turn.content) {
133
- if (block.type === "tool_call") {
134
- calls.push({
135
- id: block.id,
136
- name: block.name,
137
- arguments: block.arguments,
138
- });
139
- }
140
- }
141
- return calls;
142
- }
143
-
144
- function extractTextContent(turn: AssistantTurn): string {
145
- // Both regular text and refusal blocks carry human-readable model
146
- // output that the connector needs to surface — a refusal-only turn
147
- // (OpenAI strict-mode policy decline) would otherwise route through
148
- // the empty-response branch below and never reach the reply path,
149
- // leaving the human waiting for an answer the model already
150
- // declined to give. The structural "this was a refusal" signal is
151
- // preserved at the persistence layer (event-collector emits a
152
- // refusal turn-part); the reply path only needs the words.
153
- const parts: string[] = [];
154
- for (const block of turn.content) {
155
- if (block.type === "text") {
156
- parts.push(block.text);
157
- } else if (block.type === "refusal") {
158
- parts.push(block.reason);
159
- }
160
- }
161
- return parts.join("\n").trim();
162
- }
163
-
164
- const ERROR_PREAMBLE: Record<string, string> = {
165
- credential_failure:
166
- "This agent could not complete your request due to a credential error",
167
- quota_exhausted:
168
- "This agent could not complete your request because the API quota has been exhausted",
169
- context_overflow:
170
- "This agent could not complete your request because the conversation exceeded the model's context limit",
171
- retryable:
172
- "This agent encountered a temporary error communicating with the inference provider",
173
- fatal:
174
- "This agent could not complete your request due to an unrecoverable inference error",
175
- aborted: "This agent's inference request was aborted",
176
- };
177
-
178
- function formatInferenceError(error: {
179
- category: string;
180
- message: string;
181
- statusCode?: number;
182
- }): string {
183
- const preamble = ERROR_PREAMBLE[error.category] ?? ERROR_PREAMBLE["fatal"];
184
- const status =
185
- error.statusCode !== undefined ? ` [HTTP ${error.statusCode}]` : "";
186
- return `${preamble}${status}: ${error.message}`;
187
- }
188
-
189
- export class DefaultDirector implements ReactorDirector {
190
- private readonly systemPrompt: string;
191
- private readonly toolDefinitions: ToolDefinition[];
192
- private readonly policy: DefaultDirectorPolicy;
193
-
194
- // Track outstanding tool results so we only re-infer once per batch.
195
- private pendingToolResults = 0;
196
-
197
- constructor(
198
- systemPrompt: string,
199
- toolDefinitions: ToolDefinition[] = [],
200
- policy: DefaultDirectorPolicy = {},
201
- ) {
202
- this.systemPrompt = systemPrompt;
203
- this.toolDefinitions = toolDefinitions;
204
- this.policy = policy;
205
- }
206
-
207
- async decide(
208
- event: ReactorInboundEvent,
209
- state: ReactorState,
210
- capabilities: ReactorCapabilities,
211
- ): Promise<ReactorAction | ReactorAction[]> {
212
- switch (event.type) {
213
- case "message.received": {
214
- return capabilities.infer({
215
- systemPrompt: this.systemPrompt,
216
- tools: this.toolDefinitions,
217
- });
218
- }
219
-
220
- case "inference.done": {
221
- // The hook gates the entire inference.done branch (including
222
- // tool extraction and the reactive-mode wait shortcut). An
223
- // abort/halt from the policy drops any tool calls the model
224
- // emitted in this turn; see AfterInferenceHook TSDoc for the
225
- // implications.
226
- if (this.policy.afterInferenceDone !== undefined) {
227
- let decision: AfterInferenceDecision;
228
- try {
229
- decision = await this.policy.afterInferenceDone(state, event.turn);
230
- } catch (cause) {
231
- const message =
232
- cause instanceof Error ? cause.message : String(cause);
233
- logger.error`afterInferenceDone policy threw: ${message}`;
234
- decision = {
235
- type: "abort",
236
- reason: `afterInferenceDone policy threw: ${message}`,
237
- };
238
- }
239
- if (decision.type === "abort") {
240
- return [
241
- capabilities.checkpoint("after-inference-abort"),
242
- capabilities.reply(decision.reason),
243
- capabilities.done(),
244
- ];
245
- }
246
- if (decision.type === "halt") {
247
- return [
248
- capabilities.checkpoint("after-inference-halt"),
249
- capabilities.reply(decision.reason),
250
- capabilities.wait(),
251
- ];
252
- }
253
- // decision.type === "continue" — fall through.
254
- }
255
-
256
- const toolCalls = extractToolCalls(event.turn);
257
- if (toolCalls.length > 0) {
258
- this.pendingToolResults = toolCalls.length;
259
- return [
260
- capabilities.checkpoint("tool-execution"),
261
- capabilities.executeTools(toolCalls, true),
262
- ];
263
- }
264
-
265
- // No tool calls — the model is done reasoning for this turn.
266
- if (this.policy.mode === "reactive") {
267
- return [
268
- capabilities.checkpoint("inference-done"),
269
- capabilities.wait(),
270
- ];
271
- }
272
-
273
- // Conversational agent: send reply via the connector.
274
- const replyContent = extractTextContent(event.turn);
275
- if (replyContent.length > 0) {
276
- return [
277
- capabilities.checkpoint("inference-done"),
278
- capabilities.reply(replyContent),
279
- ];
280
- }
281
-
282
- // Empty response (no text, no tool calls) — checkpoint and wait for
283
- // the next inbound message. The reactor only shuts down on explicit
284
- // stop (abort), never because the model produced an empty turn.
285
- return [capabilities.checkpoint("inference-done"), capabilities.wait()];
286
- }
287
-
288
- case "tool.done": {
289
- this.pendingToolResults--;
290
- if (this.pendingToolResults > 0) {
291
- return [];
292
- }
293
- if (this.policy.mode === "reactive") {
294
- return [capabilities.checkpoint("tool-done"), capabilities.wait()];
295
- }
296
- // All tool results received — re-infer with complete context.
297
- return [
298
- capabilities.checkpoint("tool-done"),
299
- capabilities.infer({
300
- systemPrompt: this.systemPrompt,
301
- tools: this.toolDefinitions,
302
- }),
303
- ];
304
- }
305
-
306
- case "inference.error": {
307
- const statusDetail =
308
- event.error.statusCode !== undefined
309
- ? ` [HTTP ${event.error.statusCode}]`
310
- : "";
311
-
312
- logger.error`Inference error in default director: ${event.error.message}${statusDetail} (category: ${event.error.category})`;
313
-
314
- const userMessage = formatInferenceError(event.error);
315
- return [
316
- capabilities.checkpoint("inference-error"),
317
- capabilities.reply(userMessage),
318
- ];
319
- }
320
-
321
- case "reactor.gate.cleared": {
322
- return [
323
- capabilities.checkpoint("gate-cleared"),
324
- capabilities.infer({
325
- systemPrompt: this.systemPrompt,
326
- tools: this.toolDefinitions,
327
- }),
328
- ];
329
- }
330
-
331
- case "abort": {
332
- return capabilities.done();
333
- }
334
- }
335
- }
336
- }
337
-
338
- export function createDefaultDirector(
339
- systemPrompt: string,
340
- toolDefinitions: ToolDefinition[] = [],
341
- policy: DefaultDirectorPolicy = {},
342
- ): ReactorDirector {
343
- return new DefaultDirector(systemPrompt, toolDefinitions, policy);
344
- }
package/src/director.ts DELETED
@@ -1,87 +0,0 @@
1
- // Director interface types and capabilities factory.
2
- //
3
- // The capabilities object is passed to the director on every decision call.
4
- // It provides a type-safe API for constructing reactor actions without
5
- // requiring the director to import or construct action literals directly.
6
- //
7
- // (INFERENCE.md § Reactor Director › Core Director)
8
-
9
- import type {
10
- ReactorAction,
11
- ReactorCapabilities,
12
- GateType,
13
- ForkMode,
14
- InferenceOptions,
15
- ToolCall,
16
- } from "@intx/types/runtime";
17
-
18
- /**
19
- * Builds a frozen capabilities object. The same instance is reused across
20
- * calls since all methods are pure constructors.
21
- */
22
- export function createCapabilities(): ReactorCapabilities {
23
- return {
24
- infer(options?: InferenceOptions): ReactorAction {
25
- return {
26
- type: "infer",
27
- ...(options !== undefined ? { options } : {}),
28
- };
29
- },
30
-
31
- executeTools(
32
- calls: ToolCall[],
33
- parallel?: boolean,
34
- addToHistory?: boolean,
35
- ): ReactorAction {
36
- return {
37
- type: "execute_tools",
38
- calls,
39
- ...(parallel !== undefined ? { parallel } : {}),
40
- ...(addToHistory !== undefined ? { addToHistory } : {}),
41
- };
42
- },
43
-
44
- suspend(gate: {
45
- type: GateType;
46
- gateId: string;
47
- timeoutMs: number;
48
- correlationId?: string;
49
- }): ReactorAction {
50
- return { type: "suspend", gate };
51
- },
52
-
53
- fork(mode: ForkMode, forkId: string): ReactorAction {
54
- return { type: "fork", mode, forkId };
55
- },
56
-
57
- emit(
58
- eventType: `custom.${string}`,
59
- data: Record<string, unknown>,
60
- ): ReactorAction {
61
- return { type: "emit", eventType, data };
62
- },
63
-
64
- reply(content: string): ReactorAction {
65
- return { type: "reply", content };
66
- },
67
-
68
- checkpoint(reason?: string): ReactorAction {
69
- return {
70
- type: "checkpoint",
71
- message: reason !== undefined ? `checkpoint: ${reason}` : "checkpoint",
72
- };
73
- },
74
-
75
- compact(compactor: string, reason: string): ReactorAction {
76
- return { type: "compact", compactor, reason };
77
- },
78
-
79
- wait(): ReactorAction {
80
- return { type: "wait" };
81
- },
82
-
83
- done(): ReactorAction {
84
- return { type: "done" };
85
- },
86
- };
87
- }
@@ -1,133 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- classifyHTTPError,
4
- classifyNetworkError,
5
- classifyAbortError,
6
- classifyStreamError,
7
- classifyProtocolMismatch,
8
- ProtocolMismatchError,
9
- } from "./errors";
10
-
11
- describe("classifyHTTPError", () => {
12
- test("401 → credential_failure", () => {
13
- const err = classifyHTTPError(401, "Unauthorized");
14
- expect(err.category).toBe("credential_failure");
15
- expect(err.statusCode).toBe(401);
16
- });
17
-
18
- test("403 → credential_failure", () => {
19
- const err = classifyHTTPError(403, "Forbidden");
20
- expect(err.category).toBe("credential_failure");
21
- expect(err.statusCode).toBe(403);
22
- });
23
-
24
- test("429 → quota_exhausted", () => {
25
- const err = classifyHTTPError(429, "Too Many Requests");
26
- expect(err.category).toBe("quota_exhausted");
27
- expect(err.statusCode).toBe(429);
28
- });
29
-
30
- test("500 → retryable", () => {
31
- const err = classifyHTTPError(500, "Internal Server Error");
32
- expect(err.category).toBe("retryable");
33
- expect(err.statusCode).toBe(500);
34
- });
35
-
36
- test("503 → retryable", () => {
37
- const err = classifyHTTPError(503, "Service Unavailable");
38
- expect(err.category).toBe("retryable");
39
- });
40
-
41
- test("400 with context_length_exceeded → context_overflow", () => {
42
- const err = classifyHTTPError(
43
- 400,
44
- "context_length_exceeded: too many tokens",
45
- );
46
- expect(err.category).toBe("context_overflow");
47
- });
48
-
49
- test("400 with 'input is too long' → context_overflow", () => {
50
- const err = classifyHTTPError(400, "input is too long for this model");
51
- expect(err.category).toBe("context_overflow");
52
- });
53
-
54
- test("400 with generic message → fatal", () => {
55
- const err = classifyHTTPError(400, "Bad request");
56
- expect(err.category).toBe("fatal");
57
- });
58
-
59
- test("404 → fatal", () => {
60
- const err = classifyHTTPError(404, "Not Found");
61
- expect(err.category).toBe("fatal");
62
- });
63
-
64
- test("carries raw body", () => {
65
- const raw = { error: { message: "oops" } };
66
- const err = classifyHTTPError(500, "Server Error", raw);
67
- expect(err.raw).toBe(raw);
68
- });
69
- });
70
-
71
- describe("classifyNetworkError", () => {
72
- test("Error instance → retryable with message", () => {
73
- const err = classifyNetworkError(new Error("ECONNRESET"));
74
- expect(err.category).toBe("retryable");
75
- expect(err.message).toBe("ECONNRESET");
76
- });
77
-
78
- test("string → retryable", () => {
79
- const err = classifyNetworkError("network timeout");
80
- expect(err.category).toBe("retryable");
81
- expect(err.message).toBe("network timeout");
82
- });
83
- });
84
-
85
- describe("classifyAbortError", () => {
86
- test("always returns aborted category", () => {
87
- const err = classifyAbortError();
88
- expect(err.category).toBe("aborted");
89
- });
90
- });
91
-
92
- describe("classifyStreamError", () => {
93
- test("AbortError → aborted", () => {
94
- const abort = new DOMException("Aborted", "AbortError");
95
- const err = classifyStreamError(abort);
96
- expect(err.category).toBe("aborted");
97
- });
98
-
99
- test("generic Error → retryable", () => {
100
- const err = classifyStreamError(new Error("stream corrupted"));
101
- expect(err.category).toBe("retryable");
102
- expect(err.message).toBe("stream corrupted");
103
- });
104
-
105
- test("ProtocolMismatchError → protocol_mismatch with raw passed through", () => {
106
- const raw = { choices: [{ delta: { role: 42 } }] };
107
- const cause = new ProtocolMismatchError(
108
- "delta.role must be a string (was number)",
109
- raw,
110
- );
111
- const err = classifyStreamError(cause);
112
- expect(err.category).toBe("protocol_mismatch");
113
- expect(err.message).toBe("delta.role must be a string (was number)");
114
- expect(err.raw).toBe(raw);
115
- });
116
- });
117
-
118
- describe("classifyProtocolMismatch", () => {
119
- test("constructs a protocol_mismatch error with the given detail and raw", () => {
120
- const raw = { malformed: true };
121
- const err = classifyProtocolMismatch("bad chunk", raw);
122
- expect(err.category).toBe("protocol_mismatch");
123
- expect(err.message).toBe("bad chunk");
124
- expect(err.raw).toBe(raw);
125
- });
126
-
127
- test("omits raw when none is supplied", () => {
128
- const err = classifyProtocolMismatch("bad chunk");
129
- expect(err.category).toBe("protocol_mismatch");
130
- expect(err.message).toBe("bad chunk");
131
- expect("raw" in err).toBe(false);
132
- });
133
- });
package/src/errors.ts DELETED
@@ -1,115 +0,0 @@
1
- import type { InferenceError } from "@intx/types/runtime";
2
-
3
- export type { InferenceError };
4
-
5
- export function classifyHTTPError(
6
- statusCode: number,
7
- message: string,
8
- raw?: unknown,
9
- retryAfterMs?: number,
10
- ): InferenceError {
11
- if (statusCode === 401 || statusCode === 403) {
12
- return { category: "credential_failure", message, statusCode, raw };
13
- }
14
-
15
- if (statusCode === 429) {
16
- return {
17
- category: "quota_exhausted",
18
- message,
19
- statusCode,
20
- ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
21
- raw,
22
- };
23
- }
24
-
25
- if (statusCode === 400) {
26
- // Context-overflow manifests as a 400 with a provider-specific message.
27
- // Check for known patterns before falling through to fatal.
28
- if (isContextOverflowMessage(message)) {
29
- return { category: "context_overflow", message, statusCode, raw };
30
- }
31
- return { category: "fatal", message, statusCode, raw };
32
- }
33
-
34
- if (statusCode >= 500 && statusCode < 600) {
35
- return { category: "retryable", message, statusCode, raw };
36
- }
37
-
38
- return { category: "fatal", message, statusCode, raw };
39
- }
40
-
41
- export function classifyNetworkError(cause: unknown): InferenceError {
42
- const message = cause instanceof Error ? cause.message : String(cause);
43
- return { category: "retryable", message, raw: cause };
44
- }
45
-
46
- export function classifyAbortError(): InferenceError {
47
- return { category: "aborted", message: "inference aborted" };
48
- }
49
-
50
- export function classifyTimeoutError(
51
- kind: "inactivity" | "total",
52
- thresholdMs: number,
53
- ): InferenceError {
54
- const message =
55
- kind === "inactivity"
56
- ? `inference call exceeded inactivity timeout (${String(thresholdMs)} ms with no events from the provider)`
57
- : `inference call exceeded total timeout (${String(thresholdMs)} ms wall-clock)`;
58
- return { category: "timeout", message };
59
- }
60
-
61
- /**
62
- * The one throw type a response parser is permitted to raise. See the
63
- * `ResponseParser` contract on `adapter.ts` for full semantics. `raw`
64
- * carries the offending bytes or parsed object so operators can
65
- * inspect what came over the wire.
66
- */
67
- export class ProtocolMismatchError extends Error {
68
- readonly raw: unknown;
69
- constructor(detail: string, raw?: unknown) {
70
- super(detail);
71
- this.name = "ProtocolMismatchError";
72
- this.raw = raw;
73
- }
74
- }
75
-
76
- export function classifyProtocolMismatch(
77
- detail: string,
78
- raw?: unknown,
79
- ): InferenceError {
80
- return {
81
- category: "protocol_mismatch",
82
- message: detail,
83
- ...(raw !== undefined ? { raw } : {}),
84
- };
85
- }
86
-
87
- export function classifyStreamError(cause: unknown): InferenceError {
88
- if (isAbortError(cause)) {
89
- return classifyAbortError();
90
- }
91
- if (cause instanceof ProtocolMismatchError) {
92
- return classifyProtocolMismatch(cause.message, cause.raw);
93
- }
94
- const message = cause instanceof Error ? cause.message : String(cause);
95
- return { category: "retryable", message, raw: cause };
96
- }
97
-
98
- function isContextOverflowMessage(message: string): boolean {
99
- const lower = message.toLowerCase();
100
- return (
101
- lower.includes("context_length_exceeded") ||
102
- lower.includes("context length") ||
103
- lower.includes("too many tokens") ||
104
- lower.includes("maximum context") ||
105
- lower.includes("input is too long")
106
- );
107
- }
108
-
109
- function isAbortError(value: unknown): boolean {
110
- return (
111
- value instanceof Error &&
112
- (value.name === "AbortError" ||
113
- value.message === "The user aborted a request.")
114
- );
115
- }