kernl 0.1.4 → 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 (55) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/CHANGELOG.md +12 -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 +7 -3
  35. package/dist/tool/toolkit.d.ts.map +1 -1
  36. package/dist/tool/toolkit.js +7 -3
  37. package/dist/types/agent.d.ts +5 -5
  38. package/dist/types/agent.d.ts.map +1 -1
  39. package/dist/types/thread.d.ts +10 -16
  40. package/dist/types/thread.d.ts.map +1 -1
  41. package/package.json +7 -5
  42. package/src/agent.ts +97 -86
  43. package/src/index.ts +1 -1
  44. package/src/kernl.ts +51 -2
  45. package/src/mcp/__tests__/integration.test.ts +17 -0
  46. package/src/thread/__tests__/fixtures/mock-model.ts +71 -0
  47. package/src/thread/__tests__/integration.test.ts +349 -0
  48. package/src/thread/__tests__/thread.test.ts +625 -775
  49. package/src/thread/thread.ts +134 -381
  50. package/src/thread/utils.ts +36 -1
  51. package/src/tool/index.ts +1 -1
  52. package/src/tool/tool.ts +6 -2
  53. package/src/tool/toolkit.ts +10 -3
  54. package/src/types/agent.ts +9 -6
  55. package/src/types/thread.ts +25 -17
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { z } from "zod";
3
+ import { openai } from "@ai-sdk/openai";
4
+ import { AISDKLanguageModel } from "@kernl-sdk/ai";
5
+ import { Agent } from "../../agent";
6
+ import { Kernl } from "../../kernl";
7
+ import { tool, Toolkit } from "../../tool";
8
+ import { Thread } from "../thread";
9
+ /**
10
+ * Integration tests for Thread streaming with real AI SDK providers.
11
+ *
12
+ * These tests require an OPENAI_API_KEY environment variable to be set.
13
+ * They will be skipped if the API key is not available.
14
+ *
15
+ * Run with: OPENAI_API_KEY=your-key pnpm test:run
16
+ */
17
+ const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
18
+ describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
19
+ let kernl;
20
+ let model;
21
+ beforeAll(() => {
22
+ kernl = new Kernl();
23
+ model = new AISDKLanguageModel(openai("gpt-4o"));
24
+ });
25
+ describe("stream()", () => {
26
+ it("should yield both delta events and complete items", async () => {
27
+ const agent = new Agent({
28
+ id: "test-stream",
29
+ name: "Test Stream Agent",
30
+ instructions: "You are a helpful assistant.",
31
+ model,
32
+ });
33
+ const input = [
34
+ {
35
+ kind: "message",
36
+ id: "msg-1",
37
+ role: "user",
38
+ content: [
39
+ { kind: "text", text: "Say 'Hello World' and nothing else." },
40
+ ],
41
+ },
42
+ ];
43
+ const thread = new Thread(kernl, agent, input);
44
+ const events = [];
45
+ for await (const event of thread.stream()) {
46
+ events.push(event);
47
+ }
48
+ expect(events.length).toBeGreaterThan(0);
49
+ // Should have text-delta events (for streaming UX)
50
+ const textDeltas = events.filter((e) => e.kind === "text-delta");
51
+ expect(textDeltas.length).toBeGreaterThan(0);
52
+ // Should have text-start event
53
+ const textStarts = events.filter((e) => e.kind === "text-start");
54
+ expect(textStarts.length).toBeGreaterThan(0);
55
+ // Should have text-end event
56
+ const textEnds = events.filter((e) => e.kind === "text-end");
57
+ expect(textEnds.length).toBeGreaterThan(0);
58
+ // Should have complete Message item (for history)
59
+ const messages = events.filter((e) => e.kind === "message");
60
+ expect(messages.length).toBeGreaterThan(0);
61
+ const assistantMessage = messages.find((m) => m.role === "assistant");
62
+ expect(assistantMessage).toBeDefined();
63
+ expect(assistantMessage.content).toBeDefined();
64
+ expect(assistantMessage.content.length).toBeGreaterThan(0);
65
+ // Message should have accumulated text from all deltas
66
+ const textContent = assistantMessage.content.find((c) => c.kind === "text");
67
+ expect(textContent).toBeDefined();
68
+ expect(textContent.text).toBeDefined();
69
+ expect(textContent.text.length).toBeGreaterThan(0);
70
+ // Verify accumulated text matches concatenated deltas
71
+ const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
72
+ expect(textContent.text).toBe(accumulatedFromDeltas);
73
+ // Should have finish event
74
+ const finishEvents = events.filter((e) => e.kind === "finish");
75
+ expect(finishEvents.length).toBe(1);
76
+ }, 30000);
77
+ it("should filter deltas from history but include complete items", async () => {
78
+ const agent = new Agent({
79
+ id: "test-history",
80
+ name: "Test History Agent",
81
+ instructions: "You are a helpful assistant.",
82
+ model,
83
+ });
84
+ const input = [
85
+ {
86
+ kind: "message",
87
+ id: "msg-1",
88
+ role: "user",
89
+ content: [{ kind: "text", text: "Count to 3" }],
90
+ },
91
+ ];
92
+ const thread = new Thread(kernl, agent, input);
93
+ const streamEvents = [];
94
+ for await (const event of thread.stream()) {
95
+ streamEvents.push(event);
96
+ }
97
+ // Access private history via type assertion for testing
98
+ const history = thread.history;
99
+ // History should only contain complete items (message, reasoning, tool-call, tool-result)
100
+ // TypeScript already enforces this via ThreadEvent type, but let's verify at runtime
101
+ for (const event of history) {
102
+ expect(["message", "reasoning", "tool-call", "tool-result"]).toContain(event.kind);
103
+ }
104
+ // Stream events should include deltas (but history should not)
105
+ const streamDeltas = streamEvents.filter((e) => e.kind === "text-delta" ||
106
+ e.kind === "text-start" ||
107
+ e.kind === "text-end");
108
+ expect(streamDeltas.length).toBeGreaterThan(0);
109
+ // History should contain the input message
110
+ expect(history[0]).toEqual(input[0]);
111
+ // History should contain complete Message items
112
+ const historyMessages = history.filter((e) => e.kind === "message");
113
+ expect(historyMessages.length).toBeGreaterThan(1); // input + assistant response
114
+ // Verify assistant message has complete text (not deltas)
115
+ const assistantMessage = historyMessages.find((m) => m.role === "assistant");
116
+ expect(assistantMessage).toBeDefined();
117
+ const textContent = assistantMessage.content.find((c) => c.kind === "text");
118
+ expect(textContent.text).toBeTruthy();
119
+ expect(textContent.text.length).toBeGreaterThan(0);
120
+ }, 30000);
121
+ it("should work with tool calls", async () => {
122
+ const addTool = tool({
123
+ id: "add",
124
+ name: "add",
125
+ description: "Add two numbers together",
126
+ parameters: z.object({
127
+ a: z.number().describe("The first number"),
128
+ b: z.number().describe("The second number"),
129
+ }),
130
+ execute: async (ctx, { a, b }) => {
131
+ return a + b;
132
+ },
133
+ });
134
+ const toolkit = new Toolkit({
135
+ id: "math",
136
+ tools: [addTool],
137
+ });
138
+ const agent = new Agent({
139
+ id: "test-tools",
140
+ name: "Test Tools Agent",
141
+ instructions: "You are a helpful assistant that can do math.",
142
+ model,
143
+ toolkits: [toolkit],
144
+ });
145
+ const input = [
146
+ {
147
+ kind: "message",
148
+ id: "msg-1",
149
+ role: "user",
150
+ content: [{ kind: "text", text: "What is 25 + 17?" }],
151
+ },
152
+ ];
153
+ const thread = new Thread(kernl, agent, input);
154
+ const events = [];
155
+ for await (const event of thread.stream()) {
156
+ events.push(event);
157
+ }
158
+ expect(events.length).toBeGreaterThan(0);
159
+ // Should have tool calls
160
+ const toolCalls = events.filter((e) => e.kind === "tool-call");
161
+ expect(toolCalls.length).toBeGreaterThan(0);
162
+ // Verify tool was called with correct parameters
163
+ const addToolCall = toolCalls.find((tc) => tc.toolId === "add");
164
+ expect(addToolCall).toBeDefined();
165
+ expect(JSON.parse(addToolCall.arguments)).toEqual({ a: 25, b: 17 });
166
+ // Should have tool results
167
+ const toolResults = events.filter((e) => e.kind === "tool-result");
168
+ expect(toolResults.length).toBeGreaterThan(0);
169
+ // Verify tool result is correct
170
+ const addToolResult = toolResults.find((tr) => tr.callId === addToolCall.callId);
171
+ expect(addToolResult).toBeDefined();
172
+ expect(addToolResult.result).toBe(42);
173
+ // History should contain tool calls and results
174
+ const history = thread.history;
175
+ const historyToolCalls = history.filter((e) => e.kind === "tool-call");
176
+ const historyToolResults = history.filter((e) => e.kind === "tool-result");
177
+ expect(historyToolCalls.length).toBe(toolCalls.length);
178
+ expect(historyToolResults.length).toBe(toolResults.length);
179
+ // Verify the assistant's final response references the correct answer
180
+ const messages = events.filter((e) => e.kind === "message");
181
+ const assistantMessage = messages.find((m) => m.role === "assistant");
182
+ expect(assistantMessage).toBeDefined();
183
+ const textContent = assistantMessage.content.find((c) => c.kind === "text");
184
+ expect(textContent).toBeDefined();
185
+ expect(textContent.text).toContain("42");
186
+ }, 30000);
187
+ });
188
+ describe("execute()", () => {
189
+ it("should consume stream and return final response", async () => {
190
+ const agent = new Agent({
191
+ id: "test-blocking",
192
+ name: "Test Blocking Agent",
193
+ instructions: "You are a helpful assistant.",
194
+ model,
195
+ });
196
+ const input = [
197
+ {
198
+ kind: "message",
199
+ id: "msg-1",
200
+ role: "user",
201
+ content: [{ kind: "text", text: "Say 'Testing' and nothing else." }],
202
+ },
203
+ ];
204
+ const thread = new Thread(kernl, agent, input);
205
+ const result = await thread.execute();
206
+ // Should have a response
207
+ expect(result.response).toBeDefined();
208
+ expect(typeof result.response).toBe("string");
209
+ expect(result.response.length).toBeGreaterThan(0);
210
+ // Should have final state
211
+ expect(result.state).toBe("stopped");
212
+ }, 30000);
213
+ it("should validate structured output in blocking mode", async () => {
214
+ const responseSchema = z.object({
215
+ name: z.string(),
216
+ age: z.number(),
217
+ });
218
+ const agent = new Agent({
219
+ id: "test-structured",
220
+ name: "Test Structured Agent",
221
+ instructions: "You are a helpful assistant. Return JSON with name and age fields.",
222
+ model,
223
+ responseType: responseSchema,
224
+ });
225
+ const input = [
226
+ {
227
+ kind: "message",
228
+ id: "msg-1",
229
+ role: "user",
230
+ content: [
231
+ {
232
+ kind: "text",
233
+ text: 'Return a JSON object with name "Alice" and age 30',
234
+ },
235
+ ],
236
+ },
237
+ ];
238
+ const thread = new Thread(kernl, agent, input);
239
+ const result = await thread.execute();
240
+ // Response should be validated and parsed
241
+ expect(result.response).toBeDefined();
242
+ expect(typeof result.response).toBe("object");
243
+ expect(result.response.name).toBeTruthy();
244
+ expect(typeof result.response.age).toBe("number");
245
+ }, 30000);
246
+ });
247
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stream.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/stream.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { z } from "zod";
3
+ import { openai } from "@ai-sdk/openai";
4
+ import { AISDKLanguageModel } from "@kernl-sdk/ai";
5
+ import { Agent } from "../../agent";
6
+ import { Kernl } from "../../kernl";
7
+ import { tool, Toolkit } from "../../tool";
8
+ import { Thread } from "../thread";
9
+ /**
10
+ * Integration tests for Thread streaming with real AI SDK providers.
11
+ *
12
+ * These tests require an OPENAI_API_KEY environment variable to be set.
13
+ * They will be skipped if the API key is not available.
14
+ *
15
+ * Run with: OPENAI_API_KEY=your-key pnpm test:run
16
+ */
17
+ const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
18
+ describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
19
+ let kernl;
20
+ let model;
21
+ beforeAll(() => {
22
+ kernl = new Kernl();
23
+ model = new AISDKLanguageModel(openai("gpt-4o-mini"));
24
+ });
25
+ describe("stream()", () => {
26
+ it("should yield both delta events and complete items", async () => {
27
+ const agent = new Agent({
28
+ id: "test-stream",
29
+ name: "Test Stream Agent",
30
+ instructions: "You are a helpful assistant.",
31
+ model,
32
+ });
33
+ const input = [
34
+ {
35
+ kind: "message",
36
+ id: "msg-1",
37
+ role: "user",
38
+ content: [
39
+ { kind: "text", text: "Say 'Hello World' and nothing else." },
40
+ ],
41
+ },
42
+ ];
43
+ const thread = new Thread(kernl, agent, input);
44
+ const events = [];
45
+ for await (const event of thread.stream()) {
46
+ events.push(event);
47
+ }
48
+ expect(events.length).toBeGreaterThan(0);
49
+ // Should have text-delta events (for streaming UX)
50
+ const textDeltas = events.filter((e) => e.kind === "text-delta");
51
+ expect(textDeltas.length).toBeGreaterThan(0);
52
+ // Should have text-start event
53
+ const textStarts = events.filter((e) => e.kind === "text-start");
54
+ expect(textStarts.length).toBeGreaterThan(0);
55
+ // Should have text-end event
56
+ const textEnds = events.filter((e) => e.kind === "text-end");
57
+ expect(textEnds.length).toBeGreaterThan(0);
58
+ // Should have complete Message item (for history)
59
+ const messages = events.filter((e) => e.kind === "message");
60
+ expect(messages.length).toBeGreaterThan(0);
61
+ const assistantMessage = messages.find((m) => m.role === "assistant");
62
+ expect(assistantMessage).toBeDefined();
63
+ expect(assistantMessage.content).toBeDefined();
64
+ expect(assistantMessage.content.length).toBeGreaterThan(0);
65
+ // Message should have accumulated text from all deltas
66
+ const textContent = assistantMessage.content.find((c) => c.kind === "text");
67
+ expect(textContent).toBeDefined();
68
+ expect(textContent.text).toBeDefined();
69
+ expect(textContent.text.length).toBeGreaterThan(0);
70
+ // Verify accumulated text matches concatenated deltas
71
+ const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
72
+ expect(textContent.text).toBe(accumulatedFromDeltas);
73
+ // Should have finish event
74
+ const finishEvents = events.filter((e) => e.kind === "finish");
75
+ expect(finishEvents.length).toBe(1);
76
+ });
77
+ it("should filter deltas from history but include complete items", async () => {
78
+ const agent = new Agent({
79
+ id: "test-history",
80
+ name: "Test History Agent",
81
+ instructions: "You are a helpful assistant.",
82
+ model,
83
+ });
84
+ const input = [
85
+ {
86
+ kind: "message",
87
+ id: "msg-1",
88
+ role: "user",
89
+ content: [{ kind: "text", text: "Count to 3" }],
90
+ },
91
+ ];
92
+ const thread = new Thread(kernl, agent, input);
93
+ const streamEvents = [];
94
+ for await (const event of thread.stream()) {
95
+ streamEvents.push(event);
96
+ }
97
+ // Access private history via type assertion for testing
98
+ const history = thread.history;
99
+ // History should only contain complete items (message, reasoning, tool-call, tool-result)
100
+ // TypeScript already enforces this via ThreadEvent type, but let's verify at runtime
101
+ for (const event of history) {
102
+ expect(["message", "reasoning", "tool-call", "tool-result"]).toContain(event.kind);
103
+ }
104
+ // Stream events should include deltas (but history should not)
105
+ const streamDeltas = streamEvents.filter((e) => e.kind === "text-delta" ||
106
+ e.kind === "text-start" ||
107
+ e.kind === "text-end");
108
+ expect(streamDeltas.length).toBeGreaterThan(0);
109
+ // History should contain the input message
110
+ expect(history[0]).toEqual(input[0]);
111
+ // History should contain complete Message items
112
+ const historyMessages = history.filter((e) => e.kind === "message");
113
+ expect(historyMessages.length).toBeGreaterThan(1); // input + assistant response
114
+ // Verify assistant message has complete text (not deltas)
115
+ const assistantMessage = historyMessages.find((m) => m.role === "assistant");
116
+ expect(assistantMessage).toBeDefined();
117
+ const textContent = assistantMessage.content.find((c) => c.kind === "text");
118
+ expect(textContent.text).toBeTruthy();
119
+ expect(textContent.text.length).toBeGreaterThan(0);
120
+ });
121
+ it("should work with tool calls", async () => {
122
+ const calculateTool = tool({
123
+ name: "calculate",
124
+ description: "Perform a mathematical calculation",
125
+ parameters: z.object({
126
+ expression: z
127
+ .string()
128
+ .describe("The mathematical expression to evaluate"),
129
+ }),
130
+ execute: async (ctx, { expression }) => {
131
+ // Simple eval for testing (don't do this in production!)
132
+ try {
133
+ return { result: eval(expression) };
134
+ }
135
+ catch {
136
+ return { error: "Invalid expression" };
137
+ }
138
+ },
139
+ });
140
+ const toolkit = new Toolkit({
141
+ id: "math",
142
+ tools: [calculateTool],
143
+ });
144
+ const agent = new Agent({
145
+ id: "test-tools",
146
+ name: "Test Tools Agent",
147
+ instructions: "You are a helpful assistant that can do calculations.",
148
+ model,
149
+ toolkits: [toolkit],
150
+ });
151
+ const input = [
152
+ {
153
+ kind: "message",
154
+ id: "msg-1",
155
+ role: "user",
156
+ content: [{ kind: "text", text: "What is 25 + 17?" }],
157
+ },
158
+ ];
159
+ const thread = new Thread(kernl, agent, input);
160
+ const events = [];
161
+ for await (const event of thread.stream()) {
162
+ events.push(event);
163
+ }
164
+ expect(events.length).toBeGreaterThan(0);
165
+ // Should have tool calls
166
+ const toolCalls = events.filter((e) => e.kind === "tool-call");
167
+ expect(toolCalls.length).toBeGreaterThan(0);
168
+ // Should have tool results
169
+ const toolResults = events.filter((e) => e.kind === "tool-result");
170
+ expect(toolResults.length).toBeGreaterThan(0);
171
+ // History should contain tool calls and results
172
+ const history = thread.history;
173
+ const historyToolCalls = history.filter((e) => e.kind === "tool-call");
174
+ const historyToolResults = history.filter((e) => e.kind === "tool-result");
175
+ expect(historyToolCalls.length).toBe(toolCalls.length);
176
+ expect(historyToolResults.length).toBe(toolResults.length);
177
+ });
178
+ });
179
+ describe("execute()", () => {
180
+ it("should consume stream and return final response", async () => {
181
+ const agent = new Agent({
182
+ id: "test-blocking",
183
+ name: "Test Blocking Agent",
184
+ instructions: "You are a helpful assistant.",
185
+ model,
186
+ });
187
+ const input = [
188
+ {
189
+ kind: "message",
190
+ id: "msg-1",
191
+ role: "user",
192
+ content: [{ kind: "text", text: "Say 'Testing' and nothing else." }],
193
+ },
194
+ ];
195
+ const thread = new Thread(kernl, agent, input);
196
+ const result = await thread.execute();
197
+ // Should have a response
198
+ expect(result.response).toBeDefined();
199
+ expect(typeof result.response).toBe("string");
200
+ expect(result.response.length).toBeGreaterThan(0);
201
+ // Should have final state
202
+ expect(result.state).toBe("stopped");
203
+ // History should NOT contain deltas
204
+ const history = thread.history;
205
+ const historyDeltas = history.filter((e) => e.kind === "text-delta" ||
206
+ e.kind === "text-start" ||
207
+ e.kind === "text-end");
208
+ expect(historyDeltas.length).toBe(0);
209
+ });
210
+ it("should validate structured output in blocking mode", async () => {
211
+ const responseSchema = z.object({
212
+ name: z.string(),
213
+ age: z.number(),
214
+ });
215
+ const agent = new Agent({
216
+ id: "test-structured",
217
+ name: "Test Structured Agent",
218
+ instructions: "You are a helpful assistant. Return JSON with name and age fields.",
219
+ model,
220
+ responseType: responseSchema,
221
+ });
222
+ const input = [
223
+ {
224
+ kind: "message",
225
+ id: "msg-1",
226
+ role: "user",
227
+ content: [
228
+ {
229
+ kind: "text",
230
+ text: 'Return a JSON object with name "Alice" and age 30',
231
+ },
232
+ ],
233
+ },
234
+ ];
235
+ const thread = new Thread(kernl, agent, input);
236
+ const result = await thread.execute();
237
+ // Response should be validated and parsed
238
+ expect(result.response).toBeDefined();
239
+ expect(typeof result.response).toBe("object");
240
+ expect(result.response.name).toBeTruthy();
241
+ expect(typeof result.response.age).toBe("number");
242
+ });
243
+ });
244
+ });