kernl 0.7.3 → 0.8.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 +1 -1
- package/CHANGELOG.md +24 -0
- package/dist/agent/__tests__/systools.test.js +13 -9
- package/dist/agent/types.d.ts +20 -12
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent.d.ts +12 -8
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +14 -14
- package/dist/api/resources/agents/agents.d.ts +5 -5
- package/dist/api/resources/agents/agents.d.ts.map +1 -1
- package/dist/api/resources/agents/agents.js +1 -1
- package/dist/guardrail.d.ts +19 -19
- package/dist/guardrail.d.ts.map +1 -1
- package/dist/kernl/kernl.d.ts +6 -6
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/lib/error.d.ts +3 -3
- package/dist/lib/error.d.ts.map +1 -1
- package/dist/lifecycle.d.ts +6 -6
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/memory/__tests__/encoder.test.d.ts +2 -0
- package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
- package/dist/memory/__tests__/encoder.test.js +120 -0
- package/dist/memory/codecs/domain.d.ts +5 -0
- package/dist/memory/codecs/domain.d.ts.map +1 -1
- package/dist/memory/codecs/domain.js +6 -0
- package/dist/memory/encoder.d.ts +25 -2
- package/dist/memory/encoder.d.ts.map +1 -1
- package/dist/memory/encoder.js +46 -5
- package/dist/memory/index.d.ts +2 -2
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/schema.d.ts.map +1 -1
- package/dist/memory/schema.js +5 -0
- package/dist/memory/types.d.ts +21 -4
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/storage/in-memory.d.ts.map +1 -1
- package/dist/storage/in-memory.js +4 -2
- package/dist/thread/__tests__/integration.test.js +1 -1
- package/dist/thread/__tests__/thread.test.js +8 -8
- package/dist/thread/thread.d.ts +5 -5
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +13 -2
- package/dist/thread/types.d.ts +9 -6
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +7 -6
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/utils.js +9 -8
- package/dist/tool/sys/memory.d.ts +1 -1
- package/dist/tool/sys/memory.d.ts.map +1 -1
- package/dist/tool/sys/memory.js +29 -8
- package/package.json +4 -3
- package/src/agent/__tests__/systools.test.ts +13 -9
- package/src/agent/types.ts +25 -29
- package/src/agent.ts +29 -28
- package/src/api/resources/agents/agents.ts +8 -8
- package/src/guardrail.ts +28 -28
- package/src/kernl/kernl.ts +12 -12
- package/src/lib/error.ts +3 -3
- package/src/lifecycle.ts +6 -6
- package/src/memory/__tests__/encoder.test.ts +153 -0
- package/src/memory/codecs/domain.ts +6 -0
- package/src/memory/encoder.ts +51 -6
- package/src/memory/index.ts +2 -1
- package/src/memory/schema.ts +5 -0
- package/src/memory/types.ts +20 -4
- package/src/storage/in-memory.ts +6 -2
- package/src/thread/__tests__/integration.test.ts +130 -146
- package/src/thread/__tests__/thread.test.ts +8 -8
- package/src/thread/thread.ts +21 -7
- package/src/thread/types.ts +9 -6
- package/src/thread/utils.ts +15 -14
- package/src/tool/sys/memory.ts +33 -9
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from "vitest";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { openai } from "@ai-sdk/openai";
|
|
4
4
|
import { AISDKLanguageModel } from "@kernl-sdk/ai";
|
|
5
|
+
import "@kernl-sdk/ai/openai"; // (TMP)
|
|
5
6
|
|
|
6
7
|
import { Agent } from "@/agent";
|
|
7
8
|
import { Kernl } from "@/kernl";
|
|
@@ -23,21 +24,17 @@ import type { LanguageModelItem } from "@kernl-sdk/protocol";
|
|
|
23
24
|
|
|
24
25
|
const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
|
|
25
26
|
|
|
26
|
-
describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
describe("stream()", () => {
|
|
38
|
-
it(
|
|
39
|
-
"should yield both delta events and complete items",
|
|
40
|
-
async () => {
|
|
27
|
+
describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
|
|
28
|
+
let kernl: Kernl;
|
|
29
|
+
let model: AISDKLanguageModel;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
kernl = new Kernl();
|
|
33
|
+
model = new AISDKLanguageModel(openai("gpt-4.1"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("stream()", () => {
|
|
37
|
+
it("should yield both delta events and complete items", async () => {
|
|
41
38
|
const agent = new Agent({
|
|
42
39
|
id: "test-stream",
|
|
43
40
|
name: "Test Stream Agent",
|
|
@@ -103,13 +100,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
103
100
|
// Should have finish event
|
|
104
101
|
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
105
102
|
expect(finishEvents.length).toBe(1);
|
|
106
|
-
|
|
107
|
-
30000,
|
|
108
|
-
);
|
|
103
|
+
}, 30000);
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
"should filter deltas from history but include complete items",
|
|
112
|
-
async () => {
|
|
105
|
+
it("should filter deltas from history but include complete items", async () => {
|
|
113
106
|
const agent = new Agent({
|
|
114
107
|
id: "test-history",
|
|
115
108
|
name: "Test History Agent",
|
|
@@ -153,8 +146,12 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
153
146
|
);
|
|
154
147
|
expect(streamDeltas.length).toBeGreaterThan(0);
|
|
155
148
|
|
|
156
|
-
// History should contain the input message
|
|
157
|
-
expect(history[0]).
|
|
149
|
+
// History should contain the input message (with ThreadEvent headers added)
|
|
150
|
+
expect(history[0]).toMatchObject({
|
|
151
|
+
kind: "message",
|
|
152
|
+
role: "user",
|
|
153
|
+
content: [{ kind: "text", text: "Count to 3" }],
|
|
154
|
+
});
|
|
158
155
|
|
|
159
156
|
// History should contain complete Message items
|
|
160
157
|
const historyMessages = history.filter((e) => e.kind === "message");
|
|
@@ -170,11 +167,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
170
167
|
);
|
|
171
168
|
expect(textContent.text).toBeTruthy();
|
|
172
169
|
expect(textContent.text.length).toBeGreaterThan(0);
|
|
173
|
-
|
|
174
|
-
30000,
|
|
175
|
-
);
|
|
170
|
+
}, 30000);
|
|
176
171
|
|
|
177
|
-
|
|
172
|
+
it("should work with tool calls", async () => {
|
|
178
173
|
const addTool = tool({
|
|
179
174
|
id: "add",
|
|
180
175
|
name: "add",
|
|
@@ -257,122 +252,118 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
257
252
|
|
|
258
253
|
// Verify the assistant's final response references the correct answer
|
|
259
254
|
const messages = events.filter((e) => e.kind === "message");
|
|
260
|
-
const assistantMessage = messages.find(
|
|
255
|
+
const assistantMessage = messages.find(
|
|
256
|
+
(m: any) => m.role === "assistant",
|
|
257
|
+
);
|
|
261
258
|
expect(assistantMessage).toBeDefined();
|
|
262
259
|
const textContent = (assistantMessage as any).content.find(
|
|
263
260
|
(c: any) => c.kind === "text",
|
|
264
261
|
);
|
|
265
262
|
expect(textContent).toBeDefined();
|
|
266
263
|
expect(textContent.text).toContain("42");
|
|
264
|
+
}, 30000);
|
|
265
|
+
|
|
266
|
+
it("should properly encode tool results with matching callIds for multi-turn", async () => {
|
|
267
|
+
const multiplyTool = tool({
|
|
268
|
+
id: "multiply",
|
|
269
|
+
name: "multiply",
|
|
270
|
+
description: "Multiply two numbers",
|
|
271
|
+
parameters: z.object({
|
|
272
|
+
a: z.number().describe("First number"),
|
|
273
|
+
b: z.number().describe("Second number"),
|
|
274
|
+
}),
|
|
275
|
+
execute: async (ctx, { a, b }) => {
|
|
276
|
+
return a * b;
|
|
267
277
|
},
|
|
268
|
-
|
|
269
|
-
);
|
|
278
|
+
});
|
|
270
279
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const agent = new Agent({
|
|
293
|
-
id: "test-multi-turn",
|
|
294
|
-
name: "Test Multi-Turn Agent",
|
|
295
|
-
instructions: "You are a helpful assistant that can do math.",
|
|
296
|
-
model,
|
|
297
|
-
toolkits: [toolkit],
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const input: LanguageModelItem[] = [
|
|
301
|
-
{
|
|
302
|
-
kind: "message",
|
|
303
|
-
id: "msg-1",
|
|
304
|
-
role: "user",
|
|
305
|
-
content: [{ kind: "text", text: "What is 7 times 6?" }],
|
|
306
|
-
},
|
|
307
|
-
];
|
|
308
|
-
|
|
309
|
-
const thread = new Thread({ agent, input });
|
|
310
|
-
const events: ThreadStreamEvent[] = [];
|
|
311
|
-
|
|
312
|
-
// Collect all events from the stream
|
|
313
|
-
for await (const event of thread.stream()) {
|
|
314
|
-
events.push(event);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Find the tool call and result
|
|
318
|
-
const toolCalls = events.filter(
|
|
319
|
-
(e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
|
|
320
|
-
e.kind === "tool-call",
|
|
321
|
-
);
|
|
322
|
-
const toolResults = events.filter(
|
|
323
|
-
(e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
|
|
324
|
-
e.kind === "tool-result",
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
expect(toolCalls.length).toBeGreaterThan(0);
|
|
328
|
-
expect(toolResults.length).toBeGreaterThan(0);
|
|
329
|
-
|
|
330
|
-
const multiplyCall = toolCalls[0];
|
|
331
|
-
const multiplyResult = toolResults[0];
|
|
332
|
-
|
|
333
|
-
// Verify callId matches between tool call and result
|
|
334
|
-
expect(multiplyCall.callId).toBe(multiplyResult.callId);
|
|
335
|
-
expect(multiplyCall.toolId).toBe("multiply");
|
|
336
|
-
expect(multiplyResult.toolId).toBe("multiply");
|
|
337
|
-
|
|
338
|
-
// Verify the tool result has the correct structure
|
|
339
|
-
expect(multiplyResult.callId).toBeDefined();
|
|
340
|
-
expect(typeof multiplyResult.callId).toBe("string");
|
|
341
|
-
expect(multiplyResult.callId.length).toBeGreaterThan(0);
|
|
342
|
-
|
|
343
|
-
// Verify history contains both with matching callIds
|
|
344
|
-
const history = (thread as any).history as ThreadEvent[];
|
|
345
|
-
const historyToolCall = history.find(
|
|
346
|
-
(e) => e.kind === "tool-call" && e.toolId === "multiply",
|
|
347
|
-
);
|
|
348
|
-
const historyToolResult = history.find(
|
|
349
|
-
(e) => e.kind === "tool-result" && e.toolId === "multiply",
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
expect(historyToolCall).toBeDefined();
|
|
353
|
-
expect(historyToolResult).toBeDefined();
|
|
354
|
-
expect((historyToolCall as any).callId).toBe(
|
|
355
|
-
(historyToolResult as any).callId,
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
// Verify final response uses the tool result
|
|
359
|
-
const messages = events.filter((e) => e.kind === "message");
|
|
360
|
-
const assistantMessage = messages.find((m: any) => m.role === "assistant");
|
|
361
|
-
expect(assistantMessage).toBeDefined();
|
|
362
|
-
const textContent = (assistantMessage as any).content.find(
|
|
363
|
-
(c: any) => c.kind === "text",
|
|
364
|
-
);
|
|
365
|
-
expect(textContent).toBeDefined();
|
|
366
|
-
expect(textContent.text).toContain("42");
|
|
280
|
+
const toolkit = new Toolkit({
|
|
281
|
+
id: "math",
|
|
282
|
+
tools: [multiplyTool],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const agent = new Agent({
|
|
286
|
+
id: "test-multi-turn",
|
|
287
|
+
name: "Test Multi-Turn Agent",
|
|
288
|
+
instructions: "You are a helpful assistant that can do math.",
|
|
289
|
+
model,
|
|
290
|
+
toolkits: [toolkit],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const input: LanguageModelItem[] = [
|
|
294
|
+
{
|
|
295
|
+
kind: "message",
|
|
296
|
+
id: "msg-1",
|
|
297
|
+
role: "user",
|
|
298
|
+
content: [{ kind: "text", text: "What is 7 times 6?" }],
|
|
367
299
|
},
|
|
368
|
-
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const thread = new Thread({ agent, input });
|
|
303
|
+
const events: ThreadStreamEvent[] = [];
|
|
304
|
+
|
|
305
|
+
// Collect all events from the stream
|
|
306
|
+
for await (const event of thread.stream()) {
|
|
307
|
+
events.push(event);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Find the tool call and result
|
|
311
|
+
const toolCalls = events.filter(
|
|
312
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
|
|
313
|
+
e.kind === "tool-call",
|
|
314
|
+
);
|
|
315
|
+
const toolResults = events.filter(
|
|
316
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
|
|
317
|
+
e.kind === "tool-result",
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
321
|
+
expect(toolResults.length).toBeGreaterThan(0);
|
|
322
|
+
|
|
323
|
+
const multiplyCall = toolCalls[0];
|
|
324
|
+
const multiplyResult = toolResults[0];
|
|
325
|
+
|
|
326
|
+
// Verify callId matches between tool call and result
|
|
327
|
+
expect(multiplyCall.callId).toBe(multiplyResult.callId);
|
|
328
|
+
expect(multiplyCall.toolId).toBe("multiply");
|
|
329
|
+
expect(multiplyResult.toolId).toBe("multiply");
|
|
330
|
+
|
|
331
|
+
// Verify the tool result has the correct structure
|
|
332
|
+
expect(multiplyResult.callId).toBeDefined();
|
|
333
|
+
expect(typeof multiplyResult.callId).toBe("string");
|
|
334
|
+
expect(multiplyResult.callId.length).toBeGreaterThan(0);
|
|
335
|
+
|
|
336
|
+
// Verify history contains both with matching callIds
|
|
337
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
338
|
+
const historyToolCall = history.find(
|
|
339
|
+
(e) => e.kind === "tool-call" && e.toolId === "multiply",
|
|
340
|
+
);
|
|
341
|
+
const historyToolResult = history.find(
|
|
342
|
+
(e) => e.kind === "tool-result" && e.toolId === "multiply",
|
|
369
343
|
);
|
|
370
|
-
});
|
|
371
344
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
expect(historyToolCall).toBeDefined();
|
|
346
|
+
expect(historyToolResult).toBeDefined();
|
|
347
|
+
expect((historyToolCall as any).callId).toBe(
|
|
348
|
+
(historyToolResult as any).callId,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Verify final response uses the tool result
|
|
352
|
+
const messages = events.filter((e) => e.kind === "message");
|
|
353
|
+
const assistantMessage = messages.find(
|
|
354
|
+
(m: any) => m.role === "assistant",
|
|
355
|
+
);
|
|
356
|
+
expect(assistantMessage).toBeDefined();
|
|
357
|
+
const textContent = (assistantMessage as any).content.find(
|
|
358
|
+
(c: any) => c.kind === "text",
|
|
359
|
+
);
|
|
360
|
+
expect(textContent).toBeDefined();
|
|
361
|
+
expect(textContent.text).toContain("42");
|
|
362
|
+
}, 30000);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("execute()", () => {
|
|
366
|
+
it("should consume stream and return final response", async () => {
|
|
376
367
|
const agent = new Agent({
|
|
377
368
|
id: "test-blocking",
|
|
378
369
|
name: "Test Blocking Agent",
|
|
@@ -399,14 +390,10 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
399
390
|
|
|
400
391
|
// Should have final state
|
|
401
392
|
expect(result.state).toBe("stopped");
|
|
402
|
-
|
|
403
|
-
30000,
|
|
404
|
-
);
|
|
393
|
+
}, 30000);
|
|
405
394
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
async () => {
|
|
409
|
-
const responseSchema = z.object({
|
|
395
|
+
it("should validate structured output in blocking mode", async () => {
|
|
396
|
+
const PersonSchema = z.object({
|
|
410
397
|
name: z.string(),
|
|
411
398
|
age: z.number(),
|
|
412
399
|
});
|
|
@@ -417,7 +404,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
417
404
|
instructions:
|
|
418
405
|
"You are a helpful assistant. Return JSON with name and age fields.",
|
|
419
406
|
model,
|
|
420
|
-
|
|
407
|
+
output: PersonSchema,
|
|
421
408
|
});
|
|
422
409
|
|
|
423
410
|
const input: LanguageModelItem[] = [
|
|
@@ -442,9 +429,6 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
442
429
|
expect(typeof result.response).toBe("object");
|
|
443
430
|
expect((result.response as any).name).toBeTruthy();
|
|
444
431
|
expect(typeof (result.response as any).age).toBe("number");
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
449
|
-
},
|
|
450
|
-
);
|
|
432
|
+
}, 30000);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -1061,7 +1061,7 @@ describe("Thread", () => {
|
|
|
1061
1061
|
});
|
|
1062
1062
|
|
|
1063
1063
|
describe("Final Output Parsing", () => {
|
|
1064
|
-
it("should return text output when
|
|
1064
|
+
it("should return text output when output is 'text'", async () => {
|
|
1065
1065
|
const model = createMockModel(async (req: LanguageModelRequest) => {
|
|
1066
1066
|
return {
|
|
1067
1067
|
content: [
|
|
@@ -1087,7 +1087,7 @@ describe("Thread", () => {
|
|
|
1087
1087
|
name: "Test",
|
|
1088
1088
|
instructions: "Test agent",
|
|
1089
1089
|
model,
|
|
1090
|
-
|
|
1090
|
+
output: "text",
|
|
1091
1091
|
});
|
|
1092
1092
|
|
|
1093
1093
|
const kernl = new Kernl();
|
|
@@ -1136,7 +1136,7 @@ describe("Thread", () => {
|
|
|
1136
1136
|
name: "Test",
|
|
1137
1137
|
instructions: "Test agent",
|
|
1138
1138
|
model,
|
|
1139
|
-
|
|
1139
|
+
output: responseSchema,
|
|
1140
1140
|
});
|
|
1141
1141
|
|
|
1142
1142
|
const kernl = new Kernl();
|
|
@@ -1186,7 +1186,7 @@ describe("Thread", () => {
|
|
|
1186
1186
|
name: "Test",
|
|
1187
1187
|
instructions: "Test agent",
|
|
1188
1188
|
model,
|
|
1189
|
-
|
|
1189
|
+
output: responseSchema,
|
|
1190
1190
|
});
|
|
1191
1191
|
|
|
1192
1192
|
const kernl = new Kernl();
|
|
@@ -1231,7 +1231,7 @@ describe("Thread", () => {
|
|
|
1231
1231
|
name: "Test",
|
|
1232
1232
|
instructions: "Test agent",
|
|
1233
1233
|
model,
|
|
1234
|
-
|
|
1234
|
+
output: responseSchema,
|
|
1235
1235
|
});
|
|
1236
1236
|
|
|
1237
1237
|
const kernl = new Kernl();
|
|
@@ -1277,7 +1277,7 @@ describe("Thread", () => {
|
|
|
1277
1277
|
name: "Test",
|
|
1278
1278
|
instructions: "Test agent",
|
|
1279
1279
|
model,
|
|
1280
|
-
|
|
1280
|
+
output: responseSchema,
|
|
1281
1281
|
});
|
|
1282
1282
|
|
|
1283
1283
|
const kernl = new Kernl();
|
|
@@ -1336,7 +1336,7 @@ describe("Thread", () => {
|
|
|
1336
1336
|
name: "Test",
|
|
1337
1337
|
instructions: "Test agent",
|
|
1338
1338
|
model,
|
|
1339
|
-
|
|
1339
|
+
output: responseSchema,
|
|
1340
1340
|
});
|
|
1341
1341
|
|
|
1342
1342
|
const kernl = new Kernl();
|
|
@@ -1405,7 +1405,7 @@ describe("Thread", () => {
|
|
|
1405
1405
|
name: "Test",
|
|
1406
1406
|
instructions: "Test agent",
|
|
1407
1407
|
model,
|
|
1408
|
-
|
|
1408
|
+
output: "text",
|
|
1409
1409
|
});
|
|
1410
1410
|
|
|
1411
1411
|
const kernl = new Kernl();
|
package/src/thread/thread.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
|
+
import { ZodType } from "zod";
|
|
3
|
+
import * as z from "zod";
|
|
2
4
|
|
|
3
5
|
import { Agent } from "@/agent";
|
|
4
6
|
import { Context } from "@/context";
|
|
@@ -30,7 +32,8 @@ import type {
|
|
|
30
32
|
ThreadExecuteResult,
|
|
31
33
|
PerformActionsResult,
|
|
32
34
|
} from "./types";
|
|
33
|
-
import type {
|
|
35
|
+
import type { AgentOutputType } from "@/agent/types";
|
|
36
|
+
import type { LanguageModelResponseType } from "@kernl-sdk/protocol";
|
|
34
37
|
|
|
35
38
|
import {
|
|
36
39
|
tevent,
|
|
@@ -82,11 +85,11 @@ import {
|
|
|
82
85
|
*/
|
|
83
86
|
export class Thread<
|
|
84
87
|
TContext = unknown,
|
|
85
|
-
|
|
88
|
+
TOutput extends AgentOutputType = "text",
|
|
86
89
|
> {
|
|
87
90
|
readonly tid: string;
|
|
88
91
|
readonly namespace: string;
|
|
89
|
-
readonly agent: Agent<TContext,
|
|
92
|
+
readonly agent: Agent<TContext, TOutput>;
|
|
90
93
|
readonly context: Context<TContext>;
|
|
91
94
|
readonly model: LanguageModel; /* inherited from the agent unless specified */
|
|
92
95
|
readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
|
|
@@ -106,7 +109,7 @@ export class Thread<
|
|
|
106
109
|
private abort?: AbortController;
|
|
107
110
|
private storage?: ThreadStore;
|
|
108
111
|
|
|
109
|
-
constructor(options: ThreadOptions<TContext,
|
|
112
|
+
constructor(options: ThreadOptions<TContext, TOutput>) {
|
|
110
113
|
this.tid = options.tid ?? `tid_${randomID()}`;
|
|
111
114
|
this.namespace = options.namespace ?? "kernl";
|
|
112
115
|
this.agent = options.agent;
|
|
@@ -142,7 +145,7 @@ export class Thread<
|
|
|
142
145
|
* Blocking execution - runs until terminal state or interruption
|
|
143
146
|
*/
|
|
144
147
|
async execute(): Promise<
|
|
145
|
-
ThreadExecuteResult<ResolvedAgentResponse<
|
|
148
|
+
ThreadExecuteResult<ResolvedAgentResponse<TOutput>>
|
|
146
149
|
> {
|
|
147
150
|
for await (const _event of this.stream()) {
|
|
148
151
|
// just consume the stream (already in history in _execute())
|
|
@@ -158,7 +161,7 @@ export class Thread<
|
|
|
158
161
|
|
|
159
162
|
const text = getFinalResponse(items);
|
|
160
163
|
assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
|
|
161
|
-
const parsed = parseFinalResponse(text, this.agent.
|
|
164
|
+
const parsed = parseFinalResponse(text, this.agent.output);
|
|
162
165
|
|
|
163
166
|
return { response: parsed, state: this.state };
|
|
164
167
|
}
|
|
@@ -405,7 +408,7 @@ export class Thread<
|
|
|
405
408
|
e.kind === "tool-result" &&
|
|
406
409
|
(e.state as any) === "requires_approval" // (TODO): fix this
|
|
407
410
|
) {
|
|
408
|
-
//
|
|
411
|
+
// find the original tool call for this pending approval
|
|
409
412
|
const call = intentions.toolCalls.find((c) => c.callId === e.callId);
|
|
410
413
|
call && pendingApprovals.push(call);
|
|
411
414
|
} else {
|
|
@@ -508,10 +511,21 @@ export class Thread<
|
|
|
508
511
|
);
|
|
509
512
|
const tools = enabled.map((tool) => tool.serialize());
|
|
510
513
|
|
|
514
|
+
// derive responseType from agent.output
|
|
515
|
+
let responseType: LanguageModelResponseType | undefined;
|
|
516
|
+
if (this.agent.output && this.agent.output !== "text") {
|
|
517
|
+
const schema = this.agent.output as ZodType;
|
|
518
|
+
responseType = {
|
|
519
|
+
kind: "json",
|
|
520
|
+
schema: z.toJSONSchema(schema, { target: "draft-7" }) as any,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
511
524
|
return {
|
|
512
525
|
input: filtered,
|
|
513
526
|
settings,
|
|
514
527
|
tools,
|
|
528
|
+
responseType,
|
|
515
529
|
};
|
|
516
530
|
}
|
|
517
531
|
}
|
package/src/thread/types.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { Task } from "@/task";
|
|
|
15
15
|
import { Context } from "@/context";
|
|
16
16
|
import { Agent } from "@/agent";
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type { AgentOutputType } from "@/agent/types";
|
|
19
19
|
import type { ThreadStore } from "@/storage";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -23,7 +23,10 @@ import type { ThreadStore } from "@/storage";
|
|
|
23
23
|
*/
|
|
24
24
|
export type PublicThreadEvent = LanguageModelItem & ThreadEventBase;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Text output type - indicates the agent returns a plain string.
|
|
28
|
+
*/
|
|
29
|
+
export type TextOutput = "text";
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Thread state values as a const array (for zod schemas).
|
|
@@ -61,10 +64,10 @@ export const REQUIRES_APPROVAL = "requires_approval";
|
|
|
61
64
|
*/
|
|
62
65
|
export interface IThread<
|
|
63
66
|
TContext = unknown,
|
|
64
|
-
|
|
67
|
+
TOutput extends AgentOutputType = "text",
|
|
65
68
|
> {
|
|
66
69
|
tid: string;
|
|
67
|
-
agent: Agent<TContext,
|
|
70
|
+
agent: Agent<TContext, TOutput>;
|
|
68
71
|
model: LanguageModel;
|
|
69
72
|
|
|
70
73
|
context: Context<TContext>;
|
|
@@ -148,9 +151,9 @@ export interface ThreadExecuteResult<TResponse = unknown> {
|
|
|
148
151
|
*/
|
|
149
152
|
export interface ThreadOptions<
|
|
150
153
|
TContext = unknown,
|
|
151
|
-
|
|
154
|
+
TOutput extends AgentOutputType = "text",
|
|
152
155
|
> {
|
|
153
|
-
agent: Agent<TContext,
|
|
156
|
+
agent: Agent<TContext, TOutput>;
|
|
154
157
|
input?: LanguageModelItem[];
|
|
155
158
|
history?: ThreadEvent[];
|
|
156
159
|
context?: Context<TContext>;
|
package/src/thread/utils.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
|
|
|
8
8
|
import { ModelBehaviorError } from "@/lib/error";
|
|
9
9
|
|
|
10
10
|
/* types */
|
|
11
|
-
import type {
|
|
11
|
+
import type { AgentOutputType } from "@/agent/types";
|
|
12
12
|
import type {
|
|
13
13
|
ThreadEvent,
|
|
14
14
|
ThreadEventBase,
|
|
@@ -129,30 +129,31 @@ export function getFinalResponse(items: LanguageModelItem[]): string | null {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
*
|
|
132
|
+
* Parse the final response according to the output type schema.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* - If
|
|
134
|
+
* This serves as a safety net validation after native structured output from the provider.
|
|
135
|
+
*
|
|
136
|
+
* - If output is "text", returns the text as-is
|
|
137
|
+
* - If output is a ZodType, parses and validates the text as JSON
|
|
137
138
|
*
|
|
138
139
|
* @throws {ModelBehaviorError} if structured output validation fails
|
|
139
140
|
*/
|
|
140
|
-
export function parseFinalResponse<
|
|
141
|
+
export function parseFinalResponse<TOutput extends AgentOutputType>(
|
|
141
142
|
text: string,
|
|
142
|
-
|
|
143
|
-
): ResolvedAgentResponse<
|
|
144
|
-
if (
|
|
145
|
-
return text as ResolvedAgentResponse<
|
|
143
|
+
output: TOutput,
|
|
144
|
+
): ResolvedAgentResponse<TOutput> {
|
|
145
|
+
if (output === "text") {
|
|
146
|
+
return text as ResolvedAgentResponse<TOutput>; // text output - return as-is
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
// structured output - decode JSON and validate with schema
|
|
149
|
-
if (
|
|
150
|
+
if (output && typeof output === "object") {
|
|
150
151
|
// (TODO): prob better way of checking this here
|
|
151
|
-
const schema =
|
|
152
|
+
const schema = output as ZodType;
|
|
152
153
|
|
|
153
154
|
try {
|
|
154
155
|
const validated = json(schema).decode(text); // (TODO): it would be nice if we could use `decodeSafe` here
|
|
155
|
-
return validated as ResolvedAgentResponse<
|
|
156
|
+
return validated as ResolvedAgentResponse<TOutput>;
|
|
156
157
|
} catch (error) {
|
|
157
158
|
throw new ModelBehaviorError(
|
|
158
159
|
`Failed to parse structured output: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -161,5 +162,5 @@ export function parseFinalResponse<TResponse extends AgentResponseType>(
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// Fallback - should not reach here
|
|
164
|
-
return text as ResolvedAgentResponse<
|
|
165
|
+
return text as ResolvedAgentResponse<TOutput>;
|
|
165
166
|
}
|