kernl 0.12.0 → 0.12.1

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.
@@ -19,6 +19,7 @@ import {
19
19
  LanguageModel,
20
20
  LanguageModelItem,
21
21
  LanguageModelRequest,
22
+ LanguageModelStreamEvent,
22
23
  type LanguageModelUsage,
23
24
  type LanguageModelFinishReason,
24
25
  } from "@kernl-sdk/protocol";
@@ -33,6 +34,7 @@ import type {
33
34
  ThreadStreamEvent,
34
35
  ThreadExecuteResult,
35
36
  PerformActionsResult,
37
+ PublicThreadEvent,
36
38
  } from "./types";
37
39
  import type { AgentOutputType } from "@/agent/types";
38
40
  import type { LanguageModelResponseType } from "@kernl-sdk/protocol";
@@ -176,35 +178,15 @@ export class Thread<
176
178
 
177
179
  await this.checkpoint(); /* c1: persist RUNNING state + initial input */
178
180
 
179
- this.agent.emit("thread.start", {
180
- kind: "thread.start",
181
- threadId: this.tid,
182
- agentId: this.agent.id,
183
- namespace: this.namespace,
184
- context: this.context,
185
- });
181
+ this.emit("thread.start");
186
182
 
187
183
  yield { kind: "stream.start" }; // always yield start immediately
188
184
 
189
185
  try {
190
186
  yield* this._execute();
191
-
192
- this.agent.emit("thread.stop", {
193
- kind: "thread.stop",
194
- threadId: this.tid,
195
- agentId: this.agent.id,
196
- namespace: this.namespace,
197
- context: this.context,
198
- state: STOPPED,
199
- result: this.tickres,
200
- });
187
+ this.emit("thread.stop", { state: STOPPED, result: this.tickres });
201
188
  } catch (err) {
202
- this.agent.emit("thread.stop", {
203
- kind: "thread.stop",
204
- threadId: this.tid,
205
- agentId: this.agent.id,
206
- namespace: this.namespace,
207
- context: this.context,
189
+ this.emit("thread.stop", {
208
190
  state: STOPPED,
209
191
  error: err instanceof Error ? err.message : String(err),
210
192
  });
@@ -236,12 +218,14 @@ export class Thread<
236
218
  err = e.error;
237
219
  logger.error(e.error); // (TODO): onError callback in options
238
220
  }
239
- // we don't want deltas in the history
221
+ // complete items get persisted with seq, deltas are ephemeral
240
222
  if (notDelta(e)) {
241
- events.push(e);
242
- this.append(e);
223
+ const [seqd] = this.append(e);
224
+ events.push(seqd);
225
+ yield seqd;
226
+ } else {
227
+ yield e;
243
228
  }
244
- yield e;
245
229
  }
246
230
 
247
231
  // if an error event occurred → throw it
@@ -267,10 +251,10 @@ export class Thread<
267
251
  const { actions, pendingApprovals } =
268
252
  await this.performActions(intentions);
269
253
 
270
- // append + yield action events
254
+ // append + yield action events (sequenced)
271
255
  for (const a of actions) {
272
- this.append(a);
273
- yield a;
256
+ const [seqd] = this.append(a);
257
+ yield seqd;
274
258
  }
275
259
 
276
260
  await this.checkpoint(); /* c3: tick complete */
@@ -293,23 +277,16 @@ export class Thread<
293
277
  * NOTE: Streaming structured outputs deferred until concrete use cases emerge.
294
278
  * For now, we stream text-delta and tool events, final validation happens in _execute().
295
279
  */
296
- private async *tick(): AsyncGenerator<ThreadStreamEvent> {
280
+ private async *tick(): AsyncGenerator<LanguageModelStreamEvent> {
297
281
  this._tick++;
298
282
 
299
283
  // (TODO): check limits (if this._tick > this.limits.maxTicks)
300
284
  // (TODO): run input guardrails on first tick (if this._tick === 1)
285
+ // (TODO): compaction if necessary
301
286
 
302
287
  const req = await this.prepareModelRequest(this.history);
303
288
 
304
- this.agent.emit("model.call.start", {
305
- kind: "model.call.start",
306
- provider: this.model.provider,
307
- modelId: this.model.modelId,
308
- settings: req.settings ?? {},
309
- threadId: this.tid,
310
- agentId: this.agent.id,
311
- context: this.context,
312
- });
289
+ this.emit("model.call.start", { settings: req.settings ?? {} });
313
290
 
314
291
  let usage: LanguageModelUsage | undefined;
315
292
  let finishReason: LanguageModelFinishReason = "unknown";
@@ -333,27 +310,9 @@ export class Thread<
333
310
  }
334
311
  }
335
312
 
336
- this.agent.emit("model.call.end", {
337
- kind: "model.call.end",
338
- provider: this.model.provider,
339
- modelId: this.model.modelId,
340
- finishReason,
341
- usage,
342
- threadId: this.tid,
343
- agentId: this.agent.id,
344
- context: this.context,
345
- });
313
+ this.emit("model.call.end", { finishReason, usage });
346
314
  } catch (error) {
347
- this.agent.emit("model.call.end", {
348
- kind: "model.call.end",
349
- provider: this.model.provider,
350
- modelId: this.model.modelId,
351
- finishReason: "error",
352
- threadId: this.tid,
353
- agentId: this.agent.id,
354
- context: this.context,
355
- });
356
-
315
+ this.emit("model.call.end", { finishReason: "error" });
357
316
  yield {
358
317
  kind: "error",
359
318
  error: error instanceof Error ? error : new Error(String(error)),
@@ -439,9 +398,31 @@ export class Thread<
439
398
  this.abort?.abort();
440
399
  }
441
400
 
442
- // ----------------------------
443
- // utils
444
- // ----------------------------
401
+ /**
402
+ * Emit an agent event with common fields auto-filled.
403
+ */
404
+ private emit(kind: string, payload?: Record<string, unknown>): void {
405
+ const base = {
406
+ kind,
407
+ threadId: this.tid,
408
+ agentId: this.agent.id,
409
+ context: this.context,
410
+ };
411
+
412
+ let auto = {};
413
+ switch (kind) {
414
+ case "thread.start":
415
+ case "thread.stop":
416
+ auto = { namespace: this.namespace };
417
+ break;
418
+ case "model.call.start":
419
+ case "model.call.end":
420
+ auto = { provider: this.model.provider, modelId: this.model.modelId };
421
+ break;
422
+ }
423
+
424
+ this.agent.emit(kind as any, { ...base, ...auto, ...payload } as any);
425
+ }
445
426
 
446
427
  /**
447
428
  * Perform the actions returned by the model
@@ -497,11 +478,7 @@ export class Thread<
497
478
  calls.map(async (call: ToolCall) => {
498
479
  const parsedArgs = JSON.parse(call.arguments || "{}");
499
480
 
500
- this.agent.emit("tool.call.start", {
501
- kind: "tool.call.start",
502
- threadId: this.tid,
503
- agentId: this.agent.id,
504
- context: this.context,
481
+ this.emit("tool.call.start", {
505
482
  toolId: call.toolId,
506
483
  callId: call.callId,
507
484
  args: parsedArgs,
@@ -526,11 +503,7 @@ export class Thread<
526
503
  ctx.approve(call.callId); // mark this call as approved
527
504
  const res = await tool.invoke(ctx, call.arguments, call.callId);
528
505
 
529
- this.agent.emit("tool.call.end", {
530
- kind: "tool.call.end",
531
- threadId: this.tid,
532
- agentId: this.agent.id,
533
- context: this.context,
506
+ this.emit("tool.call.end", {
534
507
  toolId: call.toolId,
535
508
  callId: call.callId,
536
509
  state: res.state,
@@ -550,11 +523,7 @@ export class Thread<
550
523
  error: res.error,
551
524
  };
552
525
  } catch (error) {
553
- this.agent.emit("tool.call.end", {
554
- kind: "tool.call.end",
555
- threadId: this.tid,
556
- agentId: this.agent.id,
557
- context: this.context,
526
+ this.emit("tool.call.end", {
558
527
  toolId: call.toolId,
559
528
  callId: call.callId,
560
529
  state: FAILED,
@@ -2,13 +2,27 @@ import {
2
2
  ToolCall,
3
3
  LanguageModel,
4
4
  LanguageModelItem,
5
- LanguageModelStreamEvent,
6
5
  RUNNING,
7
6
  STOPPED,
8
7
  INTERRUPTIBLE,
9
8
  UNINTERRUPTIBLE,
10
9
  ZOMBIE,
11
10
  DEAD,
11
+ // Stream event types
12
+ TextStartEvent,
13
+ TextEndEvent,
14
+ TextDeltaEvent,
15
+ ReasoningStartEvent,
16
+ ReasoningEndEvent,
17
+ ReasoningDeltaEvent,
18
+ ToolInputStartEvent,
19
+ ToolInputEndEvent,
20
+ ToolInputDeltaEvent,
21
+ StartEvent,
22
+ FinishEvent,
23
+ AbortEvent,
24
+ ErrorEvent,
25
+ RawEvent,
12
26
  } from "@kernl-sdk/protocol";
13
27
 
14
28
  import { Task } from "@/task";
@@ -128,9 +142,41 @@ export type ThreadEvent =
128
142
  | ThreadSystemEvent;
129
143
 
130
144
  /**
131
- * Stream events - use protocol definition directly.
145
+ * Incremental content chunks (ephemeral, not persisted).
132
146
  */
133
- export type ThreadStreamEvent = LanguageModelStreamEvent;
147
+ export type StreamDeltaEvent =
148
+ | TextDeltaEvent
149
+ | ReasoningDeltaEvent
150
+ | ToolInputDeltaEvent;
151
+
152
+ /**
153
+ * Boundary markers + control flow (ephemeral, not persisted).
154
+ */
155
+ export type StreamControlEvent =
156
+ | TextStartEvent
157
+ | TextEndEvent
158
+ | ReasoningStartEvent
159
+ | ReasoningEndEvent
160
+ | ToolInputStartEvent
161
+ | ToolInputEndEvent
162
+ | StartEvent
163
+ | FinishEvent
164
+ | AbortEvent
165
+ | ErrorEvent
166
+ | RawEvent;
167
+
168
+ /**
169
+ * All ephemeral stream types (not persisted to history).
170
+ */
171
+ export type StreamEvent = StreamDeltaEvent | StreamControlEvent;
172
+
173
+ /**
174
+ * Thread stream events = sequenced ThreadEvents + ephemeral StreamEvents.
175
+ *
176
+ * Complete items (Message, ToolCall, etc.) are yielded as ThreadEvents with seq.
177
+ * Deltas and control events are yielded as StreamEvents without seq.
178
+ */
179
+ export type ThreadStreamEvent = ThreadEvent | StreamEvent;
134
180
 
135
181
  /**
136
182
  * Result of thread execution
@@ -4,7 +4,11 @@ import type { ResolvedAgentResponse } from "@/guardrail";
4
4
 
5
5
  /* lib */
6
6
  import { json, randomID } from "@kernl-sdk/shared/lib";
7
- import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
7
+ import {
8
+ ToolCall,
9
+ LanguageModelItem,
10
+ LanguageModelStreamEvent,
11
+ } from "@kernl-sdk/protocol";
8
12
  import { ModelBehaviorError } from "@/lib/error";
9
13
 
10
14
  /* types */
@@ -12,7 +16,6 @@ import type { AgentOutputType } from "@/agent/types";
12
16
  import type {
13
17
  ThreadEvent,
14
18
  ThreadEventBase,
15
- ThreadStreamEvent,
16
19
  ActionSet,
17
20
  PublicThreadEvent,
18
21
  } from "./types";
@@ -21,7 +24,7 @@ import type {
21
24
  * Create a ThreadEvent from a LanguageModelItem with thread metadata.
22
25
  *
23
26
  * @example
24
- * ```typescript
27
+ * ```ts
25
28
  * tevent({
26
29
  * kind: "message",
27
30
  * seq: 0,
@@ -57,7 +60,9 @@ export function tevent(event: {
57
60
  /**
58
61
  * Check if an event is a tool call
59
62
  */
60
- export function isActionIntention(event: LanguageModelItem): event is ToolCall {
63
+ export function isActionIntention(
64
+ event: ThreadEvent,
65
+ ): event is ToolCall & ThreadEventBase {
61
66
  return event.kind === "tool.call";
62
67
  }
63
68
 
@@ -65,7 +70,7 @@ export function isActionIntention(event: LanguageModelItem): event is ToolCall {
65
70
  * Extract action intentions from a list of events.
66
71
  * Returns ActionSet if there are any tool calls, null otherwise.
67
72
  */
68
- export function getIntentions(events: LanguageModelItem[]): ActionSet | null {
73
+ export function getIntentions(events: ThreadEvent[]): ActionSet | null {
69
74
  const toolCalls = events.filter(isActionIntention);
70
75
  return toolCalls.length > 0 ? { toolCalls } : null;
71
76
  }
@@ -74,7 +79,9 @@ export function getIntentions(events: LanguageModelItem[]): ActionSet | null {
74
79
  * Check if an event is NOT a delta/start/end event (i.e., a complete item).
75
80
  * Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
76
81
  */
77
- export function notDelta(event: ThreadStreamEvent): event is LanguageModelItem {
82
+ export function notDelta(
83
+ event: LanguageModelStreamEvent,
84
+ ): event is LanguageModelItem {
78
85
  switch (event.kind) {
79
86
  case "message":
80
87
  case "reasoning":
@@ -112,7 +119,7 @@ export function isPublicEvent(event: ThreadEvent): event is PublicThreadEvent {
112
119
  * Extract the final text response from a list of items.
113
120
  * Returns null if no assistant message with text content is found.
114
121
  */
115
- export function getFinalResponse(items: LanguageModelItem[]): string | null {
122
+ export function getFinalResponse(items: ThreadEvent[]): string | null {
116
123
  // scan backwards for the last assistant message
117
124
  for (let i = items.length - 1; i >= 0; i--) {
118
125
  const item = items[i];
@@ -1,2 +0,0 @@
1
- import "@kernl-sdk/ai/openai";
2
- //# sourceMappingURL=integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/integration.test.ts"],"names":[],"mappings":"AAIA,OAAO,sBAAsB,CAAC"}
@@ -1,320 +0,0 @@
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
- import "@kernl-sdk/ai/openai"; // (TMP)
6
- import { Agent } from "../../agent.js";
7
- import { Kernl } from "../../kernl/index.js";
8
- import { tool, Toolkit } from "../../tool/index.js";
9
- import { Thread } from "../thread.js";
10
- /**
11
- * Integration tests for Thread streaming with real AI SDK providers.
12
- *
13
- * These tests require an OPENAI_API_KEY environment variable to be set.
14
- * They will be skipped if the API key is not available.
15
- *
16
- * Run with: OPENAI_API_KEY=your-key pnpm test:run
17
- */
18
- const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
19
- describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
20
- let kernl;
21
- let model;
22
- beforeAll(() => {
23
- kernl = new Kernl();
24
- model = new AISDKLanguageModel(openai("gpt-4.1"));
25
- });
26
- describe("stream()", () => {
27
- it("should yield both delta events and complete items", async () => {
28
- const agent = new Agent({
29
- id: "test-stream",
30
- name: "Test Stream Agent",
31
- instructions: "You are a helpful assistant.",
32
- model,
33
- });
34
- const input = [
35
- {
36
- kind: "message",
37
- id: "msg-1",
38
- role: "user",
39
- content: [
40
- { kind: "text", text: "Say 'Hello World' and nothing else." },
41
- ],
42
- },
43
- ];
44
- const thread = new Thread({ agent, input });
45
- const events = [];
46
- for await (const event of thread.stream()) {
47
- events.push(event);
48
- }
49
- expect(events.length).toBeGreaterThan(0);
50
- // Should have text-delta events (for streaming UX)
51
- const textDeltas = events.filter((e) => e.kind === "text.delta");
52
- expect(textDeltas.length).toBeGreaterThan(0);
53
- // Should have text-start event
54
- const textStarts = events.filter((e) => e.kind === "text.start");
55
- expect(textStarts.length).toBeGreaterThan(0);
56
- // Should have text-end event
57
- const textEnds = events.filter((e) => e.kind === "text.end");
58
- expect(textEnds.length).toBeGreaterThan(0);
59
- // Should have complete Message item (for history)
60
- const messages = events.filter((e) => e.kind === "message");
61
- expect(messages.length).toBeGreaterThan(0);
62
- const assistantMessage = messages.find((m) => m.role === "assistant");
63
- expect(assistantMessage).toBeDefined();
64
- expect(assistantMessage.content).toBeDefined();
65
- expect(assistantMessage.content.length).toBeGreaterThan(0);
66
- // Message should have accumulated text from all deltas
67
- const textContent = assistantMessage.content.find((c) => c.kind === "text");
68
- expect(textContent).toBeDefined();
69
- expect(textContent.text).toBeDefined();
70
- expect(textContent.text.length).toBeGreaterThan(0);
71
- // Verify accumulated text matches concatenated deltas
72
- const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
73
- expect(textContent.text).toBe(accumulatedFromDeltas);
74
- // Should have finish event
75
- const finishEvents = events.filter((e) => e.kind === "finish");
76
- expect(finishEvents.length).toBe(1);
77
- }, 30000);
78
- it("should filter deltas from history but include complete items", async () => {
79
- const agent = new Agent({
80
- id: "test-history",
81
- name: "Test History Agent",
82
- instructions: "You are a helpful assistant.",
83
- model,
84
- });
85
- const input = [
86
- {
87
- kind: "message",
88
- id: "msg-1",
89
- role: "user",
90
- content: [{ kind: "text", text: "Count to 3" }],
91
- },
92
- ];
93
- const thread = new Thread({ agent, input });
94
- const streamEvents = [];
95
- for await (const event of thread.stream()) {
96
- streamEvents.push(event);
97
- }
98
- // Access private history via type assertion for testing
99
- const history = thread.history;
100
- // History should only contain complete items (message, reasoning, tool-call, tool-result)
101
- // TypeScript already enforces this via ThreadEvent type, but let's verify at runtime
102
- for (const event of history) {
103
- expect(["message", "reasoning", "tool-call", "tool-result"]).toContain(event.kind);
104
- }
105
- // Stream events should include deltas (but history should not)
106
- const streamDeltas = streamEvents.filter((e) => e.kind === "text.delta" ||
107
- e.kind === "text.start" ||
108
- e.kind === "text.end");
109
- expect(streamDeltas.length).toBeGreaterThan(0);
110
- // History should contain the input message (with ThreadEvent headers added)
111
- expect(history[0]).toMatchObject({
112
- kind: "message",
113
- role: "user",
114
- content: [{ kind: "text", text: "Count to 3" }],
115
- });
116
- // History should contain complete Message items
117
- const historyMessages = history.filter((e) => e.kind === "message");
118
- expect(historyMessages.length).toBeGreaterThan(1); // input + assistant response
119
- // Verify assistant message has complete text (not deltas)
120
- const assistantMessage = historyMessages.find((m) => m.role === "assistant");
121
- expect(assistantMessage).toBeDefined();
122
- const textContent = assistantMessage.content.find((c) => c.kind === "text");
123
- expect(textContent.text).toBeTruthy();
124
- expect(textContent.text.length).toBeGreaterThan(0);
125
- }, 30000);
126
- it("should work with tool calls", async () => {
127
- const addTool = tool({
128
- id: "add",
129
- name: "add",
130
- description: "Add two numbers together",
131
- parameters: z.object({
132
- a: z.number().describe("The first number"),
133
- b: z.number().describe("The second number"),
134
- }),
135
- execute: async (ctx, { a, b }) => {
136
- return a + b;
137
- },
138
- });
139
- const toolkit = new Toolkit({
140
- id: "math",
141
- tools: [addTool],
142
- });
143
- const agent = new Agent({
144
- id: "test-tools",
145
- name: "Test Tools Agent",
146
- instructions: "You are a helpful assistant that can do math.",
147
- model,
148
- toolkits: [toolkit],
149
- });
150
- const input = [
151
- {
152
- kind: "message",
153
- id: "msg-1",
154
- role: "user",
155
- content: [{ kind: "text", text: "What is 25 + 17?" }],
156
- },
157
- ];
158
- const thread = new Thread({ agent, input });
159
- const events = [];
160
- for await (const event of thread.stream()) {
161
- events.push(event);
162
- }
163
- expect(events.length).toBeGreaterThan(0);
164
- // Should have tool calls
165
- const toolCalls = events.filter((e) => e.kind === "tool.call");
166
- expect(toolCalls.length).toBeGreaterThan(0);
167
- // Verify tool was called with correct parameters
168
- const addToolCall = toolCalls.find((tc) => tc.toolId === "add");
169
- expect(addToolCall).toBeDefined();
170
- expect(JSON.parse(addToolCall.arguments)).toEqual({ a: 25, b: 17 });
171
- // Should have tool results
172
- const toolResults = events.filter((e) => e.kind === "tool.result");
173
- expect(toolResults.length).toBeGreaterThan(0);
174
- // Verify tool result is correct
175
- const addToolResult = toolResults.find((tr) => tr.callId === addToolCall.callId);
176
- expect(addToolResult).toBeDefined();
177
- expect(addToolResult.result).toBe(42);
178
- // History should contain tool calls and results
179
- const history = thread.history;
180
- const historyToolCalls = history.filter((e) => e.kind === "tool.call");
181
- const historyToolResults = history.filter((e) => e.kind === "tool.result");
182
- expect(historyToolCalls.length).toBe(toolCalls.length);
183
- expect(historyToolResults.length).toBe(toolResults.length);
184
- // Verify the assistant's final response references the correct answer
185
- const messages = events.filter((e) => e.kind === "message");
186
- const assistantMessage = messages.find((m) => m.role === "assistant");
187
- expect(assistantMessage).toBeDefined();
188
- const textContent = assistantMessage.content.find((c) => c.kind === "text");
189
- expect(textContent).toBeDefined();
190
- expect(textContent.text).toContain("42");
191
- }, 30000);
192
- it("should properly encode tool results with matching callIds for multi-turn", async () => {
193
- const multiplyTool = tool({
194
- id: "multiply",
195
- name: "multiply",
196
- description: "Multiply two numbers",
197
- parameters: z.object({
198
- a: z.number().describe("First number"),
199
- b: z.number().describe("Second number"),
200
- }),
201
- execute: async (ctx, { a, b }) => {
202
- return a * b;
203
- },
204
- });
205
- const toolkit = new Toolkit({
206
- id: "math",
207
- tools: [multiplyTool],
208
- });
209
- const agent = new Agent({
210
- id: "test-multi-turn",
211
- name: "Test Multi-Turn Agent",
212
- instructions: "You are a helpful assistant that can do math.",
213
- model,
214
- toolkits: [toolkit],
215
- });
216
- const input = [
217
- {
218
- kind: "message",
219
- id: "msg-1",
220
- role: "user",
221
- content: [{ kind: "text", text: "What is 7 times 6?" }],
222
- },
223
- ];
224
- const thread = new Thread({ agent, input });
225
- const events = [];
226
- // Collect all events from the stream
227
- for await (const event of thread.stream()) {
228
- events.push(event);
229
- }
230
- // Find the tool call and result
231
- const toolCalls = events.filter((e) => e.kind === "tool.call");
232
- const toolResults = events.filter((e) => e.kind === "tool.result");
233
- expect(toolCalls.length).toBeGreaterThan(0);
234
- expect(toolResults.length).toBeGreaterThan(0);
235
- const multiplyCall = toolCalls[0];
236
- const multiplyResult = toolResults[0];
237
- // Verify callId matches between tool call and result
238
- expect(multiplyCall.callId).toBe(multiplyResult.callId);
239
- expect(multiplyCall.toolId).toBe("multiply");
240
- expect(multiplyResult.toolId).toBe("multiply");
241
- // Verify the tool result has the correct structure
242
- expect(multiplyResult.callId).toBeDefined();
243
- expect(typeof multiplyResult.callId).toBe("string");
244
- expect(multiplyResult.callId.length).toBeGreaterThan(0);
245
- // Verify history contains both with matching callIds
246
- const history = thread.history;
247
- const historyToolCall = history.find((e) => e.kind === "tool.call" && e.toolId === "multiply");
248
- const historyToolResult = history.find((e) => e.kind === "tool.result" && e.toolId === "multiply");
249
- expect(historyToolCall).toBeDefined();
250
- expect(historyToolResult).toBeDefined();
251
- expect(historyToolCall.callId).toBe(historyToolResult.callId);
252
- // Verify final response uses the tool result
253
- const messages = events.filter((e) => e.kind === "message");
254
- const assistantMessage = messages.find((m) => m.role === "assistant");
255
- expect(assistantMessage).toBeDefined();
256
- const textContent = assistantMessage.content.find((c) => c.kind === "text");
257
- expect(textContent).toBeDefined();
258
- expect(textContent.text).toContain("42");
259
- }, 30000);
260
- });
261
- describe("execute()", () => {
262
- it("should consume stream and return final response", async () => {
263
- const agent = new Agent({
264
- id: "test-blocking",
265
- name: "Test Blocking Agent",
266
- instructions: "You are a helpful assistant.",
267
- model,
268
- });
269
- const input = [
270
- {
271
- kind: "message",
272
- id: "msg-1",
273
- role: "user",
274
- content: [{ kind: "text", text: "Say 'Testing' and nothing else." }],
275
- },
276
- ];
277
- const thread = new Thread({ agent, input });
278
- const result = await thread.execute();
279
- // Should have a response
280
- expect(result.response).toBeDefined();
281
- expect(typeof result.response).toBe("string");
282
- expect(result.response.length).toBeGreaterThan(0);
283
- // Should have final state
284
- expect(result.state).toBe("stopped");
285
- }, 30000);
286
- it("should validate structured output in blocking mode", async () => {
287
- const PersonSchema = z.object({
288
- name: z.string(),
289
- age: z.number(),
290
- });
291
- const agent = new Agent({
292
- id: "test-structured",
293
- name: "Test Structured Agent",
294
- instructions: "You are a helpful assistant. Return JSON with name and age fields.",
295
- model,
296
- output: PersonSchema,
297
- });
298
- const input = [
299
- {
300
- kind: "message",
301
- id: "msg-1",
302
- role: "user",
303
- content: [
304
- {
305
- kind: "text",
306
- text: 'Return a JSON object with name "Alice" and age 30',
307
- },
308
- ],
309
- },
310
- ];
311
- const thread = new Thread({ agent, input });
312
- const result = await thread.execute();
313
- // Response should be validated and parsed
314
- expect(result.response).toBeDefined();
315
- expect(typeof result.response).toBe("object");
316
- expect(result.response.name).toBeTruthy();
317
- expect(typeof result.response.age).toBe("number");
318
- }, 30000);
319
- });
320
- });