kernl 0.1.3 → 0.2.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/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +18 -0
- package/dist/agent.d.ts +20 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +60 -41
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/kernl.d.ts +27 -1
- package/dist/kernl.d.ts.map +1 -1
- package/dist/kernl.js +36 -2
- package/dist/mcp/__tests__/integration.test.js +16 -0
- package/dist/thread/__tests__/fixtures/mock-model.d.ts +7 -0
- package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -0
- package/dist/thread/__tests__/fixtures/mock-model.js +59 -0
- package/dist/thread/__tests__/integration.test.d.ts +2 -0
- package/dist/thread/__tests__/integration.test.d.ts.map +1 -0
- package/dist/thread/__tests__/integration.test.js +247 -0
- package/dist/thread/__tests__/stream.test.d.ts +2 -0
- package/dist/thread/__tests__/stream.test.d.ts.map +1 -0
- package/dist/thread/__tests__/stream.test.js +244 -0
- package/dist/thread/__tests__/thread.test.js +612 -763
- package/dist/thread/thread.d.ts +30 -25
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +114 -314
- package/dist/thread/utils.d.ts +16 -1
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/utils.js +30 -0
- package/dist/tool/index.d.ts +1 -1
- package/dist/tool/index.d.ts.map +1 -1
- package/dist/tool/index.js +1 -1
- package/dist/tool/tool.d.ts.map +1 -1
- package/dist/tool/tool.js +6 -2
- package/dist/tool/toolkit.d.ts +13 -3
- package/dist/tool/toolkit.d.ts.map +1 -1
- package/dist/tool/toolkit.js +11 -3
- package/dist/tool/types.d.ts +8 -0
- package/dist/tool/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +5 -5
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/thread.d.ts +10 -16
- package/dist/types/thread.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/agent.ts +97 -86
- package/src/index.ts +1 -1
- package/src/kernl.ts +51 -2
- package/src/mcp/__tests__/integration.test.ts +17 -0
- package/src/thread/__tests__/fixtures/mock-model.ts +71 -0
- package/src/thread/__tests__/integration.test.ts +349 -0
- package/src/thread/__tests__/thread.test.ts +625 -775
- package/src/thread/thread.ts +134 -381
- package/src/thread/utils.ts +36 -1
- package/src/tool/index.ts +1 -1
- package/src/tool/tool.ts +6 -2
- package/src/tool/toolkit.ts +19 -3
- package/src/tool/types.ts +10 -0
- package/src/types/agent.ts +9 -6
- package/src/types/thread.ts +25 -17
|
@@ -9,9 +9,18 @@ import { tool } from "@/tool";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { Thread } from "@/thread";
|
|
11
11
|
import { createMCPToolStaticFilter } from "../utils";
|
|
12
|
+
import { createMockModel } from "@/thread/__tests__/fixtures/mock-model";
|
|
12
13
|
|
|
13
14
|
const TEST_SERVER = path.join(__dirname, "fixtures", "server.ts");
|
|
14
15
|
|
|
16
|
+
// Mock model for tests that only need toolkit functionality
|
|
17
|
+
const mockModel = createMockModel(async () => ({
|
|
18
|
+
content: [],
|
|
19
|
+
finishReason: "stop",
|
|
20
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
21
|
+
warnings: [],
|
|
22
|
+
}));
|
|
23
|
+
|
|
15
24
|
describe("MCP Integration Tests", () => {
|
|
16
25
|
describe("MCPToolkit Integration", () => {
|
|
17
26
|
let server: MCPServerStdio;
|
|
@@ -158,6 +167,7 @@ describe("MCP Integration Tests", () => {
|
|
|
158
167
|
id: "test-agent",
|
|
159
168
|
name: "Test Agent",
|
|
160
169
|
instructions: "Test",
|
|
170
|
+
model: mockModel,
|
|
161
171
|
toolkits: [toolkit],
|
|
162
172
|
});
|
|
163
173
|
|
|
@@ -194,6 +204,7 @@ describe("MCP Integration Tests", () => {
|
|
|
194
204
|
id: "test-agent",
|
|
195
205
|
name: "Test Agent",
|
|
196
206
|
instructions: "Test",
|
|
207
|
+
model: mockModel,
|
|
197
208
|
toolkits: [toolkit],
|
|
198
209
|
});
|
|
199
210
|
|
|
@@ -232,6 +243,7 @@ describe("MCP Integration Tests", () => {
|
|
|
232
243
|
id: "test-agent",
|
|
233
244
|
name: "Test Agent",
|
|
234
245
|
instructions: "Test",
|
|
246
|
+
model: mockModel,
|
|
235
247
|
toolkits: [toolkit],
|
|
236
248
|
});
|
|
237
249
|
|
|
@@ -265,6 +277,7 @@ describe("MCP Integration Tests", () => {
|
|
|
265
277
|
id: "test-agent",
|
|
266
278
|
name: "Test Agent",
|
|
267
279
|
instructions: "Test",
|
|
280
|
+
model: mockModel,
|
|
268
281
|
toolkits: [mcpToolkit],
|
|
269
282
|
});
|
|
270
283
|
|
|
@@ -294,6 +307,7 @@ describe("MCP Integration Tests", () => {
|
|
|
294
307
|
id: "test-agent",
|
|
295
308
|
name: "Test Agent",
|
|
296
309
|
instructions: "Test",
|
|
310
|
+
model: mockModel,
|
|
297
311
|
toolkits: [mcpToolkit],
|
|
298
312
|
});
|
|
299
313
|
|
|
@@ -325,6 +339,7 @@ describe("MCP Integration Tests", () => {
|
|
|
325
339
|
id: "test-agent",
|
|
326
340
|
name: "Test Agent",
|
|
327
341
|
instructions: "Test",
|
|
342
|
+
model: mockModel,
|
|
328
343
|
toolkits: [mcpToolkit],
|
|
329
344
|
});
|
|
330
345
|
|
|
@@ -382,6 +397,7 @@ describe("MCP Integration Tests", () => {
|
|
|
382
397
|
id: "test-agent",
|
|
383
398
|
name: "Test Agent",
|
|
384
399
|
instructions: "Test",
|
|
400
|
+
model: mockModel,
|
|
385
401
|
toolkits: [mcpToolkit, functionToolkit],
|
|
386
402
|
});
|
|
387
403
|
|
|
@@ -431,6 +447,7 @@ describe("MCP Integration Tests", () => {
|
|
|
431
447
|
id: "test-agent",
|
|
432
448
|
name: "Test Agent",
|
|
433
449
|
instructions: "Test",
|
|
450
|
+
model: mockModel,
|
|
434
451
|
toolkits: [mcpToolkit, functionToolkit],
|
|
435
452
|
});
|
|
436
453
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LanguageModel,
|
|
3
|
+
LanguageModelRequest,
|
|
4
|
+
LanguageModelResponse,
|
|
5
|
+
LanguageModelItem,
|
|
6
|
+
LanguageModelStreamEvent,
|
|
7
|
+
} from "@kernl-sdk/protocol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper to convert LanguageModelResponse content to stream events.
|
|
11
|
+
* Yields both delta events (for streaming UX) and complete items (for history).
|
|
12
|
+
*/
|
|
13
|
+
async function* streamFromResponse(
|
|
14
|
+
response: LanguageModelResponse,
|
|
15
|
+
): AsyncGenerator<LanguageModelStreamEvent> {
|
|
16
|
+
for (const item of response.content) {
|
|
17
|
+
if (item.kind === "message") {
|
|
18
|
+
// Stream message with text deltas
|
|
19
|
+
for (const contentItem of item.content) {
|
|
20
|
+
if (contentItem.kind === "text") {
|
|
21
|
+
// Yield text-start
|
|
22
|
+
yield {
|
|
23
|
+
kind: "text-start" as const,
|
|
24
|
+
id: item.id,
|
|
25
|
+
};
|
|
26
|
+
// Yield text-delta
|
|
27
|
+
yield {
|
|
28
|
+
kind: "text-delta" as const,
|
|
29
|
+
id: item.id,
|
|
30
|
+
text: contentItem.text,
|
|
31
|
+
};
|
|
32
|
+
// Yield text-end
|
|
33
|
+
yield {
|
|
34
|
+
kind: "text-end" as const,
|
|
35
|
+
id: item.id,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Yield complete message
|
|
40
|
+
yield item;
|
|
41
|
+
} else {
|
|
42
|
+
// For tool-call, reasoning, tool-result - just yield as-is
|
|
43
|
+
yield item;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Yield finish event
|
|
47
|
+
yield {
|
|
48
|
+
kind: "finish" as const,
|
|
49
|
+
finishReason: response.finishReason,
|
|
50
|
+
usage: response.usage,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a mock LanguageModel that automatically implements streaming
|
|
56
|
+
* based on the generate() implementation.
|
|
57
|
+
*/
|
|
58
|
+
export function createMockModel(
|
|
59
|
+
generateFn: (req: LanguageModelRequest) => Promise<LanguageModelResponse>,
|
|
60
|
+
): LanguageModel {
|
|
61
|
+
return {
|
|
62
|
+
spec: "1.0" as const,
|
|
63
|
+
provider: "test",
|
|
64
|
+
modelId: "test-model",
|
|
65
|
+
generate: generateFn,
|
|
66
|
+
stream: async function* (req: LanguageModelRequest) {
|
|
67
|
+
const response = await generateFn(req);
|
|
68
|
+
yield* streamFromResponse(response);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { openai } from "@ai-sdk/openai";
|
|
4
|
+
import { AISDKLanguageModel } from "@kernl-sdk/ai";
|
|
5
|
+
|
|
6
|
+
import { Agent } from "@/agent";
|
|
7
|
+
import { Kernl } from "@/kernl";
|
|
8
|
+
import { tool, Toolkit } from "@/tool";
|
|
9
|
+
|
|
10
|
+
import { Thread } from "../thread";
|
|
11
|
+
|
|
12
|
+
import type { ThreadEvent, ThreadStreamEvent } from "@/types/thread";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Integration tests for Thread streaming with real AI SDK providers.
|
|
16
|
+
*
|
|
17
|
+
* These tests require an OPENAI_API_KEY environment variable to be set.
|
|
18
|
+
* They will be skipped if the API key is not available.
|
|
19
|
+
*
|
|
20
|
+
* Run with: OPENAI_API_KEY=your-key pnpm test:run
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
|
|
24
|
+
|
|
25
|
+
describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
26
|
+
"Thread streaming integration",
|
|
27
|
+
() => {
|
|
28
|
+
let kernl: Kernl;
|
|
29
|
+
let model: AISDKLanguageModel;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
kernl = new Kernl();
|
|
33
|
+
model = new AISDKLanguageModel(openai("gpt-4o"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("stream()", () => {
|
|
37
|
+
it(
|
|
38
|
+
"should yield both delta events and complete items",
|
|
39
|
+
async () => {
|
|
40
|
+
const agent = new Agent({
|
|
41
|
+
id: "test-stream",
|
|
42
|
+
name: "Test Stream Agent",
|
|
43
|
+
instructions: "You are a helpful assistant.",
|
|
44
|
+
model,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const input: ThreadEvent[] = [
|
|
48
|
+
{
|
|
49
|
+
kind: "message",
|
|
50
|
+
id: "msg-1",
|
|
51
|
+
role: "user",
|
|
52
|
+
content: [
|
|
53
|
+
{ kind: "text", text: "Say 'Hello World' and nothing else." },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const thread = new Thread(kernl, agent, input);
|
|
59
|
+
const events = [];
|
|
60
|
+
|
|
61
|
+
for await (const event of thread.stream()) {
|
|
62
|
+
events.push(event);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(events.length).toBeGreaterThan(0);
|
|
66
|
+
|
|
67
|
+
// Should have text-delta events (for streaming UX)
|
|
68
|
+
const textDeltas = events.filter((e) => e.kind === "text-delta");
|
|
69
|
+
expect(textDeltas.length).toBeGreaterThan(0);
|
|
70
|
+
|
|
71
|
+
// Should have text-start event
|
|
72
|
+
const textStarts = events.filter((e) => e.kind === "text-start");
|
|
73
|
+
expect(textStarts.length).toBeGreaterThan(0);
|
|
74
|
+
|
|
75
|
+
// Should have text-end event
|
|
76
|
+
const textEnds = events.filter((e) => e.kind === "text-end");
|
|
77
|
+
expect(textEnds.length).toBeGreaterThan(0);
|
|
78
|
+
|
|
79
|
+
// Should have complete Message item (for history)
|
|
80
|
+
const messages = events.filter((e) => e.kind === "message");
|
|
81
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
82
|
+
|
|
83
|
+
const assistantMessage = messages.find(
|
|
84
|
+
(m: any) => m.role === "assistant",
|
|
85
|
+
);
|
|
86
|
+
expect(assistantMessage).toBeDefined();
|
|
87
|
+
expect((assistantMessage as any).content).toBeDefined();
|
|
88
|
+
expect((assistantMessage as any).content.length).toBeGreaterThan(0);
|
|
89
|
+
|
|
90
|
+
// Message should have accumulated text from all deltas
|
|
91
|
+
const textContent = (assistantMessage as any).content.find(
|
|
92
|
+
(c: any) => c.kind === "text",
|
|
93
|
+
);
|
|
94
|
+
expect(textContent).toBeDefined();
|
|
95
|
+
expect(textContent.text).toBeDefined();
|
|
96
|
+
expect(textContent.text.length).toBeGreaterThan(0);
|
|
97
|
+
|
|
98
|
+
// Verify accumulated text matches concatenated deltas
|
|
99
|
+
const accumulatedFromDeltas = textDeltas.map((d: any) => d.text).join("");
|
|
100
|
+
expect(textContent.text).toBe(accumulatedFromDeltas);
|
|
101
|
+
|
|
102
|
+
// Should have finish event
|
|
103
|
+
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
104
|
+
expect(finishEvents.length).toBe(1);
|
|
105
|
+
},
|
|
106
|
+
30000,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
it(
|
|
110
|
+
"should filter deltas from history but include complete items",
|
|
111
|
+
async () => {
|
|
112
|
+
const agent = new Agent({
|
|
113
|
+
id: "test-history",
|
|
114
|
+
name: "Test History Agent",
|
|
115
|
+
instructions: "You are a helpful assistant.",
|
|
116
|
+
model,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const input: ThreadEvent[] = [
|
|
120
|
+
{
|
|
121
|
+
kind: "message",
|
|
122
|
+
id: "msg-1",
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [{ kind: "text", text: "Count to 3" }],
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const thread = new Thread(kernl, agent, input);
|
|
129
|
+
const streamEvents = [];
|
|
130
|
+
|
|
131
|
+
for await (const event of thread.stream()) {
|
|
132
|
+
streamEvents.push(event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Access private history via type assertion for testing
|
|
136
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
137
|
+
|
|
138
|
+
// History should only contain complete items (message, reasoning, tool-call, tool-result)
|
|
139
|
+
// TypeScript already enforces this via ThreadEvent type, but let's verify at runtime
|
|
140
|
+
for (const event of history) {
|
|
141
|
+
expect(["message", "reasoning", "tool-call", "tool-result"]).toContain(
|
|
142
|
+
event.kind,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Stream events should include deltas (but history should not)
|
|
147
|
+
const streamDeltas = streamEvents.filter(
|
|
148
|
+
(e: any) =>
|
|
149
|
+
e.kind === "text-delta" ||
|
|
150
|
+
e.kind === "text-start" ||
|
|
151
|
+
e.kind === "text-end",
|
|
152
|
+
);
|
|
153
|
+
expect(streamDeltas.length).toBeGreaterThan(0);
|
|
154
|
+
|
|
155
|
+
// History should contain the input message
|
|
156
|
+
expect(history[0]).toEqual(input[0]);
|
|
157
|
+
|
|
158
|
+
// History should contain complete Message items
|
|
159
|
+
const historyMessages = history.filter((e) => e.kind === "message");
|
|
160
|
+
expect(historyMessages.length).toBeGreaterThan(1); // input + assistant response
|
|
161
|
+
|
|
162
|
+
// Verify assistant message has complete text (not deltas)
|
|
163
|
+
const assistantMessage = historyMessages.find(
|
|
164
|
+
(m: any) => m.role === "assistant",
|
|
165
|
+
);
|
|
166
|
+
expect(assistantMessage).toBeDefined();
|
|
167
|
+
const textContent = (assistantMessage as any).content.find(
|
|
168
|
+
(c: any) => c.kind === "text",
|
|
169
|
+
);
|
|
170
|
+
expect(textContent.text).toBeTruthy();
|
|
171
|
+
expect(textContent.text.length).toBeGreaterThan(0);
|
|
172
|
+
},
|
|
173
|
+
30000,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
it("should work with tool calls", async () => {
|
|
177
|
+
const addTool = tool({
|
|
178
|
+
id: "add",
|
|
179
|
+
name: "add",
|
|
180
|
+
description: "Add two numbers together",
|
|
181
|
+
parameters: z.object({
|
|
182
|
+
a: z.number().describe("The first number"),
|
|
183
|
+
b: z.number().describe("The second number"),
|
|
184
|
+
}),
|
|
185
|
+
execute: async (ctx, { a, b }) => {
|
|
186
|
+
return a + b;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const toolkit = new Toolkit({
|
|
191
|
+
id: "math",
|
|
192
|
+
tools: [addTool],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const agent = new Agent({
|
|
196
|
+
id: "test-tools",
|
|
197
|
+
name: "Test Tools Agent",
|
|
198
|
+
instructions: "You are a helpful assistant that can do math.",
|
|
199
|
+
model,
|
|
200
|
+
toolkits: [toolkit],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const input: ThreadEvent[] = [
|
|
204
|
+
{
|
|
205
|
+
kind: "message",
|
|
206
|
+
id: "msg-1",
|
|
207
|
+
role: "user",
|
|
208
|
+
content: [{ kind: "text", text: "What is 25 + 17?" }],
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const thread = new Thread(kernl, agent, input);
|
|
213
|
+
const events: ThreadStreamEvent[] = [];
|
|
214
|
+
|
|
215
|
+
for await (const event of thread.stream()) {
|
|
216
|
+
events.push(event);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
expect(events.length).toBeGreaterThan(0);
|
|
220
|
+
|
|
221
|
+
// Should have tool calls
|
|
222
|
+
const toolCalls = events.filter(
|
|
223
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
|
|
224
|
+
e.kind === "tool-call",
|
|
225
|
+
);
|
|
226
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
227
|
+
|
|
228
|
+
// Verify tool was called with correct parameters
|
|
229
|
+
const addToolCall = toolCalls.find((tc) => tc.toolId === "add");
|
|
230
|
+
expect(addToolCall).toBeDefined();
|
|
231
|
+
expect(JSON.parse(addToolCall!.arguments)).toEqual({ a: 25, b: 17 });
|
|
232
|
+
|
|
233
|
+
// Should have tool results
|
|
234
|
+
const toolResults = events.filter(
|
|
235
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
|
|
236
|
+
e.kind === "tool-result",
|
|
237
|
+
);
|
|
238
|
+
expect(toolResults.length).toBeGreaterThan(0);
|
|
239
|
+
|
|
240
|
+
// Verify tool result is correct
|
|
241
|
+
const addToolResult = toolResults.find(
|
|
242
|
+
(tr) => tr.callId === addToolCall!.callId,
|
|
243
|
+
);
|
|
244
|
+
expect(addToolResult).toBeDefined();
|
|
245
|
+
expect(addToolResult!.result).toBe(42);
|
|
246
|
+
|
|
247
|
+
// History should contain tool calls and results
|
|
248
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
249
|
+
const historyToolCalls = history.filter((e) => e.kind === "tool-call");
|
|
250
|
+
const historyToolResults = history.filter(
|
|
251
|
+
(e) => e.kind === "tool-result",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(historyToolCalls.length).toBe(toolCalls.length);
|
|
255
|
+
expect(historyToolResults.length).toBe(toolResults.length);
|
|
256
|
+
|
|
257
|
+
// Verify the assistant's final response references the correct answer
|
|
258
|
+
const messages = events.filter((e) => e.kind === "message");
|
|
259
|
+
const assistantMessage = messages.find((m: any) => m.role === "assistant");
|
|
260
|
+
expect(assistantMessage).toBeDefined();
|
|
261
|
+
const textContent = (assistantMessage as any).content.find(
|
|
262
|
+
(c: any) => c.kind === "text",
|
|
263
|
+
);
|
|
264
|
+
expect(textContent).toBeDefined();
|
|
265
|
+
expect(textContent.text).toContain("42");
|
|
266
|
+
},
|
|
267
|
+
30000,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("execute()", () => {
|
|
272
|
+
it(
|
|
273
|
+
"should consume stream and return final response",
|
|
274
|
+
async () => {
|
|
275
|
+
const agent = new Agent({
|
|
276
|
+
id: "test-blocking",
|
|
277
|
+
name: "Test Blocking Agent",
|
|
278
|
+
instructions: "You are a helpful assistant.",
|
|
279
|
+
model,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const input: ThreadEvent[] = [
|
|
283
|
+
{
|
|
284
|
+
kind: "message",
|
|
285
|
+
id: "msg-1",
|
|
286
|
+
role: "user",
|
|
287
|
+
content: [{ kind: "text", text: "Say 'Testing' and nothing else." }],
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const thread = new Thread(kernl, agent, input);
|
|
292
|
+
const result = await thread.execute();
|
|
293
|
+
|
|
294
|
+
// Should have a response
|
|
295
|
+
expect(result.response).toBeDefined();
|
|
296
|
+
expect(typeof result.response).toBe("string");
|
|
297
|
+
expect(result.response.length).toBeGreaterThan(0);
|
|
298
|
+
|
|
299
|
+
// Should have final state
|
|
300
|
+
expect(result.state).toBe("stopped");
|
|
301
|
+
},
|
|
302
|
+
30000,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
it(
|
|
306
|
+
"should validate structured output in blocking mode",
|
|
307
|
+
async () => {
|
|
308
|
+
const responseSchema = z.object({
|
|
309
|
+
name: z.string(),
|
|
310
|
+
age: z.number(),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const agent = new Agent({
|
|
314
|
+
id: "test-structured",
|
|
315
|
+
name: "Test Structured Agent",
|
|
316
|
+
instructions:
|
|
317
|
+
"You are a helpful assistant. Return JSON with name and age fields.",
|
|
318
|
+
model,
|
|
319
|
+
responseType: responseSchema,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const input: ThreadEvent[] = [
|
|
323
|
+
{
|
|
324
|
+
kind: "message",
|
|
325
|
+
id: "msg-1",
|
|
326
|
+
role: "user",
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
kind: "text",
|
|
330
|
+
text: 'Return a JSON object with name "Alice" and age 30',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const thread = new Thread(kernl, agent, input);
|
|
337
|
+
const result = await thread.execute();
|
|
338
|
+
|
|
339
|
+
// Response should be validated and parsed
|
|
340
|
+
expect(result.response).toBeDefined();
|
|
341
|
+
expect(typeof result.response).toBe("object");
|
|
342
|
+
expect((result.response as any).name).toBeTruthy();
|
|
343
|
+
expect(typeof (result.response as any).age).toBe("number");
|
|
344
|
+
},
|
|
345
|
+
30000,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
);
|