@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.
- package/LICENSE +176 -0
- package/dist/actions.d.ts +16 -0
- package/dist/actions.js +200 -0
- package/dist/adapter.d.ts +40 -0
- package/dist/adapter.js +31 -0
- package/dist/assembly.d.ts +75 -0
- package/dist/assembly.js +133 -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 +46 -0
- package/dist/authz-extension.js +184 -0
- package/dist/correlation.d.ts +26 -0
- package/dist/correlation.js +39 -0
- package/dist/default-director.d.ts +111 -0
- package/dist/default-director.js +228 -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 +28 -0
- package/dist/gates.js +103 -0
- package/dist/harness.d.ts +147 -0
- package/dist/harness.js +1407 -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 +37 -0
- package/dist/providers/anthropic.js +917 -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 +5 -0
- package/dist/providers/google-genai.js +1205 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.js +56 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.js +903 -0
- package/dist/reactor.d.ts +50 -0
- package/dist/reactor.js +1233 -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 +132 -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 +22 -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,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;
|
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,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
|
+
}
|