@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Default conversational director — reference ReactorDirector implementation.
|
|
2
|
+
//
|
|
3
|
+
// Inbound-event → action map (see INFERENCE.md § Director Decision Function
|
|
4
|
+
// for the director contract and action-validation rules):
|
|
5
|
+
//
|
|
6
|
+
// message.received → infer
|
|
7
|
+
// inference.done (tools) → checkpoint + execute_tools
|
|
8
|
+
// tool.done → checkpoint + infer (re-infer with tool results)
|
|
9
|
+
// inference.done (no tools) → checkpoint + reply (connector sends the message)
|
|
10
|
+
// inference.error → checkpoint + reply (error message to user)
|
|
11
|
+
// abort → done
|
|
12
|
+
// reactor.gate.cleared → checkpoint + infer (resume after gate)
|
|
13
|
+
//
|
|
14
|
+
// The inference.done branch additionally runs the optional afterInferenceDone
|
|
15
|
+
// policy hook, whose continue/abort/halt decisions route independently of the
|
|
16
|
+
// event map above. See AfterInferenceDecision for that contract.
|
|
17
|
+
//
|
|
18
|
+
// The director never throws. Inference errors are surfaced to the user as a
|
|
19
|
+
// reply so the problem is visible, and the agent remains alive for retries.
|
|
20
|
+
import { getLogger } from "@intx/log";
|
|
21
|
+
const logger = getLogger(["interchange", "inference", "default-director"]);
|
|
22
|
+
function extractToolCalls(turn) {
|
|
23
|
+
const calls = [];
|
|
24
|
+
for (const block of turn.content) {
|
|
25
|
+
if (block.type === "tool_call") {
|
|
26
|
+
calls.push({
|
|
27
|
+
id: block.id,
|
|
28
|
+
name: block.name,
|
|
29
|
+
arguments: block.arguments,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return calls;
|
|
34
|
+
}
|
|
35
|
+
function extractTextContent(turn) {
|
|
36
|
+
// Both regular text and refusal blocks carry human-readable model
|
|
37
|
+
// output that the connector needs to surface — a refusal-only turn
|
|
38
|
+
// (OpenAI strict-mode policy decline) would otherwise route through
|
|
39
|
+
// the empty-response branch below and never reach the reply path,
|
|
40
|
+
// leaving the human waiting for an answer the model already
|
|
41
|
+
// declined to give. The structural "this was a refusal" signal is
|
|
42
|
+
// preserved at the persistence layer (event-collector emits a
|
|
43
|
+
// refusal turn-part); the reply path only needs the words.
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const block of turn.content) {
|
|
46
|
+
if (block.type === "text") {
|
|
47
|
+
parts.push(block.text);
|
|
48
|
+
}
|
|
49
|
+
else if (block.type === "refusal") {
|
|
50
|
+
parts.push(block.reason);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return parts.join("\n").trim();
|
|
54
|
+
}
|
|
55
|
+
const ERROR_PREAMBLE = {
|
|
56
|
+
credential_failure: "This agent could not complete your request due to a credential error",
|
|
57
|
+
quota_exhausted: "This agent could not complete your request because the API quota has been exhausted",
|
|
58
|
+
context_overflow: "This agent could not complete your request because the conversation exceeded the model's context limit",
|
|
59
|
+
retryable: "This agent encountered a temporary error communicating with the inference provider",
|
|
60
|
+
fatal: "This agent could not complete your request due to an unrecoverable inference error",
|
|
61
|
+
aborted: "This agent's inference request was aborted",
|
|
62
|
+
};
|
|
63
|
+
function formatInferenceError(error) {
|
|
64
|
+
const preamble = ERROR_PREAMBLE[error.category] ?? ERROR_PREAMBLE["fatal"];
|
|
65
|
+
const status = error.statusCode !== undefined ? ` [HTTP ${error.statusCode}]` : "";
|
|
66
|
+
return `${preamble}${status}: ${error.message}`;
|
|
67
|
+
}
|
|
68
|
+
export class DefaultDirector {
|
|
69
|
+
systemPrompt;
|
|
70
|
+
toolDefinitions;
|
|
71
|
+
policy;
|
|
72
|
+
// Track outstanding tool results so we only re-infer once per batch.
|
|
73
|
+
pendingToolResults = 0;
|
|
74
|
+
constructor(systemPrompt, toolDefinitions = [], policy = {}) {
|
|
75
|
+
this.systemPrompt = systemPrompt;
|
|
76
|
+
this.toolDefinitions = toolDefinitions;
|
|
77
|
+
this.policy = policy;
|
|
78
|
+
}
|
|
79
|
+
async decide(event, state, capabilities) {
|
|
80
|
+
switch (event.type) {
|
|
81
|
+
case "message.received": {
|
|
82
|
+
return capabilities.infer({
|
|
83
|
+
systemPrompt: this.systemPrompt,
|
|
84
|
+
tools: this.toolDefinitions,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
case "inference.done": {
|
|
88
|
+
// The hook gates the entire inference.done branch (including
|
|
89
|
+
// tool extraction and the reactive-mode wait shortcut). An
|
|
90
|
+
// abort/halt from the policy drops any tool calls the model
|
|
91
|
+
// emitted in this turn; see AfterInferenceHook TSDoc for the
|
|
92
|
+
// implications.
|
|
93
|
+
if (this.policy.afterInferenceDone !== undefined) {
|
|
94
|
+
let decision;
|
|
95
|
+
try {
|
|
96
|
+
decision = await this.policy.afterInferenceDone(state, event.turn);
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
100
|
+
logger.error `afterInferenceDone policy threw: ${message}`;
|
|
101
|
+
decision = {
|
|
102
|
+
type: "abort",
|
|
103
|
+
reason: `afterInferenceDone policy threw: ${message}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (decision.type === "abort") {
|
|
107
|
+
// A reply invites the next inbound message, but abort is
|
|
108
|
+
// terminal — the reactor rejects reply paired with done. The
|
|
109
|
+
// reason is therefore not surfaced on this path.
|
|
110
|
+
return [
|
|
111
|
+
capabilities.checkpoint("after-inference-abort"),
|
|
112
|
+
capabilities.done(),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
if (decision.type === "halt") {
|
|
116
|
+
// A reply already returns the reactor to waiting for the next
|
|
117
|
+
// inbound message, so no separate wait is needed (and the
|
|
118
|
+
// reactor rejects reply paired with wait).
|
|
119
|
+
return [
|
|
120
|
+
capabilities.checkpoint("after-inference-halt"),
|
|
121
|
+
capabilities.reply(decision.reason),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// decision.type === "continue" — fall through.
|
|
125
|
+
}
|
|
126
|
+
const toolCalls = extractToolCalls(event.turn);
|
|
127
|
+
if (toolCalls.length > 0) {
|
|
128
|
+
this.pendingToolResults = toolCalls.length;
|
|
129
|
+
return [
|
|
130
|
+
capabilities.checkpoint("tool-execution"),
|
|
131
|
+
capabilities.executeTools(toolCalls, true),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
// No tool calls — the model is done reasoning for this turn.
|
|
135
|
+
if (this.policy.mode === "reactive") {
|
|
136
|
+
return [
|
|
137
|
+
capabilities.checkpoint("inference-done"),
|
|
138
|
+
capabilities.wait(),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
// Conversational agent: send reply via the connector.
|
|
142
|
+
const replyContent = extractTextContent(event.turn);
|
|
143
|
+
if (replyContent.length > 0) {
|
|
144
|
+
return [
|
|
145
|
+
capabilities.checkpoint("inference-done"),
|
|
146
|
+
capabilities.reply(replyContent),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
// Empty response (no text, no tool calls) — checkpoint and wait for
|
|
150
|
+
// the next inbound message. The reactor only shuts down on explicit
|
|
151
|
+
// stop (abort), never because the model produced an empty turn.
|
|
152
|
+
return [capabilities.checkpoint("inference-done"), capabilities.wait()];
|
|
153
|
+
}
|
|
154
|
+
case "tool.done": {
|
|
155
|
+
this.pendingToolResults--;
|
|
156
|
+
if (this.pendingToolResults > 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
if (this.policy.mode === "reactive") {
|
|
160
|
+
return [capabilities.checkpoint("tool-done"), capabilities.wait()];
|
|
161
|
+
}
|
|
162
|
+
// All tool results received — re-infer with complete context.
|
|
163
|
+
return [
|
|
164
|
+
capabilities.checkpoint("tool-done"),
|
|
165
|
+
capabilities.infer({
|
|
166
|
+
systemPrompt: this.systemPrompt,
|
|
167
|
+
tools: this.toolDefinitions,
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
case "inference.error": {
|
|
172
|
+
const statusDetail = event.error.statusCode !== undefined
|
|
173
|
+
? ` [HTTP ${event.error.statusCode}]`
|
|
174
|
+
: "";
|
|
175
|
+
logger.error `Inference error in default director: ${event.error.message}${statusDetail} (category: ${event.error.category})`;
|
|
176
|
+
const userMessage = formatInferenceError(event.error);
|
|
177
|
+
return [
|
|
178
|
+
capabilities.checkpoint("inference-error"),
|
|
179
|
+
capabilities.reply(userMessage),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
case "reactor.gate.cleared": {
|
|
183
|
+
return [
|
|
184
|
+
capabilities.checkpoint("gate-cleared"),
|
|
185
|
+
capabilities.infer({
|
|
186
|
+
systemPrompt: this.systemPrompt,
|
|
187
|
+
tools: this.toolDefinitions,
|
|
188
|
+
}),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
case "abort": {
|
|
192
|
+
return capabilities.done();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function createDefaultDirector(systemPrompt, toolDefinitions = [], policy = {}) {
|
|
198
|
+
return new DefaultDirector(systemPrompt, toolDefinitions, policy);
|
|
199
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactorCapabilities } from "@intx/types/runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a frozen capabilities object. The same instance is reused across
|
|
4
|
+
* calls since all methods are pure constructors.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createCapabilities(): ReactorCapabilities;
|
package/dist/director.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* Builds a frozen capabilities object. The same instance is reused across
|
|
10
|
+
* calls since all methods are pure constructors.
|
|
11
|
+
*/
|
|
12
|
+
export function createCapabilities() {
|
|
13
|
+
return {
|
|
14
|
+
infer(options) {
|
|
15
|
+
return {
|
|
16
|
+
type: "infer",
|
|
17
|
+
...(options !== undefined ? { options } : {}),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
executeTools(calls, parallel, addToHistory) {
|
|
21
|
+
return {
|
|
22
|
+
type: "execute_tools",
|
|
23
|
+
calls,
|
|
24
|
+
...(parallel !== undefined ? { parallel } : {}),
|
|
25
|
+
...(addToHistory !== undefined ? { addToHistory } : {}),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
suspend(gate) {
|
|
29
|
+
return { type: "suspend", gate };
|
|
30
|
+
},
|
|
31
|
+
fork(mode, forkId) {
|
|
32
|
+
return { type: "fork", mode, forkId };
|
|
33
|
+
},
|
|
34
|
+
emit(eventType, data) {
|
|
35
|
+
return { type: "emit", eventType, data };
|
|
36
|
+
},
|
|
37
|
+
reply(content) {
|
|
38
|
+
return { type: "reply", content };
|
|
39
|
+
},
|
|
40
|
+
checkpoint(reason) {
|
|
41
|
+
return {
|
|
42
|
+
type: "checkpoint",
|
|
43
|
+
message: reason !== undefined ? `checkpoint: ${reason}` : "checkpoint",
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
compact(compactor, reason) {
|
|
47
|
+
return { type: "compact", compactor, reason };
|
|
48
|
+
},
|
|
49
|
+
wait() {
|
|
50
|
+
return { type: "wait" };
|
|
51
|
+
},
|
|
52
|
+
done() {
|
|
53
|
+
return { type: "done" };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InferenceError } from "@intx/types/runtime";
|
|
2
|
+
export type { InferenceError };
|
|
3
|
+
export declare function classifyHTTPError(statusCode: number, message: string, raw?: unknown, retryAfterMs?: number): InferenceError;
|
|
4
|
+
export declare function classifyNetworkError(cause: unknown): InferenceError;
|
|
5
|
+
export declare function classifyAbortError(): InferenceError;
|
|
6
|
+
export declare function classifyTimeoutError(kind: "inactivity" | "total", thresholdMs: number): InferenceError;
|
|
7
|
+
/**
|
|
8
|
+
* The one throw type a response parser is permitted to raise. See the
|
|
9
|
+
* `ResponseParser` contract on `adapter.ts` for full semantics. `raw`
|
|
10
|
+
* carries the offending bytes or parsed object so operators can
|
|
11
|
+
* inspect what came over the wire.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ProtocolMismatchError extends Error {
|
|
14
|
+
readonly raw: unknown;
|
|
15
|
+
constructor(detail: string, raw?: unknown);
|
|
16
|
+
}
|
|
17
|
+
export declare function classifyProtocolMismatch(detail: string, raw?: unknown): InferenceError;
|
|
18
|
+
export declare function classifyStreamError(cause: unknown): InferenceError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function classifyHTTPError(statusCode, message, raw, retryAfterMs) {
|
|
2
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
3
|
+
return { category: "credential_failure", message, statusCode, raw };
|
|
4
|
+
}
|
|
5
|
+
if (statusCode === 429) {
|
|
6
|
+
return {
|
|
7
|
+
category: "quota_exhausted",
|
|
8
|
+
message,
|
|
9
|
+
statusCode,
|
|
10
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
11
|
+
raw,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (statusCode === 400) {
|
|
15
|
+
// Context-overflow manifests as a 400 with a provider-specific message.
|
|
16
|
+
// Check for known patterns before falling through to fatal.
|
|
17
|
+
if (isContextOverflowMessage(message)) {
|
|
18
|
+
return { category: "context_overflow", message, statusCode, raw };
|
|
19
|
+
}
|
|
20
|
+
return { category: "fatal", message, statusCode, raw };
|
|
21
|
+
}
|
|
22
|
+
if (statusCode >= 500 && statusCode < 600) {
|
|
23
|
+
return { category: "retryable", message, statusCode, raw };
|
|
24
|
+
}
|
|
25
|
+
return { category: "fatal", message, statusCode, raw };
|
|
26
|
+
}
|
|
27
|
+
export function classifyNetworkError(cause) {
|
|
28
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
29
|
+
return { category: "retryable", message, raw: cause };
|
|
30
|
+
}
|
|
31
|
+
export function classifyAbortError() {
|
|
32
|
+
return { category: "aborted", message: "inference aborted" };
|
|
33
|
+
}
|
|
34
|
+
export function classifyTimeoutError(kind, thresholdMs) {
|
|
35
|
+
const message = kind === "inactivity"
|
|
36
|
+
? `inference call exceeded inactivity timeout (${String(thresholdMs)} ms with no events from the provider)`
|
|
37
|
+
: `inference call exceeded total timeout (${String(thresholdMs)} ms wall-clock)`;
|
|
38
|
+
return { category: "timeout", message };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The one throw type a response parser is permitted to raise. See the
|
|
42
|
+
* `ResponseParser` contract on `adapter.ts` for full semantics. `raw`
|
|
43
|
+
* carries the offending bytes or parsed object so operators can
|
|
44
|
+
* inspect what came over the wire.
|
|
45
|
+
*/
|
|
46
|
+
export class ProtocolMismatchError extends Error {
|
|
47
|
+
raw;
|
|
48
|
+
constructor(detail, raw) {
|
|
49
|
+
super(detail);
|
|
50
|
+
this.name = "ProtocolMismatchError";
|
|
51
|
+
this.raw = raw;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function classifyProtocolMismatch(detail, raw) {
|
|
55
|
+
return {
|
|
56
|
+
category: "protocol_mismatch",
|
|
57
|
+
message: detail,
|
|
58
|
+
...(raw !== undefined ? { raw } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function classifyStreamError(cause) {
|
|
62
|
+
if (isAbortError(cause)) {
|
|
63
|
+
return classifyAbortError();
|
|
64
|
+
}
|
|
65
|
+
if (cause instanceof ProtocolMismatchError) {
|
|
66
|
+
return classifyProtocolMismatch(cause.message, cause.raw);
|
|
67
|
+
}
|
|
68
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
69
|
+
return { category: "retryable", message, raw: cause };
|
|
70
|
+
}
|
|
71
|
+
function isContextOverflowMessage(message) {
|
|
72
|
+
const lower = message.toLowerCase();
|
|
73
|
+
return (lower.includes("context_length_exceeded") ||
|
|
74
|
+
lower.includes("context length") ||
|
|
75
|
+
lower.includes("too many tokens") ||
|
|
76
|
+
lower.includes("maximum context") ||
|
|
77
|
+
lower.includes("input is too long"));
|
|
78
|
+
}
|
|
79
|
+
function isAbortError(value) {
|
|
80
|
+
return (value instanceof Error &&
|
|
81
|
+
(value.name === "AbortError" ||
|
|
82
|
+
value.message === "The user aborted a request."));
|
|
83
|
+
}
|
package/dist/gates.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GateType } from "@intx/types/runtime";
|
|
2
|
+
export type GateRecord = {
|
|
3
|
+
gateId: string;
|
|
4
|
+
type: GateType;
|
|
5
|
+
timeoutAt: number;
|
|
6
|
+
correlationId: string | undefined;
|
|
7
|
+
resolve: (reason: "resolved" | "timeout" | "shutdown") => void;
|
|
8
|
+
onCleared: (gateId: string, reason: "resolved" | "timeout" | "shutdown") => void;
|
|
9
|
+
timer: ReturnType<typeof setTimeout>;
|
|
10
|
+
};
|
|
11
|
+
export type GateSnapshot = {
|
|
12
|
+
gateId: string;
|
|
13
|
+
type: GateType;
|
|
14
|
+
timeoutAt: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Manages active gates. All gates must have a positive timeout.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createGateManager(): {
|
|
20
|
+
register: (gateId: string, type: GateType, timeoutMs: number, correlationId: string | undefined, onCleared: (gateId: string, reason: "resolved" | "timeout" | "shutdown") => void) => Promise<"resolved" | "timeout" | "shutdown">;
|
|
21
|
+
clear: (gateId: string) => boolean;
|
|
22
|
+
shutdown: () => void;
|
|
23
|
+
findByCorrelationId: (correlationId: string) => GateRecord | undefined;
|
|
24
|
+
snapshot: () => GateSnapshot[];
|
|
25
|
+
has: (gateId: string) => boolean;
|
|
26
|
+
};
|
|
27
|
+
export type GateManager = ReturnType<typeof createGateManager>;
|
package/dist/gates.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Gate management for the agent reactor.
|
|
2
|
+
//
|
|
3
|
+
// Gates block the reactor until an external condition resolves. Each gate has
|
|
4
|
+
// a type, an ID, and a mandatory timeout. The gate manager owns all active
|
|
5
|
+
// gates and exposes methods to register, clear, and time out gates.
|
|
6
|
+
//
|
|
7
|
+
// (INFERENCE.md § Gates, Gate Timeouts, Gate Behavior During Suspension)
|
|
8
|
+
/**
|
|
9
|
+
* Manages active gates. All gates must have a positive timeout.
|
|
10
|
+
*/
|
|
11
|
+
export function createGateManager() {
|
|
12
|
+
const gates = new Map();
|
|
13
|
+
function register(gateId, type, timeoutMs, correlationId, onCleared) {
|
|
14
|
+
if (timeoutMs <= 0) {
|
|
15
|
+
throw new Error(`Gate "${gateId}" must have a positive timeout (got ${timeoutMs})`);
|
|
16
|
+
}
|
|
17
|
+
if (gates.has(gateId)) {
|
|
18
|
+
throw new Error(`Gate "${gateId}" is already registered`);
|
|
19
|
+
}
|
|
20
|
+
const timeoutAt = Date.now() + timeoutMs;
|
|
21
|
+
let resolveGate;
|
|
22
|
+
const promise = new Promise((resolve) => {
|
|
23
|
+
resolveGate = resolve;
|
|
24
|
+
});
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
if (gates.has(gateId)) {
|
|
27
|
+
gates.delete(gateId);
|
|
28
|
+
resolveGate("timeout");
|
|
29
|
+
onCleared(gateId, "timeout");
|
|
30
|
+
}
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
gates.set(gateId, {
|
|
33
|
+
gateId,
|
|
34
|
+
type,
|
|
35
|
+
timeoutAt,
|
|
36
|
+
correlationId,
|
|
37
|
+
resolve: resolveGate,
|
|
38
|
+
onCleared,
|
|
39
|
+
timer,
|
|
40
|
+
});
|
|
41
|
+
return promise;
|
|
42
|
+
}
|
|
43
|
+
function clear(gateId) {
|
|
44
|
+
const gate = gates.get(gateId);
|
|
45
|
+
if (gate === undefined)
|
|
46
|
+
return false;
|
|
47
|
+
clearTimeout(gate.timer);
|
|
48
|
+
gates.delete(gateId);
|
|
49
|
+
gate.resolve("resolved");
|
|
50
|
+
gate.onCleared(gateId, "resolved");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function shutdown() {
|
|
54
|
+
const entries = Array.from(gates.values());
|
|
55
|
+
gates.clear();
|
|
56
|
+
for (const gate of entries) {
|
|
57
|
+
clearTimeout(gate.timer);
|
|
58
|
+
gate.resolve("shutdown");
|
|
59
|
+
gate.onCleared(gate.gateId, "shutdown");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function findByCorrelationId(correlationId) {
|
|
63
|
+
for (const gate of gates.values()) {
|
|
64
|
+
if (gate.correlationId === correlationId)
|
|
65
|
+
return gate;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function snapshot() {
|
|
70
|
+
return Array.from(gates.values()).map((g) => ({
|
|
71
|
+
gateId: g.gateId,
|
|
72
|
+
type: g.type,
|
|
73
|
+
timeoutAt: g.timeoutAt,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function has(gateId) {
|
|
77
|
+
return gates.has(gateId);
|
|
78
|
+
}
|
|
79
|
+
return { register, clear, shutdown, findByCorrelationId, snapshot, has };
|
|
80
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ConversationTurn, InferenceEvent, InferenceOptions, InferenceSource } from "@intx/types/runtime";
|
|
2
|
+
import type { AdapterRegistry } from "./adapter.js";
|
|
3
|
+
/**
|
|
4
|
+
* Default per-call inactivity timeout (ms). Two minutes is conservative
|
|
5
|
+
* for reasoning-heavy models that emit `inference.thinking.delta` tokens
|
|
6
|
+
* regularly when actually working — sustained silence past this means
|
|
7
|
+
* the provider stream has genuinely stalled, not that the model is
|
|
8
|
+
* thinking. Operators can tune via `InferenceOptions.inactivityTimeoutMs`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_INACTIVITY_TIMEOUT_MS = 120000;
|
|
11
|
+
/**
|
|
12
|
+
* Default per-call total wall-clock cap (ms). Matches Anthropic's
|
|
13
|
+
* documented per-call recommendation and fits within typical CI
|
|
14
|
+
* timeouts. Operators can tune via `InferenceOptions.totalTimeoutMs`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DEFAULT_TOTAL_TIMEOUT_MS = 600000;
|
|
17
|
+
export declare const HarnessId: unique symbol;
|
|
18
|
+
/**
|
|
19
|
+
* Runtime dependencies injected into `runInference`. Code-only — not part of
|
|
20
|
+
* any persisted schema. Test harnesses substitute `fetch` (and stamp the
|
|
21
|
+
* `[HarnessId]` tag for per-harness identity) so production `runInference`
|
|
22
|
+
* never reaches `globalThis.fetch`.
|
|
23
|
+
*
|
|
24
|
+
* `fetch` is intentionally typed as a plain function rather than
|
|
25
|
+
* `typeof globalThis.fetch` — the latter is augmented per-runtime (Bun adds
|
|
26
|
+
* `preconnect`; Node and the DOM lib do not) and `runInference` only ever
|
|
27
|
+
* invokes the call signature.
|
|
28
|
+
*
|
|
29
|
+
* The `[HarnessId]` tag is enumerable via `Object.getOwnPropertySymbols`
|
|
30
|
+
* (and `Reflect.ownKeys`, which is the superset). Do not pass `Dependencies`
|
|
31
|
+
* instances through reflective serializers or expose them across trust
|
|
32
|
+
* boundaries. (`JSON.stringify` is safe — it walks string keys only.)
|
|
33
|
+
*/
|
|
34
|
+
export type Dependencies = {
|
|
35
|
+
readonly fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
36
|
+
/**
|
|
37
|
+
* Time-based scheduler used by the harness's per-call timeouts (see
|
|
38
|
+
* `InferenceOptions.inactivityTimeoutMs` / `totalTimeoutMs`). Production
|
|
39
|
+
* passes the default wrapper around `setTimeout` / `clearTimeout`; the
|
|
40
|
+
* deterministic test harness injects a scheduler that wraps its virtual
|
|
41
|
+
* clock so timeout tests fire at virtual-time-N without sleeping real
|
|
42
|
+
* wall-clock. Required — every caller must make an explicit choice
|
|
43
|
+
* between the production scheduler and a virtual one. Use
|
|
44
|
+
* `createDefaultScheduler()` for the production default.
|
|
45
|
+
*/
|
|
46
|
+
readonly scheduler: Scheduler;
|
|
47
|
+
/**
|
|
48
|
+
* Registry resolving an inference source to its provider adapter.
|
|
49
|
+
* `runSingleAttempt` consults this on every call via `adapters.resolve`,
|
|
50
|
+
* so it is required — the caller makes an explicit choice of provider set.
|
|
51
|
+
* Construct it via `createDependencies(adapters)` (core) or, for the
|
|
52
|
+
* built-in set, `@intx/inference/providers`' `createDefaultDependencies()`.
|
|
53
|
+
*/
|
|
54
|
+
readonly adapters: AdapterRegistry;
|
|
55
|
+
readonly [HarnessId]?: symbol;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Minimal scheduling abstraction. `setTimeout` returns a canceller; the
|
|
59
|
+
* canceller is idempotent (multiple calls are safe). The harness uses
|
|
60
|
+
* this for both the inactivity timer (which is re-armed on every event)
|
|
61
|
+
* and the total wall-clock cap. `now()` is a monotonic time source in
|
|
62
|
+
* the same `delayMs` units `setTimeout` accepts — deltas across two
|
|
63
|
+
* `now()` reads describe elapsed time the same way `setTimeout(...,
|
|
64
|
+
* delta)` would have measured it.
|
|
65
|
+
*/
|
|
66
|
+
export type Scheduler = {
|
|
67
|
+
setTimeout(callback: () => void, delayMs: number): () => void;
|
|
68
|
+
now(): number;
|
|
69
|
+
};
|
|
70
|
+
export declare function createDefaultScheduler(): Scheduler;
|
|
71
|
+
/**
|
|
72
|
+
* Construct runtime dependencies for `runInference` from an explicit adapter
|
|
73
|
+
* registry, binding `fetch` to `globalThis.fetch` and `scheduler` to the
|
|
74
|
+
* production wrapper. The registry is required so the caller makes an explicit
|
|
75
|
+
* choice of provider set; `@intx/inference/providers`' zero-arg
|
|
76
|
+
* `createDefaultDependencies()` is the honest default that supplies the
|
|
77
|
+
* built-in registry.
|
|
78
|
+
*
|
|
79
|
+
* @param adapters - Registry resolving inference sources to provider adapters
|
|
80
|
+
* @returns Fully-populated dependencies
|
|
81
|
+
*/
|
|
82
|
+
export declare function createDependencies(adapters: AdapterRegistry): Dependencies;
|
|
83
|
+
export type InferenceHarnessOptions = {
|
|
84
|
+
turns: ConversationTurn[];
|
|
85
|
+
source: InferenceSource;
|
|
86
|
+
inferenceOptions?: InferenceOptions;
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
nextSeq: () => number;
|
|
89
|
+
deps: Dependencies;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Run a single inference call with mechanical retry. Wraps
|
|
93
|
+
* `runSingleAttempt` and consults the configured `RetryPolicy` (or the
|
|
94
|
+
* default from `createDefaultRetryPolicy`) on every `inference.error`.
|
|
95
|
+
*
|
|
96
|
+
* Events from each attempt are buffered until the attempt terminates;
|
|
97
|
+
* the wrapper only flushes them to the caller once it knows whether
|
|
98
|
+
* the attempt resolved (`inference.done` or a policy-approved abort)
|
|
99
|
+
* or whether the attempt's events should be discarded in favour of a
|
|
100
|
+
* retry. The buffer-and-flush model is what guarantees the caller
|
|
101
|
+
* sees a single clean event stream — exactly one `inference.start`,
|
|
102
|
+
* no orphaned partial deltas, no leaked `inference.error`s from
|
|
103
|
+
* attempts the policy chose to retry. The cost is that no events
|
|
104
|
+
* reach the caller until the wrapper knows the attempt's terminal
|
|
105
|
+
* shape, even on a successful first attempt. That trade-off is the
|
|
106
|
+
* deliberate consequence of making "one clean stream" a hard contract
|
|
107
|
+
* rather than a best-effort one. Consumers that need token-by-token
|
|
108
|
+
* partials must pin a custom non-buffering wrapper — no streaming-
|
|
109
|
+
* partials emission API exists today.
|
|
110
|
+
*
|
|
111
|
+
* The buffer is per-call and bounded by the size of one attempt's
|
|
112
|
+
* event stream — no cross-call accumulation.
|
|
113
|
+
*
|
|
114
|
+
* Caller-visible seqs stay contiguous across retries. Each attempt
|
|
115
|
+
* runs against a private seq allocator; on flush the wrapper
|
|
116
|
+
* re-stamps the buffered events with seqs from the caller's
|
|
117
|
+
* `nextSeq`, so a retry that discards an attempt does not leave a
|
|
118
|
+
* gap in the consumer's seq stream.
|
|
119
|
+
*
|
|
120
|
+
* Between attempts the wrapper emits one `inference.retry` event with
|
|
121
|
+
* the failed attempt's number, the policy-chosen `delayMs`, and the
|
|
122
|
+
* classified error that triggered the retry. The `setTimeout` await
|
|
123
|
+
* is driven by `deps.scheduler`, so virtual-clock test harnesses
|
|
124
|
+
* advance retry delays without sleeping real wall-clock. The
|
|
125
|
+
* caller-supplied `signal` short-circuits the retry delay: aborting
|
|
126
|
+
* the signal mid-delay wakes the await immediately and the next
|
|
127
|
+
* `runSingleAttempt` invocation surfaces `inference.error` of
|
|
128
|
+
* category `aborted` from its entry-time signal check, which the
|
|
129
|
+
* default policy aborts on.
|
|
130
|
+
*
|
|
131
|
+
* Policy-failure handling: if the policy throws synchronously or its
|
|
132
|
+
* returned Promise rejects, the wrapper treats the failure as
|
|
133
|
+
* `{ kind: "abort" }` and surfaces the *original* `inference.error`
|
|
134
|
+
* to the caller. The policy's own exception is logged at `warn` so
|
|
135
|
+
* operators can see when a custom policy is failing under load, and
|
|
136
|
+
* dropped — the inference error is what the caller needs to act on,
|
|
137
|
+
* not the bug in the policy callback.
|
|
138
|
+
*
|
|
139
|
+
* Synchronous throws from `runSingleAttempt` (`ProtocolMismatchError`
|
|
140
|
+
* raised by the streaming parse or the finalization walk, etc.)
|
|
141
|
+
* propagate out of `runInference`. The current attempt's buffered
|
|
142
|
+
* events are discarded along with the throw — those represent
|
|
143
|
+
* protocol bugs the policy mechanism is not equipped to absorb, and
|
|
144
|
+
* the caller's `for await` rejects so the failure surfaces rather
|
|
145
|
+
* than being silently buffered.
|
|
146
|
+
*/
|
|
147
|
+
export declare function runInference(opts: InferenceHarnessOptions): AsyncIterable<InferenceEvent>;
|