@clinebot/agents 0.0.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/README.md +145 -0
- package/dist/agent-input.d.ts +2 -0
- package/dist/agent.d.ts +56 -0
- package/dist/extensions.d.ts +21 -0
- package/dist/hooks/engine.d.ts +42 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/node.d.ts +2 -0
- package/dist/hooks/subprocess-runner.d.ts +16 -0
- package/dist/hooks/subprocess.d.ts +268 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +49 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/policies.d.ts +14 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/types.d.ts +35 -0
- package/dist/message-builder.d.ts +31 -0
- package/dist/prompts/cline.d.ts +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/runtime/agent-runtime-bus.d.ts +13 -0
- package/dist/runtime/conversation-store.d.ts +16 -0
- package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
- package/dist/runtime/tool-orchestrator.d.ts +39 -0
- package/dist/runtime/turn-processor.d.ts +21 -0
- package/dist/teams/index.d.ts +3 -0
- package/dist/teams/multi-agent.d.ts +566 -0
- package/dist/teams/spawn-agent-tool.d.ts +85 -0
- package/dist/teams/team-tools.d.ts +51 -0
- package/dist/tools/ask-question.d.ts +12 -0
- package/dist/tools/create.d.ts +59 -0
- package/dist/tools/execution.d.ts +61 -0
- package/dist/tools/formatting.d.ts +20 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/registry.d.ts +26 -0
- package/dist/tools/validation.d.ts +27 -0
- package/dist/types.d.ts +826 -0
- package/package.json +54 -0
- package/src/agent-input.ts +116 -0
- package/src/agent.test.ts +931 -0
- package/src/agent.ts +1050 -0
- package/src/example.test.ts +564 -0
- package/src/extensions.ts +337 -0
- package/src/hooks/engine.test.ts +163 -0
- package/src/hooks/engine.ts +537 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/lifecycle.ts +239 -0
- package/src/hooks/node.ts +18 -0
- package/src/hooks/subprocess-runner.ts +140 -0
- package/src/hooks/subprocess.test.ts +180 -0
- package/src/hooks/subprocess.ts +620 -0
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +21 -0
- package/src/index.ts +133 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/policies.test.ts +51 -0
- package/src/mcp/policies.ts +53 -0
- package/src/mcp/tools.test.ts +76 -0
- package/src/mcp/tools.ts +60 -0
- package/src/mcp/types.ts +41 -0
- package/src/message-builder.test.ts +175 -0
- package/src/message-builder.ts +429 -0
- package/src/prompts/cline.ts +49 -0
- package/src/prompts/index.ts +1 -0
- package/src/runtime/agent-runtime-bus.ts +53 -0
- package/src/runtime/conversation-store.ts +61 -0
- package/src/runtime/lifecycle-orchestrator.ts +90 -0
- package/src/runtime/tool-orchestrator.ts +177 -0
- package/src/runtime/turn-processor.ts +250 -0
- package/src/streaming.test.ts +197 -0
- package/src/streaming.ts +307 -0
- package/src/teams/index.ts +63 -0
- package/src/teams/multi-agent.lifecycle.test.ts +48 -0
- package/src/teams/multi-agent.ts +1866 -0
- package/src/teams/spawn-agent-tool.test.ts +172 -0
- package/src/teams/spawn-agent-tool.ts +223 -0
- package/src/teams/team-tools.test.ts +448 -0
- package/src/teams/team-tools.ts +929 -0
- package/src/tools/ask-question.ts +78 -0
- package/src/tools/create.ts +104 -0
- package/src/tools/execution.ts +311 -0
- package/src/tools/formatting.ts +73 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/tools.test.ts +292 -0
- package/src/tools/validation.ts +73 -0
- package/src/types.ts +966 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { createTool } from "./tools/create.js";
|
|
7
|
+
import type { AgentExtension, Tool } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type FakeChunk = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
type FakeHandler = {
|
|
12
|
+
createMessage: ReturnType<typeof vi.fn>;
|
|
13
|
+
getModel: ReturnType<typeof vi.fn>;
|
|
14
|
+
getMessages: ReturnType<typeof vi.fn>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const createHandlerMock = vi.fn<(config: unknown) => FakeHandler>();
|
|
18
|
+
const toProviderConfigMock = vi.fn((settings: any) => ({
|
|
19
|
+
knownModels: settings?.model
|
|
20
|
+
? {
|
|
21
|
+
[settings.model]: {
|
|
22
|
+
id: settings.model,
|
|
23
|
+
pricing: { input: 1, output: 1 },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: undefined,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("@clinebot/llms", () => ({
|
|
30
|
+
providers: {
|
|
31
|
+
createHandler: (config: unknown) => createHandlerMock(config),
|
|
32
|
+
toProviderConfig: (settings: unknown) => toProviderConfigMock(settings),
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
async function* streamChunks(chunks: FakeChunk[]): AsyncGenerator<FakeChunk> {
|
|
37
|
+
for (const chunk of chunks) {
|
|
38
|
+
yield chunk;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeHandler(turns: FakeChunk[][]): FakeHandler {
|
|
43
|
+
let index = 0;
|
|
44
|
+
return {
|
|
45
|
+
createMessage: vi.fn(() => {
|
|
46
|
+
const chunks = turns[index] ?? [];
|
|
47
|
+
index += 1;
|
|
48
|
+
return streamChunks(chunks);
|
|
49
|
+
}),
|
|
50
|
+
getModel: vi.fn(() => ({
|
|
51
|
+
id: "mock-model",
|
|
52
|
+
info: {},
|
|
53
|
+
})),
|
|
54
|
+
getMessages: vi.fn(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("Agent", () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("runs a basic single turn and returns final text", async () => {
|
|
64
|
+
const { Agent } = await import("./agent.js");
|
|
65
|
+
const handler = makeHandler([
|
|
66
|
+
[
|
|
67
|
+
{ type: "text", id: "r1", text: "Hello from model" },
|
|
68
|
+
{ type: "usage", id: "r1", inputTokens: 10, outputTokens: 5 },
|
|
69
|
+
{ type: "done", id: "r1", success: true },
|
|
70
|
+
],
|
|
71
|
+
]);
|
|
72
|
+
createHandlerMock.mockReturnValue(handler);
|
|
73
|
+
|
|
74
|
+
const events: string[] = [];
|
|
75
|
+
const agent = new Agent({
|
|
76
|
+
providerId: "anthropic",
|
|
77
|
+
modelId: "mock-model",
|
|
78
|
+
systemPrompt: "You are helpful.",
|
|
79
|
+
tools: [],
|
|
80
|
+
onEvent: (event) => events.push(event.type),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await agent.run("Say hello");
|
|
84
|
+
|
|
85
|
+
expect(result.finishReason).toBe("completed");
|
|
86
|
+
expect(result.text).toBe("Hello from model");
|
|
87
|
+
expect(result.iterations).toBe(1);
|
|
88
|
+
expect(result.usage.inputTokens).toBe(10);
|
|
89
|
+
expect(result.usage.outputTokens).toBe(5);
|
|
90
|
+
expect(events).toContain("done");
|
|
91
|
+
expect(toProviderConfigMock).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("keeps totalCost undefined when usage chunks omit cost", async () => {
|
|
95
|
+
const { Agent } = await import("./agent.js");
|
|
96
|
+
const handler = makeHandler([
|
|
97
|
+
[
|
|
98
|
+
{ type: "text", id: "r1", text: "Hello from model" },
|
|
99
|
+
{ type: "usage", id: "r1", inputTokens: 10, outputTokens: 5 },
|
|
100
|
+
{ type: "done", id: "r1", success: true },
|
|
101
|
+
],
|
|
102
|
+
]);
|
|
103
|
+
createHandlerMock.mockReturnValue(handler);
|
|
104
|
+
|
|
105
|
+
const agent = new Agent({
|
|
106
|
+
providerId: "anthropic",
|
|
107
|
+
modelId: "mock-model",
|
|
108
|
+
systemPrompt: "You are helpful.",
|
|
109
|
+
tools: [],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await agent.run("Say hello");
|
|
113
|
+
|
|
114
|
+
expect(result.usage.totalCost).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("passes providerConfig through to handler creation", async () => {
|
|
118
|
+
const { Agent } = await import("./agent.js");
|
|
119
|
+
const handler = makeHandler([
|
|
120
|
+
[
|
|
121
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
122
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
123
|
+
{ type: "done", id: "r1", success: true },
|
|
124
|
+
],
|
|
125
|
+
]);
|
|
126
|
+
createHandlerMock.mockReturnValue(handler);
|
|
127
|
+
|
|
128
|
+
const agent = new Agent({
|
|
129
|
+
providerId: "vertex",
|
|
130
|
+
modelId: "claude-sonnet-4@20250514",
|
|
131
|
+
systemPrompt: "You are helpful.",
|
|
132
|
+
tools: [],
|
|
133
|
+
providerConfig: {
|
|
134
|
+
providerId: "vertex",
|
|
135
|
+
modelId: "claude-sonnet-4@20250514",
|
|
136
|
+
gcp: {
|
|
137
|
+
projectId: "test-project",
|
|
138
|
+
region: "us-central1",
|
|
139
|
+
},
|
|
140
|
+
} as LlmsProviders.ProviderConfig,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await agent.run("hello");
|
|
144
|
+
|
|
145
|
+
expect(createHandlerMock).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
providerId: "vertex",
|
|
148
|
+
modelId: "claude-sonnet-4@20250514",
|
|
149
|
+
gcp: {
|
|
150
|
+
projectId: "test-project",
|
|
151
|
+
region: "us-central1",
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("emits loop logs to provided logger", async () => {
|
|
158
|
+
const { Agent } = await import("./agent.js");
|
|
159
|
+
const handler = makeHandler([
|
|
160
|
+
[
|
|
161
|
+
{ type: "text", id: "r1", text: "Hello from model" },
|
|
162
|
+
{ type: "usage", id: "r1", inputTokens: 10, outputTokens: 5 },
|
|
163
|
+
{ type: "done", id: "r1", success: true },
|
|
164
|
+
],
|
|
165
|
+
]);
|
|
166
|
+
createHandlerMock.mockReturnValue(handler);
|
|
167
|
+
|
|
168
|
+
const logger = {
|
|
169
|
+
debug: vi.fn(),
|
|
170
|
+
info: vi.fn(),
|
|
171
|
+
warn: vi.fn(),
|
|
172
|
+
error: vi.fn(),
|
|
173
|
+
};
|
|
174
|
+
const agent = new Agent({
|
|
175
|
+
providerId: "anthropic",
|
|
176
|
+
modelId: "mock-model",
|
|
177
|
+
systemPrompt: "You are helpful.",
|
|
178
|
+
tools: [],
|
|
179
|
+
logger,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await agent.run("Say hello");
|
|
183
|
+
|
|
184
|
+
expect(
|
|
185
|
+
logger.info.mock.calls.some(
|
|
186
|
+
([message]) =>
|
|
187
|
+
typeof message === "string" && message.includes("Agent loop started"),
|
|
188
|
+
),
|
|
189
|
+
).toBe(true);
|
|
190
|
+
expect(
|
|
191
|
+
logger.info.mock.calls.some(
|
|
192
|
+
([message]) =>
|
|
193
|
+
typeof message === "string" &&
|
|
194
|
+
message.includes("Agent loop finished"),
|
|
195
|
+
),
|
|
196
|
+
).toBe(true);
|
|
197
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("executes tool calls and applies tool policy approval", async () => {
|
|
201
|
+
const { Agent } = await import("./agent.js");
|
|
202
|
+
const mathTool: Tool<{ a: number; b: number }, { total: number }> =
|
|
203
|
+
createTool({
|
|
204
|
+
name: "math_add",
|
|
205
|
+
description: "Add two numbers",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
a: { type: "number" },
|
|
210
|
+
b: { type: "number" },
|
|
211
|
+
},
|
|
212
|
+
required: ["a", "b"],
|
|
213
|
+
},
|
|
214
|
+
execute: async ({ a, b }) => ({ total: a + b }),
|
|
215
|
+
});
|
|
216
|
+
const genericMathTool = mathTool as Tool;
|
|
217
|
+
|
|
218
|
+
const handler = makeHandler([
|
|
219
|
+
[
|
|
220
|
+
{
|
|
221
|
+
type: "tool_calls",
|
|
222
|
+
id: "r1",
|
|
223
|
+
tool_call: {
|
|
224
|
+
call_id: "call_1",
|
|
225
|
+
function: {
|
|
226
|
+
name: "math_add",
|
|
227
|
+
arguments: JSON.stringify({ a: 2, b: 3 }),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{ type: "usage", id: "r1", inputTokens: 20, outputTokens: 8 },
|
|
232
|
+
{ type: "done", id: "r1", success: true },
|
|
233
|
+
],
|
|
234
|
+
[
|
|
235
|
+
{ type: "text", id: "r2", text: "Done" },
|
|
236
|
+
{ type: "usage", id: "r2", inputTokens: 12, outputTokens: 4 },
|
|
237
|
+
{ type: "done", id: "r2", success: true },
|
|
238
|
+
],
|
|
239
|
+
]);
|
|
240
|
+
createHandlerMock.mockReturnValue(handler);
|
|
241
|
+
|
|
242
|
+
const approval = vi.fn().mockResolvedValue({ approved: true });
|
|
243
|
+
const agent = new Agent({
|
|
244
|
+
providerId: "anthropic",
|
|
245
|
+
modelId: "mock-model",
|
|
246
|
+
systemPrompt: "Use tools",
|
|
247
|
+
tools: [genericMathTool],
|
|
248
|
+
toolPolicies: {
|
|
249
|
+
math_add: { autoApprove: false },
|
|
250
|
+
},
|
|
251
|
+
requestToolApproval: approval,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const result = await agent.run("compute");
|
|
255
|
+
|
|
256
|
+
expect(approval).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(result.finishReason).toBe("completed");
|
|
258
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
259
|
+
expect(result.toolCalls[0]?.output).toEqual({ total: 5 });
|
|
260
|
+
expect(result.text).toBe("Done");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("requests approval when a tool_call_before hook returns review", async () => {
|
|
264
|
+
const { Agent } = await import("./agent.js");
|
|
265
|
+
const runCommandsTool = createTool({
|
|
266
|
+
name: "run_commands",
|
|
267
|
+
description: "Run shell commands",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
commands: {
|
|
272
|
+
type: "array",
|
|
273
|
+
items: { type: "string" },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
required: ["commands"],
|
|
277
|
+
},
|
|
278
|
+
execute: async (input: { commands: string[] }) => input.commands,
|
|
279
|
+
}) as Tool;
|
|
280
|
+
const handler = makeHandler([
|
|
281
|
+
[
|
|
282
|
+
{
|
|
283
|
+
type: "tool_calls",
|
|
284
|
+
id: "r1",
|
|
285
|
+
tool_call: {
|
|
286
|
+
call_id: "call_1",
|
|
287
|
+
function: {
|
|
288
|
+
name: "run_commands",
|
|
289
|
+
arguments: JSON.stringify({ commands: ["git status"] }),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{ type: "usage", id: "r1", inputTokens: 20, outputTokens: 8 },
|
|
294
|
+
{ type: "done", id: "r1", success: true },
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
{ type: "text", id: "r2", text: "Done" },
|
|
298
|
+
{ type: "usage", id: "r2", inputTokens: 12, outputTokens: 4 },
|
|
299
|
+
{ type: "done", id: "r2", success: true },
|
|
300
|
+
],
|
|
301
|
+
]);
|
|
302
|
+
createHandlerMock.mockReturnValue(handler);
|
|
303
|
+
|
|
304
|
+
const approval = vi.fn().mockResolvedValue({ approved: true });
|
|
305
|
+
const agent = new Agent({
|
|
306
|
+
providerId: "anthropic",
|
|
307
|
+
modelId: "mock-model",
|
|
308
|
+
systemPrompt: "Use tools",
|
|
309
|
+
tools: [runCommandsTool],
|
|
310
|
+
hooks: {
|
|
311
|
+
onToolCallStart: async () => ({
|
|
312
|
+
review: true,
|
|
313
|
+
context: "Git commands require explicit user approval.",
|
|
314
|
+
}),
|
|
315
|
+
},
|
|
316
|
+
requestToolApproval: approval,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const result = await agent.run("check git status");
|
|
320
|
+
|
|
321
|
+
expect(approval).toHaveBeenCalledTimes(1);
|
|
322
|
+
expect(result.finishReason).toBe("completed");
|
|
323
|
+
expect(result.toolCalls[0]?.error).toBeUndefined();
|
|
324
|
+
expect(result.toolCalls[0]?.output).toEqual(["git status"]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does not request approval when no hook asks for review", async () => {
|
|
328
|
+
const { Agent } = await import("./agent.js");
|
|
329
|
+
const runCommandsTool = createTool({
|
|
330
|
+
name: "run_commands",
|
|
331
|
+
description: "Run shell commands",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
commands: {
|
|
336
|
+
type: "array",
|
|
337
|
+
items: { type: "string" },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
required: ["commands"],
|
|
341
|
+
},
|
|
342
|
+
execute: async (input: { commands: string[] }) => input.commands,
|
|
343
|
+
}) as Tool;
|
|
344
|
+
const handler = makeHandler([
|
|
345
|
+
[
|
|
346
|
+
{
|
|
347
|
+
type: "tool_calls",
|
|
348
|
+
id: "r1",
|
|
349
|
+
tool_call: {
|
|
350
|
+
call_id: "call_1",
|
|
351
|
+
function: {
|
|
352
|
+
name: "run_commands",
|
|
353
|
+
arguments: JSON.stringify({ commands: ["git status"] }),
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{ type: "usage", id: "r1", inputTokens: 20, outputTokens: 8 },
|
|
358
|
+
{ type: "done", id: "r1", success: true },
|
|
359
|
+
],
|
|
360
|
+
[
|
|
361
|
+
{ type: "text", id: "r2", text: "Done" },
|
|
362
|
+
{ type: "usage", id: "r2", inputTokens: 12, outputTokens: 4 },
|
|
363
|
+
{ type: "done", id: "r2", success: true },
|
|
364
|
+
],
|
|
365
|
+
]);
|
|
366
|
+
createHandlerMock.mockReturnValue(handler);
|
|
367
|
+
|
|
368
|
+
const approval = vi.fn().mockResolvedValue({ approved: true });
|
|
369
|
+
const agent = new Agent({
|
|
370
|
+
providerId: "anthropic",
|
|
371
|
+
modelId: "mock-model",
|
|
372
|
+
systemPrompt: "Use tools",
|
|
373
|
+
tools: [runCommandsTool],
|
|
374
|
+
requestToolApproval: approval,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const result = await agent.run("check git status");
|
|
378
|
+
|
|
379
|
+
expect(approval).not.toHaveBeenCalled();
|
|
380
|
+
expect(result.finishReason).toBe("completed");
|
|
381
|
+
expect(result.toolCalls[0]?.error).toBeUndefined();
|
|
382
|
+
expect(result.toolCalls[0]?.output).toEqual(["git status"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("finalizes streamed tool arguments at end of turn", async () => {
|
|
386
|
+
const { Agent } = await import("./agent.js");
|
|
387
|
+
const teamLogTool = createTool({
|
|
388
|
+
name: "team_log_update",
|
|
389
|
+
description: "Append a mission log update",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
kind: { type: "string" },
|
|
394
|
+
summary: { type: "string" },
|
|
395
|
+
},
|
|
396
|
+
required: ["kind", "summary"],
|
|
397
|
+
},
|
|
398
|
+
execute: async ({ kind, summary }) => ({ kind, summary }),
|
|
399
|
+
}) as Tool;
|
|
400
|
+
|
|
401
|
+
const handler = makeHandler([
|
|
402
|
+
[
|
|
403
|
+
{
|
|
404
|
+
type: "tool_calls",
|
|
405
|
+
id: "r1",
|
|
406
|
+
tool_call: {
|
|
407
|
+
call_id: "call_1",
|
|
408
|
+
function: {
|
|
409
|
+
name: "team_log_update",
|
|
410
|
+
arguments: '{"update":"Spawned two-agent team"}',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
type: "tool_calls",
|
|
416
|
+
id: "r1",
|
|
417
|
+
tool_call: {
|
|
418
|
+
call_id: "call_1",
|
|
419
|
+
function: {
|
|
420
|
+
arguments:
|
|
421
|
+
'{"kind":"progress","summary":"Spawned two-agent team"}',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
{ type: "usage", id: "r1", inputTokens: 10, outputTokens: 5 },
|
|
426
|
+
{ type: "done", id: "r1", success: true },
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
{ type: "text", id: "r2", text: "Done" },
|
|
430
|
+
{ type: "usage", id: "r2", inputTokens: 2, outputTokens: 1 },
|
|
431
|
+
{ type: "done", id: "r2", success: true },
|
|
432
|
+
],
|
|
433
|
+
]);
|
|
434
|
+
createHandlerMock.mockReturnValue(handler);
|
|
435
|
+
|
|
436
|
+
const agent = new Agent({
|
|
437
|
+
providerId: "anthropic",
|
|
438
|
+
modelId: "mock-model",
|
|
439
|
+
systemPrompt: "Use tools",
|
|
440
|
+
tools: [teamLogTool],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const result = await agent.run("log status");
|
|
444
|
+
|
|
445
|
+
expect(result.finishReason).toBe("completed");
|
|
446
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
447
|
+
expect(result.toolCalls[0]?.error).toBeUndefined();
|
|
448
|
+
expect(result.toolCalls[0]?.output).toEqual({
|
|
449
|
+
kind: "progress",
|
|
450
|
+
summary: "Spawned two-agent team",
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("passes through array-shaped read_files tool args", async () => {
|
|
455
|
+
const { Agent } = await import("./agent.js");
|
|
456
|
+
const readFilesTool = createTool({
|
|
457
|
+
name: "read_files",
|
|
458
|
+
description: "Read multiple files",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {
|
|
462
|
+
file_paths: {
|
|
463
|
+
type: "array",
|
|
464
|
+
items: { type: "string" },
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
required: ["file_paths"],
|
|
468
|
+
},
|
|
469
|
+
execute: async ({ file_paths }) => ({ file_paths }),
|
|
470
|
+
}) as Tool;
|
|
471
|
+
|
|
472
|
+
const handler = makeHandler([
|
|
473
|
+
[
|
|
474
|
+
{
|
|
475
|
+
type: "tool_calls",
|
|
476
|
+
id: "r1",
|
|
477
|
+
tool_call: {
|
|
478
|
+
call_id: "call_1",
|
|
479
|
+
function: {
|
|
480
|
+
name: "read_files",
|
|
481
|
+
arguments: '["/tmp/a.ts","/tmp/b.ts"]',
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{ type: "usage", id: "r1", inputTokens: 10, outputTokens: 5 },
|
|
486
|
+
{ type: "done", id: "r1", success: true },
|
|
487
|
+
],
|
|
488
|
+
[
|
|
489
|
+
{ type: "text", id: "r2", text: "Done" },
|
|
490
|
+
{ type: "usage", id: "r2", inputTokens: 2, outputTokens: 1 },
|
|
491
|
+
{ type: "done", id: "r2", success: true },
|
|
492
|
+
],
|
|
493
|
+
]);
|
|
494
|
+
createHandlerMock.mockReturnValue(handler);
|
|
495
|
+
|
|
496
|
+
const agent = new Agent({
|
|
497
|
+
providerId: "anthropic",
|
|
498
|
+
modelId: "mock-model",
|
|
499
|
+
systemPrompt: "Use tools",
|
|
500
|
+
tools: [readFilesTool],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const result = await agent.run("read files");
|
|
504
|
+
expect(result.finishReason).toBe("completed");
|
|
505
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
506
|
+
expect(result.toolCalls[0]?.error).toBeUndefined();
|
|
507
|
+
expect(result.toolCalls[0]?.input).toEqual(["/tmp/a.ts", "/tmp/b.ts"]);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("continues conversation and clearHistory resets message state", async () => {
|
|
511
|
+
const { Agent } = await import("./agent.js");
|
|
512
|
+
const handler = makeHandler([
|
|
513
|
+
[
|
|
514
|
+
{ type: "text", id: "r1", text: "First turn" },
|
|
515
|
+
{ type: "usage", id: "r1", inputTokens: 4, outputTokens: 3 },
|
|
516
|
+
{ type: "done", id: "r1", success: true },
|
|
517
|
+
],
|
|
518
|
+
[
|
|
519
|
+
{ type: "text", id: "r2", text: "Second turn" },
|
|
520
|
+
{ type: "usage", id: "r2", inputTokens: 5, outputTokens: 2 },
|
|
521
|
+
{ type: "done", id: "r2", success: true },
|
|
522
|
+
],
|
|
523
|
+
]);
|
|
524
|
+
createHandlerMock.mockReturnValue(handler);
|
|
525
|
+
|
|
526
|
+
const agent = new Agent({
|
|
527
|
+
providerId: "anthropic",
|
|
528
|
+
modelId: "mock-model",
|
|
529
|
+
systemPrompt: "Continue support",
|
|
530
|
+
tools: [],
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
await agent.run("one");
|
|
534
|
+
const beforeContinueMessages = agent.getMessages();
|
|
535
|
+
expect(beforeContinueMessages.length).toBeGreaterThanOrEqual(2);
|
|
536
|
+
|
|
537
|
+
const second = await agent.continue("two");
|
|
538
|
+
expect(second.text).toBe("Second turn");
|
|
539
|
+
expect(agent.getMessages().length).toBeGreaterThan(
|
|
540
|
+
beforeContinueMessages.length,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
agent.clearHistory();
|
|
544
|
+
expect(agent.getMessages()).toEqual([]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("restores preloaded messages via config and restore()", async () => {
|
|
548
|
+
const { Agent } = await import("./agent.js");
|
|
549
|
+
const handler = makeHandler([
|
|
550
|
+
[
|
|
551
|
+
{ type: "text", id: "r1", text: "restored" },
|
|
552
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
553
|
+
{ type: "done", id: "r1", success: true },
|
|
554
|
+
],
|
|
555
|
+
[
|
|
556
|
+
{ type: "text", id: "r2", text: "restored-again" },
|
|
557
|
+
{ type: "usage", id: "r2", inputTokens: 1, outputTokens: 1 },
|
|
558
|
+
{ type: "done", id: "r2", success: true },
|
|
559
|
+
],
|
|
560
|
+
]);
|
|
561
|
+
createHandlerMock.mockReturnValue(handler);
|
|
562
|
+
|
|
563
|
+
const initial: LlmsProviders.Message[] = [
|
|
564
|
+
{ role: "user", content: [{ type: "text", text: "history" }] },
|
|
565
|
+
];
|
|
566
|
+
const agent = new Agent({
|
|
567
|
+
providerId: "anthropic",
|
|
568
|
+
modelId: "mock-model",
|
|
569
|
+
systemPrompt: "Restore support",
|
|
570
|
+
tools: [],
|
|
571
|
+
initialMessages: initial,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(agent.getMessages()).toEqual(initial);
|
|
575
|
+
const first = await agent.continue("resume");
|
|
576
|
+
expect(first.text).toBe("restored");
|
|
577
|
+
|
|
578
|
+
const restored: LlmsProviders.Message[] = [
|
|
579
|
+
{ role: "assistant", content: [{ type: "text", text: "new-state" }] },
|
|
580
|
+
];
|
|
581
|
+
const conversationIdBeforeRestore = agent.getConversationId();
|
|
582
|
+
agent.restore(restored);
|
|
583
|
+
expect(agent.getMessages()).toEqual(restored);
|
|
584
|
+
expect(agent.getConversationId()).toBe(conversationIdBeforeRestore);
|
|
585
|
+
const second = await agent.continue("resume-2");
|
|
586
|
+
expect(second.text).toBe("restored-again");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("supports shutdown hooks and early run cancellation via hook control", async () => {
|
|
590
|
+
const { Agent } = await import("./agent.js");
|
|
591
|
+
const handler = makeHandler([
|
|
592
|
+
[
|
|
593
|
+
{ type: "text", id: "r1", text: "Should not run" },
|
|
594
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
595
|
+
{ type: "done", id: "r1", success: true },
|
|
596
|
+
],
|
|
597
|
+
]);
|
|
598
|
+
createHandlerMock.mockReturnValue(handler);
|
|
599
|
+
|
|
600
|
+
const onSessionShutdown = vi.fn().mockResolvedValue(undefined);
|
|
601
|
+
|
|
602
|
+
const agent = new Agent({
|
|
603
|
+
providerId: "anthropic",
|
|
604
|
+
modelId: "mock-model",
|
|
605
|
+
systemPrompt: "cancel fast",
|
|
606
|
+
tools: [],
|
|
607
|
+
hooks: {
|
|
608
|
+
onRunStart: () => ({ cancel: true }),
|
|
609
|
+
onSessionShutdown,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const result = await agent.run("cancel this");
|
|
614
|
+
expect(result.finishReason).toBe("aborted");
|
|
615
|
+
expect(result.iterations).toBe(0);
|
|
616
|
+
expect(handler.createMessage).not.toHaveBeenCalled();
|
|
617
|
+
|
|
618
|
+
await agent.shutdown("test-end");
|
|
619
|
+
expect(onSessionShutdown).toHaveBeenCalledTimes(1);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("dispatches onRuntimeEvent through HookEngine extension stage", async () => {
|
|
623
|
+
const { Agent } = await import("./agent.js");
|
|
624
|
+
const handler = makeHandler([
|
|
625
|
+
[
|
|
626
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
627
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
628
|
+
{ type: "done", id: "r1", success: true },
|
|
629
|
+
],
|
|
630
|
+
]);
|
|
631
|
+
createHandlerMock.mockReturnValue(handler);
|
|
632
|
+
|
|
633
|
+
const onRuntimeEvent = vi.fn().mockResolvedValue(undefined);
|
|
634
|
+
const extension: AgentExtension = {
|
|
635
|
+
name: "runtime-ext",
|
|
636
|
+
manifest: {
|
|
637
|
+
capabilities: ["hooks"],
|
|
638
|
+
hookStages: ["runtime_event"],
|
|
639
|
+
},
|
|
640
|
+
onRuntimeEvent,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const agent = new Agent({
|
|
644
|
+
providerId: "anthropic",
|
|
645
|
+
modelId: "mock-model",
|
|
646
|
+
systemPrompt: "runtime events",
|
|
647
|
+
tools: [],
|
|
648
|
+
extensions: [extension],
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await agent.run("trigger");
|
|
652
|
+
|
|
653
|
+
expect(onRuntimeEvent).toHaveBeenCalled();
|
|
654
|
+
expect(
|
|
655
|
+
onRuntimeEvent.mock.calls.some((args) => args[0]?.event?.type === "done"),
|
|
656
|
+
).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("registers extension contributions through ContributionRegistry setup", async () => {
|
|
660
|
+
const { Agent } = await import("./agent.js");
|
|
661
|
+
const handler = makeHandler([
|
|
662
|
+
[
|
|
663
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
664
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
665
|
+
{ type: "done", id: "r1", success: true },
|
|
666
|
+
],
|
|
667
|
+
]);
|
|
668
|
+
createHandlerMock.mockReturnValue(handler);
|
|
669
|
+
|
|
670
|
+
const extensionTool = createTool({
|
|
671
|
+
name: "ext_echo",
|
|
672
|
+
description: "Echo back text",
|
|
673
|
+
inputSchema: {
|
|
674
|
+
type: "object",
|
|
675
|
+
properties: { value: { type: "string" } },
|
|
676
|
+
required: ["value"],
|
|
677
|
+
},
|
|
678
|
+
execute: async ({ value }: { value: string }) => ({ value }),
|
|
679
|
+
}) as Tool;
|
|
680
|
+
const extension: AgentExtension = {
|
|
681
|
+
name: "contrib-ext",
|
|
682
|
+
manifest: {
|
|
683
|
+
capabilities: ["tools", "commands"],
|
|
684
|
+
},
|
|
685
|
+
setup: (api) => {
|
|
686
|
+
api.registerTool(extensionTool);
|
|
687
|
+
api.registerCommand({ name: "ext:hello", description: "hello cmd" });
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const agent = new Agent({
|
|
692
|
+
providerId: "anthropic",
|
|
693
|
+
modelId: "mock-model",
|
|
694
|
+
systemPrompt: "contrib events",
|
|
695
|
+
tools: [],
|
|
696
|
+
extensions: [extension],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
await agent.run("trigger");
|
|
700
|
+
const registry = agent.getExtensionRegistry();
|
|
701
|
+
expect(registry.tools.map((tool) => tool.name)).toContain("ext_echo");
|
|
702
|
+
expect(registry.commands.map((command) => command.name)).toContain(
|
|
703
|
+
"ext:hello",
|
|
704
|
+
);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("validates extension manifest hook stage declarations", async () => {
|
|
708
|
+
const { Agent } = await import("./agent.js");
|
|
709
|
+
|
|
710
|
+
const invalidExtension: AgentExtension = {
|
|
711
|
+
name: "invalid-ext",
|
|
712
|
+
manifest: {
|
|
713
|
+
capabilities: ["hooks"],
|
|
714
|
+
hookStages: ["runtime_event"],
|
|
715
|
+
},
|
|
716
|
+
onInput: () => undefined,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
expect(
|
|
720
|
+
() =>
|
|
721
|
+
new Agent({
|
|
722
|
+
providerId: "anthropic",
|
|
723
|
+
modelId: "mock-model",
|
|
724
|
+
systemPrompt: "invalid",
|
|
725
|
+
tools: [],
|
|
726
|
+
extensions: [invalidExtension],
|
|
727
|
+
}),
|
|
728
|
+
).toThrow(/declared but handler "onRuntimeEvent" is missing/i);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("supports event subscriptions without mutating config callbacks", async () => {
|
|
732
|
+
const { Agent } = await import("./agent.js");
|
|
733
|
+
const handler = makeHandler([
|
|
734
|
+
[
|
|
735
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
736
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
737
|
+
{ type: "done", id: "r1", success: true },
|
|
738
|
+
],
|
|
739
|
+
]);
|
|
740
|
+
createHandlerMock.mockReturnValue(handler);
|
|
741
|
+
|
|
742
|
+
const onEvent = vi.fn();
|
|
743
|
+
const subscriberA = vi.fn();
|
|
744
|
+
const subscriberB = vi.fn();
|
|
745
|
+
const agent = new Agent({
|
|
746
|
+
providerId: "anthropic",
|
|
747
|
+
modelId: "mock-model",
|
|
748
|
+
systemPrompt: "events",
|
|
749
|
+
tools: [],
|
|
750
|
+
onEvent,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
const unsubscribeA = agent.subscribeEvents(subscriberA);
|
|
754
|
+
const unsubscribeB = agent.subscribeEvents(subscriberB);
|
|
755
|
+
unsubscribeB();
|
|
756
|
+
|
|
757
|
+
await agent.run("hello");
|
|
758
|
+
|
|
759
|
+
expect(onEvent).toHaveBeenCalled();
|
|
760
|
+
expect(subscriberA).toHaveBeenCalled();
|
|
761
|
+
expect(subscriberB).not.toHaveBeenCalled();
|
|
762
|
+
|
|
763
|
+
unsubscribeA();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("dispatches newly supported extension lifecycle stages", async () => {
|
|
767
|
+
const { Agent } = await import("./agent.js");
|
|
768
|
+
const handler = makeHandler([
|
|
769
|
+
[
|
|
770
|
+
{ type: "text", id: "r1", text: "done" },
|
|
771
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
772
|
+
{ type: "done", id: "r1", success: true },
|
|
773
|
+
],
|
|
774
|
+
]);
|
|
775
|
+
createHandlerMock.mockReturnValue(handler);
|
|
776
|
+
|
|
777
|
+
const onRunStart = vi.fn(() => undefined);
|
|
778
|
+
const onIterationStart = vi.fn(() => undefined);
|
|
779
|
+
const onTurnStart = vi.fn(() => undefined);
|
|
780
|
+
const onIterationEnd = vi.fn(async () => undefined);
|
|
781
|
+
const onRunEnd = vi.fn(async () => undefined);
|
|
782
|
+
const extension: AgentExtension = {
|
|
783
|
+
name: "lifecycle-extension",
|
|
784
|
+
manifest: {
|
|
785
|
+
capabilities: ["hooks"],
|
|
786
|
+
hookStages: [
|
|
787
|
+
"run_start",
|
|
788
|
+
"iteration_start",
|
|
789
|
+
"turn_start",
|
|
790
|
+
"iteration_end",
|
|
791
|
+
"run_end",
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
onRunStart,
|
|
795
|
+
onIterationStart,
|
|
796
|
+
onTurnStart,
|
|
797
|
+
onIterationEnd,
|
|
798
|
+
onRunEnd,
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const agent = new Agent({
|
|
802
|
+
providerId: "anthropic",
|
|
803
|
+
modelId: "mock-model",
|
|
804
|
+
systemPrompt: "hooks",
|
|
805
|
+
tools: [],
|
|
806
|
+
extensions: [extension],
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
await agent.run("hello");
|
|
810
|
+
|
|
811
|
+
expect(onRunStart).toHaveBeenCalledTimes(1);
|
|
812
|
+
expect(onIterationStart).toHaveBeenCalledTimes(1);
|
|
813
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
814
|
+
expect(onIterationEnd).toHaveBeenCalledTimes(1);
|
|
815
|
+
expect(onRunEnd).toHaveBeenCalledTimes(1);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it("rejects overlapping runs on the same agent instance", async () => {
|
|
819
|
+
const { Agent } = await import("./agent.js");
|
|
820
|
+
let releaseFirstTurn!: () => void;
|
|
821
|
+
const firstTurnBlocked = new Promise<void>((resolve) => {
|
|
822
|
+
releaseFirstTurn = resolve;
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const handler = {
|
|
826
|
+
createMessage: vi.fn(async function* () {
|
|
827
|
+
yield { type: "text", id: "r1", text: "working" };
|
|
828
|
+
await firstTurnBlocked;
|
|
829
|
+
yield { type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 };
|
|
830
|
+
yield { type: "done", id: "r1", success: true };
|
|
831
|
+
}),
|
|
832
|
+
getModel: vi.fn(() => ({ id: "mock-model", info: {} })),
|
|
833
|
+
getMessages: vi.fn(),
|
|
834
|
+
};
|
|
835
|
+
createHandlerMock.mockReturnValue(handler);
|
|
836
|
+
|
|
837
|
+
const agent = new Agent({
|
|
838
|
+
providerId: "anthropic",
|
|
839
|
+
modelId: "mock-model",
|
|
840
|
+
systemPrompt: "concurrency",
|
|
841
|
+
tools: [],
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const firstRun = agent.run("first");
|
|
845
|
+
await Promise.resolve();
|
|
846
|
+
|
|
847
|
+
await expect(agent.continue("second")).rejects.toThrow(
|
|
848
|
+
/state is "running"/i,
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
releaseFirstTurn();
|
|
852
|
+
await firstRun;
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("adds image blocks to initial user content when provided", async () => {
|
|
856
|
+
const { Agent } = await import("./agent.js");
|
|
857
|
+
const handler = makeHandler([
|
|
858
|
+
[
|
|
859
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
860
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
861
|
+
{ type: "done", id: "r1", success: true },
|
|
862
|
+
],
|
|
863
|
+
]);
|
|
864
|
+
createHandlerMock.mockReturnValue(handler);
|
|
865
|
+
|
|
866
|
+
const agent = new Agent({
|
|
867
|
+
providerId: "anthropic",
|
|
868
|
+
modelId: "mock-model",
|
|
869
|
+
systemPrompt: "You are helpful.",
|
|
870
|
+
tools: [],
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
await agent.run("Analyze this image", ["data:image/png;base64,aGVsbG8="]);
|
|
874
|
+
|
|
875
|
+
expect(handler.createMessage).toHaveBeenCalledTimes(1);
|
|
876
|
+
const requestMessages = handler.createMessage.mock.calls[0]?.[1] as Array<{
|
|
877
|
+
role: string;
|
|
878
|
+
content: unknown;
|
|
879
|
+
}>;
|
|
880
|
+
expect(requestMessages[0]?.role).toBe("user");
|
|
881
|
+
expect(requestMessages[0]?.content).toEqual([
|
|
882
|
+
{ type: "text", text: "Analyze this image" },
|
|
883
|
+
{ type: "image", mediaType: "image/png", data: "aGVsbG8=" },
|
|
884
|
+
]);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it("adds attached file content block to initial user content", async () => {
|
|
888
|
+
const { Agent } = await import("./agent.js");
|
|
889
|
+
const handler = makeHandler([
|
|
890
|
+
[
|
|
891
|
+
{ type: "text", id: "r1", text: "ok" },
|
|
892
|
+
{ type: "usage", id: "r1", inputTokens: 1, outputTokens: 1 },
|
|
893
|
+
{ type: "done", id: "r1", success: true },
|
|
894
|
+
],
|
|
895
|
+
]);
|
|
896
|
+
createHandlerMock.mockReturnValue(handler);
|
|
897
|
+
|
|
898
|
+
const tempDir = await mkdtemp(join(tmpdir(), "agents-run-files-"));
|
|
899
|
+
const filePath = join(tempDir, "note.txt");
|
|
900
|
+
try {
|
|
901
|
+
await writeFile(filePath, "hello from file", "utf8");
|
|
902
|
+
const agent = new Agent({
|
|
903
|
+
providerId: "anthropic",
|
|
904
|
+
modelId: "mock-model",
|
|
905
|
+
systemPrompt: "You are helpful.",
|
|
906
|
+
tools: [],
|
|
907
|
+
userFileContentLoader: (path) => readFile(path, "utf8"),
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
await agent.run("Use this file", undefined, [filePath]);
|
|
911
|
+
|
|
912
|
+
expect(handler.createMessage).toHaveBeenCalledTimes(1);
|
|
913
|
+
const requestMessages = handler.createMessage.mock
|
|
914
|
+
.calls[0]?.[1] as Array<{
|
|
915
|
+
role: string;
|
|
916
|
+
content: unknown;
|
|
917
|
+
}>;
|
|
918
|
+
expect(requestMessages[0]?.role).toBe("user");
|
|
919
|
+
expect(requestMessages[0]?.content).toEqual([
|
|
920
|
+
{ type: "text", text: "Use this file" },
|
|
921
|
+
{
|
|
922
|
+
type: "file",
|
|
923
|
+
path: filePath.replace(/\\/g, "/"),
|
|
924
|
+
content: "hello from file",
|
|
925
|
+
},
|
|
926
|
+
]);
|
|
927
|
+
} finally {
|
|
928
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
});
|