@intx/inference 0.1.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 (43) hide show
  1. package/README.md +46 -0
  2. package/package.json +20 -0
  3. package/src/actions.ts +245 -0
  4. package/src/adapter.ts +57 -0
  5. package/src/assembly.test.ts +728 -0
  6. package/src/assembly.ts +250 -0
  7. package/src/audit-collector.test.ts +332 -0
  8. package/src/audit-collector.ts +172 -0
  9. package/src/auth.test.ts +117 -0
  10. package/src/auth.ts +61 -0
  11. package/src/authz-extension.test.ts +269 -0
  12. package/src/authz-extension.ts +145 -0
  13. package/src/correlation.ts +61 -0
  14. package/src/default-director.test.ts +314 -0
  15. package/src/default-director.ts +344 -0
  16. package/src/director.ts +87 -0
  17. package/src/errors.test.ts +133 -0
  18. package/src/errors.ts +115 -0
  19. package/src/gates.ts +128 -0
  20. package/src/harness.test.ts +655 -0
  21. package/src/harness.ts +1571 -0
  22. package/src/index.ts +76 -0
  23. package/src/providers/anthropic.test.ts +771 -0
  24. package/src/providers/anthropic.ts +810 -0
  25. package/src/providers/google-genai-files.ts +289 -0
  26. package/src/providers/google-genai.ts +1518 -0
  27. package/src/providers/openai.ts +719 -0
  28. package/src/providers/registry.ts +33 -0
  29. package/src/reactor.test.ts +3660 -0
  30. package/src/reactor.ts +1058 -0
  31. package/src/retry-policy.ts +99 -0
  32. package/src/scheduler.test.ts +41 -0
  33. package/src/sse.test.ts +133 -0
  34. package/src/sse.ts +76 -0
  35. package/src/state.ts +135 -0
  36. package/src/transform.test.ts +207 -0
  37. package/src/transform.ts +159 -0
  38. package/src/transforms/index.ts +2 -0
  39. package/src/transforms/size-cap.test.ts +172 -0
  40. package/src/transforms/size-cap.ts +110 -0
  41. package/src/turns.ts +54 -0
  42. package/tsconfig.json +4 -0
  43. package/tsconfig.tsbuildinfo +1 -0
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @intx/inference
2
+
3
+ Provider-agnostic inference runtime. Adapters for Anthropic,
4
+ OpenAI-compatible relays (including OpenCode Zen), and Google
5
+ GenAI; SSE parsing; retry and error classification; message
6
+ transforms; and the reactor harness that drives a turn from
7
+ prompt to settled tool calls.
8
+
9
+ Consumed by `@intx/harness`, which composes the reactor with tool
10
+ runners, directors, and runtime capabilities.
11
+
12
+ ```ts
13
+ import { createReactorAssembly, createDefaultDirector } from "@intx/inference";
14
+
15
+ const director = createDefaultDirector(
16
+ systemPrompt,
17
+ toolDefinitions, // ToolDefinition[] — the tool surface advertised to the model
18
+ policy,
19
+ );
20
+
21
+ const assembly = createReactorAssembly({
22
+ sessionId,
23
+ director,
24
+ source, // InferenceSource: id, provider, model, apiKey, baseURL
25
+ toolRunner, // ToolRunner — dispatches the tool calls the director emits
26
+ contextStore,
27
+ onEvent: (event) => {
28
+ // event: ReactorEmittedEvent — persist or forward
29
+ },
30
+ });
31
+
32
+ assembly.reactor.start();
33
+ ```
34
+
35
+ Provider selection is driven by `source.provider`; the assembly
36
+ resolves the matching adapter internally. `ReactorAssemblyConfig`
37
+ in `src/assembly.ts` documents the optional fields (`authorize`,
38
+ `auditStore`, transforms, compactors, correlation, gate
39
+ timeouts).
40
+
41
+ The package is the lower half of the agent stack: it knows how to
42
+ talk to model providers, how to drive a multi-turn exchange to
43
+ completion, and how to compose extensions (gates, audit,
44
+ correlation, authz) into the reactor pipeline. The higher-level
45
+ agent surface — config validation, tool runners, deploy trees,
46
+ runtime capabilities — lives in `@intx/harness`.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@intx/inference",
3
+ "version": "0.1.2",
4
+ "license": "LGPL-2.1-only",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@intx/log": "0.0.0",
14
+ "@intx/types": "0.0.0",
15
+ "arktype": "^2.1.29"
16
+ },
17
+ "devDependencies": {
18
+ "@intx/mime": "0.0.0"
19
+ }
20
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,245 @@
1
+ // Action types and validation for the agent reactor.
2
+ //
3
+ // The reactor validates the set of actions returned by the director before
4
+ // executing any of them. Invalid combinations produce a reactor.error rather
5
+ // than partial execution — the reactor does not guess intent.
6
+ //
7
+ // Validation rules (INFERENCE.md § Action Validation):
8
+ // - At most one `infer` action.
9
+ // - At most one `done` action.
10
+ // - At most one `reply` action.
11
+ // - `infer` + `done` together is invalid.
12
+ // - `reply` + `infer` together is invalid.
13
+ // - `reply` + `execute_tools` together is invalid.
14
+ // - `reply` + `done` together is invalid.
15
+ // - `reply` + `suspend` together is invalid.
16
+ // - `wait` + `infer` together is invalid.
17
+ // - `wait` + `execute_tools` together is invalid.
18
+ // - `wait` + `suspend` together is invalid.
19
+ // - `wait` + `reply` together is invalid.
20
+ // - `wait` + `done` together is invalid.
21
+ // - `suspend` cannot appear alongside `infer` or `execute_tools`.
22
+ // - `fork` is composable — may appear alongside any other action.
23
+ // - At most one `checkpoint` action; composable with any other action.
24
+ // - At most one `wait` action.
25
+ // - Multiple `execute_tools` are merged into a single parallel batch.
26
+ // - `emit` is always valid and composable.
27
+ // - At most one `compact` action; composable with `checkpoint`, `emit`, and
28
+ // `fork`. Not composable with `infer`, `execute_tools`, `reply`, `suspend`,
29
+ // `wait`, or `done`. Context-overflow recovery runs compaction in its own
30
+ // cycle and re-infers on the next director invocation.
31
+
32
+ import type { ReactorAction, ToolCall } from "@intx/types/runtime";
33
+
34
+ export type ValidationResult =
35
+ | { ok: true; normalized: ReactorAction[] }
36
+ | { ok: false; error: string };
37
+
38
+ /**
39
+ * Validate and normalize a set of actions returned by the director.
40
+ *
41
+ * On success, returns the normalized action list with multiple `execute_tools`
42
+ * collapsed into a single batched action. On failure, returns a diagnostic
43
+ * error string.
44
+ */
45
+ export function validateActions(
46
+ actions: ReactorAction | ReactorAction[],
47
+ ): ValidationResult {
48
+ const list = Array.isArray(actions) ? actions : [actions];
49
+
50
+ // An empty action list means "no-op, keep waiting for the next event."
51
+ // This is valid — the reactor loop continues to waitForEvent().
52
+ if (list.length === 0) {
53
+ return { ok: true, normalized: [] };
54
+ }
55
+
56
+ const inferActions = list.filter((a) => a.type === "infer");
57
+ const doneActions = list.filter((a) => a.type === "done");
58
+ const replyActions = list.filter((a) => a.type === "reply");
59
+ const suspendActions = list.filter((a) => a.type === "suspend");
60
+ const executeActions = list.filter(
61
+ (a): a is Extract<ReactorAction, { type: "execute_tools" }> =>
62
+ a.type === "execute_tools",
63
+ );
64
+ const waitActions = list.filter((a) => a.type === "wait");
65
+ const forkActions = list.filter((a) => a.type === "fork");
66
+ const emitActions = list.filter((a) => a.type === "emit");
67
+ const checkpointActions = list.filter((a) => a.type === "checkpoint");
68
+ const compactActions = list.filter((a) => a.type === "compact");
69
+
70
+ if (checkpointActions.length > 1) {
71
+ return {
72
+ ok: false,
73
+ error: "Multiple checkpoint actions are not allowed",
74
+ };
75
+ }
76
+
77
+ if (inferActions.length > 1) {
78
+ return { ok: false, error: "Multiple infer actions are not allowed" };
79
+ }
80
+
81
+ if (doneActions.length > 1) {
82
+ return { ok: false, error: "Multiple done actions are not allowed" };
83
+ }
84
+
85
+ if (inferActions.length > 0 && doneActions.length > 0) {
86
+ return { ok: false, error: "infer and done cannot appear together" };
87
+ }
88
+
89
+ if (replyActions.length > 1) {
90
+ return { ok: false, error: "Multiple reply actions are not allowed" };
91
+ }
92
+
93
+ if (replyActions.length > 0 && inferActions.length > 0) {
94
+ return { ok: false, error: "reply and infer cannot appear together" };
95
+ }
96
+
97
+ if (replyActions.length > 0 && executeActions.length > 0) {
98
+ return {
99
+ ok: false,
100
+ error: "reply and execute_tools cannot appear together",
101
+ };
102
+ }
103
+
104
+ if (replyActions.length > 0 && doneActions.length > 0) {
105
+ return { ok: false, error: "reply and done cannot appear together" };
106
+ }
107
+
108
+ if (replyActions.length > 0 && suspendActions.length > 0) {
109
+ return { ok: false, error: "reply and suspend cannot appear together" };
110
+ }
111
+
112
+ if (suspendActions.length > 0) {
113
+ if (inferActions.length > 0) {
114
+ return {
115
+ ok: false,
116
+ error: "suspend cannot appear alongside infer",
117
+ };
118
+ }
119
+ if (executeActions.length > 0) {
120
+ return {
121
+ ok: false,
122
+ error: "suspend cannot appear alongside execute_tools",
123
+ };
124
+ }
125
+ if (suspendActions.length > 1) {
126
+ return { ok: false, error: "Multiple suspend actions are not allowed" };
127
+ }
128
+ }
129
+
130
+ if (waitActions.length > 1) {
131
+ return { ok: false, error: "Multiple wait actions are not allowed" };
132
+ }
133
+
134
+ if (waitActions.length > 0) {
135
+ if (inferActions.length > 0) {
136
+ return { ok: false, error: "wait and infer cannot appear together" };
137
+ }
138
+ if (executeActions.length > 0) {
139
+ return {
140
+ ok: false,
141
+ error: "wait and execute_tools cannot appear together",
142
+ };
143
+ }
144
+ if (suspendActions.length > 0) {
145
+ return { ok: false, error: "wait and suspend cannot appear together" };
146
+ }
147
+ if (replyActions.length > 0) {
148
+ return { ok: false, error: "wait and reply cannot appear together" };
149
+ }
150
+ if (doneActions.length > 0) {
151
+ return { ok: false, error: "wait and done cannot appear together" };
152
+ }
153
+ }
154
+
155
+ if (compactActions.length > 1) {
156
+ return { ok: false, error: "Multiple compact actions are not allowed" };
157
+ }
158
+
159
+ if (compactActions.length > 0) {
160
+ if (inferActions.length > 0) {
161
+ return { ok: false, error: "compact and infer cannot appear together" };
162
+ }
163
+ if (executeActions.length > 0) {
164
+ return {
165
+ ok: false,
166
+ error: "compact and execute_tools cannot appear together",
167
+ };
168
+ }
169
+ if (replyActions.length > 0) {
170
+ return { ok: false, error: "compact and reply cannot appear together" };
171
+ }
172
+ if (suspendActions.length > 0) {
173
+ return { ok: false, error: "compact and suspend cannot appear together" };
174
+ }
175
+ if (waitActions.length > 0) {
176
+ return { ok: false, error: "compact and wait cannot appear together" };
177
+ }
178
+ if (doneActions.length > 0) {
179
+ return { ok: false, error: "compact and done cannot appear together" };
180
+ }
181
+ }
182
+
183
+ // Verify fork actions have unique IDs.
184
+ const forkIds = forkActions.map(
185
+ (a) => (a as Extract<ReactorAction, { type: "fork" }>).forkId,
186
+ );
187
+ const uniqueForkIds = new Set(forkIds);
188
+ if (forkIds.length !== uniqueForkIds.size) {
189
+ return { ok: false, error: "Duplicate fork IDs in action list" };
190
+ }
191
+
192
+ // Build normalized list: collapse execute_tools into one parallel batch.
193
+ const normalized: ReactorAction[] = [];
194
+
195
+ for (const a of checkpointActions) {
196
+ normalized.push(a);
197
+ }
198
+
199
+ for (const a of emitActions) {
200
+ normalized.push(a);
201
+ }
202
+
203
+ for (const a of forkActions) {
204
+ normalized.push(a);
205
+ }
206
+
207
+ for (const a of compactActions) {
208
+ normalized.push(a);
209
+ }
210
+
211
+ if (executeActions.length > 0) {
212
+ const merged: ToolCall[] = executeActions.flatMap((a) => a.calls);
213
+ const allAddToHistory = executeActions.every(
214
+ (a) => a.addToHistory !== false,
215
+ );
216
+ normalized.push({
217
+ type: "execute_tools",
218
+ calls: merged,
219
+ parallel: true,
220
+ ...(!allAddToHistory ? { addToHistory: false } : {}),
221
+ });
222
+ }
223
+
224
+ for (const a of replyActions) {
225
+ normalized.push(a);
226
+ }
227
+
228
+ for (const a of inferActions) {
229
+ normalized.push(a);
230
+ }
231
+
232
+ for (const a of suspendActions) {
233
+ normalized.push(a);
234
+ }
235
+
236
+ for (const a of waitActions) {
237
+ normalized.push(a);
238
+ }
239
+
240
+ for (const a of doneActions) {
241
+ normalized.push(a);
242
+ }
243
+
244
+ return { ok: true, normalized };
245
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type {
2
+ ConversationTurn,
3
+ InferenceEvent,
4
+ InferenceOptions,
5
+ } from "@intx/types/runtime";
6
+
7
+ // The request shape the harness passes to fetch.
8
+ export type BuiltRequest = {
9
+ url: string;
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ };
13
+
14
+ // A request builder takes the internal message format and produces a
15
+ // provider-specific HTTP request. Pure function — no state, no side effects.
16
+ export type RequestBuilder = (
17
+ messages: ConversationTurn[],
18
+ model: string,
19
+ options: InferenceOptions,
20
+ ) => BuiltRequest;
21
+
22
+ // A response parser converts one SSE data payload string into zero or more
23
+ // internal inference events. May close over per-request state (e.g., for
24
+ // correlating content block indices with tool call IDs). Each adapter
25
+ // instance is created per inference call, so state does not leak across
26
+ // requests.
27
+ //
28
+ // The parser may return an empty array for events it doesn't care about
29
+ // (e.g., Anthropic's ping events). It MAY throw `ProtocolMismatchError`
30
+ // when the upstream chunk violates the provider's protocol (malformed
31
+ // JSON, schema validation failure, out-of-order events); the harness's
32
+ // stream-error catch converts that into an `inference.error` with
33
+ // category `"protocol_mismatch"` via `classifyStreamError`. No other
34
+ // throw type is permitted, and adapter-returned `inference.error` or
35
+ // `inference.done` events are silently dropped — the harness owns
36
+ // emission of those terminal types and the only path through which
37
+ // adapter-detected failures can surface is `ProtocolMismatchError`.
38
+ export type ResponseParser = (sseData: string) => InferenceEvent[];
39
+
40
+ // An adapter pairs a request builder with a response parser. Registration
41
+ // is a map keyed by provider identifier — no class hierarchy required.
42
+
43
+ // Extracts a retry delay from provider-specific response headers on a 429.
44
+ // Returns milliseconds to wait, or undefined if no retry info is available.
45
+ export type RetryAfterExtractor = (headers: Headers) => number | undefined;
46
+
47
+ // Extracts a pacing delay from response headers on ANY response (including
48
+ // success). Checks remaining rate limit capacity and returns how long to
49
+ // wait before the next request, or undefined if no pacing is needed.
50
+ export type PacingExtractor = (headers: Headers) => number | undefined;
51
+
52
+ export type ProviderAdapter = {
53
+ buildRequest: RequestBuilder;
54
+ parseResponse: ResponseParser;
55
+ extractRetryAfterMs?: RetryAfterExtractor;
56
+ extractPacingDelayMs?: PacingExtractor;
57
+ };