kernl 0.7.4 → 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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -1
  3. package/dist/agent/types.d.ts +20 -12
  4. package/dist/agent/types.d.ts.map +1 -1
  5. package/dist/agent.d.ts +7 -7
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +3 -14
  8. package/dist/api/resources/agents/agents.d.ts +5 -5
  9. package/dist/api/resources/agents/agents.d.ts.map +1 -1
  10. package/dist/api/resources/agents/agents.js +1 -1
  11. package/dist/guardrail.d.ts +19 -19
  12. package/dist/guardrail.d.ts.map +1 -1
  13. package/dist/kernl/kernl.d.ts +6 -6
  14. package/dist/kernl/kernl.d.ts.map +1 -1
  15. package/dist/lib/error.d.ts +3 -3
  16. package/dist/lib/error.d.ts.map +1 -1
  17. package/dist/lifecycle.d.ts +6 -6
  18. package/dist/lifecycle.d.ts.map +1 -1
  19. package/dist/memory/__tests__/encoder.test.d.ts +2 -0
  20. package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
  21. package/dist/memory/__tests__/encoder.test.js +120 -0
  22. package/dist/memory/codecs/domain.d.ts +5 -0
  23. package/dist/memory/codecs/domain.d.ts.map +1 -1
  24. package/dist/memory/codecs/domain.js +6 -0
  25. package/dist/memory/encoder.d.ts +25 -2
  26. package/dist/memory/encoder.d.ts.map +1 -1
  27. package/dist/memory/encoder.js +46 -5
  28. package/dist/memory/index.d.ts +1 -1
  29. package/dist/memory/index.d.ts.map +1 -1
  30. package/dist/memory/index.js +1 -1
  31. package/dist/memory/schema.d.ts.map +1 -1
  32. package/dist/memory/schema.js +5 -0
  33. package/dist/memory/types.d.ts +1 -0
  34. package/dist/memory/types.d.ts.map +1 -1
  35. package/dist/thread/__tests__/integration.test.js +1 -1
  36. package/dist/thread/__tests__/thread.test.js +8 -8
  37. package/dist/thread/thread.d.ts +5 -5
  38. package/dist/thread/thread.d.ts.map +1 -1
  39. package/dist/thread/thread.js +13 -2
  40. package/dist/thread/types.d.ts +9 -6
  41. package/dist/thread/types.d.ts.map +1 -1
  42. package/dist/thread/utils.d.ts +7 -6
  43. package/dist/thread/utils.d.ts.map +1 -1
  44. package/dist/thread/utils.js +9 -8
  45. package/package.json +5 -4
  46. package/src/agent/types.ts +25 -29
  47. package/src/agent.ts +15 -28
  48. package/src/api/resources/agents/agents.ts +8 -8
  49. package/src/guardrail.ts +28 -28
  50. package/src/kernl/kernl.ts +12 -12
  51. package/src/lib/error.ts +3 -3
  52. package/src/lifecycle.ts +6 -6
  53. package/src/memory/__tests__/encoder.test.ts +153 -0
  54. package/src/memory/codecs/domain.ts +6 -0
  55. package/src/memory/encoder.ts +51 -6
  56. package/src/memory/index.ts +1 -1
  57. package/src/memory/schema.ts +5 -0
  58. package/src/memory/types.ts +1 -0
  59. package/src/thread/__tests__/integration.test.ts +130 -146
  60. package/src/thread/__tests__/thread.test.ts +8 -8
  61. package/src/thread/thread.ts +21 -7
  62. package/src/thread/types.ts +9 -6
  63. package/src/thread/utils.ts +15 -14
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import type { EmbeddingModel } from "@kernl-sdk/protocol";
3
+
4
+ import { MemoryByteEncoder, ObjectTextCodec } from "../encoder";
5
+ import type { MemoryByte } from "../types";
6
+
7
+ // Mock embedder that returns predictable vectors
8
+ function createMockEmbedder(): EmbeddingModel<string> {
9
+ return {
10
+ provider: "test",
11
+ modelId: "test-embedder",
12
+ embed: vi.fn(async ({ values }: { values: string[] }) => ({
13
+ embeddings: values.map((v) => [v.length, 0, 0]), // simple: [length, 0, 0]
14
+ })),
15
+ } as unknown as EmbeddingModel<string>;
16
+ }
17
+
18
+ describe("ObjectTextCodec", () => {
19
+ it("encodes simple object to YAML", () => {
20
+ const obj = { name: "Tony", preference: "coffee" };
21
+ const result = ObjectTextCodec.encode(obj);
22
+
23
+ expect(result).toContain("name: Tony");
24
+ expect(result).toContain("preference: coffee");
25
+ });
26
+
27
+ it("sorts keys for determinism", () => {
28
+ const obj1 = { z: 1, a: 2, m: 3 };
29
+ const obj2 = { a: 2, m: 3, z: 1 };
30
+
31
+ expect(ObjectTextCodec.encode(obj1)).toBe(ObjectTextCodec.encode(obj2));
32
+ });
33
+
34
+ it("truncates long objects with ellipsis", () => {
35
+ const longValue = "x".repeat(4000);
36
+ const obj = { data: longValue };
37
+ const result = ObjectTextCodec.encode(obj);
38
+
39
+ expect(result.length).toBeLessThanOrEqual(3005); // 3000 + "\n..."
40
+ expect(result.endsWith("\n...")).toBe(true);
41
+ });
42
+
43
+ it("handles nested objects", () => {
44
+ const obj = {
45
+ user: {
46
+ name: "Tony",
47
+ prefs: { coffee: { shots: 2 } },
48
+ },
49
+ };
50
+ const result = ObjectTextCodec.encode(obj);
51
+
52
+ expect(result).toContain("user:");
53
+ expect(result).toContain("name: Tony");
54
+ expect(result).toContain("shots: 2");
55
+ });
56
+
57
+ it("handles arrays", () => {
58
+ const obj = { items: ["a", "b", "c"] };
59
+ const result = ObjectTextCodec.encode(obj);
60
+
61
+ expect(result).toContain("items:");
62
+ expect(result).toContain("- a");
63
+ });
64
+ });
65
+
66
+ describe("MemoryByteEncoder", () => {
67
+ describe("encode", () => {
68
+ it("encodes text-only content", async () => {
69
+ const embedder = createMockEmbedder();
70
+ const encoder = new MemoryByteEncoder(embedder);
71
+
72
+ const byte: MemoryByte = { text: "Hello world" };
73
+ const result = await encoder.encode(byte);
74
+
75
+ expect(result.text).toBe("Hello world");
76
+ expect(result.objtext).toBeUndefined();
77
+ expect(result.tvec).toBeDefined();
78
+ expect(embedder.embed).toHaveBeenCalledWith({ values: ["Hello world"] });
79
+ });
80
+
81
+ it("encodes object-only content with projection", async () => {
82
+ const embedder = createMockEmbedder();
83
+ const encoder = new MemoryByteEncoder(embedder);
84
+
85
+ const byte: MemoryByte = {
86
+ object: { preference: "coffee", shots: 2 },
87
+ };
88
+ const result = await encoder.encode(byte);
89
+
90
+ // text falls back to objtext projection
91
+ expect(result.text).toContain("preference: coffee");
92
+ expect(result.objtext).toContain("preference: coffee");
93
+ expect(result.tvec).toBeDefined();
94
+ });
95
+
96
+ it("combines text and object for embedding", async () => {
97
+ const embedder = createMockEmbedder();
98
+ const encoder = new MemoryByteEncoder(embedder);
99
+
100
+ const byte: MemoryByte = {
101
+ text: "Tony likes coffee",
102
+ object: { shots: 2, sugar: false },
103
+ };
104
+ const result = await encoder.encode(byte);
105
+
106
+ expect(result.text).toBe("Tony likes coffee");
107
+ expect(result.objtext).toContain("shots: 2");
108
+
109
+ // embedding should be called with combined text
110
+ const embedCall = (embedder.embed as ReturnType<typeof vi.fn>).mock
111
+ .calls[0][0];
112
+ expect(embedCall.values[0]).toContain("Tony likes coffee");
113
+ expect(embedCall.values[0]).toContain("shots: 2");
114
+ });
115
+
116
+ it("does not set metadata (handled by domain codec)", async () => {
117
+ const embedder = createMockEmbedder();
118
+ const encoder = new MemoryByteEncoder(embedder);
119
+
120
+ const byte: MemoryByte = {
121
+ text: "test",
122
+ object: { key: "value" },
123
+ };
124
+ const result = await encoder.encode(byte);
125
+
126
+ expect(result.metadata).toBeUndefined();
127
+ });
128
+
129
+ it("returns undefined tvec when no content", async () => {
130
+ const embedder = createMockEmbedder();
131
+ const encoder = new MemoryByteEncoder(embedder);
132
+
133
+ const byte: MemoryByte = {};
134
+ const result = await encoder.encode(byte);
135
+
136
+ expect(result.text).toBeUndefined();
137
+ expect(result.objtext).toBeUndefined();
138
+ expect(result.tvec).toBeUndefined();
139
+ expect(embedder.embed).not.toHaveBeenCalled();
140
+ });
141
+ });
142
+
143
+ describe("embed", () => {
144
+ it("exposes embed method for query embedding", async () => {
145
+ const embedder = createMockEmbedder();
146
+ const encoder = new MemoryByteEncoder(embedder);
147
+
148
+ const vec = await encoder.embed("search query");
149
+
150
+ expect(vec).toEqual([12, 0, 0]); // "search query".length = 12
151
+ });
152
+ });
153
+ });
@@ -58,6 +58,11 @@ export const MEMORY_FILTER: Codec<MemoryFilter, SearchFilter> = {
58
58
 
59
59
  /**
60
60
  * Create a codec for MemoryRecord -> IndexMemoryRecord.
61
+ *
62
+ * Combines:
63
+ * - Record scope/timestamps from MemoryRecord
64
+ * - Indexed content (text, object projection, embeddings) from byte codec
65
+ * - User metadata from record.metadata (not from content.object)
61
66
  */
62
67
  export function recordCodec(
63
68
  bytecodec: MemoryByteCodec,
@@ -76,6 +81,7 @@ export function recordCodec(
76
81
  createdAt: record.createdAt,
77
82
  updatedAt: record.updatedAt,
78
83
  ...indexable,
84
+ metadata: record.metadata ?? null, // user metadata, not content.object
79
85
  };
80
86
  },
81
87
  async decode(): Promise<MemoryRecord> {
@@ -2,10 +2,41 @@
2
2
  * MemoryByte encoder - converts MemoryByte to IndexableByte with embeddings.
3
3
  */
4
4
 
5
- import type { EmbeddingModel } from "@kernl-sdk/protocol";
5
+ import type { EmbeddingModel, JSONObject } from "@kernl-sdk/protocol";
6
+ import { stringify as yamlStringify } from "yaml";
6
7
 
7
8
  import type { MemoryByte, IndexableByte, MemoryByteCodec } from "./types";
8
9
 
10
+ // ---------------------
11
+ // ObjectTextCodec
12
+ // ---------------------
13
+
14
+ const MAX_OBJECT_TEXT_LENGTH = 3000;
15
+
16
+ /**
17
+ * Codec for converting JSONObject to a canonical text representation.
18
+ *
19
+ * Uses YAML for human-readable, deterministic output suitable for:
20
+ * - Full-text search indexing
21
+ * - Embedding input (combined with text field)
22
+ *
23
+ * TODO: Allow users to pass custom codec via MemoryOptions.
24
+ */
25
+ export const ObjectTextCodec = {
26
+ /**
27
+ * Encode a JSONObject to canonical text.
28
+ * Uses YAML with sorted keys for determinism.
29
+ * Truncates at MAX_OBJECT_TEXT_LENGTH chars.
30
+ */
31
+ encode(obj: JSONObject): string {
32
+ const yaml = yamlStringify(obj, { sortMapEntries: true });
33
+ if (yaml.length <= MAX_OBJECT_TEXT_LENGTH) {
34
+ return yaml;
35
+ }
36
+ return yaml.slice(0, MAX_OBJECT_TEXT_LENGTH) + "\n...";
37
+ },
38
+ };
39
+
9
40
  /**
10
41
  * Encoder that converts MemoryByte to IndexableByte.
11
42
  *
@@ -20,21 +51,35 @@ export class MemoryByteEncoder implements MemoryByteCodec {
20
51
 
21
52
  /**
22
53
  * Encode a MemoryByte to IndexableByte.
23
- * Extracts text and computes embeddings for each modality.
54
+ *
55
+ * - Produces `objtext` string projection for FTS indexing
56
+ * - Combines text + objtext for embedding input
57
+ * - Returns text (fallback to objtext if no text provided)
58
+ *
59
+ * Note: metadata is NOT set here - it comes from record.metadata
60
+ * via the domain codec, not from MemoryByte.object.
24
61
  */
25
62
  async encode(byte: MemoryByte): Promise<IndexableByte> {
26
- const text = byte.text;
27
- const tvec = text ? await this.embed(text) : undefined;
63
+ const objtext = byte.object
64
+ ? ObjectTextCodec.encode(byte.object) // encode object as embeddable string
65
+ : undefined;
66
+
67
+ // (TODO): this behavior deserves consideration - do we always want to merge text + object?
68
+ //
69
+ // combine text + object for richer embedding
70
+ const combined = [byte.text, objtext].filter(Boolean).join("\n");
71
+ const tvec = combined ? await this.embed(combined) : undefined;
28
72
 
29
73
  // TODO: embed other modalities (image, audio, video)
74
+ //
30
75
  // const ivec = byte.image ? await this.embedImage(byte.image) : undefined;
31
76
  // const avec = byte.audio ? await this.embedAudio(byte.audio) : undefined;
32
77
  // const vvec = byte.video ? await this.embedVideo(byte.video) : undefined;
33
78
 
34
79
  return {
35
- text,
80
+ text: byte.text ?? objtext, // fallback to projection if no text
81
+ objtext,
36
82
  tvec,
37
- metadata: byte.object ?? null,
38
83
  };
39
84
  }
40
85
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  export { Memory } from "./memory";
6
- export { MemoryByteEncoder } from "./encoder";
6
+ export { MemoryByteEncoder, ObjectTextCodec } from "./encoder";
7
7
  export { buildMemoryIndexSchema } from "./schema";
8
8
  export { MemoryIndexHandle } from "./handle";
9
9
  export type { MemoryIndexHandleConfig } from "./handle";
@@ -105,6 +105,11 @@ export function buildMemoryIndexSchema(
105
105
  fts: true,
106
106
  optional: true,
107
107
  },
108
+ objtext: {
109
+ type: "string",
110
+ fts: true,
111
+ optional: true,
112
+ },
108
113
 
109
114
  // vector fields for different modalities
110
115
  tvec: {
@@ -63,6 +63,7 @@ export interface MemoryByte {
63
63
  */
64
64
  export interface IndexableByte {
65
65
  text?: string; // canonical semantic text
66
+ objtext?: string; // string projection of object for indexing (not full JSON)
66
67
  tvec?: number[]; // text embedding
67
68
  ivec?: number[]; // image embedding
68
69
  avec?: number[]; // audio embedding
@@ -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
- "Thread streaming integration",
28
- () => {
29
- let kernl: Kernl;
30
- let model: AISDKLanguageModel;
31
-
32
- beforeAll(() => {
33
- kernl = new Kernl();
34
- model = new AISDKLanguageModel(openai("gpt-4o"));
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
- it(
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]).toEqual(input[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
- it("should work with tool calls", async () => {
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((m: any) => m.role === "assistant");
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
- 30000,
269
- );
278
+ });
270
279
 
271
- it(
272
- "should properly encode tool results with matching callIds for multi-turn",
273
- async () => {
274
- const multiplyTool = tool({
275
- id: "multiply",
276
- name: "multiply",
277
- description: "Multiply two numbers",
278
- parameters: z.object({
279
- a: z.number().describe("First number"),
280
- b: z.number().describe("Second number"),
281
- }),
282
- execute: async (ctx, { a, b }) => {
283
- return a * b;
284
- },
285
- });
286
-
287
- const toolkit = new Toolkit({
288
- id: "math",
289
- tools: [multiplyTool],
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
- 30000,
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
- describe("execute()", () => {
373
- it(
374
- "should consume stream and return final response",
375
- async () => {
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
- it(
407
- "should validate structured output in blocking mode",
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
- responseType: responseSchema,
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
- 30000,
447
- );
448
- });
449
- },
450
- );
432
+ }, 30000);
433
+ });
434
+ });