@intx/inference 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +176 -0
  2. package/dist/actions.d.ts +16 -0
  3. package/dist/actions.js +200 -0
  4. package/dist/adapter.d.ts +40 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +75 -0
  7. package/dist/assembly.js +133 -0
  8. package/dist/audit-collector.d.ts +10 -0
  9. package/dist/audit-collector.js +139 -0
  10. package/dist/auth.d.ts +24 -0
  11. package/{src/auth.ts → dist/auth.js} +13 -19
  12. package/dist/authz-extension.d.ts +46 -0
  13. package/dist/authz-extension.js +184 -0
  14. package/dist/correlation.d.ts +26 -0
  15. package/dist/correlation.js +39 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +228 -0
  18. package/dist/director.d.ts +6 -0
  19. package/dist/director.js +56 -0
  20. package/dist/errors.d.ts +18 -0
  21. package/dist/errors.js +83 -0
  22. package/dist/gates.d.ts +28 -0
  23. package/dist/gates.js +103 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1407 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +21 -0
  28. package/dist/manifest.d.ts +31 -0
  29. package/dist/manifest.js +44 -0
  30. package/dist/providers/anthropic.d.ts +37 -0
  31. package/dist/providers/anthropic.js +917 -0
  32. package/dist/providers/google-genai-files.d.ts +48 -0
  33. package/dist/providers/google-genai-files.js +205 -0
  34. package/dist/providers/google-genai.d.ts +5 -0
  35. package/dist/providers/google-genai.js +1205 -0
  36. package/dist/providers/index.d.ts +38 -0
  37. package/dist/providers/index.js +56 -0
  38. package/dist/providers/openai.d.ts +9 -0
  39. package/dist/providers/openai.js +903 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +1233 -0
  42. package/dist/retry-policy.d.ts +31 -0
  43. package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
  44. package/dist/sse.d.ts +1 -0
  45. package/dist/sse.js +63 -0
  46. package/dist/state.d.ts +23 -0
  47. package/dist/state.js +100 -0
  48. package/dist/tool-name.d.ts +6 -0
  49. package/dist/tool-name.js +110 -0
  50. package/dist/transform.d.ts +11 -0
  51. package/dist/transform.js +132 -0
  52. package/dist/transforms/index.d.ts +2 -0
  53. package/dist/transforms/index.js +1 -0
  54. package/dist/transforms/size-cap.d.ts +12 -0
  55. package/dist/transforms/size-cap.js +80 -0
  56. package/dist/turns.d.ts +21 -0
  57. package/dist/turns.js +135 -0
  58. package/package.json +22 -6
  59. package/src/actions.ts +0 -245
  60. package/src/adapter.ts +0 -57
  61. package/src/assembly.test.ts +0 -728
  62. package/src/assembly.ts +0 -250
  63. package/src/audit-collector.test.ts +0 -332
  64. package/src/audit-collector.ts +0 -172
  65. package/src/auth.test.ts +0 -117
  66. package/src/authz-extension.test.ts +0 -269
  67. package/src/authz-extension.ts +0 -145
  68. package/src/correlation.ts +0 -61
  69. package/src/default-director.test.ts +0 -314
  70. package/src/default-director.ts +0 -344
  71. package/src/director.ts +0 -87
  72. package/src/errors.test.ts +0 -133
  73. package/src/errors.ts +0 -115
  74. package/src/gates.ts +0 -128
  75. package/src/harness.test.ts +0 -655
  76. package/src/harness.ts +0 -1571
  77. package/src/index.ts +0 -76
  78. package/src/providers/anthropic.test.ts +0 -771
  79. package/src/providers/anthropic.ts +0 -810
  80. package/src/providers/google-genai-files.ts +0 -289
  81. package/src/providers/google-genai.ts +0 -1518
  82. package/src/providers/openai.ts +0 -719
  83. package/src/providers/registry.ts +0 -33
  84. package/src/reactor.test.ts +0 -3660
  85. package/src/reactor.ts +0 -1058
  86. package/src/scheduler.test.ts +0 -41
  87. package/src/sse.test.ts +0 -133
  88. package/src/sse.ts +0 -76
  89. package/src/state.ts +0 -135
  90. package/src/transform.test.ts +0 -207
  91. package/src/transform.ts +0 -159
  92. package/src/transforms/index.ts +0 -2
  93. package/src/transforms/size-cap.test.ts +0 -172
  94. package/src/transforms/size-cap.ts +0 -110
  95. package/src/turns.ts +0 -54
  96. package/tsconfig.json +0 -4
  97. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,111 @@
1
+ import { type ReactorDirector, type ReactorInboundEvent, type ReactorState, type ReactorCapabilities, type ReactorAction, type AssistantTurn, type ToolDefinition } from "@intx/types/runtime";
2
+ /**
3
+ * Decision returned by an `afterInferenceDone` policy hook.
4
+ *
5
+ * continue — proceed with the director's normal post-inference logic
6
+ * (tool extraction, reply, or wait per the existing flow).
7
+ * abort — terminate the agent. Routes to `[checkpoint, done]` and
8
+ * the reactor shuts down. Stronger than the
9
+ * `inference.error` branch, which only replies and stays
10
+ * alive — `abort` is for "session is over, do not accept
11
+ * further inputs."
12
+ * halt — pause the current cycle without terminating. Routes to
13
+ * `[checkpoint, reply]`; the reply returns the reactor to
14
+ * waiting for the next inbound event, so it stays alive.
15
+ * There is no auto-resume; an external event (mail, gate
16
+ * clearance, etc.) must reach the reactor for the agent to
17
+ * make progress again.
18
+ *
19
+ * `reason` on a `halt` becomes the connector reply text verbatim, so
20
+ * policy authors choose what is safe to surface to the user. On an
21
+ * `abort` the reason is not surfaced: a terminal action cannot carry a
22
+ * reply, since a reply invites continuation. Delivering an abort reason
23
+ * to the user needs a dedicated terminal-notice path, which does not
24
+ * exist today.
25
+ */
26
+ export type AfterInferenceDecision = {
27
+ type: "continue";
28
+ } | {
29
+ type: "abort";
30
+ reason: string;
31
+ } | {
32
+ type: "halt";
33
+ reason: string;
34
+ };
35
+ /**
36
+ * Function shape for an after-inference-done policy hook.
37
+ *
38
+ * The hook fires only on `inference.done` (a successful cycle). Errored
39
+ * cycles do not invoke it. `mode: "reactive"` does not change firing —
40
+ * the hook gates the entire `inference.done` branch, including the
41
+ * reactive-wait shortcut, so a budget check applies to reactive agents
42
+ * the same way it does to conversational ones.
43
+ *
44
+ * The hook receives the post-cycle `ReactorState` (with `lastCycleSource`
45
+ * and `lastCycleUsage` populated for the just-completed call) and the
46
+ * assistant turn. Returns a decision (sync or async) that controls
47
+ * whether the director continues, terminates the agent, or pauses the
48
+ * cycle.
49
+ *
50
+ * Canonical use case: cost-aware gating. Read `state.lastCycleSource`
51
+ * + `state.lastCycleUsage`, price the call against user-supplied rate
52
+ * data, decide whether the budget is exhausted. Token caps, time caps,
53
+ * wallet checks, and governance triggers fit the same shape; the
54
+ * type stays policy-agnostic.
55
+ *
56
+ * "Downgrade to cheaper model" policies do NOT use this hook to return
57
+ * a new source. Compose them via an external observer of
58
+ * `lastCycleSource` / `lastCycleUsage` that calls `setSource` from
59
+ * outside the director.
60
+ *
61
+ * The hook blocks the reactor's inference.done branch: keep its
62
+ * latency low. The return type admits a Promise, but every await
63
+ * inside the hook is wall-clock time the agent isn't making progress.
64
+ * Small lookups (in-memory caches, fast DB reads) are fine; arbitrary
65
+ * waits are not.
66
+ *
67
+ * Tool calls and `halt`: if the model emitted tool calls and the hook
68
+ * returns `halt` (or `abort`), those tool calls are dropped — the
69
+ * director never executes them. On resume, the model's next inference
70
+ * sees an assistant turn with unanswered tool calls; depending on the
71
+ * provider this is either a validation error or a confused model.
72
+ * Policy authors that combine `halt` with tool-heavy agents need to
73
+ * understand this.
74
+ */
75
+ export type AfterInferenceHook = (state: ReactorState, turn: AssistantTurn) => AfterInferenceDecision | Promise<AfterInferenceDecision>;
76
+ export type DefaultDirectorPolicy = {
77
+ /**
78
+ * Controls the agent's behavior after inference completes.
79
+ *
80
+ * "conversational" (default) — The standard agentic loop. After tools
81
+ * complete, re-infer so the model can reason about results, issue more
82
+ * tool calls, or compose a reply. When inference produces text without
83
+ * tool calls, send it as a connector reply.
84
+ *
85
+ * "reactive" — The agent acts on each message by executing tools, then
86
+ * returns to the event loop to wait for the next inbound event. It does
87
+ * not re-infer after tools complete and does not send connector replies.
88
+ * Use this for agents that perform a single action per message.
89
+ */
90
+ mode?: "conversational" | "reactive";
91
+ /**
92
+ * Optional policy hook fired after every successful `inference.done`.
93
+ * See `AfterInferenceHook` for the contract: firing boundary, return
94
+ * shape, composition patterns, and policy-author caveats.
95
+ *
96
+ * If the hook throws or rejects, the director catches the error,
97
+ * routes to `{ type: "abort", reason: "afterInferenceDone policy
98
+ * threw: <message>" }`, and logs at error level. The director's
99
+ * never-throws contract is preserved.
100
+ */
101
+ afterInferenceDone?: AfterInferenceHook;
102
+ };
103
+ export declare class DefaultDirector implements ReactorDirector {
104
+ private readonly systemPrompt;
105
+ private readonly toolDefinitions;
106
+ private readonly policy;
107
+ private pendingToolResults;
108
+ constructor(systemPrompt: string, toolDefinitions?: ToolDefinition[], policy?: DefaultDirectorPolicy);
109
+ decide(event: ReactorInboundEvent, state: ReactorState, capabilities: ReactorCapabilities): Promise<ReactorAction | ReactorAction[]>;
110
+ }
111
+ export declare function createDefaultDirector(systemPrompt: string, toolDefinitions?: ToolDefinition[], policy?: DefaultDirectorPolicy): ReactorDirector;
@@ -0,0 +1,228 @@
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
+ // resume.execute_tools → execute_tools (re-run a parked approved call)
14
+ // resume.tool_result → checkpoint + infer (parked call denied/timed out)
15
+ //
16
+ // The inference.done branch additionally runs the optional afterInferenceDone
17
+ // policy hook, whose continue/abort/halt decisions route independently of the
18
+ // event map above. See AfterInferenceDecision for that contract.
19
+ //
20
+ // The director never throws. Inference errors are surfaced to the user as a
21
+ // reply so the problem is visible, and the agent remains alive for retries.
22
+ import { getLogger } from "@intx/log";
23
+ import { formatSafetyRatingText, } from "@intx/types/runtime";
24
+ const logger = getLogger(["interchange", "inference", "default-director"]);
25
+ function extractToolCalls(turn) {
26
+ const calls = [];
27
+ for (const block of turn.content) {
28
+ if (block.type === "tool_call") {
29
+ calls.push({
30
+ id: block.id,
31
+ name: block.name,
32
+ arguments: block.arguments,
33
+ });
34
+ }
35
+ }
36
+ return calls;
37
+ }
38
+ function extractTextContent(turn) {
39
+ // Text, refusal, and safety_rating blocks all carry human-readable
40
+ // output the connector needs to surface. A refusal-only or
41
+ // safety-only turn would otherwise route through the empty-response
42
+ // branch below and never reach the reply path, leaving the human
43
+ // waiting for an answer the model already declined or blocked.
44
+ // Structural part kinds are preserved at the persistence layer;
45
+ // the reply path only needs the words.
46
+ const parts = [];
47
+ for (const block of turn.content) {
48
+ if (block.type === "text") {
49
+ parts.push(block.text);
50
+ }
51
+ else if (block.type === "refusal") {
52
+ parts.push(block.reason);
53
+ }
54
+ else if (block.type === "safety_rating") {
55
+ parts.push(formatSafetyRatingText(block));
56
+ }
57
+ }
58
+ return parts.join("\n").trim();
59
+ }
60
+ const ERROR_PREAMBLE = {
61
+ credential_failure: "This agent could not complete your request due to a credential error",
62
+ quota_exhausted: "This agent could not complete your request because the API quota has been exhausted",
63
+ context_overflow: "This agent could not complete your request because the conversation exceeded the model's context limit",
64
+ retryable: "This agent encountered a temporary error communicating with the inference provider",
65
+ fatal: "This agent could not complete your request due to an unrecoverable inference error",
66
+ aborted: "This agent's inference request was aborted",
67
+ };
68
+ function formatInferenceError(error) {
69
+ const preamble = ERROR_PREAMBLE[error.category] ?? ERROR_PREAMBLE["fatal"];
70
+ const status = error.statusCode !== undefined ? ` [HTTP ${error.statusCode}]` : "";
71
+ return `${preamble}${status}: ${error.message}`;
72
+ }
73
+ export class DefaultDirector {
74
+ systemPrompt;
75
+ toolDefinitions;
76
+ policy;
77
+ // Track outstanding tool results so we only re-infer once per batch.
78
+ pendingToolResults = 0;
79
+ constructor(systemPrompt, toolDefinitions = [], policy = {}) {
80
+ this.systemPrompt = systemPrompt;
81
+ this.toolDefinitions = toolDefinitions;
82
+ this.policy = policy;
83
+ }
84
+ async decide(event, state, capabilities) {
85
+ switch (event.type) {
86
+ case "message.received": {
87
+ return capabilities.infer({
88
+ systemPrompt: this.systemPrompt,
89
+ tools: this.toolDefinitions,
90
+ });
91
+ }
92
+ case "inference.done": {
93
+ // The hook gates the entire inference.done branch (including
94
+ // tool extraction and the reactive-mode wait shortcut). An
95
+ // abort/halt from the policy drops any tool calls the model
96
+ // emitted in this turn; see AfterInferenceHook TSDoc for the
97
+ // implications.
98
+ if (this.policy.afterInferenceDone !== undefined) {
99
+ let decision;
100
+ try {
101
+ decision = await this.policy.afterInferenceDone(state, event.turn);
102
+ }
103
+ catch (cause) {
104
+ const message = cause instanceof Error ? cause.message : String(cause);
105
+ logger.error `afterInferenceDone policy threw: ${message}`;
106
+ decision = {
107
+ type: "abort",
108
+ reason: `afterInferenceDone policy threw: ${message}`,
109
+ };
110
+ }
111
+ if (decision.type === "abort") {
112
+ // A reply invites the next inbound message, but abort is
113
+ // terminal — the reactor rejects reply paired with done. The
114
+ // reason is therefore not surfaced on this path.
115
+ return [
116
+ capabilities.checkpoint("after-inference-abort"),
117
+ capabilities.done(),
118
+ ];
119
+ }
120
+ if (decision.type === "halt") {
121
+ // A reply already returns the reactor to waiting for the next
122
+ // inbound message, so no separate wait is needed (and the
123
+ // reactor rejects reply paired with wait).
124
+ return [
125
+ capabilities.checkpoint("after-inference-halt"),
126
+ capabilities.reply(decision.reason),
127
+ ];
128
+ }
129
+ // decision.type === "continue" — fall through.
130
+ }
131
+ const toolCalls = extractToolCalls(event.turn);
132
+ if (toolCalls.length > 0) {
133
+ this.pendingToolResults = toolCalls.length;
134
+ return [
135
+ capabilities.checkpoint("tool-execution"),
136
+ capabilities.executeTools(toolCalls, true),
137
+ ];
138
+ }
139
+ // No tool calls — the model is done reasoning for this turn.
140
+ if (this.policy.mode === "reactive") {
141
+ return [
142
+ capabilities.checkpoint("inference-done"),
143
+ capabilities.wait(),
144
+ ];
145
+ }
146
+ // Conversational agent: send reply via the connector.
147
+ const replyContent = extractTextContent(event.turn);
148
+ if (replyContent.length > 0) {
149
+ return [
150
+ capabilities.checkpoint("inference-done"),
151
+ capabilities.reply(replyContent),
152
+ ];
153
+ }
154
+ // Empty response (no text, no tool calls) — checkpoint and wait for
155
+ // the next inbound message. The reactor only shuts down on explicit
156
+ // stop (abort), never because the model produced an empty turn.
157
+ return [capabilities.checkpoint("inference-done"), capabilities.wait()];
158
+ }
159
+ case "resume.execute_tools": {
160
+ // A resumed approval re-runs its parked tool call. The reactor drives
161
+ // the execution; this director owns the outstanding-result count, so
162
+ // seed it to the number of calls about to run — exactly as the
163
+ // inference.done branch seeds it for a fresh tool batch. Without this
164
+ // seed the count stays zero and the re-dispatched call's tool.done
165
+ // would decrement to -1 and re-infer off a negative count by accident.
166
+ this.pendingToolResults = event.calls.length;
167
+ return capabilities.executeTools(event.calls, false, true);
168
+ }
169
+ case "resume.tool_result": {
170
+ // A parked approval ended without running its tool (rejected or timed
171
+ // out). The reactor appends the synthetic error result that answers the
172
+ // parked call, then this re-infers once so the model sees the failure
173
+ // and continues. No tool ran, so pendingToolResults is untouched — the
174
+ // counter only gates batches of real executions.
175
+ return [
176
+ capabilities.checkpoint("resume-tool-result"),
177
+ capabilities.infer({
178
+ systemPrompt: this.systemPrompt,
179
+ tools: this.toolDefinitions,
180
+ }),
181
+ ];
182
+ }
183
+ case "tool.done": {
184
+ this.pendingToolResults--;
185
+ if (this.pendingToolResults > 0) {
186
+ return [];
187
+ }
188
+ if (this.policy.mode === "reactive") {
189
+ return [capabilities.checkpoint("tool-done"), capabilities.wait()];
190
+ }
191
+ // All tool results received — re-infer with complete context.
192
+ return [
193
+ capabilities.checkpoint("tool-done"),
194
+ capabilities.infer({
195
+ systemPrompt: this.systemPrompt,
196
+ tools: this.toolDefinitions,
197
+ }),
198
+ ];
199
+ }
200
+ case "inference.error": {
201
+ const statusDetail = event.error.statusCode !== undefined
202
+ ? ` [HTTP ${event.error.statusCode}]`
203
+ : "";
204
+ logger.error `Inference error in default director: ${event.error.message}${statusDetail} (category: ${event.error.category})`;
205
+ const userMessage = formatInferenceError(event.error);
206
+ return [
207
+ capabilities.checkpoint("inference-error"),
208
+ capabilities.reply(userMessage),
209
+ ];
210
+ }
211
+ case "reactor.gate.cleared": {
212
+ return [
213
+ capabilities.checkpoint("gate-cleared"),
214
+ capabilities.infer({
215
+ systemPrompt: this.systemPrompt,
216
+ tools: this.toolDefinitions,
217
+ }),
218
+ ];
219
+ }
220
+ case "abort": {
221
+ return capabilities.done();
222
+ }
223
+ }
224
+ }
225
+ }
226
+ export function createDefaultDirector(systemPrompt, toolDefinitions = [], policy = {}) {
227
+ return new DefaultDirector(systemPrompt, toolDefinitions, policy);
228
+ }
@@ -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;
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,28 @@
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
+ clearSilently: (gateId: string) => boolean;
23
+ shutdown: () => void;
24
+ findByCorrelationId: (correlationId: string) => GateRecord | undefined;
25
+ snapshot: () => GateSnapshot[];
26
+ has: (gateId: string) => boolean;
27
+ };
28
+ export type GateManager = ReturnType<typeof createGateManager>;
package/dist/gates.js ADDED
@@ -0,0 +1,103 @@
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
+ // Clear a gate without invoking its onCleared callback. The caller has
54
+ // already decided how the reactor resumes and does not want the standard
55
+ // cleared-event enqueue that onCleared drives. Used by the approval
56
+ // re-dispatch path, which resumes by re-running the parked tool call rather
57
+ // than by re-inferring off a gate-cleared event: firing onCleared there
58
+ // would enqueue a second, spurious continuation.
59
+ function clearSilently(gateId) {
60
+ const gate = gates.get(gateId);
61
+ if (gate === undefined)
62
+ return false;
63
+ clearTimeout(gate.timer);
64
+ gates.delete(gateId);
65
+ gate.resolve("resolved");
66
+ return true;
67
+ }
68
+ function shutdown() {
69
+ const entries = Array.from(gates.values());
70
+ gates.clear();
71
+ for (const gate of entries) {
72
+ clearTimeout(gate.timer);
73
+ gate.resolve("shutdown");
74
+ gate.onCleared(gate.gateId, "shutdown");
75
+ }
76
+ }
77
+ function findByCorrelationId(correlationId) {
78
+ for (const gate of gates.values()) {
79
+ if (gate.correlationId === correlationId)
80
+ return gate;
81
+ }
82
+ return undefined;
83
+ }
84
+ function snapshot() {
85
+ return Array.from(gates.values()).map((g) => ({
86
+ gateId: g.gateId,
87
+ type: g.type,
88
+ timeoutAt: g.timeoutAt,
89
+ }));
90
+ }
91
+ function has(gateId) {
92
+ return gates.has(gateId);
93
+ }
94
+ return {
95
+ register,
96
+ clear,
97
+ clearSilently,
98
+ shutdown,
99
+ findByCorrelationId,
100
+ snapshot,
101
+ has,
102
+ };
103
+ }