kernl 0.2.1 → 0.6.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 (292) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/CHANGELOG.md +147 -0
  4. package/LICENSE +1 -1
  5. package/dist/agent/__tests__/concurrency.test.d.ts +2 -0
  6. package/dist/agent/__tests__/concurrency.test.d.ts.map +1 -0
  7. package/dist/agent/__tests__/concurrency.test.js +152 -0
  8. package/dist/agent/__tests__/run.test.d.ts +2 -0
  9. package/dist/agent/__tests__/run.test.d.ts.map +1 -0
  10. package/dist/agent/__tests__/run.test.js +357 -0
  11. package/dist/agent/index.d.ts +1 -0
  12. package/dist/agent/index.d.ts.map +1 -0
  13. package/dist/agent.d.ts +35 -12
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js +102 -15
  16. package/dist/api/__tests__/cursor-page.test.d.ts +2 -0
  17. package/dist/api/__tests__/cursor-page.test.d.ts.map +1 -0
  18. package/dist/api/__tests__/cursor-page.test.js +414 -0
  19. package/dist/api/__tests__/offset-page.test.d.ts +2 -0
  20. package/dist/api/__tests__/offset-page.test.d.ts.map +1 -0
  21. package/dist/api/__tests__/offset-page.test.js +510 -0
  22. package/dist/api/__tests__/threads.test.d.ts +2 -0
  23. package/dist/api/__tests__/threads.test.d.ts.map +1 -0
  24. package/dist/api/__tests__/threads.test.js +338 -0
  25. package/dist/api/models/index.d.ts +2 -0
  26. package/dist/api/models/index.d.ts.map +1 -0
  27. package/dist/api/models/thread.d.ts +120 -0
  28. package/dist/api/models/thread.d.ts.map +1 -0
  29. package/dist/api/pagination/base.d.ts +48 -0
  30. package/dist/api/pagination/base.d.ts.map +1 -0
  31. package/dist/api/pagination/base.js +45 -0
  32. package/dist/api/pagination/cursor.d.ts +44 -0
  33. package/dist/api/pagination/cursor.d.ts.map +1 -0
  34. package/dist/api/pagination/cursor.js +52 -0
  35. package/dist/api/pagination/offset.d.ts +42 -0
  36. package/dist/api/pagination/offset.d.ts.map +1 -0
  37. package/dist/api/pagination/offset.js +55 -0
  38. package/dist/api/resources/threads/events.d.ts +21 -0
  39. package/dist/api/resources/threads/events.d.ts.map +1 -0
  40. package/dist/api/resources/threads/events.js +24 -0
  41. package/dist/api/resources/threads/index.d.ts +4 -0
  42. package/dist/api/resources/threads/index.d.ts.map +1 -0
  43. package/dist/api/resources/threads/index.js +2 -0
  44. package/dist/api/resources/threads/threads.d.ts +57 -0
  45. package/dist/api/resources/threads/threads.d.ts.map +1 -0
  46. package/dist/api/resources/threads/threads.js +199 -0
  47. package/dist/api/resources/threads/types.d.ts +123 -0
  48. package/dist/api/resources/threads/types.d.ts.map +1 -0
  49. package/dist/api/resources/threads/utils.d.ts +18 -0
  50. package/dist/api/resources/threads/utils.d.ts.map +1 -0
  51. package/dist/api/resources/threads/utils.js +78 -0
  52. package/dist/context.d.ts +5 -1
  53. package/dist/context.d.ts.map +1 -1
  54. package/dist/context.js +6 -1
  55. package/dist/guardrail.d.ts +4 -4
  56. package/dist/index.d.ts +12 -4
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +10 -3
  59. package/dist/internal.d.ts +4 -0
  60. package/dist/internal.d.ts.map +1 -0
  61. package/dist/internal.js +2 -0
  62. package/dist/kernl/index.d.ts +3 -0
  63. package/dist/kernl/index.d.ts.map +1 -0
  64. package/dist/kernl/index.js +2 -0
  65. package/dist/kernl/kernl.d.ts +64 -0
  66. package/dist/kernl/kernl.d.ts.map +1 -0
  67. package/dist/kernl/kernl.js +116 -0
  68. package/dist/kernl/threads.d.ts +110 -0
  69. package/dist/kernl/threads.d.ts.map +1 -0
  70. package/dist/kernl/threads.js +126 -0
  71. package/dist/kernl.d.ts +27 -11
  72. package/dist/kernl.d.ts.map +1 -1
  73. package/dist/kernl.js +74 -11
  74. package/dist/lib/env.d.ts +3 -3
  75. package/dist/lib/env.js +1 -1
  76. package/dist/lib/error.d.ts +3 -3
  77. package/dist/lib/logger.js +1 -1
  78. package/dist/lifecycle.d.ts +5 -5
  79. package/dist/mcp/__tests__/base.test.js +2 -2
  80. package/dist/mcp/__tests__/fixtures/utils.d.ts +1 -1
  81. package/dist/mcp/__tests__/fixtures/utils.js +1 -1
  82. package/dist/mcp/__tests__/integration.test.js +16 -16
  83. package/dist/mcp/__tests__/stdio.test.js +2 -2
  84. package/dist/mcp/__tests__/utils.test.js +8 -8
  85. package/dist/mcp/base.d.ts +2 -2
  86. package/dist/mcp/http.d.ts +3 -3
  87. package/dist/mcp/http.d.ts.map +1 -1
  88. package/dist/mcp/http.js +11 -11
  89. package/dist/mcp/sse.d.ts +3 -3
  90. package/dist/mcp/sse.d.ts.map +1 -1
  91. package/dist/mcp/sse.js +9 -9
  92. package/dist/mcp/stdio.d.ts +2 -2
  93. package/dist/mcp/stdio.js +2 -2
  94. package/dist/mcp/types.d.ts +3 -3
  95. package/dist/mcp/utils.d.ts +4 -4
  96. package/dist/mcp/utils.d.ts.map +1 -1
  97. package/dist/mcp/utils.js +5 -6
  98. package/dist/storage/__tests__/in-memory.test.d.ts +2 -0
  99. package/dist/storage/__tests__/in-memory.test.d.ts.map +1 -0
  100. package/dist/storage/__tests__/in-memory.test.js +455 -0
  101. package/dist/storage/base.d.ts +64 -0
  102. package/dist/storage/base.d.ts.map +1 -0
  103. package/dist/storage/base.js +4 -0
  104. package/dist/storage/in-memory.d.ts +62 -0
  105. package/dist/storage/in-memory.d.ts.map +1 -0
  106. package/dist/storage/in-memory.js +283 -0
  107. package/dist/storage/index.d.ts +10 -0
  108. package/dist/storage/index.d.ts.map +1 -0
  109. package/dist/storage/index.js +7 -0
  110. package/dist/storage/thread.d.ts +123 -0
  111. package/dist/storage/thread.d.ts.map +1 -0
  112. package/dist/storage/thread.js +4 -0
  113. package/dist/task.d.ts +8 -6
  114. package/dist/task.d.ts.map +1 -1
  115. package/dist/task.js +10 -8
  116. package/dist/thread/__tests__/fixtures/mock-model.d.ts +1 -2
  117. package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -1
  118. package/dist/thread/__tests__/integration.test.js +10 -10
  119. package/dist/thread/__tests__/mock.d.ts +1 -1
  120. package/dist/thread/__tests__/namespace.test.d.ts +2 -0
  121. package/dist/thread/__tests__/namespace.test.d.ts.map +1 -0
  122. package/dist/thread/__tests__/namespace.test.js +131 -0
  123. package/dist/thread/__tests__/thread-persistence.test.d.ts +2 -0
  124. package/dist/thread/__tests__/thread-persistence.test.d.ts.map +1 -0
  125. package/dist/thread/__tests__/thread-persistence.test.js +351 -0
  126. package/dist/thread/__tests__/thread.test.js +55 -57
  127. package/dist/thread/index.d.ts +1 -1
  128. package/dist/thread/index.js +1 -1
  129. package/dist/thread/thread.d.ts +74 -22
  130. package/dist/thread/thread.d.ts.map +1 -1
  131. package/dist/thread/thread.js +212 -74
  132. package/dist/thread/utils.d.ts +38 -10
  133. package/dist/thread/utils.d.ts.map +1 -1
  134. package/dist/thread/utils.js +53 -9
  135. package/dist/tool/__tests__/fixtures.d.ts +8 -8
  136. package/dist/tool/__tests__/fixtures.js +3 -3
  137. package/dist/tool/__tests__/tool.test.js +2 -2
  138. package/dist/tool/__tests__/toolkit.test.js +17 -14
  139. package/dist/tool/index.d.ts +3 -3
  140. package/dist/tool/index.js +2 -2
  141. package/dist/tool/tool.d.ts +2 -2
  142. package/dist/tool/tool.js +5 -5
  143. package/dist/tool/toolkit.d.ts +4 -4
  144. package/dist/tool/toolkit.js +1 -1
  145. package/dist/tool/types.d.ts +4 -4
  146. package/dist/trace/traces.js +2 -2
  147. package/dist/types/agent.d.ts +4 -4
  148. package/dist/types/kernl.d.ts +42 -0
  149. package/dist/types/kernl.d.ts.map +1 -0
  150. package/dist/types/thread.d.ts +110 -24
  151. package/dist/types/thread.d.ts.map +1 -1
  152. package/dist/types/thread.js +12 -0
  153. package/package.json +12 -8
  154. package/src/agent/__tests__/concurrency.test.ts +194 -0
  155. package/src/agent/__tests__/run.test.ts +441 -0
  156. package/src/agent/index.ts +0 -0
  157. package/src/agent.ts +139 -24
  158. package/src/api/__tests__/cursor-page.test.ts +512 -0
  159. package/src/api/__tests__/offset-page.test.ts +624 -0
  160. package/src/api/__tests__/threads.test.ts +415 -0
  161. package/src/api/models/index.ts +6 -0
  162. package/src/api/models/thread.ts +138 -0
  163. package/src/api/pagination/base.ts +79 -0
  164. package/src/api/pagination/cursor.ts +86 -0
  165. package/src/api/pagination/offset.ts +89 -0
  166. package/src/api/resources/threads/events.ts +26 -0
  167. package/src/api/resources/threads/index.ts +9 -0
  168. package/src/api/resources/threads/threads.ts +256 -0
  169. package/src/api/resources/threads/types.ts +143 -0
  170. package/src/api/resources/threads/utils.ts +104 -0
  171. package/src/context.ts +10 -1
  172. package/src/index.ts +49 -1
  173. package/src/internal.ts +15 -0
  174. package/src/kernl.ts +86 -17
  175. package/src/mcp/__tests__/integration.test.ts +8 -9
  176. package/src/mcp/__tests__/utils.test.ts +6 -6
  177. package/src/mcp/http.ts +9 -9
  178. package/src/mcp/sse.ts +7 -7
  179. package/src/mcp/utils.ts +6 -5
  180. package/src/storage/__tests__/in-memory.test.ts +534 -0
  181. package/src/storage/base.ts +77 -0
  182. package/src/storage/in-memory.ts +372 -0
  183. package/src/storage/index.ts +21 -0
  184. package/src/storage/thread.ts +141 -0
  185. package/src/task.ts +12 -10
  186. package/src/thread/__tests__/fixtures/mock-model.ts +2 -4
  187. package/src/thread/__tests__/integration.test.ts +13 -12
  188. package/src/thread/__tests__/namespace.test.ts +158 -0
  189. package/src/thread/__tests__/thread-persistence.test.ts +367 -0
  190. package/src/thread/__tests__/thread.test.ts +52 -54
  191. package/src/thread/thread.ts +247 -96
  192. package/src/thread/utils.ts +76 -13
  193. package/src/tool/__tests__/fixtures.ts +1 -1
  194. package/src/tool/__tests__/toolkit.test.ts +15 -12
  195. package/src/tool/tool.ts +3 -3
  196. package/src/types/kernl.ts +51 -0
  197. package/src/types/thread.ts +139 -25
  198. package/vitest.config.ts +1 -0
  199. package/dist/env.d.ts +0 -45
  200. package/dist/env.d.ts.map +0 -1
  201. package/dist/env.js +0 -31
  202. package/dist/error.d.ts +0 -1
  203. package/dist/error.d.ts.map +0 -1
  204. package/dist/kernel.d.ts +0 -7
  205. package/dist/kernel.d.ts.map +0 -1
  206. package/dist/kernel.js +0 -7
  207. package/dist/lib/serde/__tests__/codec.test.d.ts +0 -2
  208. package/dist/lib/serde/__tests__/codec.test.d.ts.map +0 -1
  209. package/dist/lib/serde/__tests__/codec.test.js +0 -75
  210. package/dist/lib/serde/codec.d.ts +0 -12
  211. package/dist/lib/serde/codec.d.ts.map +0 -1
  212. package/dist/lib/serde/codec.js +0 -54
  213. package/dist/lib/serde/thread.d.ts +0 -1
  214. package/dist/lib/serde/thread.d.ts.map +0 -1
  215. package/dist/lib/serde/thread.js +0 -172
  216. package/dist/lib/serde/tool.d.ts +0 -36
  217. package/dist/lib/serde/tool.d.ts.map +0 -1
  218. package/dist/lib/utils.d.ts +0 -19
  219. package/dist/lib/utils.d.ts.map +0 -1
  220. package/dist/lib/utils.js +0 -41
  221. package/dist/logger.d.ts +0 -36
  222. package/dist/logger.d.ts.map +0 -1
  223. package/dist/logger.js +0 -43
  224. package/dist/mcp/__tests__/fixtures/echo-server.d.ts +0 -3
  225. package/dist/mcp/__tests__/fixtures/echo-server.d.ts.map +0 -1
  226. package/dist/mcp/__tests__/fixtures/echo-server.js +0 -92
  227. package/dist/mcp/__tests__/fixtures/math-server.d.ts +0 -3
  228. package/dist/mcp/__tests__/fixtures/math-server.d.ts.map +0 -1
  229. package/dist/mcp/__tests__/fixtures/math-server.js +0 -98
  230. package/dist/mcp/__tests__/fixtures/test-server.d.ts +0 -3
  231. package/dist/mcp/__tests__/fixtures/test-server.d.ts.map +0 -1
  232. package/dist/mcp/__tests__/fixtures/test-server.js +0 -163
  233. package/dist/mcp/__tests__/test-utils.d.ts +0 -17
  234. package/dist/mcp/__tests__/test-utils.d.ts.map +0 -1
  235. package/dist/mcp/__tests__/test-utils.js +0 -42
  236. package/dist/mcp/node.d.ts +0 -60
  237. package/dist/mcp/node.d.ts.map +0 -1
  238. package/dist/mcp/node.js +0 -297
  239. package/dist/model.d.ts +0 -175
  240. package/dist/model.d.ts.map +0 -1
  241. package/dist/providers/ai.d.ts +0 -1
  242. package/dist/providers/ai.d.ts.map +0 -1
  243. package/dist/providers/ai.js +0 -1
  244. package/dist/providers/default.d.ts +0 -16
  245. package/dist/providers/default.d.ts.map +0 -1
  246. package/dist/providers/default.js +0 -17
  247. package/dist/providers/registry.d.ts +0 -1
  248. package/dist/providers/registry.d.ts.map +0 -1
  249. package/dist/providers/registry.js +0 -1
  250. package/dist/sched/scheduler.d.ts +0 -20
  251. package/dist/sched/scheduler.d.ts.map +0 -1
  252. package/dist/sched/task.d.ts +0 -92
  253. package/dist/sched/task.d.ts.map +0 -1
  254. package/dist/sched/task.js +0 -102
  255. package/dist/serde/__tests__/codec.test.d.ts +0 -2
  256. package/dist/serde/__tests__/codec.test.d.ts.map +0 -1
  257. package/dist/serde/__tests__/codec.test.js +0 -75
  258. package/dist/serde/codec.d.ts +0 -12
  259. package/dist/serde/codec.d.ts.map +0 -1
  260. package/dist/serde/codec.js +0 -54
  261. package/dist/serde/json.d.ts +0 -8
  262. package/dist/serde/json.d.ts.map +0 -1
  263. package/dist/serde/json.js +0 -13
  264. package/dist/serde/thread.d.ts +0 -687
  265. package/dist/serde/thread.d.ts.map +0 -1
  266. package/dist/serde/thread.js +0 -158
  267. package/dist/serde/tool.d.ts +0 -36
  268. package/dist/serde/tool.d.ts.map +0 -1
  269. package/dist/session.d.ts +0 -1
  270. package/dist/session.d.ts.map +0 -1
  271. package/dist/session.js +0 -1
  272. package/dist/thread/__tests__/stream.test.d.ts +0 -2
  273. package/dist/thread/__tests__/stream.test.d.ts.map +0 -1
  274. package/dist/thread/__tests__/stream.test.js +0 -244
  275. package/dist/tool/mcp.d.ts +0 -75
  276. package/dist/tool/mcp.d.ts.map +0 -1
  277. package/dist/tool/mcp.js +0 -111
  278. package/dist/tools.d.ts +0 -362
  279. package/dist/tools.d.ts.map +0 -1
  280. package/dist/tools.js +0 -220
  281. package/dist/types/proto.d.ts +0 -1551
  282. package/dist/types/proto.d.ts.map +0 -1
  283. package/dist/types/proto.js +0 -531
  284. package/dist/usage.d.ts +0 -43
  285. package/dist/usage.d.ts.map +0 -1
  286. package/dist/usage.js +0 -61
  287. package/src/lib/serde/thread.ts +0 -188
  288. /package/dist/{error.js → agent/index.js} +0 -0
  289. /package/dist/{lib/serde/tool.js → api/models/index.js} +0 -0
  290. /package/dist/{model.js → api/models/thread.js} +0 -0
  291. /package/dist/{sched/scheduler.js → api/resources/threads/types.js} +0 -0
  292. /package/dist/{serde/tool.js → types/kernl.js} +0 -0
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Agent } from "@/agent";
3
+ import { Kernl } from "@/kernl";
4
+ import { createMockModel } from "@/thread/__tests__/fixtures/mock-model";
5
+ import { RuntimeError } from "@/lib/error";
6
+ import { message } from "@kernl-sdk/protocol";
7
+
8
+ describe("Concurrent execution prevention", () => {
9
+ // (TODO): this should work
10
+ it.skip("should prevent two Agent.run() calls with same threadId", async () => {
11
+ const model = createMockModel(async () => {
12
+ await new Promise((resolve) => setTimeout(resolve, 200));
13
+ return {
14
+ content: [message({ role: "assistant", text: "Done" })],
15
+ finishReason: "stop",
16
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
17
+ warnings: [],
18
+ };
19
+ });
20
+
21
+ const agent = new Agent({
22
+ id: "test-agent",
23
+ name: "Test",
24
+ instructions: "Test",
25
+ model,
26
+ });
27
+
28
+ const kernl = new Kernl();
29
+ kernl.register(agent);
30
+
31
+ const tid = "concurrent-test-1";
32
+
33
+ // Start first run
34
+ const run1 = agent.run("First", { threadId: tid });
35
+
36
+ // Try second run immediately
37
+ await expect(agent.run("Second", { threadId: tid })).rejects.toThrow(
38
+ RuntimeError,
39
+ );
40
+
41
+ await expect(agent.run("Second", { threadId: tid })).rejects.toThrow(
42
+ /already running/,
43
+ );
44
+
45
+ // Wait for first to complete
46
+ await run1;
47
+
48
+ // Now third should work
49
+ await expect(agent.run("Third", { threadId: tid })).resolves.toBeDefined();
50
+ });
51
+
52
+ // (TODO): this should work
53
+ it.skip("should prevent Agent.stream() on thread already in Agent.run()", async () => {
54
+ const model = createMockModel(async () => {
55
+ await new Promise((resolve) => setTimeout(resolve, 200));
56
+ return {
57
+ content: [message({ role: "assistant", text: "Done" })],
58
+ finishReason: "stop",
59
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
60
+ warnings: [],
61
+ };
62
+ });
63
+
64
+ const agent = new Agent({
65
+ id: "test-agent",
66
+ name: "Test",
67
+ instructions: "Test",
68
+ model,
69
+ });
70
+
71
+ const kernl = new Kernl();
72
+ kernl.register(agent);
73
+
74
+ const tid = "concurrent-test-2";
75
+
76
+ // Start run()
77
+ const runPromise = agent.run("Test", { threadId: tid });
78
+
79
+ // Try to stream same thread
80
+ const streamIterable = agent.stream("Test", { threadId: tid });
81
+ const streamIterator = streamIterable[Symbol.asyncIterator]();
82
+ await expect(streamIterator.next()).rejects.toThrow(RuntimeError);
83
+ await expect(streamIterator.next()).rejects.toThrow(/already running/);
84
+
85
+ await runPromise;
86
+ });
87
+
88
+ it("should prevent Agent.run() on thread already in Agent.stream()", async () => {
89
+ const model = createMockModel(async () => {
90
+ await new Promise((resolve) => setTimeout(resolve, 50));
91
+ return {
92
+ content: [message({ role: "assistant", text: "Done" })],
93
+ finishReason: "stop",
94
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
95
+ warnings: [],
96
+ };
97
+ });
98
+
99
+ const agent = new Agent({
100
+ id: "test-agent",
101
+ name: "Test",
102
+ instructions: "Test",
103
+ model,
104
+ });
105
+
106
+ const kernl = new Kernl();
107
+ kernl.register(agent);
108
+
109
+ const tid = "concurrent-test-3";
110
+
111
+ // Start stream() but don't await - start consuming
112
+ const streamIterator = agent.stream("Test", { threadId: tid });
113
+ const streamPromise = (async () => {
114
+ for await (const _event of streamIterator) {
115
+ // consume
116
+ }
117
+ })();
118
+
119
+ // Give stream time to start
120
+ await new Promise((resolve) => setTimeout(resolve, 10));
121
+
122
+ // Try to run() same thread
123
+ await expect(agent.run("Test", { threadId: tid })).rejects.toThrow(
124
+ RuntimeError,
125
+ );
126
+
127
+ await streamPromise;
128
+ });
129
+
130
+ it("should allow different threadIds to run concurrently", async () => {
131
+ const model = createMockModel(async () => {
132
+ await new Promise((resolve) => setTimeout(resolve, 50));
133
+ return {
134
+ content: [message({ role: "assistant", text: "Done" })],
135
+ finishReason: "stop",
136
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
137
+ warnings: [],
138
+ };
139
+ });
140
+
141
+ const agent = new Agent({
142
+ id: "test-agent",
143
+ name: "Test",
144
+ instructions: "Test",
145
+ model,
146
+ });
147
+
148
+ const kernl = new Kernl();
149
+ kernl.register(agent);
150
+
151
+ // These should all succeed
152
+ const results = await Promise.all([
153
+ agent.run("Test 1", { threadId: "thread-1" }),
154
+ agent.run("Test 2", { threadId: "thread-2" }),
155
+ agent.run("Test 3", { threadId: "thread-3" }),
156
+ ]);
157
+
158
+ expect(results).toHaveLength(3);
159
+ expect(results.every((r) => r.response === "Done")).toBe(true);
160
+ });
161
+
162
+ it("should allow same threadId after previous run completes", async () => {
163
+ const model = createMockModel(async () => ({
164
+ content: [message({ role: "assistant", text: "Done" })],
165
+ finishReason: "stop",
166
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
167
+ warnings: [],
168
+ }));
169
+
170
+ const agent = new Agent({
171
+ id: "test-agent",
172
+ name: "Test",
173
+ instructions: "Test",
174
+ model,
175
+ });
176
+
177
+ const kernl = new Kernl();
178
+ kernl.register(agent);
179
+
180
+ const tid = "reuse-thread";
181
+
182
+ // First run
183
+ const result1 = await agent.run("First", { threadId: tid });
184
+ expect(result1.response).toBe("Done");
185
+
186
+ // Second run (should work since first completed)
187
+ const result2 = await agent.run("Second", { threadId: tid });
188
+ expect(result2.response).toBe("Done");
189
+
190
+ // Third run
191
+ const result3 = await agent.run("Third", { threadId: tid });
192
+ expect(result3.response).toBe("Done");
193
+ });
194
+ });
@@ -0,0 +1,441 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Agent } from "@/agent";
3
+ import { Kernl } from "@/kernl";
4
+ import { createMockModel } from "@/thread/__tests__/fixtures/mock-model";
5
+ import { MisconfiguredError } from "@/lib/error";
6
+ import { message } from "@kernl-sdk/protocol";
7
+ import { InMemoryStorage } from "@/storage/in-memory";
8
+
9
+ describe("Agent.run() lifecycle", () => {
10
+ describe("Storage wiring", () => {
11
+ it("should pass storage to new Thread when creating", async () => {
12
+ const storage = new InMemoryStorage();
13
+ const model = createMockModel(async () => ({
14
+ content: [message({ role: "assistant", text: "Done" })],
15
+ finishReason: "stop",
16
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
17
+ warnings: [],
18
+ }));
19
+
20
+ const agent = new Agent({
21
+ id: "test-agent",
22
+ name: "Test",
23
+ instructions: "Test",
24
+ model,
25
+ });
26
+
27
+ const kernl = new Kernl({ storage: { db: storage } });
28
+ kernl.register(agent);
29
+
30
+ await agent.run("Hello");
31
+
32
+ // Verify storage was used - check that events were appended
33
+ const threads = await storage.threads.list();
34
+ expect(threads).toHaveLength(1);
35
+
36
+ const history = await storage.threads.history(threads[0].tid);
37
+ expect(history.length).toBeGreaterThan(0);
38
+ });
39
+
40
+ it("should work without storage (persist is no-op)", async () => {
41
+ const model = createMockModel(async () => ({
42
+ content: [message({ role: "assistant", text: "Done" })],
43
+ finishReason: "stop",
44
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
45
+ warnings: [],
46
+ }));
47
+
48
+ const agent = new Agent({
49
+ id: "test-agent",
50
+ name: "Test",
51
+ instructions: "Test",
52
+ model,
53
+ });
54
+
55
+ const kernl = new Kernl(); // No storage
56
+ kernl.register(agent);
57
+
58
+ const result = await agent.run("Hello");
59
+
60
+ // Should complete successfully
61
+ expect(result.response).toBe("Done");
62
+ expect(result.state).toBe("stopped");
63
+ });
64
+
65
+ it("should throw MisconfiguredError when agent not bound to kernl", async () => {
66
+ const model = createMockModel(async () => ({
67
+ content: [message({ role: "assistant", text: "Done" })],
68
+ finishReason: "stop",
69
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
70
+ warnings: [],
71
+ }));
72
+
73
+ const agent = new Agent({
74
+ id: "test-agent",
75
+ name: "Test",
76
+ instructions: "Test",
77
+ model,
78
+ });
79
+
80
+ // Don't register with kernl
81
+ await expect(agent.run("Hello")).rejects.toThrow(MisconfiguredError);
82
+ await expect(agent.run("Hello")).rejects.toThrow(/not bound to kernl/);
83
+ });
84
+ });
85
+
86
+ describe("New thread path", () => {
87
+ it("should create new thread when no threadId provided", async () => {
88
+ const model = createMockModel(async () => ({
89
+ content: [message({ role: "assistant", text: "Done" })],
90
+ finishReason: "stop",
91
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
92
+ warnings: [],
93
+ }));
94
+
95
+ const agent = new Agent({
96
+ id: "test-agent",
97
+ name: "Test",
98
+ instructions: "Test",
99
+ model,
100
+ });
101
+
102
+ const kernl = new Kernl();
103
+ kernl.register(agent);
104
+
105
+ const result = await agent.run("Hello");
106
+
107
+ expect(result.response).toBe("Done");
108
+ expect(result.state).toBe("stopped");
109
+ });
110
+
111
+ it("should create new thread when threadId not found in storage", async () => {
112
+ const storage = new InMemoryStorage();
113
+ const model = createMockModel(async () => ({
114
+ content: [message({ role: "assistant", text: "Done" })],
115
+ finishReason: "stop",
116
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
117
+ warnings: [],
118
+ }));
119
+
120
+ const agent = new Agent({
121
+ id: "test-agent",
122
+ name: "Test",
123
+ instructions: "Test",
124
+ model,
125
+ });
126
+
127
+ const kernl = new Kernl({ storage: { db: storage } });
128
+ kernl.register(agent);
129
+
130
+ await agent.run("Hello", { threadId: "non-existent" });
131
+
132
+ // Should have created new thread with the specified tid
133
+ const threads = await storage.threads.list();
134
+ expect(threads).toHaveLength(1);
135
+ expect(threads[0].tid).toBe("non-existent");
136
+ });
137
+
138
+ it("should resume existing thread from storage", async () => {
139
+ const storage = new InMemoryStorage();
140
+ const model = createMockModel(async () => ({
141
+ content: [message({ role: "assistant", text: "Response" })],
142
+ finishReason: "stop",
143
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
144
+ warnings: [],
145
+ }));
146
+
147
+ const agent = new Agent({
148
+ id: "test-agent",
149
+ name: "Test",
150
+ instructions: "Test",
151
+ model,
152
+ });
153
+
154
+ const kernl = new Kernl({ storage: { db: storage } });
155
+ kernl.register(agent);
156
+
157
+ const tid = "resume-thread";
158
+
159
+ // First run
160
+ await agent.run("First", { threadId: tid });
161
+
162
+ const firstHistory = await storage.threads.history(tid);
163
+ const firstEventCount = firstHistory.length;
164
+ expect(firstEventCount).toBeGreaterThanOrEqual(2); // user + assistant
165
+
166
+ // Second run (resume)
167
+ await agent.run("Second", { threadId: tid });
168
+
169
+ const secondHistory = await storage.threads.history(tid);
170
+ const secondEventCount = secondHistory.length;
171
+
172
+ // Should have more events (added user + assistant from second run)
173
+ expect(secondEventCount).toBeGreaterThanOrEqual(firstEventCount + 2);
174
+ });
175
+ });
176
+
177
+ describe("String vs array input", () => {
178
+ it("should handle string input (converted to message)", async () => {
179
+ const storage = new InMemoryStorage();
180
+ const model = createMockModel(async () => ({
181
+ content: [message({ role: "assistant", text: "Done" })],
182
+ finishReason: "stop",
183
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
184
+ warnings: [],
185
+ }));
186
+
187
+ const agent = new Agent({
188
+ id: "test-agent",
189
+ name: "Test",
190
+ instructions: "Test",
191
+ model,
192
+ });
193
+
194
+ const kernl = new Kernl({ storage: { db: storage } });
195
+ kernl.register(agent);
196
+
197
+ await agent.run("Hello world");
198
+
199
+ const threads = await storage.threads.list();
200
+ const events = await storage.threads.history(threads[0].tid);
201
+
202
+ // Find the user message (first event should be user message)
203
+ const userMessage = events.find((e: any) => e.role === "user");
204
+ expect(userMessage).toMatchObject({
205
+ kind: "message",
206
+ role: "user",
207
+ content: [{ kind: "text", text: "Hello world" }],
208
+ });
209
+ });
210
+
211
+ it("should handle array input (used as-is)", async () => {
212
+ const storage = new InMemoryStorage();
213
+ const model = createMockModel(async () => ({
214
+ content: [message({ role: "assistant", text: "Done" })],
215
+ finishReason: "stop",
216
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
217
+ warnings: [],
218
+ }));
219
+
220
+ const agent = new Agent({
221
+ id: "test-agent",
222
+ name: "Test",
223
+ instructions: "Test",
224
+ model,
225
+ });
226
+
227
+ const kernl = new Kernl({ storage: { db: storage } });
228
+ kernl.register(agent);
229
+
230
+ const input = [message({ role: "user", text: "Custom" })];
231
+ await agent.run(input);
232
+
233
+ const threads = await storage.threads.list();
234
+ const events = await storage.threads.history(threads[0].tid);
235
+
236
+ // Find the user message
237
+ const userMessage = events.find((e: any) => e.role === "user");
238
+ expect(userMessage).toMatchObject({
239
+ kind: "message",
240
+ role: "user",
241
+ content: [{ kind: "text", text: "Custom" }],
242
+ });
243
+ });
244
+ });
245
+ });
246
+
247
+ describe("Agent.stream() lifecycle", () => {
248
+ it("should yield stream-start event first", async () => {
249
+ const model = createMockModel(async () => ({
250
+ content: [message({ role: "assistant", text: "Done" })],
251
+ finishReason: "stop",
252
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
253
+ warnings: [],
254
+ }));
255
+
256
+ const agent = new Agent({
257
+ id: "test-agent",
258
+ name: "Test",
259
+ instructions: "Test",
260
+ model,
261
+ });
262
+
263
+ const kernl = new Kernl();
264
+ kernl.register(agent);
265
+
266
+ const events = [];
267
+ for await (const event of agent.stream("Hello")) {
268
+ events.push(event);
269
+ }
270
+
271
+ expect(events[0]).toEqual({ kind: "stream-start" });
272
+ });
273
+
274
+ it("should have same persistence behavior as run()", async () => {
275
+ const storage = new InMemoryStorage();
276
+ const model = createMockModel(async () => ({
277
+ content: [message({ role: "assistant", text: "Done" })],
278
+ finishReason: "stop",
279
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
280
+ warnings: [],
281
+ }));
282
+
283
+ const agent = new Agent({
284
+ id: "test-agent",
285
+ name: "Test",
286
+ instructions: "Test",
287
+ model,
288
+ });
289
+
290
+ const kernl = new Kernl({ storage: { db: storage } });
291
+ kernl.register(agent);
292
+
293
+ const events = [];
294
+ for await (const event of agent.stream("Hello")) {
295
+ events.push(event);
296
+ }
297
+
298
+ // Should have persisted like run()
299
+ const threads = await storage.threads.list();
300
+ expect(threads).toHaveLength(1);
301
+
302
+ const history = await storage.threads.history(threads[0].tid);
303
+ expect(history.length).toBeGreaterThan(0);
304
+
305
+ // Should have streamed events
306
+ expect(events).toEqual(
307
+ expect.arrayContaining([
308
+ { kind: "stream-start" },
309
+ expect.objectContaining({ kind: "message" }),
310
+ ]),
311
+ );
312
+ });
313
+ });
314
+
315
+ describe("Agent.threads helper", () => {
316
+ it("should throw MisconfiguredError when agent is not bound to kernl", () => {
317
+ const model = createMockModel(async () => ({
318
+ content: [message({ role: "assistant", text: "Done" })],
319
+ finishReason: "stop",
320
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
321
+ warnings: [],
322
+ }));
323
+
324
+ const agent = new Agent({
325
+ id: "test-agent",
326
+ name: "Test",
327
+ instructions: "Test",
328
+ model,
329
+ });
330
+
331
+ expect(() => agent.threads).toThrow(MisconfiguredError);
332
+ });
333
+
334
+ it("should list only threads for this agent", async () => {
335
+ const storage = new InMemoryStorage();
336
+ const model = createMockModel(async () => ({
337
+ content: [message({ role: "assistant", text: "Done" })],
338
+ finishReason: "stop",
339
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
340
+ warnings: [],
341
+ }));
342
+
343
+ const agentA = new Agent({
344
+ id: "agent-a",
345
+ name: "Agent A",
346
+ instructions: "Test",
347
+ model,
348
+ });
349
+
350
+ const agentB = new Agent({
351
+ id: "agent-b",
352
+ name: "Agent B",
353
+ instructions: "Test",
354
+ model,
355
+ });
356
+
357
+ const kernl = new Kernl({ storage: { db: storage } });
358
+ kernl.register(agentA);
359
+ kernl.register(agentB);
360
+
361
+ await agentA.run("Hello from A");
362
+ await agentB.run("Hello from B");
363
+ await agentB.run("Another from B");
364
+
365
+ const threadsAPage = await agentA.threads.list();
366
+ const threadsBPage = await agentB.threads.list();
367
+
368
+ const threadsA = await threadsAPage.collect();
369
+ const threadsB = await threadsBPage.collect();
370
+
371
+ expect(threadsA).toHaveLength(1);
372
+ expect(threadsB.length).toBeGreaterThanOrEqual(2);
373
+
374
+ for (const thread of threadsA) {
375
+ expect(thread.agentId).toBe("agent-a");
376
+ }
377
+
378
+ for (const thread of threadsB) {
379
+ expect(thread.agentId).toBe("agent-b");
380
+ }
381
+ });
382
+
383
+ it("should expose thread history via threads.history()", async () => {
384
+ const storage = new InMemoryStorage();
385
+ const model = createMockModel(async () => ({
386
+ content: [message({ role: "assistant", text: "Done" })],
387
+ finishReason: "stop",
388
+ usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
389
+ warnings: [],
390
+ }));
391
+
392
+ const agent = new Agent({
393
+ id: "test-agent",
394
+ name: "Test",
395
+ instructions: "Test",
396
+ model,
397
+ });
398
+
399
+ const kernl = new Kernl({ storage: { db: storage } });
400
+ kernl.register(agent);
401
+
402
+ await agent.run("Hello");
403
+
404
+ const threadsPage = await agent.threads.list();
405
+ const threads = await threadsPage.collect();
406
+ expect(threads).toHaveLength(1);
407
+
408
+ const tid = threads[0].tid;
409
+ const events = await agent.threads.history(tid, { order: "asc" });
410
+
411
+ // Expect exactly two events: user message then assistant message
412
+ expect(events).toHaveLength(2);
413
+
414
+ const [userEvent, assistantEvent] = events;
415
+
416
+ // Common headers
417
+ expect(userEvent.tid).toBe(tid);
418
+ expect(assistantEvent.tid).toBe(tid);
419
+ expect(userEvent.seq).toBe(0);
420
+ expect(assistantEvent.seq).toBe(1);
421
+
422
+ // User message
423
+ expect(userEvent.kind).toBe("message");
424
+ // @ts-expect-error ThreadEvent extends LanguageModelItem at runtime
425
+ expect(userEvent.role).toBe("user");
426
+ // @ts-expect-error ThreadEvent extends LanguageModelItem at runtime
427
+ expect(userEvent.content).toEqual([
428
+ { kind: "text", text: "Hello" },
429
+ ]);
430
+
431
+ // Assistant message
432
+ expect(assistantEvent.kind).toBe("message");
433
+ // @ts-expect-error ThreadEvent extends LanguageModelItem at runtime
434
+ expect(assistantEvent.role).toBe("assistant");
435
+ // @ts-expect-error ThreadEvent extends LanguageModelItem at runtime
436
+ expect(assistantEvent.content).toEqual([
437
+ { kind: "text", text: "Done" },
438
+ ]);
439
+ });
440
+ });
441
+
File without changes