@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.
- package/README.md +46 -0
- package/package.json +20 -0
- package/src/actions.ts +245 -0
- package/src/adapter.ts +57 -0
- package/src/assembly.test.ts +728 -0
- package/src/assembly.ts +250 -0
- package/src/audit-collector.test.ts +332 -0
- package/src/audit-collector.ts +172 -0
- package/src/auth.test.ts +117 -0
- package/src/auth.ts +61 -0
- package/src/authz-extension.test.ts +269 -0
- package/src/authz-extension.ts +145 -0
- package/src/correlation.ts +61 -0
- package/src/default-director.test.ts +314 -0
- package/src/default-director.ts +344 -0
- package/src/director.ts +87 -0
- package/src/errors.test.ts +133 -0
- package/src/errors.ts +115 -0
- package/src/gates.ts +128 -0
- package/src/harness.test.ts +655 -0
- package/src/harness.ts +1571 -0
- package/src/index.ts +76 -0
- package/src/providers/anthropic.test.ts +771 -0
- package/src/providers/anthropic.ts +810 -0
- package/src/providers/google-genai-files.ts +289 -0
- package/src/providers/google-genai.ts +1518 -0
- package/src/providers/openai.ts +719 -0
- package/src/providers/registry.ts +33 -0
- package/src/reactor.test.ts +3660 -0
- package/src/reactor.ts +1058 -0
- package/src/retry-policy.ts +99 -0
- package/src/scheduler.test.ts +41 -0
- package/src/sse.test.ts +133 -0
- package/src/sse.ts +76 -0
- package/src/state.ts +135 -0
- package/src/transform.test.ts +207 -0
- package/src/transform.ts +159 -0
- package/src/transforms/index.ts +2 -0
- package/src/transforms/size-cap.test.ts +172 -0
- package/src/transforms/size-cap.ts +110 -0
- package/src/turns.ts +54 -0
- package/tsconfig.json +4 -0
- 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
|
+
};
|