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.
Files changed (58) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/CHANGELOG.md +18 -0
  3. package/dist/agent.d.ts +20 -3
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +60 -41
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/kernl.d.ts +27 -1
  10. package/dist/kernl.d.ts.map +1 -1
  11. package/dist/kernl.js +36 -2
  12. package/dist/mcp/__tests__/integration.test.js +16 -0
  13. package/dist/thread/__tests__/fixtures/mock-model.d.ts +7 -0
  14. package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -0
  15. package/dist/thread/__tests__/fixtures/mock-model.js +59 -0
  16. package/dist/thread/__tests__/integration.test.d.ts +2 -0
  17. package/dist/thread/__tests__/integration.test.d.ts.map +1 -0
  18. package/dist/thread/__tests__/integration.test.js +247 -0
  19. package/dist/thread/__tests__/stream.test.d.ts +2 -0
  20. package/dist/thread/__tests__/stream.test.d.ts.map +1 -0
  21. package/dist/thread/__tests__/stream.test.js +244 -0
  22. package/dist/thread/__tests__/thread.test.js +612 -763
  23. package/dist/thread/thread.d.ts +30 -25
  24. package/dist/thread/thread.d.ts.map +1 -1
  25. package/dist/thread/thread.js +114 -314
  26. package/dist/thread/utils.d.ts +16 -1
  27. package/dist/thread/utils.d.ts.map +1 -1
  28. package/dist/thread/utils.js +30 -0
  29. package/dist/tool/index.d.ts +1 -1
  30. package/dist/tool/index.d.ts.map +1 -1
  31. package/dist/tool/index.js +1 -1
  32. package/dist/tool/tool.d.ts.map +1 -1
  33. package/dist/tool/tool.js +6 -2
  34. package/dist/tool/toolkit.d.ts +13 -3
  35. package/dist/tool/toolkit.d.ts.map +1 -1
  36. package/dist/tool/toolkit.js +11 -3
  37. package/dist/tool/types.d.ts +8 -0
  38. package/dist/tool/types.d.ts.map +1 -1
  39. package/dist/types/agent.d.ts +5 -5
  40. package/dist/types/agent.d.ts.map +1 -1
  41. package/dist/types/thread.d.ts +10 -16
  42. package/dist/types/thread.d.ts.map +1 -1
  43. package/package.json +6 -4
  44. package/src/agent.ts +97 -86
  45. package/src/index.ts +1 -1
  46. package/src/kernl.ts +51 -2
  47. package/src/mcp/__tests__/integration.test.ts +17 -0
  48. package/src/thread/__tests__/fixtures/mock-model.ts +71 -0
  49. package/src/thread/__tests__/integration.test.ts +349 -0
  50. package/src/thread/__tests__/thread.test.ts +625 -775
  51. package/src/thread/thread.ts +134 -381
  52. package/src/thread/utils.ts +36 -1
  53. package/src/tool/index.ts +1 -1
  54. package/src/tool/tool.ts +6 -2
  55. package/src/tool/toolkit.ts +19 -3
  56. package/src/tool/types.ts +10 -0
  57. package/src/types/agent.ts +9 -6
  58. package/src/types/thread.ts +25 -17
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { z } from "zod";
3
3
 
4
- import type { LanguageModel, LanguageModelRequest } from "@kernl-sdk/protocol";
4
+ import type { LanguageModelRequest } from "@kernl-sdk/protocol";
5
5
  import { IN_PROGRESS, COMPLETED, FAILED } from "@kernl-sdk/protocol";
6
6
 
7
7
  import { Thread } from "../thread";
@@ -12,37 +12,42 @@ import { tool, FunctionToolkit } from "@/tool";
12
12
  import { ModelBehaviorError } from "@/lib/error";
13
13
 
14
14
  import type { ThreadEvent } from "@/types/thread";
15
+ import { createMockModel } from "./fixtures/mock-model";
16
+
17
+ // Helper to create user message input
18
+ function userMessage(text: string): ThreadEvent[] {
19
+ return [
20
+ {
21
+ kind: "message" as const,
22
+ id: "msg-test",
23
+ role: "user" as const,
24
+ content: [{ kind: "text" as const, text }],
25
+ },
26
+ ];
27
+ }
15
28
 
16
29
  describe("Thread", () => {
17
30
  describe("Basic Execution", () => {
18
31
  it("should execute single turn and terminate with exact history", async () => {
19
- const model: LanguageModel = {
20
- spec: "1.0" as const,
21
- provider: "test",
22
- modelId: "test-model",
23
- async generate(req: LanguageModelRequest) {
24
- return {
25
- content: [
26
- {
27
- kind: "message" as const,
28
- id: "msg_1",
29
- role: "assistant" as const,
30
- content: [{ kind: "text" as const, text: "Hello world" }],
31
- },
32
- ],
33
- finishReason: "stop",
34
- usage: {
35
- inputTokens: 2,
36
- outputTokens: 2,
37
- totalTokens: 4,
32
+ const model = createMockModel(async (req: LanguageModelRequest) => {
33
+ return {
34
+ content: [
35
+ {
36
+ kind: "message" as const,
37
+ id: "msg_1",
38
+ role: "assistant" as const,
39
+ content: [{ kind: "text" as const, text: "Hello world" }],
38
40
  },
39
- warnings: [],
40
- };
41
- },
42
- stream: async function* () {
43
- throw new Error("Not implemented");
44
- },
45
- };
41
+ ],
42
+ finishReason: "stop",
43
+ usage: {
44
+ inputTokens: 2,
45
+ outputTokens: 2,
46
+ totalTokens: 4,
47
+ },
48
+ warnings: [],
49
+ };
50
+ });
46
51
 
47
52
  const agent = new Agent({
48
53
  id: "test",
@@ -52,7 +57,7 @@ describe("Thread", () => {
52
57
  });
53
58
 
54
59
  const kernl = new Kernl();
55
- const thread = new Thread(kernl, agent, "Hello world");
60
+ const thread = new Thread(kernl, agent, userMessage("Hello world"));
56
61
 
57
62
  const result = await thread.execute();
58
63
 
@@ -74,38 +79,29 @@ describe("Thread", () => {
74
79
  },
75
80
  ]);
76
81
 
77
- expect(result.state.tick).toBe(1);
78
- expect(result.state.modelResponses).toHaveLength(1);
82
+ expect(thread._tick).toBe(1);
79
83
  });
80
84
 
81
85
  it("should convert string input to UserMessage", async () => {
82
- const model: LanguageModel = {
83
- spec: "1.0" as const,
84
- provider: "test",
85
- modelId: "test-model",
86
- async generate(req: LanguageModelRequest) {
87
- return {
88
- content: [
89
- {
90
- kind: "message" as const,
91
- id: "msg_1",
92
- role: "assistant" as const,
93
- content: [{ kind: "text" as const, text: "Response" }],
94
- },
95
- ],
96
- finishReason: "stop",
97
- usage: {
98
- inputTokens: 2,
99
- outputTokens: 2,
100
- totalTokens: 4,
86
+ const model = createMockModel(async (req: LanguageModelRequest) => {
87
+ return {
88
+ content: [
89
+ {
90
+ kind: "message" as const,
91
+ id: "msg_1",
92
+ role: "assistant" as const,
93
+ content: [{ kind: "text" as const, text: "Response" }],
101
94
  },
102
- warnings: [],
103
- };
104
- },
105
- stream: async function* () {
106
- throw new Error("Not implemented");
107
- },
108
- };
95
+ ],
96
+ finishReason: "stop",
97
+ usage: {
98
+ inputTokens: 2,
99
+ outputTokens: 2,
100
+ totalTokens: 4,
101
+ },
102
+ warnings: [],
103
+ };
104
+ });
109
105
 
110
106
  const agent = new Agent({
111
107
  id: "test",
@@ -115,7 +111,7 @@ describe("Thread", () => {
115
111
  });
116
112
 
117
113
  const kernl = new Kernl();
118
- const thread = new Thread(kernl, agent, "Test input");
114
+ const thread = new Thread(kernl, agent, userMessage("Test input"));
119
115
 
120
116
  await thread.execute();
121
117
 
@@ -131,33 +127,25 @@ describe("Thread", () => {
131
127
  });
132
128
 
133
129
  it("should use array input as-is", async () => {
134
- const model: LanguageModel = {
135
- spec: "1.0" as const,
136
- provider: "test",
137
- modelId: "test-model",
138
- async generate(req: LanguageModelRequest) {
139
- return {
140
- content: [
141
- {
142
- kind: "message" as const,
143
- id: "msg_1",
144
- role: "assistant" as const,
145
- content: [{ kind: "text" as const, text: "Response" }],
146
- },
147
- ],
148
- finishReason: "stop",
149
- usage: {
150
- inputTokens: 2,
151
- outputTokens: 2,
152
- totalTokens: 4,
130
+ const model = createMockModel(async (req: LanguageModelRequest) => {
131
+ return {
132
+ content: [
133
+ {
134
+ kind: "message" as const,
135
+ id: "msg_1",
136
+ role: "assistant" as const,
137
+ content: [{ kind: "text" as const, text: "Response" }],
153
138
  },
154
- warnings: [],
155
- };
156
- },
157
- stream: async function* () {
158
- throw new Error("Not implemented");
159
- },
160
- };
139
+ ],
140
+ finishReason: "stop",
141
+ usage: {
142
+ inputTokens: 2,
143
+ outputTokens: 2,
144
+ totalTokens: 4,
145
+ },
146
+ warnings: [],
147
+ };
148
+ });
161
149
 
162
150
  const agent = new Agent({
163
151
  id: "test",
@@ -191,64 +179,56 @@ describe("Thread", () => {
191
179
  it("should execute multi-turn with tool call and exact history", async () => {
192
180
  let callCount = 0;
193
181
 
194
- const model: LanguageModel = {
195
- spec: "1.0" as const,
196
- provider: "test",
197
- modelId: "test-model",
198
- async generate(req: LanguageModelRequest) {
199
- callCount++;
200
-
201
- // First call: return tool call
202
- if (callCount === 1) {
203
- return {
204
- content: [
205
- {
206
- kind: "message" as const,
207
- id: "msg_1",
208
- role: "assistant" as const,
209
- content: [],
210
- },
211
- {
212
- kind: "tool-call" as const,
213
- toolId: "echo",
214
- state: IN_PROGRESS,
215
- callId: "call_1",
216
- arguments: JSON.stringify({ text: "test" }),
217
- },
218
- ],
219
- finishReason: "stop",
220
- usage: {
221
- inputTokens: 2,
222
- outputTokens: 2,
223
- totalTokens: 4,
224
- },
225
- warnings: [],
226
- };
227
- }
182
+ const model = createMockModel(async (req: LanguageModelRequest) => {
183
+ callCount++;
228
184
 
229
- // Second call: return final message
185
+ // First call: return tool call
186
+ if (callCount === 1) {
230
187
  return {
231
188
  content: [
232
189
  {
233
190
  kind: "message" as const,
234
- id: "msg_2",
191
+ id: "msg_1",
235
192
  role: "assistant" as const,
236
- content: [{ kind: "text" as const, text: "Done!" }],
193
+ content: [],
194
+ },
195
+ {
196
+ kind: "tool-call" as const,
197
+ toolId: "echo",
198
+ state: IN_PROGRESS,
199
+ callId: "call_1",
200
+ arguments: JSON.stringify({ text: "test" }),
237
201
  },
238
202
  ],
239
203
  finishReason: "stop",
240
204
  usage: {
241
- inputTokens: 4,
205
+ inputTokens: 2,
242
206
  outputTokens: 2,
243
- totalTokens: 6,
207
+ totalTokens: 4,
244
208
  },
245
209
  warnings: [],
246
210
  };
247
- },
248
- stream: async function* () {
249
- throw new Error("Not implemented");
250
- },
251
- };
211
+ }
212
+
213
+ // Second call: return final message
214
+ return {
215
+ content: [
216
+ {
217
+ kind: "message" as const,
218
+ id: "msg_2",
219
+ role: "assistant" as const,
220
+ content: [{ kind: "text" as const, text: "Done!" }],
221
+ },
222
+ ],
223
+ finishReason: "stop",
224
+ usage: {
225
+ inputTokens: 4,
226
+ outputTokens: 2,
227
+ totalTokens: 6,
228
+ },
229
+ warnings: [],
230
+ };
231
+ });
252
232
 
253
233
  const echoTool = tool({
254
234
  id: "echo",
@@ -268,7 +248,7 @@ describe("Thread", () => {
268
248
  });
269
249
 
270
250
  const kernl = new Kernl();
271
- const thread = new Thread(kernl, agent, "Use the echo tool");
251
+ const thread = new Thread(kernl, agent, userMessage("Use the echo tool"));
272
252
 
273
253
  const result = await thread.execute();
274
254
 
@@ -292,16 +272,16 @@ describe("Thread", () => {
292
272
  // Tool call (tick 1)
293
273
  {
294
274
  kind: "tool-call",
295
- id: "echo",
275
+ toolId: "echo",
296
276
  callId: "call_1",
297
- name: "echo",
277
+ state: IN_PROGRESS,
298
278
  arguments: JSON.stringify({ text: "test" }),
299
279
  },
300
280
  // Tool result (executed after tick 1)
301
281
  {
302
282
  kind: "tool-result",
303
283
  callId: "call_1",
304
- name: "echo",
284
+ toolId: "echo",
305
285
  state: COMPLETED,
306
286
  result: "Echo: test",
307
287
  error: null,
@@ -315,96 +295,87 @@ describe("Thread", () => {
315
295
  },
316
296
  ]);
317
297
 
318
- expect(result.state.tick).toBe(2);
319
- expect(result.state.modelResponses).toHaveLength(2);
298
+ expect(thread._tick).toBe(2);
320
299
  });
321
300
 
322
301
  it("should accumulate history across multiple turns", async () => {
323
302
  let callCount = 0;
324
303
 
325
- const model: LanguageModel = {
326
- spec: "1.0" as const,
327
- provider: "test",
328
- modelId: "test-model",
329
- async generate(req: LanguageModelRequest) {
330
- callCount++;
304
+ const model = createMockModel(async (req: LanguageModelRequest) => {
305
+ callCount++;
331
306
 
332
- if (callCount === 1) {
333
- return {
334
- content: [
335
- {
336
- kind: "message" as const,
337
- id: "msg_1",
338
- role: "assistant" as const,
339
- content: [],
340
- },
341
- {
342
- kind: "tool-call" as const,
343
- toolId: "simple",
344
- state: IN_PROGRESS,
345
- callId: "call_1",
346
- arguments: "first",
347
- },
348
- ],
349
- finishReason: "stop",
350
- usage: {
351
- inputTokens: 2,
352
- outputTokens: 2,
353
- totalTokens: 4,
307
+ if (callCount === 1) {
308
+ return {
309
+ content: [
310
+ {
311
+ kind: "message" as const,
312
+ id: "msg_1",
313
+ role: "assistant" as const,
314
+ content: [],
354
315
  },
355
- warnings: [],
356
- };
357
- }
358
-
359
- if (callCount === 2) {
360
- return {
361
- content: [
362
- {
363
- kind: "message" as const,
364
- id: "msg_2",
365
- role: "assistant" as const,
366
- content: [],
367
- },
368
- {
369
- kind: "tool-call" as const,
370
- toolId: "simple",
371
- state: IN_PROGRESS,
372
- callId: "call_2",
373
- arguments: "second",
374
- },
375
- ],
376
- finishReason: "stop",
377
- usage: {
378
- inputTokens: 3,
379
- outputTokens: 2,
380
- totalTokens: 5,
316
+ {
317
+ kind: "tool-call" as const,
318
+ toolId: "simple",
319
+ state: IN_PROGRESS,
320
+ callId: "call_1",
321
+ arguments: "first",
381
322
  },
382
- warnings: [],
383
- };
384
- }
323
+ ],
324
+ finishReason: "stop",
325
+ usage: {
326
+ inputTokens: 2,
327
+ outputTokens: 2,
328
+ totalTokens: 4,
329
+ },
330
+ warnings: [],
331
+ };
332
+ }
385
333
 
334
+ if (callCount === 2) {
386
335
  return {
387
336
  content: [
388
337
  {
389
338
  kind: "message" as const,
390
- id: "msg_3",
339
+ id: "msg_2",
391
340
  role: "assistant" as const,
392
- content: [{ kind: "text" as const, text: "All done" }],
341
+ content: [],
342
+ },
343
+ {
344
+ kind: "tool-call" as const,
345
+ toolId: "simple",
346
+ state: IN_PROGRESS,
347
+ callId: "call_2",
348
+ arguments: "second",
393
349
  },
394
350
  ],
395
351
  finishReason: "stop",
396
352
  usage: {
397
- inputTokens: 4,
353
+ inputTokens: 3,
398
354
  outputTokens: 2,
399
- totalTokens: 6,
355
+ totalTokens: 5,
400
356
  },
401
357
  warnings: [],
402
358
  };
403
- },
404
- stream: async function* () {
405
- throw new Error("Not implemented");
406
- },
407
- };
359
+ }
360
+
361
+ return {
362
+ content: [
363
+ {
364
+ kind: "message" as const,
365
+ id: "msg_3",
366
+ role: "assistant" as const,
367
+ content: [{ kind: "text" as const, text: "All done" }],
368
+ },
369
+ ],
370
+ finishReason: "stop",
371
+ usage: {
372
+ inputTokens: 4,
373
+ outputTokens: 2,
374
+ totalTokens: 6,
375
+ },
376
+ warnings: [],
377
+ };
378
+ });
408
379
 
409
380
  const simpleTool = tool({
410
381
  id: "simple",
@@ -424,7 +395,7 @@ describe("Thread", () => {
424
395
  });
425
396
 
426
397
  const kernl = new Kernl();
427
- const thread = new Thread(kernl, agent, "Start");
398
+ const thread = new Thread(kernl, agent, userMessage("Start"));
428
399
 
429
400
  const result = await thread.execute();
430
401
 
@@ -432,7 +403,7 @@ describe("Thread", () => {
432
403
 
433
404
  // Should have: 1 user msg + 3 assistant msgs + 2 tool calls + 2 tool results = 8 events
434
405
  expect(history).toHaveLength(8);
435
- expect(result.state.tick).toBe(3);
406
+ expect(thread._tick).toBe(3);
436
407
  });
437
408
  });
438
409
 
@@ -440,49 +411,25 @@ describe("Thread", () => {
440
411
  it("should handle tool not found with exact error in history", async () => {
441
412
  let callCount = 0;
442
413
 
443
- const model: LanguageModel = {
444
- spec: "1.0" as const,
445
- provider: "test",
446
- modelId: "test-model",
447
- async generate(req: LanguageModelRequest) {
448
- callCount++;
414
+ const model = createMockModel(async (req: LanguageModelRequest) => {
415
+ callCount++;
449
416
 
450
- // First call: return tool call
451
- if (callCount === 1) {
452
- return {
453
- content: [
454
- {
455
- kind: "message" as const,
456
- id: "msg_1",
457
- role: "assistant" as const,
458
- content: [],
459
- },
460
- {
461
- kind: "tool-call" as const,
462
- toolId: "nonexistent",
463
- state: IN_PROGRESS,
464
- callId: "call_1",
465
- arguments: "{}",
466
- },
467
- ],
468
- finishReason: "stop",
469
- usage: {
470
- inputTokens: 2,
471
- outputTokens: 2,
472
- totalTokens: 4,
473
- },
474
- warnings: [],
475
- };
476
- }
477
-
478
- // Second call: return terminal message
417
+ // First call: return tool call
418
+ if (callCount === 1) {
479
419
  return {
480
420
  content: [
481
421
  {
482
422
  kind: "message" as const,
483
- id: "msg_2",
423
+ id: "msg_1",
484
424
  role: "assistant" as const,
485
- content: [{ kind: "text" as const, text: "Done" }],
425
+ content: [],
426
+ },
427
+ {
428
+ kind: "tool-call" as const,
429
+ toolId: "nonexistent",
430
+ state: IN_PROGRESS,
431
+ callId: "call_1",
432
+ arguments: "{}",
486
433
  },
487
434
  ],
488
435
  finishReason: "stop",
@@ -493,11 +440,27 @@ describe("Thread", () => {
493
440
  },
494
441
  warnings: [],
495
442
  };
496
- },
497
- stream: async function* () {
498
- throw new Error("Not implemented");
499
- },
500
- };
443
+ }
444
+
445
+ // Second call: return terminal message
446
+ return {
447
+ content: [
448
+ {
449
+ kind: "message" as const,
450
+ id: "msg_2",
451
+ role: "assistant" as const,
452
+ content: [{ kind: "text" as const, text: "Done" }],
453
+ },
454
+ ],
455
+ finishReason: "stop",
456
+ usage: {
457
+ inputTokens: 2,
458
+ outputTokens: 2,
459
+ totalTokens: 4,
460
+ },
461
+ warnings: [],
462
+ };
463
+ });
501
464
 
502
465
  const agent = new Agent({
503
466
  id: "test",
@@ -508,7 +471,7 @@ describe("Thread", () => {
508
471
  });
509
472
 
510
473
  const kernl = new Kernl();
511
- const thread = new Thread(kernl, agent, "test");
474
+ const thread = new Thread(kernl, agent, userMessage("test"));
512
475
 
513
476
  await thread.execute();
514
477
 
@@ -529,49 +492,25 @@ describe("Thread", () => {
529
492
  it("should handle tool execution error", async () => {
530
493
  let callCount = 0;
531
494
 
532
- const model: LanguageModel = {
533
- spec: "1.0" as const,
534
- provider: "test",
535
- modelId: "test-model",
536
- async generate(req: LanguageModelRequest) {
537
- callCount++;
495
+ const model = createMockModel(async (req: LanguageModelRequest) => {
496
+ callCount++;
538
497
 
539
- // First call: return tool call
540
- if (callCount === 1) {
541
- return {
542
- content: [
543
- {
544
- kind: "message" as const,
545
- id: "msg_1",
546
- role: "assistant" as const,
547
- content: [],
548
- },
549
- {
550
- kind: "tool-call" as const,
551
- toolId: "failing",
552
- state: IN_PROGRESS,
553
- callId: "call_1",
554
- arguments: "{}",
555
- },
556
- ],
557
- finishReason: "stop",
558
- usage: {
559
- inputTokens: 2,
560
- outputTokens: 2,
561
- totalTokens: 4,
562
- },
563
- warnings: [],
564
- };
565
- }
566
-
567
- // Second call: return terminal message
498
+ // First call: return tool call
499
+ if (callCount === 1) {
568
500
  return {
569
501
  content: [
570
502
  {
571
503
  kind: "message" as const,
572
- id: "msg_2",
504
+ id: "msg_1",
573
505
  role: "assistant" as const,
574
- content: [{ kind: "text" as const, text: "Done" }],
506
+ content: [],
507
+ },
508
+ {
509
+ kind: "tool-call" as const,
510
+ toolId: "failing",
511
+ state: IN_PROGRESS,
512
+ callId: "call_1",
513
+ arguments: "{}",
575
514
  },
576
515
  ],
577
516
  finishReason: "stop",
@@ -582,11 +521,27 @@ describe("Thread", () => {
582
521
  },
583
522
  warnings: [],
584
523
  };
585
- },
586
- stream: async function* () {
587
- throw new Error("Not implemented");
588
- },
589
- };
524
+ }
525
+
526
+ // Second call: return terminal message
527
+ return {
528
+ content: [
529
+ {
530
+ kind: "message" as const,
531
+ id: "msg_2",
532
+ role: "assistant" as const,
533
+ content: [{ kind: "text" as const, text: "Done" }],
534
+ },
535
+ ],
536
+ finishReason: "stop",
537
+ usage: {
538
+ inputTokens: 2,
539
+ outputTokens: 2,
540
+ totalTokens: 4,
541
+ },
542
+ warnings: [],
543
+ };
544
+ });
590
545
 
591
546
  const failingTool = tool({
592
547
  id: "failing",
@@ -608,7 +563,7 @@ describe("Thread", () => {
608
563
  });
609
564
 
610
565
  const kernl = new Kernl();
611
- const thread = new Thread(kernl, agent, "test");
566
+ const thread = new Thread(kernl, agent, userMessage("test"));
612
567
 
613
568
  await thread.execute();
614
569
 
@@ -628,49 +583,25 @@ describe("Thread", () => {
628
583
  it("should execute tool successfully with result in history", async () => {
629
584
  let callCount = 0;
630
585
 
631
- const model: LanguageModel = {
632
- spec: "1.0" as const,
633
- provider: "test",
634
- modelId: "test-model",
635
- async generate(req: LanguageModelRequest) {
636
- callCount++;
637
-
638
- // First call: return tool call
639
- if (callCount === 1) {
640
- return {
641
- content: [
642
- {
643
- kind: "message" as const,
644
- id: "msg_1",
645
- role: "assistant" as const,
646
- content: [],
647
- },
648
- {
649
- kind: "tool-call" as const,
650
- toolId: "add",
651
- state: IN_PROGRESS,
652
- callId: "call_1",
653
- arguments: JSON.stringify({ a: 5, b: 3 }),
654
- },
655
- ],
656
- finishReason: "stop",
657
- usage: {
658
- inputTokens: 2,
659
- outputTokens: 2,
660
- totalTokens: 4,
661
- },
662
- warnings: [],
663
- };
664
- }
586
+ const model = createMockModel(async (req: LanguageModelRequest) => {
587
+ callCount++;
665
588
 
666
- // Second call: return terminal message
589
+ // First call: return tool call
590
+ if (callCount === 1) {
667
591
  return {
668
592
  content: [
669
593
  {
670
594
  kind: "message" as const,
671
- id: "msg_2",
595
+ id: "msg_1",
672
596
  role: "assistant" as const,
673
- content: [{ kind: "text" as const, text: "Done" }],
597
+ content: [],
598
+ },
599
+ {
600
+ kind: "tool-call" as const,
601
+ toolId: "add",
602
+ state: IN_PROGRESS,
603
+ callId: "call_1",
604
+ arguments: JSON.stringify({ a: 5, b: 3 }),
674
605
  },
675
606
  ],
676
607
  finishReason: "stop",
@@ -681,11 +612,27 @@ describe("Thread", () => {
681
612
  },
682
613
  warnings: [],
683
614
  };
684
- },
685
- stream: async function* () {
686
- throw new Error("Not implemented");
687
- },
688
- };
615
+ }
616
+
617
+ // Second call: return terminal message
618
+ return {
619
+ content: [
620
+ {
621
+ kind: "message" as const,
622
+ id: "msg_2",
623
+ role: "assistant" as const,
624
+ content: [{ kind: "text" as const, text: "Done" }],
625
+ },
626
+ ],
627
+ finishReason: "stop",
628
+ usage: {
629
+ inputTokens: 2,
630
+ outputTokens: 2,
631
+ totalTokens: 4,
632
+ },
633
+ warnings: [],
634
+ };
635
+ });
689
636
 
690
637
  const addTool = tool({
691
638
  id: "add",
@@ -703,11 +650,12 @@ describe("Thread", () => {
703
650
  });
704
651
 
705
652
  const kernl = new Kernl();
706
- const thread = new Thread(kernl, agent, "Add 5 and 3");
653
+ const thread = new Thread(kernl, agent, userMessage("Add 5 and 3"));
707
654
 
708
655
  await thread.execute();
709
656
 
710
- const history = (thread as any).history as ThreadEvent[];
657
+ // @ts-expect-error
658
+ const history = thread.history as ThreadEvent[];
711
659
 
712
660
  const toolResult = history.find((e) => e.kind === "tool-result");
713
661
  expect(toolResult).toEqual({
@@ -725,56 +673,32 @@ describe("Thread", () => {
725
673
  it("should execute multiple tools in parallel with exact history", async () => {
726
674
  let callCount = 0;
727
675
 
728
- const model: LanguageModel = {
729
- spec: "1.0" as const,
730
- provider: "test",
731
- modelId: "test-model",
732
- async generate(req: LanguageModelRequest) {
733
- callCount++;
676
+ const model = createMockModel(async (req: LanguageModelRequest) => {
677
+ callCount++;
734
678
 
735
- // First call: return multiple tool calls
736
- if (callCount === 1) {
737
- return {
738
- content: [
739
- {
740
- kind: "message" as const,
741
- id: "msg_1",
742
- role: "assistant" as const,
743
- content: [],
744
- },
745
- {
746
- kind: "tool-call" as const,
747
- toolId: "tool1",
748
- state: IN_PROGRESS,
749
- callId: "call_1",
750
- arguments: JSON.stringify({ value: "a" }),
751
- },
752
- {
753
- kind: "tool-call" as const,
754
- toolId: "tool2",
755
- state: IN_PROGRESS,
756
- callId: "call_2",
757
- arguments: JSON.stringify({ value: "b" }),
758
- },
759
- ],
760
- finishReason: "stop",
761
- usage: {
762
- inputTokens: 2,
763
- outputTokens: 2,
764
- totalTokens: 4,
765
- },
766
- warnings: [],
767
- };
768
- }
769
-
770
- // Second call: return terminal message
679
+ // First call: return multiple tool calls
680
+ if (callCount === 1) {
771
681
  return {
772
682
  content: [
773
683
  {
774
684
  kind: "message" as const,
775
- id: "msg_2",
685
+ id: "msg_1",
776
686
  role: "assistant" as const,
777
- content: [{ kind: "text" as const, text: "Done" }],
687
+ content: [],
688
+ },
689
+ {
690
+ kind: "tool-call" as const,
691
+ toolId: "tool1",
692
+ state: IN_PROGRESS,
693
+ callId: "call_1",
694
+ arguments: JSON.stringify({ value: "a" }),
695
+ },
696
+ {
697
+ kind: "tool-call" as const,
698
+ toolId: "tool2",
699
+ state: IN_PROGRESS,
700
+ callId: "call_2",
701
+ arguments: JSON.stringify({ value: "b" }),
778
702
  },
779
703
  ],
780
704
  finishReason: "stop",
@@ -785,11 +709,27 @@ describe("Thread", () => {
785
709
  },
786
710
  warnings: [],
787
711
  };
788
- },
789
- stream: async function* () {
790
- throw new Error("Not implemented");
791
- },
792
- };
712
+ }
713
+
714
+ // Second call: return terminal message
715
+ return {
716
+ content: [
717
+ {
718
+ kind: "message" as const,
719
+ id: "msg_2",
720
+ role: "assistant" as const,
721
+ content: [{ kind: "text" as const, text: "Done" }],
722
+ },
723
+ ],
724
+ finishReason: "stop",
725
+ usage: {
726
+ inputTokens: 2,
727
+ outputTokens: 2,
728
+ totalTokens: 4,
729
+ },
730
+ warnings: [],
731
+ };
732
+ });
793
733
 
794
734
  const tool1 = tool({
795
735
  id: "tool1",
@@ -816,7 +756,7 @@ describe("Thread", () => {
816
756
  });
817
757
 
818
758
  const kernl = new Kernl();
819
- const thread = new Thread(kernl, agent, "test");
759
+ const thread = new Thread(kernl, agent, userMessage("test"));
820
760
 
821
761
  await thread.execute();
822
762
 
@@ -852,47 +792,24 @@ describe("Thread", () => {
852
792
  it("should track tick counter correctly", async () => {
853
793
  let callCount = 0;
854
794
 
855
- const model: LanguageModel = {
856
- spec: "1.0" as const,
857
- provider: "test",
858
- modelId: "test-model",
859
- async generate(req: LanguageModelRequest) {
860
- callCount++;
861
-
862
- if (callCount < 3) {
863
- return {
864
- content: [
865
- {
866
- kind: "message" as const,
867
- id: `msg_${callCount}`,
868
- role: "assistant" as const,
869
- content: [],
870
- },
871
- {
872
- kind: "tool-call" as const,
873
- toolId: "simple",
874
- state: IN_PROGRESS,
875
- callId: `call_${callCount}`,
876
- arguments: "{}",
877
- },
878
- ],
879
- finishReason: "stop",
880
- usage: {
881
- inputTokens: 2,
882
- outputTokens: 2,
883
- totalTokens: 4,
884
- },
885
- warnings: [],
886
- };
887
- }
795
+ const model = createMockModel(async (req: LanguageModelRequest) => {
796
+ callCount++;
888
797
 
798
+ if (callCount < 3) {
889
799
  return {
890
800
  content: [
891
801
  {
892
802
  kind: "message" as const,
893
- id: "msg_final",
803
+ id: `msg_${callCount}`,
894
804
  role: "assistant" as const,
895
- content: [{ kind: "text" as const, text: "Done" }],
805
+ content: [],
806
+ },
807
+ {
808
+ kind: "tool-call" as const,
809
+ toolId: "simple",
810
+ state: IN_PROGRESS,
811
+ callId: `call_${callCount}`,
812
+ arguments: "{}",
896
813
  },
897
814
  ],
898
815
  finishReason: "stop",
@@ -903,11 +820,26 @@ describe("Thread", () => {
903
820
  },
904
821
  warnings: [],
905
822
  };
906
- },
907
- stream: async function* () {
908
- throw new Error("Not implemented");
909
- },
910
- };
823
+ }
824
+
825
+ return {
826
+ content: [
827
+ {
828
+ kind: "message" as const,
829
+ id: "msg_final",
830
+ role: "assistant" as const,
831
+ content: [{ kind: "text" as const, text: "Done" }],
832
+ },
833
+ ],
834
+ finishReason: "stop",
835
+ usage: {
836
+ inputTokens: 2,
837
+ outputTokens: 2,
838
+ totalTokens: 4,
839
+ },
840
+ warnings: [],
841
+ };
842
+ });
911
843
 
912
844
  const simpleTool = tool({
913
845
  id: "simple",
@@ -927,72 +859,64 @@ describe("Thread", () => {
927
859
  });
928
860
 
929
861
  const kernl = new Kernl();
930
- const thread = new Thread(kernl, agent, "test");
862
+ const thread = new Thread(kernl, agent, userMessage("test"));
931
863
 
932
864
  const result = await thread.execute();
933
865
 
934
- expect(result.state.tick).toBe(3);
866
+ expect(thread._tick).toBe(3);
935
867
  });
936
868
 
937
869
  it("should accumulate model responses", async () => {
938
870
  let callCount = 0;
939
871
 
940
- const model: LanguageModel = {
941
- spec: "1.0" as const,
942
- provider: "test",
943
- modelId: "test-model",
944
- async generate(req: LanguageModelRequest) {
945
- callCount++;
946
-
947
- if (callCount === 1) {
948
- return {
949
- content: [
950
- {
951
- kind: "message" as const,
952
- id: "msg_1",
953
- role: "assistant" as const,
954
- content: [],
955
- },
956
- {
957
- kind: "tool-call" as const,
958
- toolId: "simple",
959
- state: IN_PROGRESS,
960
- callId: "call_1",
961
- arguments: "{}",
962
- },
963
- ],
964
- finishReason: "stop",
965
- usage: {
966
- inputTokens: 10,
967
- outputTokens: 5,
968
- totalTokens: 15,
969
- },
970
- warnings: [],
971
- };
972
- }
872
+ const model = createMockModel(async (req: LanguageModelRequest) => {
873
+ callCount++;
973
874
 
875
+ if (callCount === 1) {
974
876
  return {
975
877
  content: [
976
878
  {
977
879
  kind: "message" as const,
978
- id: "msg_2",
880
+ id: "msg_1",
979
881
  role: "assistant" as const,
980
- content: [{ kind: "text" as const, text: "Done" }],
882
+ content: [],
883
+ },
884
+ {
885
+ kind: "tool-call" as const,
886
+ toolId: "simple",
887
+ state: IN_PROGRESS,
888
+ callId: "call_1",
889
+ arguments: "{}",
981
890
  },
982
891
  ],
983
892
  finishReason: "stop",
984
893
  usage: {
985
- inputTokens: 20,
986
- outputTokens: 10,
987
- totalTokens: 30,
894
+ inputTokens: 10,
895
+ outputTokens: 5,
896
+ totalTokens: 15,
988
897
  },
989
898
  warnings: [],
990
899
  };
991
- },
992
- stream: async function* () {
993
- throw new Error("Not implemented");
994
- },
995
- };
900
+ }
901
+
902
+ return {
903
+ content: [
904
+ {
905
+ kind: "message" as const,
906
+ id: "msg_2",
907
+ role: "assistant" as const,
908
+ content: [{ kind: "text" as const, text: "Done" }],
909
+ },
910
+ ],
911
+ finishReason: "stop",
912
+ usage: {
913
+ inputTokens: 20,
914
+ outputTokens: 10,
915
+ totalTokens: 30,
916
+ },
917
+ warnings: [],
918
+ };
919
+ });
996
920
 
997
921
  const simpleTool = tool({
998
922
  id: "simple",
@@ -1012,45 +936,37 @@ describe("Thread", () => {
1012
936
  });
1013
937
 
1014
938
  const kernl = new Kernl();
1015
- const thread = new Thread(kernl, agent, "test");
939
+ const thread = new Thread(kernl, agent, userMessage("test"));
1016
940
 
1017
941
  const result = await thread.execute();
1018
942
 
1019
- expect(result.state.modelResponses).toHaveLength(2);
1020
- expect(result.state.modelResponses[0].usage.inputTokens).toBe(10);
1021
- expect(result.state.modelResponses[1].usage.inputTokens).toBe(20);
943
+ // Verify the thread executed both turns
944
+ expect(thread._tick).toBe(2);
945
+ expect(result.response).toBe("Done");
1022
946
  });
1023
947
  });
1024
948
 
1025
949
  describe("Terminal State Detection", () => {
1026
950
  it("should terminate when assistant message has no tool calls", async () => {
1027
- const model: LanguageModel = {
1028
- spec: "1.0" as const,
1029
- provider: "test",
1030
- modelId: "test-model",
1031
- async generate(req: LanguageModelRequest) {
1032
- return {
1033
- content: [
1034
- {
1035
- kind: "message" as const,
1036
- id: "msg_1",
1037
- role: "assistant" as const,
1038
- content: [{ kind: "text" as const, text: "Final response" }],
1039
- },
1040
- ],
1041
- finishReason: "stop",
1042
- usage: {
1043
- inputTokens: 2,
1044
- outputTokens: 2,
1045
- totalTokens: 4,
951
+ const model = createMockModel(async (req: LanguageModelRequest) => {
952
+ return {
953
+ content: [
954
+ {
955
+ kind: "message" as const,
956
+ id: "msg_1",
957
+ role: "assistant" as const,
958
+ content: [{ kind: "text" as const, text: "Final response" }],
1046
959
  },
1047
- warnings: [],
1048
- };
1049
- },
1050
- stream: async function* () {
1051
- throw new Error("Not implemented");
1052
- },
1053
- };
960
+ ],
961
+ finishReason: "stop",
962
+ usage: {
963
+ inputTokens: 2,
964
+ outputTokens: 2,
965
+ totalTokens: 4,
966
+ },
967
+ warnings: [],
968
+ };
969
+ });
1054
970
 
1055
971
  const agent = new Agent({
1056
972
  id: "test",
@@ -1060,74 +976,64 @@ describe("Thread", () => {
1060
976
  });
1061
977
 
1062
978
  const kernl = new Kernl();
1063
- const thread = new Thread(kernl, agent, "test");
979
+ const thread = new Thread(kernl, agent, userMessage("test"));
1064
980
 
1065
981
  const result = await thread.execute();
1066
982
 
1067
- expect(result.state.tick).toBe(1);
983
+ expect(thread._tick).toBe(1);
1068
984
  });
1069
985
 
1070
986
  it("should continue when assistant message has tool calls", async () => {
1071
987
  let callCount = 0;
1072
988
 
1073
- const model: LanguageModel = {
1074
- spec: "1.0" as const,
1075
- provider: "test",
1076
- modelId: "test-model",
1077
- async generate(req: LanguageModelRequest) {
1078
- callCount++;
1079
-
1080
- if (callCount === 1) {
1081
- return {
1082
- content: [
1083
- {
1084
- kind: "message" as const,
1085
- id: "msg_1",
1086
- role: "assistant" as const,
1087
- content: [
1088
- { kind: "text" as const, text: "Let me use a tool" },
1089
- ],
1090
- },
1091
- {
1092
- kind: "tool-call" as const,
1093
- toolId: "simple",
1094
- state: IN_PROGRESS,
1095
- callId: "call_1",
1096
- arguments: "{}",
1097
- },
1098
- ],
1099
- finishReason: "stop",
1100
- usage: {
1101
- inputTokens: 2,
1102
- outputTokens: 2,
1103
- totalTokens: 4,
1104
- },
1105
- warnings: [],
1106
- };
1107
- }
989
+ const model = createMockModel(async (req: LanguageModelRequest) => {
990
+ callCount++;
1108
991
 
992
+ if (callCount === 1) {
1109
993
  return {
1110
994
  content: [
1111
995
  {
1112
996
  kind: "message" as const,
1113
- id: "msg_2",
997
+ id: "msg_1",
1114
998
  role: "assistant" as const,
1115
- content: [{ kind: "text" as const, text: "Done now" }],
999
+ content: [{ kind: "text" as const, text: "Let me use a tool" }],
1000
+ },
1001
+ {
1002
+ kind: "tool-call" as const,
1003
+ toolId: "simple",
1004
+ state: IN_PROGRESS,
1005
+ callId: "call_1",
1006
+ arguments: "{}",
1116
1007
  },
1117
1008
  ],
1118
1009
  finishReason: "stop",
1119
1010
  usage: {
1120
- inputTokens: 3,
1011
+ inputTokens: 2,
1121
1012
  outputTokens: 2,
1122
- totalTokens: 5,
1013
+ totalTokens: 4,
1123
1014
  },
1124
1015
  warnings: [],
1125
1016
  };
1126
- },
1127
- stream: async function* () {
1128
- throw new Error("Not implemented");
1129
- },
1130
- };
1017
+ }
1018
+
1019
+ return {
1020
+ content: [
1021
+ {
1022
+ kind: "message" as const,
1023
+ id: "msg_2",
1024
+ role: "assistant" as const,
1025
+ content: [{ kind: "text" as const, text: "Done now" }],
1026
+ },
1027
+ ],
1028
+ finishReason: "stop",
1029
+ usage: {
1030
+ inputTokens: 3,
1031
+ outputTokens: 2,
1032
+ totalTokens: 5,
1033
+ },
1034
+ warnings: [],
1035
+ };
1036
+ });
1131
1037
 
1132
1038
  const simpleTool = tool({
1133
1039
  id: "simple",
@@ -1147,44 +1053,36 @@ describe("Thread", () => {
1147
1053
  });
1148
1054
 
1149
1055
  const kernl = new Kernl();
1150
- const thread = new Thread(kernl, agent, "test");
1056
+ const thread = new Thread(kernl, agent, userMessage("test"));
1151
1057
 
1152
1058
  const result = await thread.execute();
1153
1059
 
1154
1060
  // Should have made 2 calls - first with tool, second without
1155
- expect(result.state.tick).toBe(2);
1061
+ expect(thread._tick).toBe(2);
1156
1062
  });
1157
1063
  });
1158
1064
 
1159
1065
  describe("Final Output Parsing", () => {
1160
1066
  it("should return text output when responseType is 'text'", async () => {
1161
- const model: LanguageModel = {
1162
- spec: "1.0" as const,
1163
- provider: "test",
1164
- modelId: "test-model",
1165
- async generate(req: LanguageModelRequest) {
1166
- return {
1167
- content: [
1168
- {
1169
- kind: "message" as const,
1170
- id: "msg_1",
1171
- role: "assistant" as const,
1172
- content: [{ kind: "text" as const, text: "Hello, world!" }],
1173
- },
1174
- ],
1175
- finishReason: "stop",
1176
- usage: {
1177
- inputTokens: 2,
1178
- outputTokens: 2,
1179
- totalTokens: 4,
1067
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1068
+ return {
1069
+ content: [
1070
+ {
1071
+ kind: "message" as const,
1072
+ id: "msg_1",
1073
+ role: "assistant" as const,
1074
+ content: [{ kind: "text" as const, text: "Hello, world!" }],
1180
1075
  },
1181
- warnings: [],
1182
- };
1183
- },
1184
- stream: async function* () {
1185
- throw new Error("Not implemented");
1186
- },
1187
- };
1076
+ ],
1077
+ finishReason: "stop",
1078
+ usage: {
1079
+ inputTokens: 2,
1080
+ outputTokens: 2,
1081
+ totalTokens: 4,
1082
+ },
1083
+ warnings: [],
1084
+ };
1085
+ });
1188
1086
 
1189
1087
  const agent = new Agent({
1190
1088
  id: "test",
@@ -1195,12 +1093,12 @@ describe("Thread", () => {
1195
1093
  });
1196
1094
 
1197
1095
  const kernl = new Kernl();
1198
- const thread = new Thread(kernl, agent, "test");
1096
+ const thread = new Thread(kernl, agent, userMessage("test"));
1199
1097
 
1200
1098
  const result = await thread.execute();
1201
1099
 
1202
1100
  expect(result.response).toBe("Hello, world!");
1203
- expect(result.state.tick).toBe(1);
1101
+ expect(thread._tick).toBe(1);
1204
1102
  });
1205
1103
 
1206
1104
  it("should parse and validate structured output with valid JSON", async () => {
@@ -1210,38 +1108,30 @@ describe("Thread", () => {
1210
1108
  email: z.string().email(),
1211
1109
  });
1212
1110
 
1213
- const model: LanguageModel = {
1214
- spec: "1.0" as const,
1215
- provider: "test",
1216
- modelId: "test-model",
1217
- async generate(req: LanguageModelRequest) {
1218
- return {
1219
- content: [
1220
- {
1221
- kind: "message" as const,
1222
- id: "msg_1",
1223
- role: "assistant" as const,
1224
- content: [
1225
- {
1226
- kind: "text" as const,
1227
- text: '{"name": "Alice", "age": 30, "email": "alice@example.com"}',
1228
- },
1229
- ],
1230
- },
1231
- ],
1232
- finishReason: "stop",
1233
- usage: {
1234
- inputTokens: 2,
1235
- outputTokens: 2,
1236
- totalTokens: 4,
1111
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1112
+ return {
1113
+ content: [
1114
+ {
1115
+ kind: "message" as const,
1116
+ id: "msg_1",
1117
+ role: "assistant" as const,
1118
+ content: [
1119
+ {
1120
+ kind: "text" as const,
1121
+ text: '{"name": "Alice", "age": 30, "email": "alice@example.com"}',
1122
+ },
1123
+ ],
1237
1124
  },
1238
- warnings: [],
1239
- };
1240
- },
1241
- stream: async function* () {
1242
- throw new Error("Not implemented");
1243
- },
1244
- };
1125
+ ],
1126
+ finishReason: "stop",
1127
+ usage: {
1128
+ inputTokens: 2,
1129
+ outputTokens: 2,
1130
+ totalTokens: 4,
1131
+ },
1132
+ warnings: [],
1133
+ };
1134
+ });
1245
1135
 
1246
1136
  const agent = new Agent({
1247
1137
  id: "test",
@@ -1252,7 +1142,7 @@ describe("Thread", () => {
1252
1142
  });
1253
1143
 
1254
1144
  const kernl = new Kernl();
1255
- const thread = new Thread(kernl, agent, "test");
1145
+ const thread = new Thread(kernl, agent, userMessage("test"));
1256
1146
 
1257
1147
  const result = await thread.execute();
1258
1148
 
@@ -1268,38 +1158,30 @@ describe("Thread", () => {
1268
1158
  name: z.string(),
1269
1159
  });
1270
1160
 
1271
- const model: LanguageModel = {
1272
- spec: "1.0" as const,
1273
- provider: "test",
1274
- modelId: "test-model",
1275
- async generate(req: LanguageModelRequest) {
1276
- return {
1277
- content: [
1278
- {
1279
- kind: "message" as const,
1280
- id: "msg_1",
1281
- role: "assistant" as const,
1282
- content: [
1283
- {
1284
- kind: "text" as const,
1285
- text: '{"name": "Alice"', // Invalid JSON - missing closing brace
1286
- },
1287
- ],
1288
- },
1289
- ],
1290
- finishReason: "stop",
1291
- usage: {
1292
- inputTokens: 2,
1293
- outputTokens: 2,
1294
- totalTokens: 4,
1161
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1162
+ return {
1163
+ content: [
1164
+ {
1165
+ kind: "message" as const,
1166
+ id: "msg_1",
1167
+ role: "assistant" as const,
1168
+ content: [
1169
+ {
1170
+ kind: "text" as const,
1171
+ text: '{"name": "Alice"', // Invalid JSON - missing closing brace
1172
+ },
1173
+ ],
1295
1174
  },
1296
- warnings: [],
1297
- };
1298
- },
1299
- stream: async function* () {
1300
- throw new Error("Not implemented");
1301
- },
1302
- };
1175
+ ],
1176
+ finishReason: "stop",
1177
+ usage: {
1178
+ inputTokens: 2,
1179
+ outputTokens: 2,
1180
+ totalTokens: 4,
1181
+ },
1182
+ warnings: [],
1183
+ };
1184
+ });
1303
1185
 
1304
1186
  const agent = new Agent({
1305
1187
  id: "test",
@@ -1310,7 +1192,7 @@ describe("Thread", () => {
1310
1192
  });
1311
1193
 
1312
1194
  const kernl = new Kernl();
1313
- const thread = new Thread(kernl, agent, "test");
1195
+ const thread = new Thread(kernl, agent, userMessage("test"));
1314
1196
 
1315
1197
  await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1316
1198
  });
@@ -1321,38 +1203,30 @@ describe("Thread", () => {
1321
1203
  age: z.number(),
1322
1204
  });
1323
1205
 
1324
- const model: LanguageModel = {
1325
- spec: "1.0" as const,
1326
- provider: "test",
1327
- modelId: "test-model",
1328
- async generate(req: LanguageModelRequest) {
1329
- return {
1330
- content: [
1331
- {
1332
- kind: "message" as const,
1333
- id: "msg_1",
1334
- role: "assistant" as const,
1335
- content: [
1336
- {
1337
- kind: "text" as const,
1338
- text: '{"name": "Alice", "age": "thirty"}', // age is string instead of number
1339
- },
1340
- ],
1341
- },
1342
- ],
1343
- finishReason: "stop",
1344
- usage: {
1345
- inputTokens: 2,
1346
- outputTokens: 2,
1347
- totalTokens: 4,
1206
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1207
+ return {
1208
+ content: [
1209
+ {
1210
+ kind: "message" as const,
1211
+ id: "msg_1",
1212
+ role: "assistant" as const,
1213
+ content: [
1214
+ {
1215
+ kind: "text" as const,
1216
+ text: '{"name": "Alice", "age": "thirty"}', // age is string instead of number
1217
+ },
1218
+ ],
1348
1219
  },
1349
- warnings: [],
1350
- };
1351
- },
1352
- stream: async function* () {
1353
- throw new Error("Not implemented");
1354
- },
1355
- };
1220
+ ],
1221
+ finishReason: "stop",
1222
+ usage: {
1223
+ inputTokens: 2,
1224
+ outputTokens: 2,
1225
+ totalTokens: 4,
1226
+ },
1227
+ warnings: [],
1228
+ };
1229
+ });
1356
1230
 
1357
1231
  const agent = new Agent({
1358
1232
  id: "test",
@@ -1363,7 +1237,7 @@ describe("Thread", () => {
1363
1237
  });
1364
1238
 
1365
1239
  const kernl = new Kernl();
1366
- const thread = new Thread(kernl, agent, "test");
1240
+ const thread = new Thread(kernl, agent, userMessage("test"));
1367
1241
 
1368
1242
  await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1369
1243
  });
@@ -1375,38 +1249,30 @@ describe("Thread", () => {
1375
1249
  email: z.string(),
1376
1250
  });
1377
1251
 
1378
- const model: LanguageModel = {
1379
- spec: "1.0" as const,
1380
- provider: "test",
1381
- modelId: "test-model",
1382
- async generate(req: LanguageModelRequest) {
1383
- return {
1384
- content: [
1385
- {
1386
- kind: "message" as const,
1387
- id: "msg_1",
1388
- role: "assistant" as const,
1389
- content: [
1390
- {
1391
- kind: "text" as const,
1392
- text: '{"name": "Alice", "age": 30}', // missing email
1393
- },
1394
- ],
1395
- },
1396
- ],
1397
- finishReason: "stop",
1398
- usage: {
1399
- inputTokens: 2,
1400
- outputTokens: 2,
1401
- totalTokens: 4,
1252
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1253
+ return {
1254
+ content: [
1255
+ {
1256
+ kind: "message" as const,
1257
+ id: "msg_1",
1258
+ role: "assistant" as const,
1259
+ content: [
1260
+ {
1261
+ kind: "text" as const,
1262
+ text: '{"name": "Alice", "age": 30}', // missing email
1263
+ },
1264
+ ],
1402
1265
  },
1403
- warnings: [],
1404
- };
1405
- },
1406
- stream: async function* () {
1407
- throw new Error("Not implemented");
1408
- },
1409
- };
1266
+ ],
1267
+ finishReason: "stop",
1268
+ usage: {
1269
+ inputTokens: 2,
1270
+ outputTokens: 2,
1271
+ totalTokens: 4,
1272
+ },
1273
+ warnings: [],
1274
+ };
1275
+ });
1410
1276
 
1411
1277
  const agent = new Agent({
1412
1278
  id: "test",
@@ -1417,7 +1283,7 @@ describe("Thread", () => {
1417
1283
  });
1418
1284
 
1419
1285
  const kernl = new Kernl();
1420
- const thread = new Thread(kernl, agent, "test");
1286
+ const thread = new Thread(kernl, agent, userMessage("test"));
1421
1287
 
1422
1288
  await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1423
1289
  });
@@ -1436,44 +1302,36 @@ describe("Thread", () => {
1436
1302
  }),
1437
1303
  });
1438
1304
 
1439
- const model: LanguageModel = {
1440
- spec: "1.0" as const,
1441
- provider: "test",
1442
- modelId: "test-model",
1443
- async generate(req: LanguageModelRequest) {
1444
- return {
1445
- content: [
1446
- {
1447
- kind: "message" as const,
1448
- id: "msg_1",
1449
- role: "assistant" as const,
1450
- content: [
1451
- {
1452
- kind: "text" as const,
1453
- text: JSON.stringify({
1454
- user: {
1455
- name: "Bob",
1456
- profile: { bio: "Engineer", age: 25 },
1457
- },
1458
- metadata: { timestamp: "2024-01-01" },
1459
- }),
1460
- },
1461
- ],
1462
- },
1463
- ],
1464
- finishReason: "stop",
1465
- usage: {
1466
- inputTokens: 2,
1467
- outputTokens: 2,
1468
- totalTokens: 4,
1305
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1306
+ return {
1307
+ content: [
1308
+ {
1309
+ kind: "message" as const,
1310
+ id: "msg_1",
1311
+ role: "assistant" as const,
1312
+ content: [
1313
+ {
1314
+ kind: "text" as const,
1315
+ text: JSON.stringify({
1316
+ user: {
1317
+ name: "Bob",
1318
+ profile: { bio: "Engineer", age: 25 },
1319
+ },
1320
+ metadata: { timestamp: "2024-01-01" },
1321
+ }),
1322
+ },
1323
+ ],
1469
1324
  },
1470
- warnings: [],
1471
- };
1472
- },
1473
- stream: async function* () {
1474
- throw new Error("Not implemented");
1475
- },
1476
- };
1325
+ ],
1326
+ finishReason: "stop",
1327
+ usage: {
1328
+ inputTokens: 2,
1329
+ outputTokens: 2,
1330
+ totalTokens: 4,
1331
+ },
1332
+ warnings: [],
1333
+ };
1334
+ });
1477
1335
 
1478
1336
  const agent = new Agent({
1479
1337
  id: "test",
@@ -1484,7 +1342,7 @@ describe("Thread", () => {
1484
1342
  });
1485
1343
 
1486
1344
  const kernl = new Kernl();
1487
- const thread = new Thread(kernl, agent, "test");
1345
+ const thread = new Thread(kernl, agent, userMessage("test"));
1488
1346
 
1489
1347
  const result = await thread.execute();
1490
1348
 
@@ -1500,42 +1358,18 @@ describe("Thread", () => {
1500
1358
  it("should continue loop when no text in assistant message", async () => {
1501
1359
  let callCount = 0;
1502
1360
 
1503
- const model: LanguageModel = {
1504
- spec: "1.0" as const,
1505
- provider: "test",
1506
- modelId: "test-model",
1507
- async generate(req: LanguageModelRequest) {
1508
- callCount++;
1361
+ const model = createMockModel(async (req: LanguageModelRequest) => {
1362
+ callCount++;
1509
1363
 
1510
- // First call: return empty message (no text)
1511
- if (callCount === 1) {
1512
- return {
1513
- content: [
1514
- {
1515
- kind: "message" as const,
1516
- id: "msg_1",
1517
- role: "assistant" as const,
1518
- content: [], // No content
1519
- },
1520
- ],
1521
- finishReason: "stop",
1522
- usage: {
1523
- inputTokens: 2,
1524
- outputTokens: 2,
1525
- totalTokens: 4,
1526
- },
1527
- warnings: [],
1528
- };
1529
- }
1530
-
1531
- // Second call: return message with text
1364
+ // First call: return empty message (no text)
1365
+ if (callCount === 1) {
1532
1366
  return {
1533
1367
  content: [
1534
1368
  {
1535
1369
  kind: "message" as const,
1536
- id: "msg_2",
1370
+ id: "msg_1",
1537
1371
  role: "assistant" as const,
1538
- content: [{ kind: "text" as const, text: "Now I have text" }],
1372
+ content: [], // No content
1539
1373
  },
1540
1374
  ],
1541
1375
  finishReason: "stop",
@@ -1546,11 +1380,27 @@ describe("Thread", () => {
1546
1380
  },
1547
1381
  warnings: [],
1548
1382
  };
1549
- },
1550
- stream: async function* () {
1551
- throw new Error("Not implemented");
1552
- },
1553
- };
1383
+ }
1384
+
1385
+ // Second call: return message with text
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ kind: "message" as const,
1390
+ id: "msg_2",
1391
+ role: "assistant" as const,
1392
+ content: [{ kind: "text" as const, text: "Now I have text" }],
1393
+ },
1394
+ ],
1395
+ finishReason: "stop",
1396
+ usage: {
1397
+ inputTokens: 2,
1398
+ outputTokens: 2,
1399
+ totalTokens: 4,
1400
+ },
1401
+ warnings: [],
1402
+ };
1403
+ });
1554
1404
 
1555
1405
  const agent = new Agent({
1556
1406
  id: "test",
@@ -1561,14 +1411,14 @@ describe("Thread", () => {
1561
1411
  });
1562
1412
 
1563
1413
  const kernl = new Kernl();
1564
- const thread = new Thread(kernl, agent, "test");
1414
+ const thread = new Thread(kernl, agent, userMessage("test"));
1565
1415
 
1566
1416
  const result = await thread.execute();
1567
1417
 
1568
1418
  // Should have made 2 calls
1569
1419
  expect(callCount).toBe(2);
1570
1420
  expect(result.response).toBe("Now I have text");
1571
- expect(result.state.tick).toBe(2);
1421
+ expect(thread._tick).toBe(2);
1572
1422
  });
1573
1423
  });
1574
1424
  });