@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.
- package/LICENSE +176 -0
- package/dist/actions.d.ts +16 -0
- package/dist/actions.js +200 -0
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.js +31 -0
- package/dist/assembly.d.ts +68 -0
- package/dist/assembly.js +132 -0
- package/dist/audit-collector.d.ts +10 -0
- package/dist/audit-collector.js +139 -0
- package/dist/auth.d.ts +24 -0
- package/{src/auth.ts → dist/auth.js} +13 -19
- package/dist/authz-extension.d.ts +32 -0
- package/dist/authz-extension.js +100 -0
- package/dist/correlation.d.ts +25 -0
- package/dist/correlation.js +32 -0
- package/dist/default-director.d.ts +111 -0
- package/dist/default-director.js +199 -0
- package/dist/director.d.ts +6 -0
- package/dist/director.js +56 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +83 -0
- package/dist/gates.d.ts +27 -0
- package/dist/gates.js +80 -0
- package/dist/harness.d.ts +147 -0
- package/dist/harness.js +1319 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +44 -0
- package/dist/providers/anthropic.d.ts +33 -0
- package/dist/providers/anthropic.js +670 -0
- package/dist/providers/google-genai-files.d.ts +48 -0
- package/dist/providers/google-genai-files.js +205 -0
- package/dist/providers/google-genai.d.ts +3 -0
- package/dist/providers/google-genai.js +1196 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.js +56 -0
- package/dist/providers/openai.d.ts +3 -0
- package/dist/providers/openai.js +609 -0
- package/dist/reactor.d.ts +50 -0
- package/dist/reactor.js +920 -0
- package/dist/retry-policy.d.ts +31 -0
- package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +63 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.js +100 -0
- package/dist/tool-name.d.ts +6 -0
- package/dist/tool-name.js +110 -0
- package/dist/transform.d.ts +11 -0
- package/dist/transform.js +117 -0
- package/dist/transforms/index.d.ts +2 -0
- package/dist/transforms/index.js +1 -0
- package/dist/transforms/size-cap.d.ts +12 -0
- package/dist/transforms/size-cap.js +80 -0
- package/dist/turns.d.ts +21 -0
- package/dist/turns.js +135 -0
- package/package.json +21 -6
- package/src/actions.ts +0 -245
- package/src/adapter.ts +0 -57
- package/src/assembly.test.ts +0 -728
- package/src/assembly.ts +0 -250
- package/src/audit-collector.test.ts +0 -332
- package/src/audit-collector.ts +0 -172
- package/src/auth.test.ts +0 -117
- package/src/authz-extension.test.ts +0 -269
- package/src/authz-extension.ts +0 -145
- package/src/correlation.ts +0 -61
- package/src/default-director.test.ts +0 -314
- package/src/default-director.ts +0 -344
- package/src/director.ts +0 -87
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -115
- package/src/gates.ts +0 -128
- package/src/harness.test.ts +0 -655
- package/src/harness.ts +0 -1571
- package/src/index.ts +0 -76
- package/src/providers/anthropic.test.ts +0 -771
- package/src/providers/anthropic.ts +0 -810
- package/src/providers/google-genai-files.ts +0 -289
- package/src/providers/google-genai.ts +0 -1518
- package/src/providers/openai.ts +0 -719
- package/src/providers/registry.ts +0 -33
- package/src/reactor.test.ts +0 -3660
- package/src/reactor.ts +0 -1058
- package/src/scheduler.test.ts +0 -41
- package/src/sse.test.ts +0 -133
- package/src/sse.ts +0 -76
- package/src/state.ts +0 -135
- package/src/transform.test.ts +0 -207
- package/src/transform.ts +0 -159
- package/src/transforms/index.ts +0 -2
- package/src/transforms/size-cap.test.ts +0 -172
- package/src/transforms/size-cap.ts +0 -110
- package/src/turns.ts +0 -54
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/src/default-director.ts
DELETED
|
@@ -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
|
-
}
|
package/src/errors.test.ts
DELETED
|
@@ -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
|
-
}
|