@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.
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 +38 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +68 -0
  7. package/dist/assembly.js +132 -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 +32 -0
  13. package/dist/authz-extension.js +100 -0
  14. package/dist/correlation.d.ts +25 -0
  15. package/dist/correlation.js +32 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +199 -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 +27 -0
  23. package/dist/gates.js +80 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1319 -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 +33 -0
  31. package/dist/providers/anthropic.js +670 -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 +3 -0
  35. package/dist/providers/google-genai.js +1196 -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 +3 -0
  39. package/dist/providers/openai.js +609 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +920 -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 +117 -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 +21 -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
package/src/transform.ts DELETED
@@ -1,159 +0,0 @@
1
- // Cross-provider message transformation.
2
- //
3
- // When conversations cross provider boundaries the message history must be
4
- // adapted: thinking blocks are stripped for foreign models, orphaned tool
5
- // calls receive synthetic error results, and tool call IDs are normalized to
6
- // a portable format.
7
- //
8
- // Transformation runs automatically when the target model differs from a
9
- // message's originating model. The originating model is tracked per-message,
10
- // not per-conversation.
11
-
12
- import type { ConversationTurn, ContentBlock } from "@intx/types/runtime";
13
-
14
- export type TransformOptions = {
15
- targetModel: string;
16
- // When true, keep thinking blocks for messages that originated from the
17
- // same model. When false, strip all thinking blocks (cross-provider replay).
18
- keepThinkingForSameModel?: boolean;
19
- };
20
-
21
- export function transformMessages(
22
- messages: ConversationTurn[],
23
- options: TransformOptions,
24
- ): ConversationTurn[] {
25
- const { targetModel, keepThinkingForSameModel = true } = options;
26
-
27
- // First pass: strip thinking blocks and filter aborted assistant messages.
28
- const filtered = messages
29
- .map((msg): ConversationTurn | null => {
30
- if (msg.role === "assistant") {
31
- const isSameModel = msg.model === targetModel;
32
- const keepThinking = keepThinkingForSameModel && isSameModel;
33
-
34
- const filteredContent = msg.content.filter((block) => {
35
- if (block.type === "thinking") {
36
- return keepThinking;
37
- }
38
- return true;
39
- });
40
-
41
- // Filter out assistant messages that have no text or tool calls
42
- // (aborted/error messages with only thinking blocks removed).
43
- const hasUsableContent = filteredContent.some(
44
- (b) => b.type === "text" || b.type === "tool_call",
45
- );
46
- if (!hasUsableContent && filteredContent.length === 0) {
47
- return null;
48
- }
49
-
50
- return { ...msg, content: filteredContent };
51
- }
52
- return msg;
53
- })
54
- .filter((msg): msg is ConversationTurn => msg !== null);
55
-
56
- // Second pass: inject synthetic tool results for orphaned tool calls.
57
- return injectOrphanedToolResults(filtered);
58
- }
59
-
60
- function injectOrphanedToolResults(
61
- messages: ConversationTurn[],
62
- ): ConversationTurn[] {
63
- const result: ConversationTurn[] = [];
64
-
65
- for (let i = 0; i < messages.length; i++) {
66
- const msg = messages[i];
67
- if (msg === undefined) continue;
68
- result.push(msg);
69
-
70
- if (msg.role !== "assistant") continue;
71
-
72
- const toolCalls = msg.content.filter(
73
- (b): b is Extract<ContentBlock, { type: "tool_call" }> =>
74
- b.type === "tool_call",
75
- );
76
-
77
- if (toolCalls.length === 0) continue;
78
-
79
- // Collect tool call IDs from this assistant message.
80
- const calledIds = new Set(toolCalls.map((tc) => tc.id));
81
-
82
- // Check the following messages for results that cover these calls.
83
- const coveredIds = new Set<string>();
84
- for (let j = i + 1; j < messages.length; j++) {
85
- const next = messages[j];
86
- if (next === undefined) break;
87
- if (next.role !== "user") break;
88
-
89
- for (const block of next.content) {
90
- if (block.type === "tool_result") {
91
- coveredIds.add(block.callId);
92
- }
93
- }
94
- }
95
-
96
- // Find which tool calls have no corresponding result.
97
- const orphanedIds = [...calledIds].filter((id) => !coveredIds.has(id));
98
-
99
- if (orphanedIds.length === 0) continue;
100
-
101
- // Inject a synthetic user message with error tool results for each orphan.
102
- const syntheticBlocks: ContentBlock[] = orphanedIds.map((id) => ({
103
- type: "tool_result" as const,
104
- callId: id,
105
- content: [
106
- {
107
- type: "text" as const,
108
- text: "Tool execution was interrupted before completion.",
109
- },
110
- ],
111
- isError: true,
112
- }));
113
-
114
- result.push({
115
- role: "user",
116
- content: syntheticBlocks,
117
- timestamp: Date.now(),
118
- });
119
- }
120
-
121
- return result;
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // Tool call ID normalization
126
- //
127
- // OpenAI Responses API generates 450+ character IDs with pipes. Anthropic
128
- // has strict format requirements. IDs are normalized to a short portable
129
- // format with a bidirectional map for round-trip fidelity.
130
- // ---------------------------------------------------------------------------
131
-
132
- const PORTABLE_ID_PREFIX = "tc_";
133
-
134
- export type IDNormalizer = {
135
- normalize(providerId: string): string;
136
- resolve(portableId: string): string | undefined;
137
- };
138
-
139
- export function createIDNormalizer(): IDNormalizer {
140
- const portableToProvider = new Map<string, string>();
141
- const providerToPortable = new Map<string, string>();
142
- let counter = 0;
143
-
144
- return {
145
- normalize(providerId: string): string {
146
- const existing = providerToPortable.get(providerId);
147
- if (existing !== undefined) return existing;
148
-
149
- const portable = `${PORTABLE_ID_PREFIX}${(++counter).toString(36)}`;
150
- providerToPortable.set(providerId, portable);
151
- portableToProvider.set(portable, providerId);
152
- return portable;
153
- },
154
-
155
- resolve(portableId: string): string | undefined {
156
- return portableToProvider.get(portableId);
157
- },
158
- };
159
- }
@@ -1,2 +0,0 @@
1
- export { createSizeCapTransform } from "./size-cap";
2
- export type { SizeCapTransformOptions } from "./size-cap";
@@ -1,172 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import { createSizeCapTransform } from "./size-cap";
4
- import type {
5
- ContextStore,
6
- ReactorState,
7
- StrategyContext,
8
- ToolCall,
9
- ToolResult,
10
- } from "@intx/types/runtime";
11
-
12
- function emptyState(): ReactorState {
13
- return {
14
- turns: [],
15
- activeForks: [],
16
- pendingOperations: [],
17
- activeGates: [],
18
- tokenUsage: {
19
- input: 0,
20
- output: 0,
21
- cacheRead: 0,
22
- cacheWrite: 0,
23
- thinking: 0,
24
- },
25
- lastCycleUsage: null,
26
- lastCycleSource: null,
27
- sessionId: "test-session",
28
- };
29
- }
30
-
31
- function emptyContext(): StrategyContext {
32
- return { state: emptyState(), trigger: "test" };
33
- }
34
-
35
- function recordingWriteBlob(): {
36
- store: Pick<ContextStore, "writeBlob">;
37
- calls: { key: string; bytes: Uint8Array; contentType?: string }[];
38
- } {
39
- const calls: { key: string; bytes: Uint8Array; contentType?: string }[] = [];
40
- const store: Pick<ContextStore, "writeBlob"> = {
41
- async writeBlob(key, bytes, contentType) {
42
- calls.push({
43
- key,
44
- bytes,
45
- ...(contentType !== undefined ? { contentType } : {}),
46
- });
47
- },
48
- };
49
- return { store, calls };
50
- }
51
-
52
- function call(id: string, name = "tool"): ToolCall {
53
- return { id, name, arguments: {} };
54
- }
55
-
56
- describe("createSizeCapTransform", () => {
57
- test("passes a within-cap result through unchanged with a within-cap record", async () => {
58
- const { store, calls } = recordingWriteBlob();
59
- const transform = createSizeCapTransform({
60
- maxChars: 100,
61
- contextStore: store,
62
- });
63
-
64
- const result: ToolResult = { callId: "c1", content: "hello world" };
65
- const out = await transform.apply(
66
- { call: call("c1"), result },
67
- emptyContext(),
68
- );
69
-
70
- expect(out.output).toBe(result);
71
- expect(out.record.strategy).toBe("size-cap");
72
- expect(out.record.reason).toBe("within-cap");
73
- expect(out.record.decisions["callId"]).toBe("c1");
74
- expect(out.record.decisions["length"]).toBe(11);
75
- expect(out.blobs).toBeUndefined();
76
- expect(calls).toHaveLength(0);
77
- });
78
-
79
- test("oversize result writes a blob and produces a truncated marker", async () => {
80
- const { store, calls } = recordingWriteBlob();
81
- const transform = createSizeCapTransform({
82
- maxChars: 10,
83
- contextStore: store,
84
- });
85
-
86
- const full = "x".repeat(50);
87
- const result: ToolResult = { callId: "callABC", content: full };
88
- const out = await transform.apply(
89
- { call: call("callABC"), result },
90
- emptyContext(),
91
- );
92
-
93
- expect(out.output).not.toBe(result);
94
- const content = out.output.content;
95
- if (typeof content !== "string") throw new Error("expected string content");
96
- expect(content.startsWith("xxxxxxxxxx\n")).toBe(true);
97
- expect(content).toContain("tool-output:///callABC");
98
- expect(content).toContain("omitted 40 chars");
99
- expect(out.record.strategy).toBe("size-cap");
100
- expect(out.record.reason).toBe("exceeded-cap");
101
- expect(out.record.decisions["callId"]).toBe("callABC");
102
- expect(out.record.decisions["originalLength"]).toBe(50);
103
- expect(out.record.decisions["kept"]).toBe(10);
104
- expect(out.record.decisions["spillKey"]).toBe("callABC");
105
- expect(out.record.decisions["spillURI"]).toBe("tool-output:///callABC");
106
-
107
- // Blob is emitted for the reactor to persist.
108
- expect(out.blobs).toBeDefined();
109
- expect(out.blobs).toHaveLength(1);
110
- expect(out.blobs?.[0]?.key).toBe("callABC");
111
- expect(out.blobs?.[0]?.contentType).toBe("text/plain");
112
-
113
- // The transform itself also wrote the blob through the supplied store
114
- // (so reactors that consume blobs from the chain stay consistent with
115
- // the transform's contract).
116
- expect(calls).toHaveLength(1);
117
- expect(calls[0]?.key).toBe("callABC");
118
- expect(calls[0]?.contentType).toBe("text/plain");
119
- expect(new TextDecoder().decode(calls[0]?.bytes)).toBe(full);
120
- });
121
-
122
- test("structured (non-string) content is JSON-stringified before length check", async () => {
123
- const { store } = recordingWriteBlob();
124
- const transform = createSizeCapTransform({
125
- maxChars: 5,
126
- contextStore: store,
127
- });
128
-
129
- const result: ToolResult = {
130
- callId: "json-1",
131
- content: { hello: "world", n: 42 },
132
- };
133
- const out = await transform.apply(
134
- { call: call("json-1"), result },
135
- emptyContext(),
136
- );
137
- expect(out.record.reason).toBe("exceeded-cap");
138
- });
139
-
140
- test("preserves isError and detail on the transformed output", async () => {
141
- const { store } = recordingWriteBlob();
142
- const transform = createSizeCapTransform({
143
- maxChars: 4,
144
- contextStore: store,
145
- });
146
-
147
- const result: ToolResult = {
148
- callId: "x1",
149
- content: "abcdefghij",
150
- isError: true,
151
- detail: { stack: "..." },
152
- };
153
- const out = await transform.apply(
154
- { call: call("x1"), result },
155
- emptyContext(),
156
- );
157
-
158
- expect(out.output.isError).toBe(true);
159
- expect(out.output.detail).toEqual({ stack: "..." });
160
- });
161
-
162
- test("rejects non-positive maxChars at construction time", () => {
163
- const { store } = recordingWriteBlob();
164
- let thrown: Error | undefined;
165
- try {
166
- createSizeCapTransform({ maxChars: 0, contextStore: store });
167
- } catch (cause) {
168
- thrown = cause instanceof Error ? cause : new Error(String(cause));
169
- }
170
- expect(thrown?.message).toContain("positive finite");
171
- });
172
- });
@@ -1,110 +0,0 @@
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
-
12
- import type {
13
- ContextStore,
14
- StrategyContext,
15
- StrategyResult,
16
- ToolResult,
17
- ToolResultTransform,
18
- } from "@intx/types/runtime";
19
-
20
- const SIZE_CAP_VERSION = "1";
21
- const SIZE_CAP_NAME = "size-cap";
22
-
23
- export type SizeCapTransformOptions = {
24
- maxChars: number;
25
- contextStore: Pick<ContextStore, "writeBlob">;
26
- };
27
-
28
- /**
29
- * Create a `ToolResultTransform` that caps inline tool result content at
30
- * `maxChars` characters. Oversized results are spilled to the context store
31
- * via `writeBlob` and the inline content becomes a truncated marker
32
- * referencing the spill by `tool-output:///{callId}` URI.
33
- */
34
- export function createSizeCapTransform(
35
- options: SizeCapTransformOptions,
36
- ): ToolResultTransform {
37
- const { maxChars, contextStore } = options;
38
- if (!Number.isFinite(maxChars) || maxChars <= 0) {
39
- throw new Error(
40
- `createSizeCapTransform: maxChars must be a positive finite number, got ${String(maxChars)}`,
41
- );
42
- }
43
-
44
- return {
45
- name: SIZE_CAP_NAME,
46
- version: SIZE_CAP_VERSION,
47
- async apply(
48
- input: { call: { id: string; name: string }; result: ToolResult },
49
- _ctx: StrategyContext,
50
- ): Promise<StrategyResult<ToolResult>> {
51
- const { call, result } = input;
52
- const text =
53
- typeof result.content === "string"
54
- ? result.content
55
- : JSON.stringify(result.content);
56
-
57
- if (text.length <= maxChars) {
58
- return {
59
- output: result,
60
- record: {
61
- strategy: SIZE_CAP_NAME,
62
- version: SIZE_CAP_VERSION,
63
- parameters: { maxChars },
64
- reason: "within-cap",
65
- decisions: { callId: call.id, length: text.length },
66
- },
67
- };
68
- }
69
-
70
- const omitted = text.length - maxChars;
71
- const kept = text.slice(0, maxChars);
72
- const spillURI = `tool-output:///${call.id}`;
73
- const marker =
74
- `${kept}\n[Tool output truncated: omitted ${String(omitted)} chars. ` +
75
- `Full output available at ${spillURI} -- use read_file with that URI to see the rest.]`;
76
-
77
- const bytes = new TextEncoder().encode(text);
78
- await contextStore.writeBlob(call.id, bytes, "text/plain");
79
-
80
- const output: ToolResult = {
81
- ...result,
82
- content: marker,
83
- };
84
-
85
- return {
86
- output,
87
- record: {
88
- strategy: SIZE_CAP_NAME,
89
- version: SIZE_CAP_VERSION,
90
- parameters: { maxChars },
91
- reason: "exceeded-cap",
92
- decisions: {
93
- callId: call.id,
94
- originalLength: text.length,
95
- kept: maxChars,
96
- spillKey: call.id,
97
- spillURI,
98
- },
99
- },
100
- blobs: [
101
- {
102
- key: call.id,
103
- bytes,
104
- contentType: "text/plain",
105
- },
106
- ],
107
- };
108
- },
109
- };
110
- }
package/src/turns.ts DELETED
@@ -1,54 +0,0 @@
1
- import type {
2
- ConversationTurn,
3
- ContentBlock,
4
- AssistantTurn,
5
- InboundMessage,
6
- ToolCall,
7
- ToolResult,
8
- } from "@intx/types/runtime";
9
-
10
- export type { ConversationTurn, ContentBlock, AssistantTurn };
11
-
12
- export type { ToolCall, ToolResult };
13
-
14
- export function createInboundTurn(
15
- message: InboundMessage,
16
- ): ConversationTurn | null {
17
- const content = message.content ?? "";
18
- if (content.length === 0) return null;
19
-
20
- const { from, subject } = message.headers;
21
- const envelope: string[] = [];
22
- if (from.length > 0) envelope.push(`[From: ${from}]`);
23
- if (subject !== undefined && subject.length > 0) {
24
- envelope.push(`[Subject: ${subject}]`);
25
- }
26
-
27
- const text =
28
- envelope.length > 0 ? `${envelope.join("\n")}\n\n${content}` : content;
29
- return {
30
- role: "user",
31
- content: [{ type: "text", text }],
32
- timestamp: Date.now(),
33
- };
34
- }
35
-
36
- export function createToolResultTurn(results: ToolResult[]): ConversationTurn {
37
- const blocks: ContentBlock[] = results.map((r) => {
38
- const raw =
39
- typeof r.content === "string" ? r.content : JSON.stringify(r.content);
40
- const block: Extract<ContentBlock, { type: "tool_result" }> = {
41
- type: "tool_result",
42
- callId: r.callId,
43
- content: [{ type: "text" as const, text: raw }],
44
- };
45
- if (r.detail !== undefined) {
46
- block.detail = r.detail;
47
- }
48
- if (r.isError !== undefined) {
49
- block.isError = r.isError;
50
- }
51
- return block;
52
- });
53
- return { role: "user", content: blocks, timestamp: Date.now() };
54
- }
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src/**/*.ts"]
4
- }