@kernl-sdk/ai 0.1.3 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +4 -0
- package/CHANGELOG.md +72 -0
- package/LICENSE +1 -1
- package/dist/__tests__/integration.test.js +277 -26
- package/dist/__tests__/language-model.test.js +2 -1
- package/dist/convert/__tests__/message.test.js +27 -2
- package/dist/convert/__tests__/stream.test.js +31 -7
- package/dist/convert/__tests__/ui-message.test.d.ts +2 -0
- package/dist/convert/__tests__/ui-message.test.d.ts.map +1 -0
- package/dist/convert/__tests__/ui-message.test.js +1836 -0
- package/dist/convert/__tests__/ui-stream.test.d.ts +2 -0
- package/dist/convert/__tests__/ui-stream.test.d.ts.map +1 -0
- package/dist/convert/__tests__/ui-stream.test.js +452 -0
- package/dist/convert/message.d.ts +2 -1
- package/dist/convert/message.d.ts.map +1 -1
- package/dist/convert/message.js +15 -9
- package/dist/convert/response.d.ts +2 -1
- package/dist/convert/response.d.ts.map +1 -1
- package/dist/convert/response.js +66 -46
- package/dist/convert/settings.d.ts +2 -1
- package/dist/convert/settings.d.ts.map +1 -1
- package/dist/convert/stream.d.ts +2 -1
- package/dist/convert/stream.d.ts.map +1 -1
- package/dist/convert/stream.js +12 -17
- package/dist/convert/tools.d.ts +2 -1
- package/dist/convert/tools.d.ts.map +1 -1
- package/dist/convert/ui-message.d.ts +40 -0
- package/dist/convert/ui-message.d.ts.map +1 -0
- package/dist/convert/ui-message.js +324 -0
- package/dist/convert/ui-stream.d.ts +29 -0
- package/dist/convert/ui-stream.d.ts.map +1 -0
- package/dist/convert/ui-stream.js +139 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/language-model.d.ts.map +1 -1
- package/dist/language-model.js +72 -83
- package/package.json +11 -7
- package/src/__tests__/integration.test.ts +789 -507
- package/src/__tests__/language-model.test.ts +2 -1
- package/src/convert/__tests__/message.test.ts +29 -2
- package/src/convert/__tests__/stream.test.ts +34 -7
- package/src/convert/__tests__/ui-message.test.ts +2008 -0
- package/src/convert/__tests__/ui-stream.test.ts +547 -0
- package/src/convert/message.ts +17 -12
- package/src/convert/response.ts +82 -52
- package/src/convert/settings.ts +2 -1
- package/src/convert/stream.ts +22 -20
- package/src/convert/tools.ts +1 -1
- package/src/convert/ui-message.ts +409 -0
- package/src/convert/ui-stream.ts +167 -0
- package/src/index.ts +2 -0
- package/src/language-model.ts +78 -87
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/src/error.ts +0 -16
- package/src/types.ts +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { UIMessageChunk } from "ai";
|
|
3
|
+
import type { LanguageModelStreamEvent } from "@kernl-sdk/protocol";
|
|
4
|
+
import { COMPLETED, FAILED, IN_PROGRESS } from "@kernl-sdk/protocol";
|
|
5
|
+
|
|
6
|
+
import { STREAM_UI_PART, toUIMessageStream } from "../ui-stream";
|
|
7
|
+
|
|
8
|
+
describe("STREAM_UI_PART codec", () => {
|
|
9
|
+
describe("encode - text events", () => {
|
|
10
|
+
it("should encode text-start event", () => {
|
|
11
|
+
const event: LanguageModelStreamEvent = {
|
|
12
|
+
kind: "text-start",
|
|
13
|
+
id: "text-1",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const result = STREAM_UI_PART.encode(event);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
type: "text-start",
|
|
20
|
+
id: "text-1",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should encode text-delta event", () => {
|
|
25
|
+
const event: LanguageModelStreamEvent = {
|
|
26
|
+
kind: "text-delta",
|
|
27
|
+
id: "text-1",
|
|
28
|
+
text: "Hello world",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = STREAM_UI_PART.encode(event);
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual({
|
|
34
|
+
type: "text-delta",
|
|
35
|
+
id: "text-1",
|
|
36
|
+
delta: "Hello world",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should encode text-end event", () => {
|
|
41
|
+
const event: LanguageModelStreamEvent = {
|
|
42
|
+
kind: "text-end",
|
|
43
|
+
id: "text-1",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = STREAM_UI_PART.encode(event);
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
type: "text-end",
|
|
50
|
+
id: "text-1",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("encode - reasoning events", () => {
|
|
56
|
+
it("should encode reasoning-start event", () => {
|
|
57
|
+
const event: LanguageModelStreamEvent = {
|
|
58
|
+
kind: "reasoning-start",
|
|
59
|
+
id: "reason-1",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = STREAM_UI_PART.encode(event);
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual({
|
|
65
|
+
type: "reasoning-start",
|
|
66
|
+
id: "reason-1",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should encode reasoning-delta event", () => {
|
|
71
|
+
const event: LanguageModelStreamEvent = {
|
|
72
|
+
kind: "reasoning-delta",
|
|
73
|
+
id: "reason-1",
|
|
74
|
+
text: "thinking step by step",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = STREAM_UI_PART.encode(event);
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
type: "reasoning-delta",
|
|
81
|
+
id: "reason-1",
|
|
82
|
+
delta: "thinking step by step",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should encode reasoning-end event", () => {
|
|
87
|
+
const event: LanguageModelStreamEvent = {
|
|
88
|
+
kind: "reasoning-end",
|
|
89
|
+
id: "reason-1",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = STREAM_UI_PART.encode(event);
|
|
93
|
+
|
|
94
|
+
expect(result).toEqual({
|
|
95
|
+
type: "reasoning-end",
|
|
96
|
+
id: "reason-1",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("encode - tool input events", () => {
|
|
102
|
+
it("should encode tool-input-start event", () => {
|
|
103
|
+
const event: LanguageModelStreamEvent = {
|
|
104
|
+
kind: "tool-input-start",
|
|
105
|
+
id: "tool-1",
|
|
106
|
+
toolName: "calculator",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = STREAM_UI_PART.encode(event);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
type: "tool-input-start",
|
|
113
|
+
toolCallId: "tool-1",
|
|
114
|
+
toolName: "calculator",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should encode tool-input-start event with title", () => {
|
|
119
|
+
const event: LanguageModelStreamEvent = {
|
|
120
|
+
kind: "tool-input-start",
|
|
121
|
+
id: "tool-1",
|
|
122
|
+
toolName: "calculator",
|
|
123
|
+
title: "Calculate sum",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const result = STREAM_UI_PART.encode(event);
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
type: "tool-input-start",
|
|
130
|
+
toolCallId: "tool-1",
|
|
131
|
+
toolName: "calculator",
|
|
132
|
+
title: "Calculate sum",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should encode tool-input-delta event", () => {
|
|
137
|
+
const event: LanguageModelStreamEvent = {
|
|
138
|
+
kind: "tool-input-delta",
|
|
139
|
+
id: "tool-1",
|
|
140
|
+
delta: '{"a": 1',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = STREAM_UI_PART.encode(event);
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
type: "tool-input-delta",
|
|
147
|
+
toolCallId: "tool-1",
|
|
148
|
+
inputTextDelta: '{"a": 1',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should return null for tool-input-end event", () => {
|
|
153
|
+
const event: LanguageModelStreamEvent = {
|
|
154
|
+
kind: "tool-input-end",
|
|
155
|
+
id: "tool-1",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = STREAM_UI_PART.encode(event);
|
|
159
|
+
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("encode - tool call and result events", () => {
|
|
165
|
+
it("should encode tool-call as tool-input-available", () => {
|
|
166
|
+
const event: LanguageModelStreamEvent = {
|
|
167
|
+
kind: "tool-call",
|
|
168
|
+
callId: "call-123",
|
|
169
|
+
toolId: "calculator",
|
|
170
|
+
state: COMPLETED,
|
|
171
|
+
arguments: '{"a": 5, "b": 3}',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = STREAM_UI_PART.encode(event);
|
|
175
|
+
|
|
176
|
+
expect(result).toEqual({
|
|
177
|
+
type: "tool-input-available",
|
|
178
|
+
toolCallId: "call-123",
|
|
179
|
+
toolName: "calculator",
|
|
180
|
+
input: { a: 5, b: 3 },
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should handle tool-call with empty arguments string", () => {
|
|
185
|
+
const event: LanguageModelStreamEvent = {
|
|
186
|
+
kind: "tool-call",
|
|
187
|
+
callId: "call-empty",
|
|
188
|
+
toolId: "list_issues",
|
|
189
|
+
state: IN_PROGRESS,
|
|
190
|
+
arguments: "{}",
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = STREAM_UI_PART.encode(event);
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual({
|
|
196
|
+
type: "tool-input-available",
|
|
197
|
+
toolCallId: "call-empty",
|
|
198
|
+
toolName: "list_issues",
|
|
199
|
+
input: {},
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should encode successful tool-result as tool-output-available", () => {
|
|
204
|
+
const event: LanguageModelStreamEvent = {
|
|
205
|
+
kind: "tool-result",
|
|
206
|
+
callId: "call-123",
|
|
207
|
+
toolId: "calculator",
|
|
208
|
+
state: COMPLETED,
|
|
209
|
+
result: { sum: 8 },
|
|
210
|
+
error: null,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = STREAM_UI_PART.encode(event);
|
|
214
|
+
|
|
215
|
+
expect(result).toEqual({
|
|
216
|
+
type: "tool-output-available",
|
|
217
|
+
toolCallId: "call-123",
|
|
218
|
+
output: { sum: 8 },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should encode failed tool-result as tool-output-error", () => {
|
|
223
|
+
const event: LanguageModelStreamEvent = {
|
|
224
|
+
kind: "tool-result",
|
|
225
|
+
callId: "call-123",
|
|
226
|
+
toolId: "calculator",
|
|
227
|
+
state: FAILED,
|
|
228
|
+
result: null,
|
|
229
|
+
error: "Division by zero",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = STREAM_UI_PART.encode(event);
|
|
233
|
+
|
|
234
|
+
expect(result).toEqual({
|
|
235
|
+
type: "tool-output-error",
|
|
236
|
+
toolCallId: "call-123",
|
|
237
|
+
errorText: "Division by zero",
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should handle failed tool-result with null error", () => {
|
|
242
|
+
const event: LanguageModelStreamEvent = {
|
|
243
|
+
kind: "tool-result",
|
|
244
|
+
callId: "call-123",
|
|
245
|
+
toolId: "calculator",
|
|
246
|
+
state: FAILED,
|
|
247
|
+
result: null,
|
|
248
|
+
error: null,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const result = STREAM_UI_PART.encode(event);
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual({
|
|
254
|
+
type: "tool-output-error",
|
|
255
|
+
toolCallId: "call-123",
|
|
256
|
+
errorText: "Unknown error",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("encode - stream control events", () => {
|
|
262
|
+
it("should encode stream-start as start", () => {
|
|
263
|
+
const event: LanguageModelStreamEvent = {
|
|
264
|
+
kind: "stream-start",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const result = STREAM_UI_PART.encode(event);
|
|
268
|
+
|
|
269
|
+
expect(result).toEqual({
|
|
270
|
+
type: "start",
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should encode finish event", () => {
|
|
275
|
+
const event: LanguageModelStreamEvent = {
|
|
276
|
+
kind: "finish",
|
|
277
|
+
finishReason: "stop",
|
|
278
|
+
usage: {
|
|
279
|
+
inputTokens: 100,
|
|
280
|
+
outputTokens: 50,
|
|
281
|
+
totalTokens: 150,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const result = STREAM_UI_PART.encode(event);
|
|
286
|
+
|
|
287
|
+
expect(result).toEqual({
|
|
288
|
+
type: "finish",
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should encode error event", () => {
|
|
293
|
+
const event: LanguageModelStreamEvent = {
|
|
294
|
+
kind: "error",
|
|
295
|
+
error: new Error("Something went wrong"),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const result = STREAM_UI_PART.encode(event);
|
|
299
|
+
|
|
300
|
+
expect(result).toEqual({
|
|
301
|
+
type: "error",
|
|
302
|
+
errorText: "Error: Something went wrong",
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should encode abort event", () => {
|
|
307
|
+
const event: LanguageModelStreamEvent = {
|
|
308
|
+
kind: "abort",
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const result = STREAM_UI_PART.encode(event);
|
|
312
|
+
|
|
313
|
+
expect(result).toEqual({
|
|
314
|
+
type: "abort",
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("encode - events that return null", () => {
|
|
320
|
+
it("should return null for message items", () => {
|
|
321
|
+
const event: LanguageModelStreamEvent = {
|
|
322
|
+
kind: "message",
|
|
323
|
+
role: "assistant",
|
|
324
|
+
id: "msg-1",
|
|
325
|
+
content: [{ kind: "text", text: "Hello" }],
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = STREAM_UI_PART.encode(event);
|
|
329
|
+
|
|
330
|
+
expect(result).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should return null for reasoning items", () => {
|
|
334
|
+
const event: LanguageModelStreamEvent = {
|
|
335
|
+
kind: "reasoning",
|
|
336
|
+
text: "I think...",
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = STREAM_UI_PART.encode(event);
|
|
340
|
+
|
|
341
|
+
expect(result).toBeNull();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should return null for raw events", () => {
|
|
345
|
+
const event: LanguageModelStreamEvent = {
|
|
346
|
+
kind: "raw",
|
|
347
|
+
rawValue: { custom: "data" },
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const result = STREAM_UI_PART.encode(event);
|
|
351
|
+
|
|
352
|
+
expect(result).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("encode - providerMetadata omission", () => {
|
|
357
|
+
it("should omit providerMetadata from text events", () => {
|
|
358
|
+
const event: LanguageModelStreamEvent = {
|
|
359
|
+
kind: "text-delta",
|
|
360
|
+
id: "text-1",
|
|
361
|
+
text: "Hello",
|
|
362
|
+
providerMetadata: {
|
|
363
|
+
anthropic: { some: "data" },
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const result = STREAM_UI_PART.encode(event);
|
|
368
|
+
|
|
369
|
+
expect(result).toEqual({
|
|
370
|
+
type: "text-delta",
|
|
371
|
+
id: "text-1",
|
|
372
|
+
delta: "Hello",
|
|
373
|
+
});
|
|
374
|
+
expect(result).not.toHaveProperty("providerMetadata");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should omit providerMetadata from tool calls", () => {
|
|
378
|
+
const event: LanguageModelStreamEvent = {
|
|
379
|
+
kind: "tool-call",
|
|
380
|
+
callId: "call-123",
|
|
381
|
+
toolId: "calculator",
|
|
382
|
+
state: COMPLETED,
|
|
383
|
+
arguments: '{"x": 1}',
|
|
384
|
+
providerMetadata: {
|
|
385
|
+
openai: { some: "data" },
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const result = STREAM_UI_PART.encode(event);
|
|
390
|
+
|
|
391
|
+
expect(result).toEqual({
|
|
392
|
+
type: "tool-input-available",
|
|
393
|
+
toolCallId: "call-123",
|
|
394
|
+
toolName: "calculator",
|
|
395
|
+
input: { x: 1 },
|
|
396
|
+
});
|
|
397
|
+
expect(result).not.toHaveProperty("providerMetadata");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("decode", () => {
|
|
402
|
+
it("should throw not implemented error", () => {
|
|
403
|
+
const chunk: UIMessageChunk = {
|
|
404
|
+
type: "text-delta",
|
|
405
|
+
id: "text-1",
|
|
406
|
+
delta: "Hello",
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
expect(() => STREAM_UI_PART.decode(chunk)).toThrow(
|
|
410
|
+
"STREAM_UI_PART.decode: Not yet implemented",
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("toUIMessageStream", () => {
|
|
417
|
+
it("should convert async iterable to readable stream", async () => {
|
|
418
|
+
const events: LanguageModelStreamEvent[] = [
|
|
419
|
+
{ kind: "stream-start" },
|
|
420
|
+
{ kind: "text-start", id: "text-1" },
|
|
421
|
+
{ kind: "text-delta", id: "text-1", text: "Hello" },
|
|
422
|
+
{ kind: "text-delta", id: "text-1", text: " world" },
|
|
423
|
+
{ kind: "text-end", id: "text-1" },
|
|
424
|
+
{ kind: "finish", finishReason: "stop", usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
async function* generateEvents() {
|
|
428
|
+
for (const event of events) {
|
|
429
|
+
yield event;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const stream = toUIMessageStream(generateEvents());
|
|
434
|
+
const reader = stream.getReader();
|
|
435
|
+
|
|
436
|
+
const chunks: UIMessageChunk[] = [];
|
|
437
|
+
while (true) {
|
|
438
|
+
const { done, value } = await reader.read();
|
|
439
|
+
if (done) break;
|
|
440
|
+
chunks.push(value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The AI SDK adds messageId automatically to start events
|
|
444
|
+
expect(chunks).toHaveLength(6);
|
|
445
|
+
expect(chunks[0]).toMatchObject({ type: "start" });
|
|
446
|
+
expect(chunks[0]).toHaveProperty("messageId");
|
|
447
|
+
expect(chunks[1]).toEqual({ type: "text-start", id: "text-1" });
|
|
448
|
+
expect(chunks[2]).toEqual({ type: "text-delta", id: "text-1", delta: "Hello" });
|
|
449
|
+
expect(chunks[3]).toEqual({ type: "text-delta", id: "text-1", delta: " world" });
|
|
450
|
+
expect(chunks[4]).toEqual({ type: "text-end", id: "text-1" });
|
|
451
|
+
expect(chunks[5]).toEqual({ type: "finish" });
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should filter out null events", async () => {
|
|
455
|
+
const events: LanguageModelStreamEvent[] = [
|
|
456
|
+
{ kind: "text-start", id: "text-1" },
|
|
457
|
+
{ kind: "text-delta", id: "text-1", text: "Hello" },
|
|
458
|
+
{ kind: "tool-input-end", id: "tool-1" }, // Should be filtered (returns null)
|
|
459
|
+
{ kind: "raw", rawValue: {} }, // Should be filtered (returns null)
|
|
460
|
+
{ kind: "text-end", id: "text-1" },
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
async function* generateEvents() {
|
|
464
|
+
for (const event of events) {
|
|
465
|
+
yield event;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const stream = toUIMessageStream(generateEvents());
|
|
470
|
+
const reader = stream.getReader();
|
|
471
|
+
|
|
472
|
+
const chunks: UIMessageChunk[] = [];
|
|
473
|
+
while (true) {
|
|
474
|
+
const { done, value } = await reader.read();
|
|
475
|
+
if (done) break;
|
|
476
|
+
chunks.push(value);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
expect(chunks).toEqual([
|
|
480
|
+
{ type: "text-start", id: "text-1" },
|
|
481
|
+
{ type: "text-delta", id: "text-1", delta: "Hello" },
|
|
482
|
+
{ type: "text-end", id: "text-1" },
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("should handle tool calls and results", async () => {
|
|
487
|
+
const events: LanguageModelStreamEvent[] = [
|
|
488
|
+
{ kind: "tool-input-start", id: "tool-1", toolName: "calculator" },
|
|
489
|
+
{ kind: "tool-input-delta", id: "tool-1", delta: '{"x":' },
|
|
490
|
+
{ kind: "tool-input-delta", id: "tool-1", delta: '5}' },
|
|
491
|
+
{ kind: "tool-call", callId: "tool-1", toolId: "calculator", state: COMPLETED, arguments: '{"x":5}' },
|
|
492
|
+
{ kind: "tool-result", callId: "tool-1", toolId: "calculator", state: COMPLETED, result: 25, error: null },
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
async function* generateEvents() {
|
|
496
|
+
for (const event of events) {
|
|
497
|
+
yield event;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const stream = toUIMessageStream(generateEvents());
|
|
502
|
+
const reader = stream.getReader();
|
|
503
|
+
|
|
504
|
+
const chunks: UIMessageChunk[] = [];
|
|
505
|
+
while (true) {
|
|
506
|
+
const { done, value } = await reader.read();
|
|
507
|
+
if (done) break;
|
|
508
|
+
chunks.push(value);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
expect(chunks).toEqual([
|
|
512
|
+
{ type: "tool-input-start", toolCallId: "tool-1", toolName: "calculator" },
|
|
513
|
+
{ type: "tool-input-delta", toolCallId: "tool-1", inputTextDelta: '{"x":' },
|
|
514
|
+
{ type: "tool-input-delta", toolCallId: "tool-1", inputTextDelta: '5}' },
|
|
515
|
+
{ type: "tool-input-available", toolCallId: "tool-1", toolName: "calculator", input: { x: 5 } },
|
|
516
|
+
{ type: "tool-output-available", toolCallId: "tool-1", output: 25 },
|
|
517
|
+
]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("should handle errors in stream", async () => {
|
|
521
|
+
const events: LanguageModelStreamEvent[] = [
|
|
522
|
+
{ kind: "text-start", id: "text-1" },
|
|
523
|
+
{ kind: "error", error: "Network timeout" },
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
async function* generateEvents() {
|
|
527
|
+
for (const event of events) {
|
|
528
|
+
yield event;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const stream = toUIMessageStream(generateEvents());
|
|
533
|
+
const reader = stream.getReader();
|
|
534
|
+
|
|
535
|
+
const chunks: UIMessageChunk[] = [];
|
|
536
|
+
while (true) {
|
|
537
|
+
const { done, value } = await reader.read();
|
|
538
|
+
if (done) break;
|
|
539
|
+
chunks.push(value);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
expect(chunks).toEqual([
|
|
543
|
+
{ type: "text-start", id: "text-1" },
|
|
544
|
+
{ type: "error", errorText: "Network timeout" },
|
|
545
|
+
]);
|
|
546
|
+
});
|
|
547
|
+
});
|
package/src/convert/message.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Codec
|
|
1
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
2
|
+
import type { LanguageModelItem } from "@kernl-sdk/protocol";
|
|
2
3
|
import type {
|
|
3
4
|
LanguageModelV3Message,
|
|
4
5
|
LanguageModelV3TextPart,
|
|
@@ -38,10 +39,11 @@ export const MESSAGE: Codec<LanguageModelItem, LanguageModelV3Message> = {
|
|
|
38
39
|
providerOptions: part.providerMetadata,
|
|
39
40
|
});
|
|
40
41
|
} else if (part.kind === "file") {
|
|
42
|
+
const data = part.data ?? (part.uri ? new URL(part.uri) : "");
|
|
41
43
|
content.push({
|
|
42
44
|
type: "file",
|
|
43
45
|
filename: part.filename,
|
|
44
|
-
data
|
|
46
|
+
data,
|
|
45
47
|
mediaType: part.mimeType,
|
|
46
48
|
providerOptions: part.providerMetadata,
|
|
47
49
|
});
|
|
@@ -71,10 +73,11 @@ export const MESSAGE: Codec<LanguageModelItem, LanguageModelV3Message> = {
|
|
|
71
73
|
providerOptions: part.providerMetadata,
|
|
72
74
|
});
|
|
73
75
|
} else if (part.kind === "file") {
|
|
76
|
+
const data = part.data ?? (part.uri ? new URL(part.uri) : "");
|
|
74
77
|
content.push({
|
|
75
78
|
type: "file",
|
|
76
79
|
filename: part.filename,
|
|
77
|
-
data
|
|
80
|
+
data,
|
|
78
81
|
mediaType: part.mimeType,
|
|
79
82
|
providerOptions: part.providerMetadata,
|
|
80
83
|
});
|
|
@@ -88,7 +91,6 @@ export const MESSAGE: Codec<LanguageModelItem, LanguageModelV3Message> = {
|
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
|
-
break;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
case "reasoning": {
|
|
@@ -127,10 +129,15 @@ export const MESSAGE: Codec<LanguageModelItem, LanguageModelV3Message> = {
|
|
|
127
129
|
type: "tool-result",
|
|
128
130
|
toolCallId: item.callId,
|
|
129
131
|
toolName: item.toolId,
|
|
130
|
-
output:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
output: item.error
|
|
133
|
+
? {
|
|
134
|
+
type: "error-text",
|
|
135
|
+
value: item.error, // (TODO): add support for 'error-json'
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
type: "json",
|
|
139
|
+
value: item.result,
|
|
140
|
+
},
|
|
134
141
|
providerOptions: item.providerMetadata,
|
|
135
142
|
},
|
|
136
143
|
],
|
|
@@ -138,13 +145,11 @@ export const MESSAGE: Codec<LanguageModelItem, LanguageModelV3Message> = {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
default:
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Unsupported LanguageModelItem kind: ${(item as any).kind}`,
|
|
143
|
-
);
|
|
148
|
+
throw new Error(`Unsupported LanguageModelItem kind`);
|
|
144
149
|
}
|
|
145
150
|
},
|
|
146
151
|
|
|
147
152
|
decode: () => {
|
|
148
|
-
throw new Error("codec:unimplemented");
|
|
153
|
+
throw new Error("MESSAGE.codec:unimplemented");
|
|
149
154
|
},
|
|
150
155
|
};
|