@intx/agent 0.1.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/README.md +47 -0
- package/package.json +19 -0
- package/src/agent.test.ts +46 -0
- package/src/agent.ts +494 -0
- package/src/index.ts +38 -0
- package/src/lock.test.ts +93 -0
- package/src/lock.ts +57 -0
- package/src/send-queue.test.ts +207 -0
- package/src/send-queue.ts +200 -0
- package/src/source.test.ts +171 -0
- package/src/source.ts +93 -0
- package/src/stream.test.ts +167 -0
- package/src/stream.ts +142 -0
- package/src/tool.test.ts +217 -0
- package/src/tool.ts +148 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/tool.test.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { ToolDefinition } from "@intx/types/runtime";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createToolRunner,
|
|
7
|
+
DuplicateToolError,
|
|
8
|
+
fromToolRunner,
|
|
9
|
+
stringTool,
|
|
10
|
+
tool,
|
|
11
|
+
} from "./tool";
|
|
12
|
+
|
|
13
|
+
const DEF_A: ToolDefinition = {
|
|
14
|
+
name: "a",
|
|
15
|
+
description: "tool a",
|
|
16
|
+
inputSchema: { type: "object" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEF_B: ToolDefinition = {
|
|
20
|
+
name: "b",
|
|
21
|
+
description: "tool b",
|
|
22
|
+
inputSchema: { type: "object" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("createToolRunner", () => {
|
|
26
|
+
test("dispatches to a full handler by tool name", async () => {
|
|
27
|
+
const runner = createToolRunner([
|
|
28
|
+
tool({
|
|
29
|
+
definition: DEF_A,
|
|
30
|
+
handler: async (call) => ({
|
|
31
|
+
callId: call.id,
|
|
32
|
+
content: `got ${String(call.arguments.x)}`,
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const result = await runner.run(
|
|
38
|
+
{ id: "c1", name: "a", arguments: { x: 5 } },
|
|
39
|
+
new AbortController().signal,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual({ callId: "c1", content: "got 5" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("lifts the string handler return into a ToolResult", async () => {
|
|
46
|
+
const runner = createToolRunner([
|
|
47
|
+
stringTool({
|
|
48
|
+
definition: DEF_A,
|
|
49
|
+
handler: async (args) => `hello ${String(args.name)}`,
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const result = await runner.run(
|
|
54
|
+
{ id: "c2", name: "a", arguments: { name: "world" } },
|
|
55
|
+
new AbortController().signal,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual({ callId: "c2", content: "hello world" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns an error ToolResult for an unknown tool name", async () => {
|
|
62
|
+
const runner = createToolRunner([
|
|
63
|
+
tool({
|
|
64
|
+
definition: DEF_A,
|
|
65
|
+
handler: async (call) => ({ callId: call.id, content: "" }),
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const result = await runner.run(
|
|
70
|
+
{ id: "c3", name: "nope", arguments: {} },
|
|
71
|
+
new AbortController().signal,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.isError).toBe(true);
|
|
75
|
+
expect(result.callId).toBe("c3");
|
|
76
|
+
expect(result.content).toBe("unknown tool: nope");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("wraps a thrown error from a full handler into an error ToolResult", async () => {
|
|
80
|
+
const runner = createToolRunner([
|
|
81
|
+
tool({
|
|
82
|
+
definition: DEF_A,
|
|
83
|
+
handler: async () => {
|
|
84
|
+
throw new Error("boom");
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const result = await runner.run(
|
|
90
|
+
{ id: "c4", name: "a", arguments: {} },
|
|
91
|
+
new AbortController().signal,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result).toEqual({ callId: "c4", content: "boom", isError: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("wraps a thrown error from a string handler into an error ToolResult", async () => {
|
|
98
|
+
const runner = createToolRunner([
|
|
99
|
+
stringTool({
|
|
100
|
+
definition: DEF_A,
|
|
101
|
+
handler: async () => {
|
|
102
|
+
throw new Error("bad input");
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const result = await runner.run(
|
|
108
|
+
{ id: "c5", name: "a", arguments: {} },
|
|
109
|
+
new AbortController().signal,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
callId: "c5",
|
|
114
|
+
content: "bad input",
|
|
115
|
+
isError: true,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("exposes definitions in registration order", () => {
|
|
120
|
+
const runner = createToolRunner([
|
|
121
|
+
tool({
|
|
122
|
+
definition: DEF_A,
|
|
123
|
+
handler: async (call) => ({ callId: call.id, content: "" }),
|
|
124
|
+
}),
|
|
125
|
+
stringTool({ definition: DEF_B, handler: async () => "x" }),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
expect(runner.definitions.map((d) => d.name)).toEqual(["a", "b"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("throws DuplicateToolError at construction on duplicate names", () => {
|
|
132
|
+
expect(() =>
|
|
133
|
+
createToolRunner([
|
|
134
|
+
tool({
|
|
135
|
+
definition: DEF_A,
|
|
136
|
+
handler: async (call) => ({ callId: call.id, content: "" }),
|
|
137
|
+
}),
|
|
138
|
+
stringTool({ definition: DEF_A, handler: async () => "x" }),
|
|
139
|
+
]),
|
|
140
|
+
).toThrow(DuplicateToolError);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("propagates the AbortSignal to the handler", async () => {
|
|
144
|
+
let received: AbortSignal | undefined;
|
|
145
|
+
const runner = createToolRunner([
|
|
146
|
+
tool({
|
|
147
|
+
definition: DEF_A,
|
|
148
|
+
handler: async (call, signal) => {
|
|
149
|
+
received = signal;
|
|
150
|
+
return { callId: call.id, content: "ok" };
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const ctl = new AbortController();
|
|
156
|
+
await runner.run({ id: "c6", name: "a", arguments: {} }, ctl.signal);
|
|
157
|
+
|
|
158
|
+
expect(received).toBe(ctl.signal);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("fromToolRunner wraps each definition as a full-handler AgentTool", async () => {
|
|
162
|
+
const calls: string[] = [];
|
|
163
|
+
const stubRunner = {
|
|
164
|
+
definitions: [DEF_A, DEF_B] as const,
|
|
165
|
+
run: async (call: {
|
|
166
|
+
id: string;
|
|
167
|
+
name: string;
|
|
168
|
+
arguments: Record<string, unknown>;
|
|
169
|
+
}) => {
|
|
170
|
+
calls.push(call.name);
|
|
171
|
+
return Promise.resolve({
|
|
172
|
+
callId: call.id,
|
|
173
|
+
content: `ran ${call.name}`,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const tools = fromToolRunner(stubRunner);
|
|
179
|
+
const runner = createToolRunner(tools);
|
|
180
|
+
|
|
181
|
+
expect(runner.definitions.map((d) => d.name)).toEqual(["a", "b"]);
|
|
182
|
+
|
|
183
|
+
const ra = await runner.run(
|
|
184
|
+
{ id: "c8", name: "a", arguments: {} },
|
|
185
|
+
new AbortController().signal,
|
|
186
|
+
);
|
|
187
|
+
expect(ra).toEqual({ callId: "c8", content: "ran a" });
|
|
188
|
+
|
|
189
|
+
const rb = await runner.run(
|
|
190
|
+
{ id: "c9", name: "b", arguments: {} },
|
|
191
|
+
new AbortController().signal,
|
|
192
|
+
);
|
|
193
|
+
expect(rb).toEqual({ callId: "c9", content: "ran b" });
|
|
194
|
+
|
|
195
|
+
expect(calls).toEqual(["a", "b"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("string handler receives the parsed arguments object", async () => {
|
|
199
|
+
let received: Record<string, unknown> | undefined;
|
|
200
|
+
const runner = createToolRunner([
|
|
201
|
+
stringTool({
|
|
202
|
+
definition: DEF_A,
|
|
203
|
+
handler: async (args) => {
|
|
204
|
+
received = args;
|
|
205
|
+
return "ok";
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
await runner.run(
|
|
211
|
+
{ id: "c7", name: "a", arguments: { foo: 1, bar: "x" } },
|
|
212
|
+
new AbortController().signal,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(received).toEqual({ foo: 1, bar: "x" });
|
|
216
|
+
});
|
|
217
|
+
});
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Tool registration and dispatch.
|
|
2
|
+
//
|
|
3
|
+
// Two registration shapes are supported:
|
|
4
|
+
//
|
|
5
|
+
// `tool({ definition, handler })` — handler receives the full
|
|
6
|
+
// ToolCall and returns the full
|
|
7
|
+
// ToolResult. Use when the
|
|
8
|
+
// handler needs the callId or
|
|
9
|
+
// wants to set isError/detail/
|
|
10
|
+
// pendingMarker.
|
|
11
|
+
//
|
|
12
|
+
// `stringTool({ definition, handler })` — sugar for the common case of
|
|
13
|
+
// "compute a string from the
|
|
14
|
+
// parsed arguments." The callId
|
|
15
|
+
// is filled in from the
|
|
16
|
+
// surrounding ToolCall, and
|
|
17
|
+
// isError is false unless the
|
|
18
|
+
// handler throws.
|
|
19
|
+
//
|
|
20
|
+
// `createToolRunner(tools)` builds a `ToolRunner` that dispatches by tool
|
|
21
|
+
// name. Per the ToolRunner contract (packages/types/src/runtime.ts), `run`
|
|
22
|
+
// must not throw — unknown tool names and handler exceptions are surfaced
|
|
23
|
+
// as `ToolResult` with `isError: true` so the model sees them and can
|
|
24
|
+
// recover.
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
ToolCall,
|
|
28
|
+
ToolDefinition,
|
|
29
|
+
ToolResult,
|
|
30
|
+
ToolRunner,
|
|
31
|
+
} from "@intx/types/runtime";
|
|
32
|
+
|
|
33
|
+
export type ToolHandler = (
|
|
34
|
+
call: ToolCall,
|
|
35
|
+
signal: AbortSignal,
|
|
36
|
+
) => Promise<ToolResult>;
|
|
37
|
+
|
|
38
|
+
export type StringToolHandler = (
|
|
39
|
+
args: Record<string, unknown>,
|
|
40
|
+
signal: AbortSignal,
|
|
41
|
+
) => Promise<string>;
|
|
42
|
+
|
|
43
|
+
export type AgentTool =
|
|
44
|
+
| { kind: "full"; definition: ToolDefinition; handler: ToolHandler }
|
|
45
|
+
| {
|
|
46
|
+
kind: "string";
|
|
47
|
+
definition: ToolDefinition;
|
|
48
|
+
handler: StringToolHandler;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function tool(args: {
|
|
52
|
+
definition: ToolDefinition;
|
|
53
|
+
handler: ToolHandler;
|
|
54
|
+
}): AgentTool {
|
|
55
|
+
return { kind: "full", definition: args.definition, handler: args.handler };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function stringTool(args: {
|
|
59
|
+
definition: ToolDefinition;
|
|
60
|
+
handler: StringToolHandler;
|
|
61
|
+
}): AgentTool {
|
|
62
|
+
return {
|
|
63
|
+
kind: "string",
|
|
64
|
+
definition: args.definition,
|
|
65
|
+
handler: args.handler,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Adapt a pre-built ToolRunner (e.g. the one returned by
|
|
71
|
+
* `createPosixTools`) into a list of AgentTools that can be passed to
|
|
72
|
+
* `createAgent({ tools })`. Each definition becomes a full-handler
|
|
73
|
+
* AgentTool that delegates to the runner's `run`.
|
|
74
|
+
*
|
|
75
|
+
* Use this when integrating tool packages whose public surface is a
|
|
76
|
+
* single ToolRunner rather than individual handlers.
|
|
77
|
+
*/
|
|
78
|
+
export function fromToolRunner(runner: {
|
|
79
|
+
readonly definitions: readonly ToolDefinition[];
|
|
80
|
+
run: ToolRunner["run"];
|
|
81
|
+
}): AgentTool[] {
|
|
82
|
+
return runner.definitions.map((definition) => ({
|
|
83
|
+
kind: "full",
|
|
84
|
+
definition,
|
|
85
|
+
handler: (call, signal) => runner.run(call, signal),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class DuplicateToolError extends Error {
|
|
90
|
+
readonly toolName: string;
|
|
91
|
+
|
|
92
|
+
constructor(toolName: string) {
|
|
93
|
+
super(`duplicate tool name: ${toolName}`);
|
|
94
|
+
this.name = "DuplicateToolError";
|
|
95
|
+
this.toolName = toolName;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type AgentToolRunner = ToolRunner & {
|
|
100
|
+
readonly definitions: readonly ToolDefinition[];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a `ToolRunner` that dispatches by tool name. Throws
|
|
105
|
+
* `DuplicateToolError` at construction if any two tools share a name.
|
|
106
|
+
*
|
|
107
|
+
* At call time, unknown tool names and exceptions from handlers are
|
|
108
|
+
* converted to `ToolResult { isError: true }` so the contract on
|
|
109
|
+
* `ToolRunner.run` ("must not throw") is upheld.
|
|
110
|
+
*/
|
|
111
|
+
export function createToolRunner(tools: AgentTool[]): AgentToolRunner {
|
|
112
|
+
const byName = new Map<string, AgentTool>();
|
|
113
|
+
for (const t of tools) {
|
|
114
|
+
if (byName.has(t.definition.name)) {
|
|
115
|
+
throw new DuplicateToolError(t.definition.name);
|
|
116
|
+
}
|
|
117
|
+
byName.set(t.definition.name, t);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const definitions: readonly ToolDefinition[] = tools.map((t) => t.definition);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
definitions,
|
|
124
|
+
async run(call, signal): Promise<ToolResult> {
|
|
125
|
+
const found = byName.get(call.name);
|
|
126
|
+
if (found === undefined) {
|
|
127
|
+
return {
|
|
128
|
+
callId: call.id,
|
|
129
|
+
content: `unknown tool: ${call.name}`,
|
|
130
|
+
isError: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
if (found.kind === "full") {
|
|
135
|
+
return await found.handler(call, signal);
|
|
136
|
+
}
|
|
137
|
+
const text = await found.handler(call.arguments, signal);
|
|
138
|
+
return { callId: call.id, content: text };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
callId: call.id,
|
|
142
|
+
content: err instanceof Error ? err.message : String(err),
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
package/tsconfig.json
ADDED