@kernl-sdk/ai 0.1.2 → 0.2.5

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 +1 -1
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/CHANGELOG.md +78 -0
  4. package/LICENSE +1 -1
  5. package/dist/__tests__/integration.test.js +277 -26
  6. package/dist/__tests__/language-model.test.js +2 -1
  7. package/dist/convert/__tests__/message.test.js +27 -2
  8. package/dist/convert/__tests__/stream.test.js +31 -7
  9. package/dist/convert/__tests__/ui-message.test.d.ts +2 -0
  10. package/dist/convert/__tests__/ui-message.test.d.ts.map +1 -0
  11. package/dist/convert/__tests__/ui-message.test.js +1836 -0
  12. package/dist/convert/__tests__/ui-stream.test.d.ts +2 -0
  13. package/dist/convert/__tests__/ui-stream.test.d.ts.map +1 -0
  14. package/dist/convert/__tests__/ui-stream.test.js +452 -0
  15. package/dist/convert/message.d.ts +2 -1
  16. package/dist/convert/message.d.ts.map +1 -1
  17. package/dist/convert/message.js +16 -10
  18. package/dist/convert/response.d.ts +2 -1
  19. package/dist/convert/response.d.ts.map +1 -1
  20. package/dist/convert/response.js +66 -46
  21. package/dist/convert/settings.d.ts +2 -1
  22. package/dist/convert/settings.d.ts.map +1 -1
  23. package/dist/convert/stream.d.ts +2 -1
  24. package/dist/convert/stream.d.ts.map +1 -1
  25. package/dist/convert/stream.js +12 -17
  26. package/dist/convert/tools.d.ts +2 -1
  27. package/dist/convert/tools.d.ts.map +1 -1
  28. package/dist/convert/ui-message.d.ts +40 -0
  29. package/dist/convert/ui-message.d.ts.map +1 -0
  30. package/dist/convert/ui-message.js +324 -0
  31. package/dist/convert/ui-stream.d.ts +29 -0
  32. package/dist/convert/ui-stream.d.ts.map +1 -0
  33. package/dist/convert/ui-stream.js +139 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/language-model.d.ts.map +1 -1
  38. package/dist/language-model.js +72 -83
  39. package/package.json +11 -7
  40. package/src/__tests__/integration.test.ts +789 -507
  41. package/src/__tests__/language-model.test.ts +2 -1
  42. package/src/convert/__tests__/message.test.ts +29 -2
  43. package/src/convert/__tests__/stream.test.ts +34 -7
  44. package/src/convert/__tests__/ui-message.test.ts +2008 -0
  45. package/src/convert/__tests__/ui-stream.test.ts +547 -0
  46. package/src/convert/message.ts +18 -13
  47. package/src/convert/response.ts +82 -52
  48. package/src/convert/settings.ts +2 -1
  49. package/src/convert/stream.ts +22 -20
  50. package/src/convert/tools.ts +1 -1
  51. package/src/convert/ui-message.ts +409 -0
  52. package/src/convert/ui-stream.ts +167 -0
  53. package/src/index.ts +2 -0
  54. package/src/language-model.ts +78 -87
  55. package/tsconfig.json +1 -1
  56. package/vitest.config.ts +1 -0
  57. package/src/error.ts +0 -16
  58. package/src/types.ts +0 -0
@@ -1,4 +1,4 @@
1
1
 
2
- > @kernl-sdk/ai@0.1.2 build /Users/andjones/Documents/projects/kernl/packages/_ai
2
+ > @kernl-sdk/ai@0.2.4 build /Users/andjones/Documents/projects/kernl/packages/providers/ai
3
3
  > tsc && tsc-alias
4
4
 
@@ -0,0 +1,4 @@
1
+
2
+ > @kernl-sdk/ai@0.1.4 check-types /Users/andjones/Documents/projects/kernl/packages/_ai
3
+ > tsc --noEmit
4
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @kernl/ai
2
2
 
3
+ ## 0.2.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Migrate packages from GitHub Packages to npm registry.
8
+
9
+ **Breaking change for `kernl` (formerly `@kernl-sdk/core`):**
10
+
11
+ The core package has been renamed from `@kernl-sdk/core` to `kernl`. Update your imports:
12
+
13
+ ```diff
14
+ - import { Agent, Kernl } from "@kernl-sdk/core";
15
+ + import { Agent, Kernl } from "kernl";
16
+ ```
17
+
18
+ All other packages remain under the `@kernl-sdk` scope and are now publicly available on npm.
19
+
20
+ - Updated dependencies
21
+ - @kernl-sdk/shared@0.1.5
22
+ - @kernl-sdk/protocol@0.2.4
23
+
24
+ ## 0.2.4
25
+
26
+ ### Patch Changes
27
+
28
+ - 7946b16: Fix handling of tool calls with no required parameters. When AI providers (particularly Anthropic) send empty string arguments for tools with all optional parameters, convert to valid JSON "{}" to prevent parsing errors. Also fix tool-call state to use IN_PROGRESS instead of COMPLETED.
29
+
30
+ ## 0.2.3
31
+
32
+ ### Patch Changes
33
+
34
+ - 8551086: Add historyToUIMessages function to convert thread history to AI SDK UIMessage format for useChat hook. Preserves providerMetadata on all parts (text, file, reasoning, tools) and groups tool calls with results.
35
+
36
+ ## 0.2.2
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies
41
+ - @kernl-sdk/shared@0.1.4
42
+ - @kernl-sdk/protocol@0.2.3
43
+
44
+ ## 0.2.1
45
+
46
+ ### Patch Changes
47
+
48
+ - 05ce1f1: fix: handle tool result errors with error-text output
49
+
50
+ When a tool call fails and returns an error, the MESSAGE codec now properly encodes the error using the AI SDK's error-text output type instead of attempting to send null as a json value. This fixes the "Missing required parameter: 'output'" error that occurred when MCP tools returned errors.
51
+
52
+ ## 0.2.0
53
+
54
+ ### Minor Changes
55
+
56
+ - 0f25713: Add UI message and stream conversion utilities for AI SDK integration
57
+ - Add `UIMessageCodec` for bidirectional conversion between kernl and AI SDK message formats
58
+ - Add `toUIMessageStream()` helper to convert kernl streams to AI SDK UIMessageStream format
59
+ - Add `STREAM_UI_PART` codec for converting LanguageModelStreamEvent to UIMessageChunk
60
+ - Enable seamless integration with AI SDK's `useChat` hook and `createUIMessageStreamResponse`
61
+ - Add comprehensive test suites for both UI message and stream conversion
62
+
63
+ ## 0.1.4
64
+
65
+ ### Patch Changes
66
+
67
+ - 2c62c0a: Migrate from @kernl to @kernl-sdk scope
68
+
69
+ All packages have been migrated to the @kernl-sdk scope for publishing to GitHub Packages under the kernl-sdk organization.
70
+
71
+ - Updated dependencies [2c62c0a]
72
+ - @kernl-sdk/shared@0.1.3
73
+ - @kernl-sdk/protocol@0.2.2
74
+
75
+ ## 0.1.3
76
+
77
+ ### Patch Changes
78
+
79
+ - 19020a1: Fix tool call argument encoding for multi-turn conversations with Anthropic models
80
+
3
81
  ## 0.1.2
4
82
 
5
83
  ### Patch Changes
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright 2025 Andrew Jones
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
@@ -1,23 +1,27 @@
1
1
  import { describe, it, expect, beforeAll } from "vitest";
2
2
  import { openai } from "@ai-sdk/openai";
3
+ import { anthropic } from "@ai-sdk/anthropic";
4
+ import { IN_PROGRESS } from "@kernl-sdk/protocol";
3
5
  import { AISDKLanguageModel } from "../language-model";
4
6
  /**
5
7
  * Integration tests for AISDKLanguageModel with real AI SDK providers.
6
8
  *
7
- * These tests require an OPENAI_API_KEY environment variable to be set.
8
- * They will be skipped if the API key is not available.
9
+ * These tests require API keys to be set:
10
+ * - OPENAI_API_KEY for OpenAI tests
11
+ * - ANTHROPIC_API_KEY for Anthropic tests
9
12
  *
10
- * Run with: OPENAI_API_KEY=your-key pnpm test:run
13
+ * Run with: OPENAI_API_KEY=your-key ANTHROPIC_API_KEY=your-key pnpm test:run
11
14
  */
12
- const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
13
- describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () => {
14
- let gpt4omini;
15
+ const SKIP_OPENAI_TESTS = !process.env.OPENAI_API_KEY;
16
+ const SKIP_ANTHROPIC_TESTS = !process.env.ANTHROPIC_API_KEY;
17
+ describe.skipIf(SKIP_OPENAI_TESTS)("AISDKLanguageModel - OpenAI", () => {
18
+ let gpt41;
15
19
  beforeAll(() => {
16
- gpt4omini = new AISDKLanguageModel(openai("gpt-4o-mini")); // gpt-4o-mini for fast, cheap testing
20
+ gpt41 = new AISDKLanguageModel(openai("gpt-4.1"));
17
21
  });
18
22
  describe("generate", () => {
19
23
  it("should generate a simple text response", async () => {
20
- const response = await gpt4omini.generate({
24
+ const response = await gpt41.generate({
21
25
  input: [
22
26
  {
23
27
  kind: "message",
@@ -44,7 +48,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
44
48
  expect(messages.length).toBeGreaterThan(0);
45
49
  });
46
50
  it("should handle system messages", async () => {
47
- const response = await gpt4omini.generate({
51
+ const response = await gpt41.generate({
48
52
  input: [
49
53
  {
50
54
  kind: "message",
@@ -73,7 +77,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
73
77
  expect(response.usage.totalTokens).toBeGreaterThan(0);
74
78
  });
75
79
  it("should handle multi-turn conversations", async () => {
76
- const response = await gpt4omini.generate({
80
+ const response = await gpt41.generate({
77
81
  input: [
78
82
  {
79
83
  kind: "message",
@@ -106,7 +110,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
106
110
  expect(assistantMessages.length).toBeGreaterThan(0);
107
111
  });
108
112
  it("should respect temperature setting", async () => {
109
- const response = await gpt4omini.generate({
113
+ const response = await gpt41.generate({
110
114
  input: [
111
115
  {
112
116
  kind: "message",
@@ -124,7 +128,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
124
128
  expect(response.usage.totalTokens).toBeGreaterThan(0);
125
129
  });
126
130
  it("should respect maxTokens setting", async () => {
127
- const response = await gpt4omini.generate({
131
+ const response = await gpt41.generate({
128
132
  input: [
129
133
  {
130
134
  kind: "message",
@@ -146,7 +150,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
146
150
  describe("stream", () => {
147
151
  it("should stream text responses", async () => {
148
152
  const events = [];
149
- for await (const event of gpt4omini.stream({
153
+ for await (const event of gpt41.stream({
150
154
  input: [
151
155
  {
152
156
  kind: "message",
@@ -173,7 +177,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
173
177
  });
174
178
  it("should stream text deltas", async () => {
175
179
  const events = [];
176
- for await (const event of gpt4omini.stream({
180
+ for await (const event of gpt41.stream({
177
181
  input: [
178
182
  {
179
183
  kind: "message",
@@ -200,7 +204,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
200
204
  });
201
205
  it("should handle limited token streams", async () => {
202
206
  const events = [];
203
- for await (const event of gpt4omini.stream({
207
+ for await (const event of gpt41.stream({
204
208
  input: [
205
209
  {
206
210
  kind: "message",
@@ -223,7 +227,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
223
227
  });
224
228
  it("should yield both delta events and complete Message items", async () => {
225
229
  const events = [];
226
- for await (const event of gpt4omini.stream({
230
+ for await (const event of gpt41.stream({
227
231
  input: [
228
232
  {
229
233
  kind: "message",
@@ -264,18 +268,75 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
264
268
  expect(textContent.text).toBeDefined();
265
269
  expect(textContent.text.length).toBeGreaterThan(0);
266
270
  // Verify accumulated text matches concatenated deltas
267
- const accumulatedFromDeltas = textDeltas
268
- .map((d) => d.text)
269
- .join("");
271
+ const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
270
272
  expect(textContent.text).toBe(accumulatedFromDeltas);
271
273
  // Should have finish event
272
274
  const finishEvents = events.filter((e) => e.kind === "finish");
273
275
  expect(finishEvents.length).toBe(1);
274
276
  });
277
+ it("should handle streaming tools with no required parameters (empty arguments)", async () => {
278
+ // Verify the empty arguments fix works in streaming mode as well
279
+ const events = [];
280
+ for await (const event of gpt41.stream({
281
+ input: [
282
+ {
283
+ kind: "message",
284
+ role: "user",
285
+ id: "msg-1",
286
+ content: [
287
+ {
288
+ kind: "text",
289
+ text: "Use the list_all_items tool",
290
+ },
291
+ ],
292
+ },
293
+ ],
294
+ tools: [
295
+ {
296
+ kind: "function",
297
+ name: "list_all_items",
298
+ description: "List all items",
299
+ parameters: {
300
+ type: "object",
301
+ properties: {
302
+ category: {
303
+ type: "string",
304
+ description: "Optional category filter",
305
+ },
306
+ },
307
+ // No required parameters
308
+ },
309
+ },
310
+ ],
311
+ settings: {
312
+ maxTokens: 200,
313
+ temperature: 0,
314
+ toolChoice: { kind: "required" }, // Force tool use
315
+ },
316
+ })) {
317
+ events.push(event);
318
+ }
319
+ expect(events.length).toBeGreaterThan(0);
320
+ // Should have a tool-call event
321
+ const toolCalls = events.filter((e) => e.kind === "tool-call");
322
+ expect(toolCalls.length).toBeGreaterThan(0);
323
+ const toolCall = toolCalls[0];
324
+ expect(toolCall.callId).toBeDefined();
325
+ expect(toolCall.toolId).toBe("list_all_items");
326
+ expect(toolCall.state).toBe(IN_PROGRESS);
327
+ // Critical assertion: arguments should be valid JSON even if empty
328
+ expect(toolCall.arguments).toBeDefined();
329
+ expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
330
+ const args = JSON.parse(toolCall.arguments);
331
+ expect(typeof args).toBe("object");
332
+ // Should have finish event
333
+ const finishEvents = events.filter((e) => e.kind === "finish");
334
+ expect(finishEvents.length).toBe(1);
335
+ });
275
336
  });
276
337
  describe("tools", () => {
277
338
  it("should call tools when requested", async () => {
278
- const response = await gpt4omini.generate({
339
+ const response = await gpt41.generate({
279
340
  input: [
280
341
  {
281
342
  kind: "message",
@@ -317,7 +378,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
317
378
  expect(toolCall.arguments).toBeDefined();
318
379
  });
319
380
  it("should handle tool choice setting", async () => {
320
- const response = await gpt4omini.generate({
381
+ const response = await gpt41.generate({
321
382
  input: [
322
383
  {
323
384
  kind: "message",
@@ -360,7 +421,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
360
421
  expect(toolCalls.length).toBeGreaterThan(0);
361
422
  });
362
423
  it("should handle multiple tool calls", async () => {
363
- const response = await gpt4omini.generate({
424
+ const response = await gpt41.generate({
364
425
  input: [
365
426
  {
366
427
  kind: "message",
@@ -403,7 +464,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
403
464
  });
404
465
  it("should handle multi-turn conversation with tool results", async () => {
405
466
  // First turn: get tool calls from the model
406
- const firstResponse = await gpt4omini.generate({
467
+ const firstResponse = await gpt41.generate({
407
468
  input: [
408
469
  {
409
470
  kind: "message",
@@ -442,7 +503,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
442
503
  expect(toolCall.callId).toBeDefined();
443
504
  expect(toolCall.toolId).toBe("calculate");
444
505
  // Second turn: send tool results back to the model
445
- const secondResponse = await gpt4omini.generate({
506
+ const secondResponse = await gpt41.generate({
446
507
  input: [
447
508
  {
448
509
  kind: "message",
@@ -487,11 +548,73 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
487
548
  const messages = secondResponse.content.filter((item) => item.kind === "message" && item.role === "assistant");
488
549
  expect(messages.length).toBeGreaterThan(0);
489
550
  });
551
+ it("should handle tools with no required parameters (empty arguments)", async () => {
552
+ // This test verifies the fix for empty string arguments
553
+ // When a tool has no required parameters and is called with no args,
554
+ // AI SDK sends input: "", which should be converted to "{}" for valid JSON
555
+ const response = await gpt41.generate({
556
+ input: [
557
+ {
558
+ kind: "message",
559
+ role: "user",
560
+ id: "msg-1",
561
+ content: [
562
+ {
563
+ kind: "text",
564
+ text: "Use the list_issues tool to get all issues",
565
+ },
566
+ ],
567
+ },
568
+ ],
569
+ tools: [
570
+ {
571
+ kind: "function",
572
+ name: "list_issues",
573
+ description: "List all issues in the system",
574
+ parameters: {
575
+ type: "object",
576
+ properties: {
577
+ status: {
578
+ type: "string",
579
+ description: "Optional status filter",
580
+ enum: ["open", "closed", "all"],
581
+ },
582
+ limit: {
583
+ type: "number",
584
+ description: "Optional limit on number of results",
585
+ },
586
+ },
587
+ // No required parameters - all are optional
588
+ },
589
+ },
590
+ ],
591
+ settings: {
592
+ maxTokens: 200,
593
+ temperature: 0,
594
+ toolChoice: { kind: "required" }, // Force tool use
595
+ },
596
+ });
597
+ expect(response.content).toBeDefined();
598
+ // Should have a tool call
599
+ const toolCalls = response.content.filter((item) => item.kind === "tool-call");
600
+ expect(toolCalls.length).toBeGreaterThan(0);
601
+ const toolCall = toolCalls[0];
602
+ expect(toolCall.callId).toBeDefined();
603
+ expect(toolCall.toolId).toBe("list_issues");
604
+ expect(toolCall.state).toBe(IN_PROGRESS);
605
+ // The critical assertion: arguments should be valid JSON
606
+ // Even if the tool was called with no args, it should be "{}" not ""
607
+ expect(toolCall.arguments).toBeDefined();
608
+ expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
609
+ // Parse should succeed and yield an object (possibly empty)
610
+ const args = JSON.parse(toolCall.arguments);
611
+ expect(typeof args).toBe("object");
612
+ });
490
613
  });
491
614
  describe("validation", () => {
492
615
  it("should throw error for invalid maxTokens", async () => {
493
616
  // AI SDK properly validates and throws errors for invalid values
494
- await expect(gpt4omini.generate({
617
+ await expect(gpt41.generate({
495
618
  input: [
496
619
  {
497
620
  kind: "message",
@@ -507,7 +630,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
507
630
  });
508
631
  it("should throw error for below minimum maxTokens", async () => {
509
632
  // OpenAI requires minimum 16 tokens
510
- await expect(gpt4omini.generate({
633
+ await expect(gpt41.generate({
511
634
  input: [
512
635
  {
513
636
  kind: "message",
@@ -523,3 +646,131 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
523
646
  });
524
647
  });
525
648
  });
649
+ describe.skipIf(SKIP_ANTHROPIC_TESTS)("AISDKLanguageModel - Anthropic", () => {
650
+ let claude;
651
+ beforeAll(() => {
652
+ claude = new AISDKLanguageModel(anthropic("claude-sonnet-4-5"));
653
+ });
654
+ describe("tools", () => {
655
+ it("should handle tools with no required parameters (Anthropic-specific)", async () => {
656
+ // This test specifically verifies Anthropic's behavior with empty arguments
657
+ // Anthropic was the provider that sent input: "" for tools with no required params
658
+ const response = await claude.generate({
659
+ input: [
660
+ {
661
+ kind: "message",
662
+ role: "user",
663
+ id: "msg-1",
664
+ content: [
665
+ {
666
+ kind: "text",
667
+ text: "Use the list_issues tool",
668
+ },
669
+ ],
670
+ },
671
+ ],
672
+ tools: [
673
+ {
674
+ kind: "function",
675
+ name: "list_issues",
676
+ description: "List all issues in the system",
677
+ parameters: {
678
+ type: "object",
679
+ properties: {
680
+ status: {
681
+ type: "string",
682
+ description: "Optional status filter",
683
+ enum: ["open", "closed", "all"],
684
+ },
685
+ assignee: {
686
+ type: "string",
687
+ description: "Optional assignee filter",
688
+ },
689
+ },
690
+ // No required parameters - all are optional
691
+ },
692
+ },
693
+ ],
694
+ settings: {
695
+ maxTokens: 200,
696
+ temperature: 0,
697
+ toolChoice: { kind: "required" }, // Force tool use
698
+ },
699
+ });
700
+ expect(response.content).toBeDefined();
701
+ // Should have a tool call
702
+ const toolCalls = response.content.filter((item) => item.kind === "tool-call");
703
+ expect(toolCalls.length).toBeGreaterThan(0);
704
+ const toolCall = toolCalls[0];
705
+ expect(toolCall.callId).toBeDefined();
706
+ expect(toolCall.toolId).toBe("list_issues");
707
+ expect(toolCall.state).toBe(IN_PROGRESS);
708
+ // Critical: Anthropic sends input: "" for tools with no required params
709
+ // Our adapter must convert this to "{}" for valid JSON
710
+ expect(toolCall.arguments).toBeDefined();
711
+ expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
712
+ const args = JSON.parse(toolCall.arguments);
713
+ expect(typeof args).toBe("object");
714
+ });
715
+ it("should handle streaming tools with no required parameters (Anthropic-specific)", async () => {
716
+ // Verify the fix works in streaming mode with Anthropic
717
+ const events = [];
718
+ for await (const event of claude.stream({
719
+ input: [
720
+ {
721
+ kind: "message",
722
+ role: "user",
723
+ id: "msg-1",
724
+ content: [
725
+ {
726
+ kind: "text",
727
+ text: "Use the get_all_data tool",
728
+ },
729
+ ],
730
+ },
731
+ ],
732
+ tools: [
733
+ {
734
+ kind: "function",
735
+ name: "get_all_data",
736
+ description: "Get all data from the system",
737
+ parameters: {
738
+ type: "object",
739
+ properties: {
740
+ format: {
741
+ type: "string",
742
+ description: "Optional output format",
743
+ enum: ["json", "csv", "xml"],
744
+ },
745
+ },
746
+ // No required parameters
747
+ },
748
+ },
749
+ ],
750
+ settings: {
751
+ maxTokens: 200,
752
+ temperature: 0,
753
+ toolChoice: { kind: "required" }, // Force tool use
754
+ },
755
+ })) {
756
+ events.push(event);
757
+ }
758
+ expect(events.length).toBeGreaterThan(0);
759
+ // Should have a tool-call event
760
+ const toolCalls = events.filter((e) => e.kind === "tool-call");
761
+ expect(toolCalls.length).toBeGreaterThan(0);
762
+ const toolCall = toolCalls[0];
763
+ expect(toolCall.callId).toBeDefined();
764
+ expect(toolCall.toolId).toBe("get_all_data");
765
+ expect(toolCall.state).toBe(IN_PROGRESS);
766
+ // Critical: arguments should be valid JSON even if Anthropic sent ""
767
+ expect(toolCall.arguments).toBeDefined();
768
+ expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
769
+ const args = JSON.parse(toolCall.arguments);
770
+ expect(typeof args).toBe("object");
771
+ // Should have finish event
772
+ const finishEvents = events.filter((e) => e.kind === "finish");
773
+ expect(finishEvents.length).toBe(1);
774
+ });
775
+ });
776
+ });
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
+ import { IN_PROGRESS } from "@kernl-sdk/protocol";
2
3
  import { AISDKLanguageModel } from "../language-model";
3
4
  /**
4
5
  * Unit tests for AISDKLanguageModel stream accumulation behavior
@@ -414,7 +415,7 @@ describe("AISDKLanguageModel", () => {
414
415
  kind: "tool-call",
415
416
  callId: "call-123",
416
417
  toolId: "calculator",
417
- state: "completed",
418
+ state: IN_PROGRESS,
418
419
  arguments: '{"expression":"2+2"}',
419
420
  });
420
421
  expect(events[1]).toMatchObject({ kind: "finish" });
@@ -204,7 +204,7 @@ describe("MESSAGE codec", () => {
204
204
  type: "tool-call",
205
205
  toolCallId: "call-123",
206
206
  toolName: "get_weather",
207
- input: JSON.stringify({ city: "SF" }),
207
+ input: { city: "SF" },
208
208
  providerOptions: undefined,
209
209
  },
210
210
  ],
@@ -251,6 +251,31 @@ describe("MESSAGE codec", () => {
251
251
  ],
252
252
  });
253
253
  });
254
+ it("should encode tool-result item with error", () => {
255
+ const result = MESSAGE.encode({
256
+ kind: "tool-result",
257
+ callId: "call-123",
258
+ toolId: "get_weather",
259
+ state: "failed",
260
+ result: null,
261
+ error: "Network timeout",
262
+ });
263
+ expect(result).toEqual({
264
+ role: "tool",
265
+ content: [
266
+ {
267
+ type: "tool-result",
268
+ toolCallId: "call-123",
269
+ toolName: "get_weather",
270
+ output: {
271
+ type: "error-text",
272
+ value: "Network timeout",
273
+ },
274
+ providerOptions: undefined,
275
+ },
276
+ ],
277
+ });
278
+ });
254
279
  });
255
280
  describe("encode - reasoning items", () => {
256
281
  it("should encode reasoning item with text", () => {
@@ -289,7 +314,7 @@ describe("MESSAGE codec", () => {
289
314
  it("should throw error for unsupported item kind", () => {
290
315
  expect(() => MESSAGE.encode({
291
316
  kind: "unknown-kind",
292
- })).toThrow("Unsupported LanguageModelItem kind: unknown-kind");
317
+ })).toThrow("Unsupported LanguageModelItem kind");
293
318
  });
294
319
  });
295
320
  describe("decode", () => {