@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,80 @@
|
|
|
1
|
+
// Tool-result size-cap transform.
|
|
2
|
+
//
|
|
3
|
+
// When a tool result's content exceeds `maxChars`, the full bytes are spilled
|
|
4
|
+
// via `contextStore.writeBlob` and the inline result is replaced with the
|
|
5
|
+
// first `maxChars` characters plus a marker pointing at the spill via a
|
|
6
|
+
// `tool-output:///{callId}` URI. The agent's `read_file` tool resolves the
|
|
7
|
+
// URI through its `BlobReader` capability.
|
|
8
|
+
//
|
|
9
|
+
// Within-cap results pass through unchanged (no blob is written) but still
|
|
10
|
+
// produce a `TransformRecord` so the manifest captures every invocation.
|
|
11
|
+
const SIZE_CAP_VERSION = "1";
|
|
12
|
+
const SIZE_CAP_NAME = "size-cap";
|
|
13
|
+
/**
|
|
14
|
+
* Create a `ToolResultTransform` that caps inline tool result content at
|
|
15
|
+
* `maxChars` characters. Oversized results are spilled to the context store
|
|
16
|
+
* via `writeBlob` and the inline content becomes a truncated marker
|
|
17
|
+
* referencing the spill by `tool-output:///{callId}` URI.
|
|
18
|
+
*/
|
|
19
|
+
export function createSizeCapTransform(options) {
|
|
20
|
+
const { maxChars, contextStore } = options;
|
|
21
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0) {
|
|
22
|
+
throw new Error(`createSizeCapTransform: maxChars must be a positive finite number, got ${String(maxChars)}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
name: SIZE_CAP_NAME,
|
|
26
|
+
version: SIZE_CAP_VERSION,
|
|
27
|
+
async apply(input, _ctx) {
|
|
28
|
+
const { call, result } = input;
|
|
29
|
+
const text = typeof result.content === "string"
|
|
30
|
+
? result.content
|
|
31
|
+
: JSON.stringify(result.content);
|
|
32
|
+
if (text.length <= maxChars) {
|
|
33
|
+
return {
|
|
34
|
+
output: result,
|
|
35
|
+
record: {
|
|
36
|
+
strategy: SIZE_CAP_NAME,
|
|
37
|
+
version: SIZE_CAP_VERSION,
|
|
38
|
+
parameters: { maxChars },
|
|
39
|
+
reason: "within-cap",
|
|
40
|
+
decisions: { callId: call.id, length: text.length },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const omitted = text.length - maxChars;
|
|
45
|
+
const kept = text.slice(0, maxChars);
|
|
46
|
+
const spillURI = `tool-output:///${call.id}`;
|
|
47
|
+
const marker = `${kept}\n[Tool output truncated: omitted ${String(omitted)} chars. ` +
|
|
48
|
+
`Full output available at ${spillURI} -- use read_file with that URI to see the rest.]`;
|
|
49
|
+
const bytes = new TextEncoder().encode(text);
|
|
50
|
+
await contextStore.writeBlob(call.id, bytes, "text/plain");
|
|
51
|
+
const output = {
|
|
52
|
+
...result,
|
|
53
|
+
content: marker,
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
output,
|
|
57
|
+
record: {
|
|
58
|
+
strategy: SIZE_CAP_NAME,
|
|
59
|
+
version: SIZE_CAP_VERSION,
|
|
60
|
+
parameters: { maxChars },
|
|
61
|
+
reason: "exceeded-cap",
|
|
62
|
+
decisions: {
|
|
63
|
+
callId: call.id,
|
|
64
|
+
originalLength: text.length,
|
|
65
|
+
kept: maxChars,
|
|
66
|
+
spillKey: call.id,
|
|
67
|
+
spillURI,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
blobs: [
|
|
71
|
+
{
|
|
72
|
+
key: call.id,
|
|
73
|
+
bytes,
|
|
74
|
+
contentType: "text/plain",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/turns.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConversationTurn, ContentBlock, AssistantTurn, InboundMessage, ToolCall, ToolResult } from "@intx/types/runtime";
|
|
2
|
+
export type { ConversationTurn, ContentBlock, AssistantTurn };
|
|
3
|
+
export type { ToolCall, ToolResult };
|
|
4
|
+
export declare function createInboundTurn(message: InboundMessage): ConversationTurn | null;
|
|
5
|
+
/**
|
|
6
|
+
* Assert that a prompt's tool_call / tool_result blocks are structurally
|
|
7
|
+
* well-formed before it is sent to a provider.
|
|
8
|
+
*
|
|
9
|
+
* Throws if a tool_call id is emitted twice, if a tool_result references a
|
|
10
|
+
* callId with no preceding tool_call, or if two tool_result blocks answer the
|
|
11
|
+
* same callId. None of these are valid in a coherent tool conversation — a
|
|
12
|
+
* tool call has exactly one result. Catching it here surfaces the corruption
|
|
13
|
+
* as an internal error at the assembly boundary, with the offending id and
|
|
14
|
+
* turn index, instead of an opaque downstream provider rejection.
|
|
15
|
+
*
|
|
16
|
+
* This deliberately does NOT require every tool_call to have a result: an
|
|
17
|
+
* unanswered tool_call is left legitimately by an after-inference halt/abort
|
|
18
|
+
* and is repaired downstream for cross-provider replay.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertWellFormedToolSequence(turns: ConversationTurn[]): void;
|
|
21
|
+
export declare function createToolResultTurn(results: ToolResult[]): ConversationTurn;
|
package/dist/turns.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { attachmentCategory, base64Encode } from "@intx/types";
|
|
2
|
+
import { getLogger } from "@intx/log";
|
|
3
|
+
const logger = getLogger(["interchange", "inference", "turns"]);
|
|
4
|
+
/**
|
|
5
|
+
* Map a received attachment to a model ContentBlock.
|
|
6
|
+
*
|
|
7
|
+
* The dispatch uses two policies that look unified but are not, and must
|
|
8
|
+
* stay distinct:
|
|
9
|
+
* - major-type dispatch for image/*, video/*, audio/* → the matching block;
|
|
10
|
+
* - allowlist-category dispatch for the document category
|
|
11
|
+
* (application/pdf, application/json, text/plain, text/csv,
|
|
12
|
+
* text/markdown) → DocumentBlock.
|
|
13
|
+
* Do NOT collapse this into "always dispatch by major type": that would
|
|
14
|
+
* route text/plain to a text block instead of DocumentBlock.
|
|
15
|
+
*
|
|
16
|
+
* This function is total — it never throws. The hub route allowlist only
|
|
17
|
+
* guards the local user-upload path; inbound attachments arrive from remote
|
|
18
|
+
* senders via fetchFull and are not subtype-filtered here (provider adapters
|
|
19
|
+
* are the contract layer that rejects unsupported media at marshal time, per
|
|
20
|
+
* INTERCHANGE message design). Any major-type media is therefore passed
|
|
21
|
+
* through to the adapter. A type that maps to no block at all (e.g. an
|
|
22
|
+
* archive) degrades to a visible text marker rather than throwing: throwing
|
|
23
|
+
* here would propagate into the reactor's ungoverned delivery path and let a
|
|
24
|
+
* single malformed remote attachment tear down the session.
|
|
25
|
+
*/
|
|
26
|
+
function attachmentToContentBlock(att) {
|
|
27
|
+
const majorType = att.contentType.split("/")[0];
|
|
28
|
+
if (majorType === "image" ||
|
|
29
|
+
majorType === "video" ||
|
|
30
|
+
majorType === "audio" ||
|
|
31
|
+
attachmentCategory(att.contentType) === "document") {
|
|
32
|
+
const source = {
|
|
33
|
+
kind: "base64",
|
|
34
|
+
mimeType: att.contentType,
|
|
35
|
+
data: base64Encode(att.data),
|
|
36
|
+
};
|
|
37
|
+
if (majorType === "image")
|
|
38
|
+
return { type: "image", source };
|
|
39
|
+
if (majorType === "video")
|
|
40
|
+
return { type: "video", source };
|
|
41
|
+
if (majorType === "audio")
|
|
42
|
+
return { type: "audio", source };
|
|
43
|
+
return { type: "document", source };
|
|
44
|
+
}
|
|
45
|
+
logger.warn `Unsupported attachment content type ${att.contentType}; surfacing as a text marker`;
|
|
46
|
+
return {
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `[Unsupported attachment: ${att.name} (${att.contentType})]`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createInboundTurn(message) {
|
|
52
|
+
const content = message.content ?? "";
|
|
53
|
+
const attachments = message.attachments ?? [];
|
|
54
|
+
if (content.length === 0 && attachments.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const blocks = [];
|
|
57
|
+
if (content.length > 0) {
|
|
58
|
+
const { from, subject } = message.headers;
|
|
59
|
+
const envelope = [];
|
|
60
|
+
if (from.length > 0)
|
|
61
|
+
envelope.push(`[From: ${from}]`);
|
|
62
|
+
if (subject !== undefined && subject.length > 0) {
|
|
63
|
+
envelope.push(`[Subject: ${subject}]`);
|
|
64
|
+
}
|
|
65
|
+
const text = envelope.length > 0 ? `${envelope.join("\n")}\n\n${content}` : content;
|
|
66
|
+
blocks.push({ type: "text", text });
|
|
67
|
+
}
|
|
68
|
+
for (const att of attachments) {
|
|
69
|
+
blocks.push(attachmentToContentBlock(att));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
role: "user",
|
|
73
|
+
content: blocks,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Assert that a prompt's tool_call / tool_result blocks are structurally
|
|
79
|
+
* well-formed before it is sent to a provider.
|
|
80
|
+
*
|
|
81
|
+
* Throws if a tool_call id is emitted twice, if a tool_result references a
|
|
82
|
+
* callId with no preceding tool_call, or if two tool_result blocks answer the
|
|
83
|
+
* same callId. None of these are valid in a coherent tool conversation — a
|
|
84
|
+
* tool call has exactly one result. Catching it here surfaces the corruption
|
|
85
|
+
* as an internal error at the assembly boundary, with the offending id and
|
|
86
|
+
* turn index, instead of an opaque downstream provider rejection.
|
|
87
|
+
*
|
|
88
|
+
* This deliberately does NOT require every tool_call to have a result: an
|
|
89
|
+
* unanswered tool_call is left legitimately by an after-inference halt/abort
|
|
90
|
+
* and is repaired downstream for cross-provider replay.
|
|
91
|
+
*/
|
|
92
|
+
export function assertWellFormedToolSequence(turns) {
|
|
93
|
+
const calledIds = new Set();
|
|
94
|
+
const answeredIds = new Set();
|
|
95
|
+
for (let turnIndex = 0; turnIndex < turns.length; turnIndex++) {
|
|
96
|
+
const turn = turns[turnIndex];
|
|
97
|
+
if (turn === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
for (const block of turn.content) {
|
|
100
|
+
if (block.type === "tool_call") {
|
|
101
|
+
if (calledIds.has(block.id)) {
|
|
102
|
+
throw new Error(`Malformed tool sequence: duplicate tool_call id ${JSON.stringify(block.id)} at turn ${String(turnIndex)}`);
|
|
103
|
+
}
|
|
104
|
+
calledIds.add(block.id);
|
|
105
|
+
}
|
|
106
|
+
else if (block.type === "tool_result") {
|
|
107
|
+
if (!calledIds.has(block.callId)) {
|
|
108
|
+
throw new Error(`Malformed tool sequence: tool_result for ${JSON.stringify(block.callId)} at turn ${String(turnIndex)} has no preceding tool_call`);
|
|
109
|
+
}
|
|
110
|
+
if (answeredIds.has(block.callId)) {
|
|
111
|
+
throw new Error(`Malformed tool sequence: duplicate tool_result for ${JSON.stringify(block.callId)} at turn ${String(turnIndex)}`);
|
|
112
|
+
}
|
|
113
|
+
answeredIds.add(block.callId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function createToolResultTurn(results) {
|
|
119
|
+
const blocks = results.map((r) => {
|
|
120
|
+
const raw = typeof r.content === "string" ? r.content : JSON.stringify(r.content);
|
|
121
|
+
const block = {
|
|
122
|
+
type: "tool_result",
|
|
123
|
+
callId: r.callId,
|
|
124
|
+
content: [{ type: "text", text: raw }],
|
|
125
|
+
};
|
|
126
|
+
if (r.detail !== undefined) {
|
|
127
|
+
block.detail = r.detail;
|
|
128
|
+
}
|
|
129
|
+
if (r.isError !== undefined) {
|
|
130
|
+
block.isError = r.isError;
|
|
131
|
+
}
|
|
132
|
+
return block;
|
|
133
|
+
});
|
|
134
|
+
return { role: "user", content: blocks, timestamp: Date.now() };
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/inference",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "LGPL-2.1-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"intx-src": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./providers": {
|
|
13
|
+
"intx-src": "./src/providers/index.ts",
|
|
14
|
+
"types": "./dist/providers/index.d.ts",
|
|
15
|
+
"default": "./dist/providers/index.js"
|
|
10
16
|
}
|
|
11
17
|
},
|
|
12
18
|
"dependencies": {
|
|
13
|
-
"@intx/log": "0.
|
|
14
|
-
"@intx/types": "0.
|
|
19
|
+
"@intx/log": "0.2.2",
|
|
20
|
+
"@intx/types": "0.2.2",
|
|
15
21
|
"arktype": "^2.1.29"
|
|
16
22
|
},
|
|
17
23
|
"devDependencies": {
|
|
18
|
-
"@intx/mime": "0.
|
|
24
|
+
"@intx/mime": "0.2.2"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
19
34
|
}
|
|
20
35
|
}
|
package/src/actions.ts
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
// Action types and validation for the agent reactor.
|
|
2
|
-
//
|
|
3
|
-
// The reactor validates the set of actions returned by the director before
|
|
4
|
-
// executing any of them. Invalid combinations produce a reactor.error rather
|
|
5
|
-
// than partial execution — the reactor does not guess intent.
|
|
6
|
-
//
|
|
7
|
-
// Validation rules (INFERENCE.md § Action Validation):
|
|
8
|
-
// - At most one `infer` action.
|
|
9
|
-
// - At most one `done` action.
|
|
10
|
-
// - At most one `reply` action.
|
|
11
|
-
// - `infer` + `done` together is invalid.
|
|
12
|
-
// - `reply` + `infer` together is invalid.
|
|
13
|
-
// - `reply` + `execute_tools` together is invalid.
|
|
14
|
-
// - `reply` + `done` together is invalid.
|
|
15
|
-
// - `reply` + `suspend` together is invalid.
|
|
16
|
-
// - `wait` + `infer` together is invalid.
|
|
17
|
-
// - `wait` + `execute_tools` together is invalid.
|
|
18
|
-
// - `wait` + `suspend` together is invalid.
|
|
19
|
-
// - `wait` + `reply` together is invalid.
|
|
20
|
-
// - `wait` + `done` together is invalid.
|
|
21
|
-
// - `suspend` cannot appear alongside `infer` or `execute_tools`.
|
|
22
|
-
// - `fork` is composable — may appear alongside any other action.
|
|
23
|
-
// - At most one `checkpoint` action; composable with any other action.
|
|
24
|
-
// - At most one `wait` action.
|
|
25
|
-
// - Multiple `execute_tools` are merged into a single parallel batch.
|
|
26
|
-
// - `emit` is always valid and composable.
|
|
27
|
-
// - At most one `compact` action; composable with `checkpoint`, `emit`, and
|
|
28
|
-
// `fork`. Not composable with `infer`, `execute_tools`, `reply`, `suspend`,
|
|
29
|
-
// `wait`, or `done`. Context-overflow recovery runs compaction in its own
|
|
30
|
-
// cycle and re-infers on the next director invocation.
|
|
31
|
-
|
|
32
|
-
import type { ReactorAction, ToolCall } from "@intx/types/runtime";
|
|
33
|
-
|
|
34
|
-
export type ValidationResult =
|
|
35
|
-
| { ok: true; normalized: ReactorAction[] }
|
|
36
|
-
| { ok: false; error: string };
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validate and normalize a set of actions returned by the director.
|
|
40
|
-
*
|
|
41
|
-
* On success, returns the normalized action list with multiple `execute_tools`
|
|
42
|
-
* collapsed into a single batched action. On failure, returns a diagnostic
|
|
43
|
-
* error string.
|
|
44
|
-
*/
|
|
45
|
-
export function validateActions(
|
|
46
|
-
actions: ReactorAction | ReactorAction[],
|
|
47
|
-
): ValidationResult {
|
|
48
|
-
const list = Array.isArray(actions) ? actions : [actions];
|
|
49
|
-
|
|
50
|
-
// An empty action list means "no-op, keep waiting for the next event."
|
|
51
|
-
// This is valid — the reactor loop continues to waitForEvent().
|
|
52
|
-
if (list.length === 0) {
|
|
53
|
-
return { ok: true, normalized: [] };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const inferActions = list.filter((a) => a.type === "infer");
|
|
57
|
-
const doneActions = list.filter((a) => a.type === "done");
|
|
58
|
-
const replyActions = list.filter((a) => a.type === "reply");
|
|
59
|
-
const suspendActions = list.filter((a) => a.type === "suspend");
|
|
60
|
-
const executeActions = list.filter(
|
|
61
|
-
(a): a is Extract<ReactorAction, { type: "execute_tools" }> =>
|
|
62
|
-
a.type === "execute_tools",
|
|
63
|
-
);
|
|
64
|
-
const waitActions = list.filter((a) => a.type === "wait");
|
|
65
|
-
const forkActions = list.filter((a) => a.type === "fork");
|
|
66
|
-
const emitActions = list.filter((a) => a.type === "emit");
|
|
67
|
-
const checkpointActions = list.filter((a) => a.type === "checkpoint");
|
|
68
|
-
const compactActions = list.filter((a) => a.type === "compact");
|
|
69
|
-
|
|
70
|
-
if (checkpointActions.length > 1) {
|
|
71
|
-
return {
|
|
72
|
-
ok: false,
|
|
73
|
-
error: "Multiple checkpoint actions are not allowed",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (inferActions.length > 1) {
|
|
78
|
-
return { ok: false, error: "Multiple infer actions are not allowed" };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (doneActions.length > 1) {
|
|
82
|
-
return { ok: false, error: "Multiple done actions are not allowed" };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (inferActions.length > 0 && doneActions.length > 0) {
|
|
86
|
-
return { ok: false, error: "infer and done cannot appear together" };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (replyActions.length > 1) {
|
|
90
|
-
return { ok: false, error: "Multiple reply actions are not allowed" };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (replyActions.length > 0 && inferActions.length > 0) {
|
|
94
|
-
return { ok: false, error: "reply and infer cannot appear together" };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (replyActions.length > 0 && executeActions.length > 0) {
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
error: "reply and execute_tools cannot appear together",
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (replyActions.length > 0 && doneActions.length > 0) {
|
|
105
|
-
return { ok: false, error: "reply and done cannot appear together" };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (replyActions.length > 0 && suspendActions.length > 0) {
|
|
109
|
-
return { ok: false, error: "reply and suspend cannot appear together" };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (suspendActions.length > 0) {
|
|
113
|
-
if (inferActions.length > 0) {
|
|
114
|
-
return {
|
|
115
|
-
ok: false,
|
|
116
|
-
error: "suspend cannot appear alongside infer",
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (executeActions.length > 0) {
|
|
120
|
-
return {
|
|
121
|
-
ok: false,
|
|
122
|
-
error: "suspend cannot appear alongside execute_tools",
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
if (suspendActions.length > 1) {
|
|
126
|
-
return { ok: false, error: "Multiple suspend actions are not allowed" };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (waitActions.length > 1) {
|
|
131
|
-
return { ok: false, error: "Multiple wait actions are not allowed" };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (waitActions.length > 0) {
|
|
135
|
-
if (inferActions.length > 0) {
|
|
136
|
-
return { ok: false, error: "wait and infer cannot appear together" };
|
|
137
|
-
}
|
|
138
|
-
if (executeActions.length > 0) {
|
|
139
|
-
return {
|
|
140
|
-
ok: false,
|
|
141
|
-
error: "wait and execute_tools cannot appear together",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (suspendActions.length > 0) {
|
|
145
|
-
return { ok: false, error: "wait and suspend cannot appear together" };
|
|
146
|
-
}
|
|
147
|
-
if (replyActions.length > 0) {
|
|
148
|
-
return { ok: false, error: "wait and reply cannot appear together" };
|
|
149
|
-
}
|
|
150
|
-
if (doneActions.length > 0) {
|
|
151
|
-
return { ok: false, error: "wait and done cannot appear together" };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (compactActions.length > 1) {
|
|
156
|
-
return { ok: false, error: "Multiple compact actions are not allowed" };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (compactActions.length > 0) {
|
|
160
|
-
if (inferActions.length > 0) {
|
|
161
|
-
return { ok: false, error: "compact and infer cannot appear together" };
|
|
162
|
-
}
|
|
163
|
-
if (executeActions.length > 0) {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
error: "compact and execute_tools cannot appear together",
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
if (replyActions.length > 0) {
|
|
170
|
-
return { ok: false, error: "compact and reply cannot appear together" };
|
|
171
|
-
}
|
|
172
|
-
if (suspendActions.length > 0) {
|
|
173
|
-
return { ok: false, error: "compact and suspend cannot appear together" };
|
|
174
|
-
}
|
|
175
|
-
if (waitActions.length > 0) {
|
|
176
|
-
return { ok: false, error: "compact and wait cannot appear together" };
|
|
177
|
-
}
|
|
178
|
-
if (doneActions.length > 0) {
|
|
179
|
-
return { ok: false, error: "compact and done cannot appear together" };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Verify fork actions have unique IDs.
|
|
184
|
-
const forkIds = forkActions.map(
|
|
185
|
-
(a) => (a as Extract<ReactorAction, { type: "fork" }>).forkId,
|
|
186
|
-
);
|
|
187
|
-
const uniqueForkIds = new Set(forkIds);
|
|
188
|
-
if (forkIds.length !== uniqueForkIds.size) {
|
|
189
|
-
return { ok: false, error: "Duplicate fork IDs in action list" };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Build normalized list: collapse execute_tools into one parallel batch.
|
|
193
|
-
const normalized: ReactorAction[] = [];
|
|
194
|
-
|
|
195
|
-
for (const a of checkpointActions) {
|
|
196
|
-
normalized.push(a);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const a of emitActions) {
|
|
200
|
-
normalized.push(a);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
for (const a of forkActions) {
|
|
204
|
-
normalized.push(a);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
for (const a of compactActions) {
|
|
208
|
-
normalized.push(a);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (executeActions.length > 0) {
|
|
212
|
-
const merged: ToolCall[] = executeActions.flatMap((a) => a.calls);
|
|
213
|
-
const allAddToHistory = executeActions.every(
|
|
214
|
-
(a) => a.addToHistory !== false,
|
|
215
|
-
);
|
|
216
|
-
normalized.push({
|
|
217
|
-
type: "execute_tools",
|
|
218
|
-
calls: merged,
|
|
219
|
-
parallel: true,
|
|
220
|
-
...(!allAddToHistory ? { addToHistory: false } : {}),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (const a of replyActions) {
|
|
225
|
-
normalized.push(a);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const a of inferActions) {
|
|
229
|
-
normalized.push(a);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
for (const a of suspendActions) {
|
|
233
|
-
normalized.push(a);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
for (const a of waitActions) {
|
|
237
|
-
normalized.push(a);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const a of doneActions) {
|
|
241
|
-
normalized.push(a);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return { ok: true, normalized };
|
|
245
|
-
}
|
package/src/adapter.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConversationTurn,
|
|
3
|
-
InferenceEvent,
|
|
4
|
-
InferenceOptions,
|
|
5
|
-
} from "@intx/types/runtime";
|
|
6
|
-
|
|
7
|
-
// The request shape the harness passes to fetch.
|
|
8
|
-
export type BuiltRequest = {
|
|
9
|
-
url: string;
|
|
10
|
-
headers: Record<string, string>;
|
|
11
|
-
body: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// A request builder takes the internal message format and produces a
|
|
15
|
-
// provider-specific HTTP request. Pure function — no state, no side effects.
|
|
16
|
-
export type RequestBuilder = (
|
|
17
|
-
messages: ConversationTurn[],
|
|
18
|
-
model: string,
|
|
19
|
-
options: InferenceOptions,
|
|
20
|
-
) => BuiltRequest;
|
|
21
|
-
|
|
22
|
-
// A response parser converts one SSE data payload string into zero or more
|
|
23
|
-
// internal inference events. May close over per-request state (e.g., for
|
|
24
|
-
// correlating content block indices with tool call IDs). Each adapter
|
|
25
|
-
// instance is created per inference call, so state does not leak across
|
|
26
|
-
// requests.
|
|
27
|
-
//
|
|
28
|
-
// The parser may return an empty array for events it doesn't care about
|
|
29
|
-
// (e.g., Anthropic's ping events). It MAY throw `ProtocolMismatchError`
|
|
30
|
-
// when the upstream chunk violates the provider's protocol (malformed
|
|
31
|
-
// JSON, schema validation failure, out-of-order events); the harness's
|
|
32
|
-
// stream-error catch converts that into an `inference.error` with
|
|
33
|
-
// category `"protocol_mismatch"` via `classifyStreamError`. No other
|
|
34
|
-
// throw type is permitted, and adapter-returned `inference.error` or
|
|
35
|
-
// `inference.done` events are silently dropped — the harness owns
|
|
36
|
-
// emission of those terminal types and the only path through which
|
|
37
|
-
// adapter-detected failures can surface is `ProtocolMismatchError`.
|
|
38
|
-
export type ResponseParser = (sseData: string) => InferenceEvent[];
|
|
39
|
-
|
|
40
|
-
// An adapter pairs a request builder with a response parser. Registration
|
|
41
|
-
// is a map keyed by provider identifier — no class hierarchy required.
|
|
42
|
-
|
|
43
|
-
// Extracts a retry delay from provider-specific response headers on a 429.
|
|
44
|
-
// Returns milliseconds to wait, or undefined if no retry info is available.
|
|
45
|
-
export type RetryAfterExtractor = (headers: Headers) => number | undefined;
|
|
46
|
-
|
|
47
|
-
// Extracts a pacing delay from response headers on ANY response (including
|
|
48
|
-
// success). Checks remaining rate limit capacity and returns how long to
|
|
49
|
-
// wait before the next request, or undefined if no pacing is needed.
|
|
50
|
-
export type PacingExtractor = (headers: Headers) => number | undefined;
|
|
51
|
-
|
|
52
|
-
export type ProviderAdapter = {
|
|
53
|
-
buildRequest: RequestBuilder;
|
|
54
|
-
parseResponse: ResponseParser;
|
|
55
|
-
extractRetryAfterMs?: RetryAfterExtractor;
|
|
56
|
-
extractPacingDelayMs?: PacingExtractor;
|
|
57
|
-
};
|