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