kernl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +53 -0
  3. package/LICENSE +201 -0
  4. package/dist/agent.d.ts +43 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +130 -0
  7. package/dist/context.d.ts +70 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +111 -0
  10. package/dist/env.d.ts +45 -0
  11. package/dist/env.d.ts.map +1 -0
  12. package/dist/env.js +31 -0
  13. package/dist/error.d.ts +1 -0
  14. package/dist/error.d.ts.map +1 -0
  15. package/dist/error.js +1 -0
  16. package/dist/guardrail.d.ts +178 -0
  17. package/dist/guardrail.d.ts.map +1 -0
  18. package/dist/guardrail.js +34 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2 -0
  22. package/dist/kernel.d.ts +7 -0
  23. package/dist/kernel.d.ts.map +1 -0
  24. package/dist/kernel.js +7 -0
  25. package/dist/kernl.d.ts +18 -0
  26. package/dist/kernl.d.ts.map +1 -0
  27. package/dist/kernl.js +16 -0
  28. package/dist/lib/env.d.ts +43 -0
  29. package/dist/lib/env.d.ts.map +1 -0
  30. package/dist/lib/env.js +29 -0
  31. package/dist/lib/error.d.ts +88 -0
  32. package/dist/lib/error.d.ts.map +1 -0
  33. package/dist/lib/error.js +117 -0
  34. package/dist/lib/logger.d.ts +36 -0
  35. package/dist/lib/logger.d.ts.map +1 -0
  36. package/dist/lib/logger.js +43 -0
  37. package/dist/lib/serde/__tests__/codec.test.d.ts +2 -0
  38. package/dist/lib/serde/__tests__/codec.test.d.ts.map +1 -0
  39. package/dist/lib/serde/__tests__/codec.test.js +75 -0
  40. package/dist/lib/serde/codec.d.ts +12 -0
  41. package/dist/lib/serde/codec.d.ts.map +1 -0
  42. package/dist/lib/serde/codec.js +54 -0
  43. package/dist/lib/serde/json.d.ts +8 -0
  44. package/dist/lib/serde/json.d.ts.map +1 -0
  45. package/dist/lib/serde/json.js +13 -0
  46. package/dist/lib/serde/thread.d.ts +1 -0
  47. package/dist/lib/serde/thread.d.ts.map +1 -0
  48. package/dist/lib/serde/thread.js +172 -0
  49. package/dist/lib/serde/tool.d.ts +36 -0
  50. package/dist/lib/serde/tool.d.ts.map +1 -0
  51. package/dist/lib/serde/tool.js +1 -0
  52. package/dist/lib/utils.d.ts +19 -0
  53. package/dist/lib/utils.d.ts.map +1 -0
  54. package/dist/lib/utils.js +41 -0
  55. package/dist/lifecycle.d.ts +133 -0
  56. package/dist/lifecycle.d.ts.map +1 -0
  57. package/dist/lifecycle.js +29 -0
  58. package/dist/logger.d.ts +36 -0
  59. package/dist/logger.d.ts.map +1 -0
  60. package/dist/logger.js +43 -0
  61. package/dist/mcp/__tests__/base.test.d.ts +2 -0
  62. package/dist/mcp/__tests__/base.test.d.ts.map +1 -0
  63. package/dist/mcp/__tests__/base.test.js +268 -0
  64. package/dist/mcp/__tests__/fixtures/echo-server.d.ts +3 -0
  65. package/dist/mcp/__tests__/fixtures/echo-server.d.ts.map +1 -0
  66. package/dist/mcp/__tests__/fixtures/echo-server.js +92 -0
  67. package/dist/mcp/__tests__/fixtures/math-server.d.ts +3 -0
  68. package/dist/mcp/__tests__/fixtures/math-server.d.ts.map +1 -0
  69. package/dist/mcp/__tests__/fixtures/math-server.js +98 -0
  70. package/dist/mcp/__tests__/fixtures/server.d.ts +3 -0
  71. package/dist/mcp/__tests__/fixtures/server.d.ts.map +1 -0
  72. package/dist/mcp/__tests__/fixtures/server.js +162 -0
  73. package/dist/mcp/__tests__/fixtures/test-server.d.ts +3 -0
  74. package/dist/mcp/__tests__/fixtures/test-server.d.ts.map +1 -0
  75. package/dist/mcp/__tests__/fixtures/test-server.js +163 -0
  76. package/dist/mcp/__tests__/fixtures/utils.d.ts +17 -0
  77. package/dist/mcp/__tests__/fixtures/utils.d.ts.map +1 -0
  78. package/dist/mcp/__tests__/fixtures/utils.js +42 -0
  79. package/dist/mcp/__tests__/integration.test.d.ts +2 -0
  80. package/dist/mcp/__tests__/integration.test.d.ts.map +1 -0
  81. package/dist/mcp/__tests__/integration.test.js +360 -0
  82. package/dist/mcp/__tests__/stdio.test.d.ts +2 -0
  83. package/dist/mcp/__tests__/stdio.test.d.ts.map +1 -0
  84. package/dist/mcp/__tests__/stdio.test.js +180 -0
  85. package/dist/mcp/__tests__/test-utils.d.ts +17 -0
  86. package/dist/mcp/__tests__/test-utils.d.ts.map +1 -0
  87. package/dist/mcp/__tests__/test-utils.js +42 -0
  88. package/dist/mcp/__tests__/utils.test.d.ts +2 -0
  89. package/dist/mcp/__tests__/utils.test.d.ts.map +1 -0
  90. package/dist/mcp/__tests__/utils.test.js +300 -0
  91. package/dist/mcp/base.d.ts +88 -0
  92. package/dist/mcp/base.d.ts.map +1 -0
  93. package/dist/mcp/base.js +68 -0
  94. package/dist/mcp/http.d.ts +34 -0
  95. package/dist/mcp/http.d.ts.map +1 -0
  96. package/dist/mcp/http.js +100 -0
  97. package/dist/mcp/node.d.ts +60 -0
  98. package/dist/mcp/node.d.ts.map +1 -0
  99. package/dist/mcp/node.js +297 -0
  100. package/dist/mcp/sse.d.ts +34 -0
  101. package/dist/mcp/sse.d.ts.map +1 -0
  102. package/dist/mcp/sse.js +97 -0
  103. package/dist/mcp/stdio.d.ts +32 -0
  104. package/dist/mcp/stdio.d.ts.map +1 -0
  105. package/dist/mcp/stdio.js +96 -0
  106. package/dist/mcp/types.d.ts +172 -0
  107. package/dist/mcp/types.d.ts.map +1 -0
  108. package/dist/mcp/types.js +16 -0
  109. package/dist/mcp/utils.d.ts +23 -0
  110. package/dist/mcp/utils.d.ts.map +1 -0
  111. package/dist/mcp/utils.js +44 -0
  112. package/dist/model.d.ts +175 -0
  113. package/dist/model.d.ts.map +1 -0
  114. package/dist/model.js +1 -0
  115. package/dist/providers/ai.d.ts +1 -0
  116. package/dist/providers/ai.d.ts.map +1 -0
  117. package/dist/providers/ai.js +1 -0
  118. package/dist/providers/default.d.ts +16 -0
  119. package/dist/providers/default.d.ts.map +1 -0
  120. package/dist/providers/default.js +17 -0
  121. package/dist/providers/registry.d.ts +1 -0
  122. package/dist/providers/registry.d.ts.map +1 -0
  123. package/dist/providers/registry.js +1 -0
  124. package/dist/sched/scheduler.d.ts +20 -0
  125. package/dist/sched/scheduler.d.ts.map +1 -0
  126. package/dist/sched/scheduler.js +1 -0
  127. package/dist/sched/task.d.ts +92 -0
  128. package/dist/sched/task.d.ts.map +1 -0
  129. package/dist/sched/task.js +102 -0
  130. package/dist/serde/__tests__/codec.test.d.ts +2 -0
  131. package/dist/serde/__tests__/codec.test.d.ts.map +1 -0
  132. package/dist/serde/__tests__/codec.test.js +75 -0
  133. package/dist/serde/codec.d.ts +12 -0
  134. package/dist/serde/codec.d.ts.map +1 -0
  135. package/dist/serde/codec.js +54 -0
  136. package/dist/serde/json.d.ts +8 -0
  137. package/dist/serde/json.d.ts.map +1 -0
  138. package/dist/serde/json.js +13 -0
  139. package/dist/serde/thread.d.ts +687 -0
  140. package/dist/serde/thread.d.ts.map +1 -0
  141. package/dist/serde/thread.js +158 -0
  142. package/dist/serde/tool.d.ts +36 -0
  143. package/dist/serde/tool.d.ts.map +1 -0
  144. package/dist/serde/tool.js +1 -0
  145. package/dist/session.d.ts +1 -0
  146. package/dist/session.d.ts.map +1 -0
  147. package/dist/session.js +1 -0
  148. package/dist/task.d.ts +87 -0
  149. package/dist/task.d.ts.map +1 -0
  150. package/dist/task.js +97 -0
  151. package/dist/thread/__tests__/mock.d.ts +28 -0
  152. package/dist/thread/__tests__/mock.d.ts.map +1 -0
  153. package/dist/thread/__tests__/mock.js +74 -0
  154. package/dist/thread/__tests__/thread.test.d.ts +2 -0
  155. package/dist/thread/__tests__/thread.test.d.ts.map +1 -0
  156. package/dist/thread/__tests__/thread.test.js +1412 -0
  157. package/dist/thread/index.d.ts +2 -0
  158. package/dist/thread/index.d.ts.map +1 -0
  159. package/dist/thread/index.js +1 -0
  160. package/dist/thread/thread.d.ts +66 -0
  161. package/dist/thread/thread.d.ts.map +1 -0
  162. package/dist/thread/thread.js +472 -0
  163. package/dist/thread/utils.d.ts +19 -0
  164. package/dist/thread/utils.d.ts.map +1 -0
  165. package/dist/thread/utils.js +50 -0
  166. package/dist/tool/__tests__/fixtures.d.ts +45 -0
  167. package/dist/tool/__tests__/fixtures.d.ts.map +1 -0
  168. package/dist/tool/__tests__/fixtures.js +97 -0
  169. package/dist/tool/__tests__/tool.test.d.ts +2 -0
  170. package/dist/tool/__tests__/tool.test.d.ts.map +1 -0
  171. package/dist/tool/__tests__/tool.test.js +172 -0
  172. package/dist/tool/__tests__/toolkit.test.d.ts +2 -0
  173. package/dist/tool/__tests__/toolkit.test.d.ts.map +1 -0
  174. package/dist/tool/__tests__/toolkit.test.js +134 -0
  175. package/dist/tool/index.d.ts +4 -0
  176. package/dist/tool/index.d.ts.map +1 -0
  177. package/dist/tool/index.js +2 -0
  178. package/dist/tool/mcp.d.ts +75 -0
  179. package/dist/tool/mcp.d.ts.map +1 -0
  180. package/dist/tool/mcp.js +111 -0
  181. package/dist/tool/tool.d.ts +95 -0
  182. package/dist/tool/tool.d.ts.map +1 -0
  183. package/dist/tool/tool.js +176 -0
  184. package/dist/tool/toolkit.d.ts +121 -0
  185. package/dist/tool/toolkit.d.ts.map +1 -0
  186. package/dist/tool/toolkit.js +180 -0
  187. package/dist/tool/types.d.ts +187 -0
  188. package/dist/tool/types.d.ts.map +1 -0
  189. package/dist/tool/types.js +1 -0
  190. package/dist/tools.d.ts +362 -0
  191. package/dist/tools.d.ts.map +1 -0
  192. package/dist/tools.js +220 -0
  193. package/dist/trace/processor.d.ts +1 -0
  194. package/dist/trace/processor.d.ts.map +1 -0
  195. package/dist/trace/processor.js +1 -0
  196. package/dist/trace/traces.d.ts +1 -0
  197. package/dist/trace/traces.d.ts.map +1 -0
  198. package/dist/trace/traces.js +73 -0
  199. package/dist/trace/utils.d.ts +22 -0
  200. package/dist/trace/utils.d.ts.map +1 -0
  201. package/dist/trace/utils.js +30 -0
  202. package/dist/types/agent.d.ts +91 -0
  203. package/dist/types/agent.d.ts.map +1 -0
  204. package/dist/types/agent.js +1 -0
  205. package/dist/types/proto.d.ts +1551 -0
  206. package/dist/types/proto.d.ts.map +1 -0
  207. package/dist/types/proto.js +531 -0
  208. package/dist/types/thread.d.ts +71 -0
  209. package/dist/types/thread.d.ts.map +1 -0
  210. package/dist/types/thread.js +5 -0
  211. package/dist/usage.d.ts +43 -0
  212. package/dist/usage.d.ts.map +1 -0
  213. package/dist/usage.js +61 -0
  214. package/package.json +52 -0
  215. package/src/agent.ts +203 -0
  216. package/src/context.ts +265 -0
  217. package/src/guardrail.ts +277 -0
  218. package/src/index.ts +3 -0
  219. package/src/kernl.ts +22 -0
  220. package/src/lib/env.ts +36 -0
  221. package/src/lib/error.ts +158 -0
  222. package/src/lib/logger.ts +78 -0
  223. package/src/lib/serde/json.ts +18 -0
  224. package/src/lib/serde/thread.ts +188 -0
  225. package/src/lifecycle.ts +181 -0
  226. package/src/mcp/__tests__/base.test.ts +344 -0
  227. package/src/mcp/__tests__/fixtures/server.ts +179 -0
  228. package/src/mcp/__tests__/fixtures/utils.ts +58 -0
  229. package/src/mcp/__tests__/integration.test.ts +447 -0
  230. package/src/mcp/__tests__/stdio.test.ts +236 -0
  231. package/src/mcp/__tests__/utils.test.ts +360 -0
  232. package/src/mcp/base.ts +162 -0
  233. package/src/mcp/http.ts +147 -0
  234. package/src/mcp/sse.ts +137 -0
  235. package/src/mcp/stdio.ts +136 -0
  236. package/src/mcp/types.ts +202 -0
  237. package/src/mcp/utils.ts +62 -0
  238. package/src/task.ts +119 -0
  239. package/src/thread/__tests__/mock.ts +95 -0
  240. package/src/thread/__tests__/thread.test.ts +1574 -0
  241. package/src/thread/index.ts +1 -0
  242. package/src/thread/thread.ts +611 -0
  243. package/src/thread/utils.ts +67 -0
  244. package/src/tool/__tests__/fixtures.ts +106 -0
  245. package/src/tool/__tests__/tool.test.ts +235 -0
  246. package/src/tool/__tests__/toolkit.test.ts +174 -0
  247. package/src/tool/index.ts +10 -0
  248. package/src/tool/tool.ts +264 -0
  249. package/src/tool/toolkit.ts +234 -0
  250. package/src/tool/types.ts +243 -0
  251. package/src/trace/processor.ts +0 -0
  252. package/src/trace/traces.ts +86 -0
  253. package/src/trace/utils.ts +38 -0
  254. package/src/types/agent.ts +145 -0
  255. package/src/types/thread.ts +86 -0
  256. package/tsconfig.json +13 -0
  257. package/vitest.config.ts +14 -0
@@ -0,0 +1,1574 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ import type { LanguageModel, LanguageModelRequest } from "@kernl/protocol";
5
+ import { IN_PROGRESS, COMPLETED, FAILED } from "@kernl/protocol";
6
+
7
+ import { Thread } from "../thread";
8
+ import { Agent } from "@/agent";
9
+ import { Kernl } from "@/kernl";
10
+ import { Context } from "@/context";
11
+ import { tool, FunctionToolkit } from "@/tool";
12
+ import { ModelBehaviorError } from "@/lib/error";
13
+
14
+ import type { ThreadEvent } from "@/types/thread";
15
+
16
+ describe("Thread", () => {
17
+ describe("Basic Execution", () => {
18
+ 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,
38
+ },
39
+ warnings: [],
40
+ };
41
+ },
42
+ stream: async function* () {
43
+ throw new Error("Not implemented");
44
+ },
45
+ };
46
+
47
+ const agent = new Agent({
48
+ id: "test",
49
+ name: "Test",
50
+ instructions: "Test agent",
51
+ model,
52
+ });
53
+
54
+ const kernl = new Kernl();
55
+ const thread = new Thread(kernl, agent, "Hello world");
56
+
57
+ const result = await thread.execute();
58
+
59
+ // Access private history via type assertion for testing
60
+ const history = (thread as any).history as ThreadEvent[];
61
+
62
+ expect(history).toEqual([
63
+ {
64
+ kind: "message",
65
+ id: expect.any(String),
66
+ role: "user",
67
+ content: [{ kind: "text", text: "Hello world" }],
68
+ },
69
+ {
70
+ kind: "message",
71
+ id: "msg_1",
72
+ role: "assistant",
73
+ content: [{ kind: "text", text: "Hello world" }],
74
+ },
75
+ ]);
76
+
77
+ expect(result.state.tick).toBe(1);
78
+ expect(result.state.modelResponses).toHaveLength(1);
79
+ });
80
+
81
+ 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,
101
+ },
102
+ warnings: [],
103
+ };
104
+ },
105
+ stream: async function* () {
106
+ throw new Error("Not implemented");
107
+ },
108
+ };
109
+
110
+ const agent = new Agent({
111
+ id: "test",
112
+ name: "Test",
113
+ instructions: "Test agent",
114
+ model,
115
+ });
116
+
117
+ const kernl = new Kernl();
118
+ const thread = new Thread(kernl, agent, "Test input");
119
+
120
+ await thread.execute();
121
+
122
+ const history = (thread as any).history as ThreadEvent[];
123
+ const firstMessage = history[0];
124
+
125
+ expect(firstMessage).toEqual({
126
+ kind: "message",
127
+ id: expect.any(String),
128
+ role: "user",
129
+ content: [{ kind: "text", text: "Test input" }],
130
+ });
131
+ });
132
+
133
+ 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,
153
+ },
154
+ warnings: [],
155
+ };
156
+ },
157
+ stream: async function* () {
158
+ throw new Error("Not implemented");
159
+ },
160
+ };
161
+
162
+ const agent = new Agent({
163
+ id: "test",
164
+ name: "Test",
165
+ instructions: "Test agent",
166
+ model,
167
+ });
168
+
169
+ const events: ThreadEvent[] = [
170
+ {
171
+ kind: "message",
172
+ id: "custom_msg",
173
+ role: "user",
174
+ content: [{ kind: "text", text: "Custom message" }],
175
+ },
176
+ ];
177
+
178
+ const kernl = new Kernl();
179
+ const thread = new Thread(kernl, agent, events);
180
+
181
+ await thread.execute();
182
+
183
+ const history = (thread as any).history as ThreadEvent[];
184
+ const firstMessage = history[0];
185
+
186
+ expect(firstMessage).toEqual(events[0]);
187
+ });
188
+ });
189
+
190
+ describe("Multi-Turn Execution", () => {
191
+ it("should execute multi-turn with tool call and exact history", async () => {
192
+ let callCount = 0;
193
+
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
+ }
228
+
229
+ // Second call: return final message
230
+ return {
231
+ content: [
232
+ {
233
+ kind: "message" as const,
234
+ id: "msg_2",
235
+ role: "assistant" as const,
236
+ content: [{ kind: "text" as const, text: "Done!" }],
237
+ },
238
+ ],
239
+ finishReason: "stop",
240
+ usage: {
241
+ inputTokens: 4,
242
+ outputTokens: 2,
243
+ totalTokens: 6,
244
+ },
245
+ warnings: [],
246
+ };
247
+ },
248
+ stream: async function* () {
249
+ throw new Error("Not implemented");
250
+ },
251
+ };
252
+
253
+ const echoTool = tool({
254
+ id: "echo",
255
+ description: "Echoes input",
256
+ parameters: z.object({ text: z.string() }),
257
+ execute: async (ctx, { text }) => `Echo: ${text}`,
258
+ });
259
+
260
+ const agent = new Agent({
261
+ id: "test",
262
+ name: "Test",
263
+ instructions: "Test agent",
264
+ model,
265
+ toolkits: [
266
+ new FunctionToolkit({ id: "test-tools", tools: [echoTool] }),
267
+ ],
268
+ });
269
+
270
+ const kernl = new Kernl();
271
+ const thread = new Thread(kernl, agent, "Use the echo tool");
272
+
273
+ const result = await thread.execute();
274
+
275
+ const history = (thread as any).history as ThreadEvent[];
276
+
277
+ expect(history).toEqual([
278
+ // Initial user message
279
+ {
280
+ kind: "message",
281
+ id: expect.any(String),
282
+ role: "user",
283
+ content: [{ kind: "text", text: "Use the echo tool" }],
284
+ },
285
+ // Assistant message (tick 1)
286
+ {
287
+ kind: "message",
288
+ id: "msg_1",
289
+ role: "assistant",
290
+ content: [],
291
+ },
292
+ // Tool call (tick 1)
293
+ {
294
+ kind: "tool-call",
295
+ id: "echo",
296
+ callId: "call_1",
297
+ name: "echo",
298
+ arguments: JSON.stringify({ text: "test" }),
299
+ },
300
+ // Tool result (executed after tick 1)
301
+ {
302
+ kind: "tool-result",
303
+ callId: "call_1",
304
+ name: "echo",
305
+ state: COMPLETED,
306
+ result: "Echo: test",
307
+ error: null,
308
+ },
309
+ // Final assistant message (tick 2)
310
+ {
311
+ kind: "message",
312
+ id: "msg_2",
313
+ role: "assistant",
314
+ content: [{ kind: "text", text: "Done!" }],
315
+ },
316
+ ]);
317
+
318
+ expect(result.state.tick).toBe(2);
319
+ expect(result.state.modelResponses).toHaveLength(2);
320
+ });
321
+
322
+ it("should accumulate history across multiple turns", async () => {
323
+ let callCount = 0;
324
+
325
+ const model: LanguageModel = {
326
+ spec: "1.0" as const,
327
+ provider: "test",
328
+ modelId: "test-model",
329
+ async generate(req: LanguageModelRequest) {
330
+ callCount++;
331
+
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,
354
+ },
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,
381
+ },
382
+ warnings: [],
383
+ };
384
+ }
385
+
386
+ return {
387
+ content: [
388
+ {
389
+ kind: "message" as const,
390
+ id: "msg_3",
391
+ role: "assistant" as const,
392
+ content: [{ kind: "text" as const, text: "All done" }],
393
+ },
394
+ ],
395
+ finishReason: "stop",
396
+ usage: {
397
+ inputTokens: 4,
398
+ outputTokens: 2,
399
+ totalTokens: 6,
400
+ },
401
+ warnings: [],
402
+ };
403
+ },
404
+ stream: async function* () {
405
+ throw new Error("Not implemented");
406
+ },
407
+ };
408
+
409
+ const simpleTool = tool({
410
+ id: "simple",
411
+ description: "Simple tool",
412
+ parameters: undefined,
413
+ execute: async (ctx, input: string) => `Result: ${input}`,
414
+ });
415
+
416
+ const agent = new Agent({
417
+ id: "test",
418
+ name: "Test",
419
+ instructions: "Test agent",
420
+ model,
421
+ toolkits: [
422
+ new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
423
+ ],
424
+ });
425
+
426
+ const kernl = new Kernl();
427
+ const thread = new Thread(kernl, agent, "Start");
428
+
429
+ const result = await thread.execute();
430
+
431
+ const history = (thread as any).history as ThreadEvent[];
432
+
433
+ // Should have: 1 user msg + 3 assistant msgs + 2 tool calls + 2 tool results = 8 events
434
+ expect(history).toHaveLength(8);
435
+ expect(result.state.tick).toBe(3);
436
+ });
437
+ });
438
+
439
+ describe("Tool Execution", () => {
440
+ it("should handle tool not found with exact error in history", async () => {
441
+ let callCount = 0;
442
+
443
+ const model: LanguageModel = {
444
+ spec: "1.0" as const,
445
+ provider: "test",
446
+ modelId: "test-model",
447
+ async generate(req: LanguageModelRequest) {
448
+ callCount++;
449
+
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
479
+ return {
480
+ content: [
481
+ {
482
+ kind: "message" as const,
483
+ id: "msg_2",
484
+ role: "assistant" as const,
485
+ content: [{ kind: "text" as const, text: "Done" }],
486
+ },
487
+ ],
488
+ finishReason: "stop",
489
+ usage: {
490
+ inputTokens: 2,
491
+ outputTokens: 2,
492
+ totalTokens: 4,
493
+ },
494
+ warnings: [],
495
+ };
496
+ },
497
+ stream: async function* () {
498
+ throw new Error("Not implemented");
499
+ },
500
+ };
501
+
502
+ const agent = new Agent({
503
+ id: "test",
504
+ name: "Test",
505
+ instructions: "Test agent",
506
+ model,
507
+ toolkits: [], // No tools available
508
+ });
509
+
510
+ const kernl = new Kernl();
511
+ const thread = new Thread(kernl, agent, "test");
512
+
513
+ await thread.execute();
514
+
515
+ const history = (thread as any).history as ThreadEvent[];
516
+
517
+ // Check that the tool result is an error
518
+ const toolResult = history.find((e) => e.kind === "tool-result");
519
+ expect(toolResult).toEqual({
520
+ kind: "tool-result",
521
+ callId: "call_1",
522
+ toolId: "nonexistent",
523
+ state: FAILED,
524
+ result: undefined,
525
+ error: "Tool nonexistent not found",
526
+ });
527
+ });
528
+
529
+ it("should handle tool execution error", async () => {
530
+ let callCount = 0;
531
+
532
+ const model: LanguageModel = {
533
+ spec: "1.0" as const,
534
+ provider: "test",
535
+ modelId: "test-model",
536
+ async generate(req: LanguageModelRequest) {
537
+ callCount++;
538
+
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
568
+ return {
569
+ content: [
570
+ {
571
+ kind: "message" as const,
572
+ id: "msg_2",
573
+ role: "assistant" as const,
574
+ content: [{ kind: "text" as const, text: "Done" }],
575
+ },
576
+ ],
577
+ finishReason: "stop",
578
+ usage: {
579
+ inputTokens: 2,
580
+ outputTokens: 2,
581
+ totalTokens: 4,
582
+ },
583
+ warnings: [],
584
+ };
585
+ },
586
+ stream: async function* () {
587
+ throw new Error("Not implemented");
588
+ },
589
+ };
590
+
591
+ const failingTool = tool({
592
+ id: "failing",
593
+ description: "Tool that throws",
594
+ parameters: undefined,
595
+ execute: async () => {
596
+ throw new Error("Execution failed!");
597
+ },
598
+ });
599
+
600
+ const agent = new Agent({
601
+ id: "test",
602
+ name: "Test",
603
+ instructions: "Test agent",
604
+ model,
605
+ toolkits: [
606
+ new FunctionToolkit({ id: "test-tools", tools: [failingTool] }),
607
+ ],
608
+ });
609
+
610
+ const kernl = new Kernl();
611
+ const thread = new Thread(kernl, agent, "test");
612
+
613
+ await thread.execute();
614
+
615
+ const history = (thread as any).history as ThreadEvent[];
616
+
617
+ const toolResult = history.find((e) => e.kind === "tool-result");
618
+ expect(toolResult).toMatchObject({
619
+ kind: "tool-result",
620
+ callId: "call_1",
621
+ toolId: "failing",
622
+ state: FAILED,
623
+ result: undefined,
624
+ });
625
+ expect((toolResult as any).error).toContain("Execution failed!");
626
+ });
627
+
628
+ it("should execute tool successfully with result in history", async () => {
629
+ let callCount = 0;
630
+
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
+ }
665
+
666
+ // Second call: return terminal message
667
+ return {
668
+ content: [
669
+ {
670
+ kind: "message" as const,
671
+ id: "msg_2",
672
+ role: "assistant" as const,
673
+ content: [{ kind: "text" as const, text: "Done" }],
674
+ },
675
+ ],
676
+ finishReason: "stop",
677
+ usage: {
678
+ inputTokens: 2,
679
+ outputTokens: 2,
680
+ totalTokens: 4,
681
+ },
682
+ warnings: [],
683
+ };
684
+ },
685
+ stream: async function* () {
686
+ throw new Error("Not implemented");
687
+ },
688
+ };
689
+
690
+ const addTool = tool({
691
+ id: "add",
692
+ description: "Adds two numbers",
693
+ parameters: z.object({ a: z.number(), b: z.number() }),
694
+ execute: async (ctx, { a, b }) => a + b,
695
+ });
696
+
697
+ const agent = new Agent({
698
+ id: "test",
699
+ name: "Test",
700
+ instructions: "Test agent",
701
+ model,
702
+ toolkits: [new FunctionToolkit({ id: "test-tools", tools: [addTool] })],
703
+ });
704
+
705
+ const kernl = new Kernl();
706
+ const thread = new Thread(kernl, agent, "Add 5 and 3");
707
+
708
+ await thread.execute();
709
+
710
+ const history = (thread as any).history as ThreadEvent[];
711
+
712
+ const toolResult = history.find((e) => e.kind === "tool-result");
713
+ expect(toolResult).toEqual({
714
+ kind: "tool-result",
715
+ callId: "call_1",
716
+ toolId: "add",
717
+ state: COMPLETED,
718
+ result: 8,
719
+ error: null,
720
+ });
721
+ });
722
+ });
723
+
724
+ describe("Parallel Tool Execution", () => {
725
+ it("should execute multiple tools in parallel with exact history", async () => {
726
+ let callCount = 0;
727
+
728
+ const model: LanguageModel = {
729
+ spec: "1.0" as const,
730
+ provider: "test",
731
+ modelId: "test-model",
732
+ async generate(req: LanguageModelRequest) {
733
+ callCount++;
734
+
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
771
+ return {
772
+ content: [
773
+ {
774
+ kind: "message" as const,
775
+ id: "msg_2",
776
+ role: "assistant" as const,
777
+ content: [{ kind: "text" as const, text: "Done" }],
778
+ },
779
+ ],
780
+ finishReason: "stop",
781
+ usage: {
782
+ inputTokens: 2,
783
+ outputTokens: 2,
784
+ totalTokens: 4,
785
+ },
786
+ warnings: [],
787
+ };
788
+ },
789
+ stream: async function* () {
790
+ throw new Error("Not implemented");
791
+ },
792
+ };
793
+
794
+ const tool1 = tool({
795
+ id: "tool1",
796
+ description: "Tool 1",
797
+ parameters: z.object({ value: z.string() }),
798
+ execute: async (ctx, { value }) => `Tool1: ${value}`,
799
+ });
800
+
801
+ const tool2 = tool({
802
+ id: "tool2",
803
+ description: "Tool 2",
804
+ parameters: z.object({ value: z.string() }),
805
+ execute: async (ctx, { value }) => `Tool2: ${value}`,
806
+ });
807
+
808
+ const agent = new Agent({
809
+ id: "test",
810
+ name: "Test",
811
+ instructions: "Test agent",
812
+ model,
813
+ toolkits: [
814
+ new FunctionToolkit({ id: "test-tools", tools: [tool1, tool2] }),
815
+ ],
816
+ });
817
+
818
+ const kernl = new Kernl();
819
+ const thread = new Thread(kernl, agent, "test");
820
+
821
+ await thread.execute();
822
+
823
+ const history = (thread as any).history as ThreadEvent[];
824
+
825
+ // Should have both tool results in history
826
+ const toolResults = history.filter((e) => e.kind === "tool-result");
827
+ expect(toolResults).toHaveLength(2);
828
+ expect(toolResults).toEqual(
829
+ expect.arrayContaining([
830
+ {
831
+ kind: "tool-result",
832
+ callId: "call_1",
833
+ toolId: "tool1",
834
+ state: COMPLETED,
835
+ result: "Tool1: a",
836
+ error: null,
837
+ },
838
+ {
839
+ kind: "tool-result",
840
+ callId: "call_2",
841
+ toolId: "tool2",
842
+ state: COMPLETED,
843
+ result: "Tool2: b",
844
+ error: null,
845
+ },
846
+ ]),
847
+ );
848
+ });
849
+ });
850
+
851
+ describe("State Management", () => {
852
+ it("should track tick counter correctly", async () => {
853
+ let callCount = 0;
854
+
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
+ }
888
+
889
+ return {
890
+ content: [
891
+ {
892
+ kind: "message" as const,
893
+ id: "msg_final",
894
+ role: "assistant" as const,
895
+ content: [{ kind: "text" as const, text: "Done" }],
896
+ },
897
+ ],
898
+ finishReason: "stop",
899
+ usage: {
900
+ inputTokens: 2,
901
+ outputTokens: 2,
902
+ totalTokens: 4,
903
+ },
904
+ warnings: [],
905
+ };
906
+ },
907
+ stream: async function* () {
908
+ throw new Error("Not implemented");
909
+ },
910
+ };
911
+
912
+ const simpleTool = tool({
913
+ id: "simple",
914
+ description: "Simple tool",
915
+ parameters: undefined,
916
+ execute: async () => "result",
917
+ });
918
+
919
+ const agent = new Agent({
920
+ id: "test",
921
+ name: "Test",
922
+ instructions: "Test agent",
923
+ model,
924
+ toolkits: [
925
+ new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
926
+ ],
927
+ });
928
+
929
+ const kernl = new Kernl();
930
+ const thread = new Thread(kernl, agent, "test");
931
+
932
+ const result = await thread.execute();
933
+
934
+ expect(result.state.tick).toBe(3);
935
+ });
936
+
937
+ it("should accumulate model responses", async () => {
938
+ let callCount = 0;
939
+
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
+ }
973
+
974
+ return {
975
+ content: [
976
+ {
977
+ kind: "message" as const,
978
+ id: "msg_2",
979
+ role: "assistant" as const,
980
+ content: [{ kind: "text" as const, text: "Done" }],
981
+ },
982
+ ],
983
+ finishReason: "stop",
984
+ usage: {
985
+ inputTokens: 20,
986
+ outputTokens: 10,
987
+ totalTokens: 30,
988
+ },
989
+ warnings: [],
990
+ };
991
+ },
992
+ stream: async function* () {
993
+ throw new Error("Not implemented");
994
+ },
995
+ };
996
+
997
+ const simpleTool = tool({
998
+ id: "simple",
999
+ description: "Simple tool",
1000
+ parameters: undefined,
1001
+ execute: async () => "result",
1002
+ });
1003
+
1004
+ const agent = new Agent({
1005
+ id: "test",
1006
+ name: "Test",
1007
+ instructions: "Test agent",
1008
+ model,
1009
+ toolkits: [
1010
+ new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
1011
+ ],
1012
+ });
1013
+
1014
+ const kernl = new Kernl();
1015
+ const thread = new Thread(kernl, agent, "test");
1016
+
1017
+ const result = await thread.execute();
1018
+
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);
1022
+ });
1023
+ });
1024
+
1025
+ describe("Terminal State Detection", () => {
1026
+ 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,
1046
+ },
1047
+ warnings: [],
1048
+ };
1049
+ },
1050
+ stream: async function* () {
1051
+ throw new Error("Not implemented");
1052
+ },
1053
+ };
1054
+
1055
+ const agent = new Agent({
1056
+ id: "test",
1057
+ name: "Test",
1058
+ instructions: "Test agent",
1059
+ model,
1060
+ });
1061
+
1062
+ const kernl = new Kernl();
1063
+ const thread = new Thread(kernl, agent, "test");
1064
+
1065
+ const result = await thread.execute();
1066
+
1067
+ expect(result.state.tick).toBe(1);
1068
+ });
1069
+
1070
+ it("should continue when assistant message has tool calls", async () => {
1071
+ let callCount = 0;
1072
+
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
+ }
1108
+
1109
+ return {
1110
+ content: [
1111
+ {
1112
+ kind: "message" as const,
1113
+ id: "msg_2",
1114
+ role: "assistant" as const,
1115
+ content: [{ kind: "text" as const, text: "Done now" }],
1116
+ },
1117
+ ],
1118
+ finishReason: "stop",
1119
+ usage: {
1120
+ inputTokens: 3,
1121
+ outputTokens: 2,
1122
+ totalTokens: 5,
1123
+ },
1124
+ warnings: [],
1125
+ };
1126
+ },
1127
+ stream: async function* () {
1128
+ throw new Error("Not implemented");
1129
+ },
1130
+ };
1131
+
1132
+ const simpleTool = tool({
1133
+ id: "simple",
1134
+ description: "Simple tool",
1135
+ parameters: undefined,
1136
+ execute: async () => "result",
1137
+ });
1138
+
1139
+ const agent = new Agent({
1140
+ id: "test",
1141
+ name: "Test",
1142
+ instructions: "Test agent",
1143
+ model,
1144
+ toolkits: [
1145
+ new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
1146
+ ],
1147
+ });
1148
+
1149
+ const kernl = new Kernl();
1150
+ const thread = new Thread(kernl, agent, "test");
1151
+
1152
+ const result = await thread.execute();
1153
+
1154
+ // Should have made 2 calls - first with tool, second without
1155
+ expect(result.state.tick).toBe(2);
1156
+ });
1157
+ });
1158
+
1159
+ describe("Final Output Parsing", () => {
1160
+ 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,
1180
+ },
1181
+ warnings: [],
1182
+ };
1183
+ },
1184
+ stream: async function* () {
1185
+ throw new Error("Not implemented");
1186
+ },
1187
+ };
1188
+
1189
+ const agent = new Agent({
1190
+ id: "test",
1191
+ name: "Test",
1192
+ instructions: "Test agent",
1193
+ model,
1194
+ responseType: "text",
1195
+ });
1196
+
1197
+ const kernl = new Kernl();
1198
+ const thread = new Thread(kernl, agent, "test");
1199
+
1200
+ const result = await thread.execute();
1201
+
1202
+ expect(result.response).toBe("Hello, world!");
1203
+ expect(result.state.tick).toBe(1);
1204
+ });
1205
+
1206
+ it("should parse and validate structured output with valid JSON", async () => {
1207
+ const responseSchema = z.object({
1208
+ name: z.string(),
1209
+ age: z.number(),
1210
+ email: z.string().email(),
1211
+ });
1212
+
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,
1237
+ },
1238
+ warnings: [],
1239
+ };
1240
+ },
1241
+ stream: async function* () {
1242
+ throw new Error("Not implemented");
1243
+ },
1244
+ };
1245
+
1246
+ const agent = new Agent({
1247
+ id: "test",
1248
+ name: "Test",
1249
+ instructions: "Test agent",
1250
+ model,
1251
+ responseType: responseSchema,
1252
+ });
1253
+
1254
+ const kernl = new Kernl();
1255
+ const thread = new Thread(kernl, agent, "test");
1256
+
1257
+ const result = await thread.execute();
1258
+
1259
+ expect(result.response).toEqual({
1260
+ name: "Alice",
1261
+ age: 30,
1262
+ email: "alice@example.com",
1263
+ });
1264
+ });
1265
+
1266
+ it("should throw ModelBehaviorError for invalid JSON syntax", async () => {
1267
+ const responseSchema = z.object({
1268
+ name: z.string(),
1269
+ });
1270
+
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,
1295
+ },
1296
+ warnings: [],
1297
+ };
1298
+ },
1299
+ stream: async function* () {
1300
+ throw new Error("Not implemented");
1301
+ },
1302
+ };
1303
+
1304
+ const agent = new Agent({
1305
+ id: "test",
1306
+ name: "Test",
1307
+ instructions: "Test agent",
1308
+ model,
1309
+ responseType: responseSchema,
1310
+ });
1311
+
1312
+ const kernl = new Kernl();
1313
+ const thread = new Thread(kernl, agent, "test");
1314
+
1315
+ await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1316
+ });
1317
+
1318
+ it("should throw ModelBehaviorError when JSON doesn't match schema", async () => {
1319
+ const responseSchema = z.object({
1320
+ name: z.string(),
1321
+ age: z.number(),
1322
+ });
1323
+
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,
1348
+ },
1349
+ warnings: [],
1350
+ };
1351
+ },
1352
+ stream: async function* () {
1353
+ throw new Error("Not implemented");
1354
+ },
1355
+ };
1356
+
1357
+ const agent = new Agent({
1358
+ id: "test",
1359
+ name: "Test",
1360
+ instructions: "Test agent",
1361
+ model,
1362
+ responseType: responseSchema,
1363
+ });
1364
+
1365
+ const kernl = new Kernl();
1366
+ const thread = new Thread(kernl, agent, "test");
1367
+
1368
+ await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1369
+ });
1370
+
1371
+ it("should throw ModelBehaviorError when required fields are missing", async () => {
1372
+ const responseSchema = z.object({
1373
+ name: z.string(),
1374
+ age: z.number(),
1375
+ email: z.string(),
1376
+ });
1377
+
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,
1402
+ },
1403
+ warnings: [],
1404
+ };
1405
+ },
1406
+ stream: async function* () {
1407
+ throw new Error("Not implemented");
1408
+ },
1409
+ };
1410
+
1411
+ const agent = new Agent({
1412
+ id: "test",
1413
+ name: "Test",
1414
+ instructions: "Test agent",
1415
+ model,
1416
+ responseType: responseSchema,
1417
+ });
1418
+
1419
+ const kernl = new Kernl();
1420
+ const thread = new Thread(kernl, agent, "test");
1421
+
1422
+ await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
1423
+ });
1424
+
1425
+ it("should handle nested structured output", async () => {
1426
+ const responseSchema = z.object({
1427
+ user: z.object({
1428
+ name: z.string(),
1429
+ profile: z.object({
1430
+ bio: z.string(),
1431
+ age: z.number(),
1432
+ }),
1433
+ }),
1434
+ metadata: z.object({
1435
+ timestamp: z.string(),
1436
+ }),
1437
+ });
1438
+
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,
1469
+ },
1470
+ warnings: [],
1471
+ };
1472
+ },
1473
+ stream: async function* () {
1474
+ throw new Error("Not implemented");
1475
+ },
1476
+ };
1477
+
1478
+ const agent = new Agent({
1479
+ id: "test",
1480
+ name: "Test",
1481
+ instructions: "Test agent",
1482
+ model,
1483
+ responseType: responseSchema,
1484
+ });
1485
+
1486
+ const kernl = new Kernl();
1487
+ const thread = new Thread(kernl, agent, "test");
1488
+
1489
+ const result = await thread.execute();
1490
+
1491
+ expect(result.response).toEqual({
1492
+ user: {
1493
+ name: "Bob",
1494
+ profile: { bio: "Engineer", age: 25 },
1495
+ },
1496
+ metadata: { timestamp: "2024-01-01" },
1497
+ });
1498
+ });
1499
+
1500
+ it("should continue loop when no text in assistant message", async () => {
1501
+ let callCount = 0;
1502
+
1503
+ const model: LanguageModel = {
1504
+ spec: "1.0" as const,
1505
+ provider: "test",
1506
+ modelId: "test-model",
1507
+ async generate(req: LanguageModelRequest) {
1508
+ callCount++;
1509
+
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
1532
+ return {
1533
+ content: [
1534
+ {
1535
+ kind: "message" as const,
1536
+ id: "msg_2",
1537
+ role: "assistant" as const,
1538
+ content: [{ kind: "text" as const, text: "Now I have text" }],
1539
+ },
1540
+ ],
1541
+ finishReason: "stop",
1542
+ usage: {
1543
+ inputTokens: 2,
1544
+ outputTokens: 2,
1545
+ totalTokens: 4,
1546
+ },
1547
+ warnings: [],
1548
+ };
1549
+ },
1550
+ stream: async function* () {
1551
+ throw new Error("Not implemented");
1552
+ },
1553
+ };
1554
+
1555
+ const agent = new Agent({
1556
+ id: "test",
1557
+ name: "Test",
1558
+ instructions: "Test agent",
1559
+ model,
1560
+ responseType: "text",
1561
+ });
1562
+
1563
+ const kernl = new Kernl();
1564
+ const thread = new Thread(kernl, agent, "test");
1565
+
1566
+ const result = await thread.execute();
1567
+
1568
+ // Should have made 2 calls
1569
+ expect(callCount).toBe(2);
1570
+ expect(result.response).toBe("Now I have text");
1571
+ expect(result.state.tick).toBe(2);
1572
+ });
1573
+ });
1574
+ });