kernl 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/.turbo/turbo-build.log +4 -5
  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 +32 -9
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js +102 -14
  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/index.d.ts +9 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +7 -0
  58. package/dist/internal.d.ts +4 -0
  59. package/dist/internal.d.ts.map +1 -0
  60. package/dist/internal.js +2 -0
  61. package/dist/kernl/index.d.ts +3 -0
  62. package/dist/kernl/index.d.ts.map +1 -0
  63. package/dist/kernl/index.js +2 -0
  64. package/dist/kernl/kernl.d.ts +64 -0
  65. package/dist/kernl/kernl.d.ts.map +1 -0
  66. package/dist/kernl/kernl.js +116 -0
  67. package/dist/kernl/threads.d.ts +110 -0
  68. package/dist/kernl/threads.d.ts.map +1 -0
  69. package/dist/kernl/threads.js +126 -0
  70. package/dist/kernl.d.ts +22 -6
  71. package/dist/kernl.d.ts.map +1 -1
  72. package/dist/kernl.js +73 -10
  73. package/dist/lib/env.d.ts +3 -3
  74. package/dist/lib/env.js +1 -1
  75. package/dist/mcp/__tests__/integration.test.js +8 -8
  76. package/dist/mcp/__tests__/utils.test.js +6 -6
  77. package/dist/mcp/http.d.ts +1 -1
  78. package/dist/mcp/http.d.ts.map +1 -1
  79. package/dist/mcp/http.js +9 -9
  80. package/dist/mcp/sse.d.ts +1 -1
  81. package/dist/mcp/sse.d.ts.map +1 -1
  82. package/dist/mcp/sse.js +7 -7
  83. package/dist/mcp/utils.d.ts +1 -1
  84. package/dist/mcp/utils.d.ts.map +1 -1
  85. package/dist/mcp/utils.js +4 -5
  86. package/dist/storage/__tests__/in-memory.test.d.ts +2 -0
  87. package/dist/storage/__tests__/in-memory.test.d.ts.map +1 -0
  88. package/dist/storage/__tests__/in-memory.test.js +455 -0
  89. package/dist/storage/base.d.ts +64 -0
  90. package/dist/storage/base.d.ts.map +1 -0
  91. package/dist/storage/base.js +4 -0
  92. package/dist/storage/in-memory.d.ts +62 -0
  93. package/dist/storage/in-memory.d.ts.map +1 -0
  94. package/dist/storage/in-memory.js +283 -0
  95. package/dist/storage/index.d.ts +10 -0
  96. package/dist/storage/index.d.ts.map +1 -0
  97. package/dist/storage/index.js +7 -0
  98. package/dist/storage/thread.d.ts +123 -0
  99. package/dist/storage/thread.d.ts.map +1 -0
  100. package/dist/storage/thread.js +4 -0
  101. package/dist/task.d.ts +5 -3
  102. package/dist/task.d.ts.map +1 -1
  103. package/dist/task.js +10 -8
  104. package/dist/thread/__tests__/fixtures/mock-model.d.ts +1 -2
  105. package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -1
  106. package/dist/thread/__tests__/integration.test.js +73 -5
  107. package/dist/thread/__tests__/namespace.test.d.ts +2 -0
  108. package/dist/thread/__tests__/namespace.test.d.ts.map +1 -0
  109. package/dist/thread/__tests__/namespace.test.js +131 -0
  110. package/dist/thread/__tests__/thread-persistence.test.d.ts +2 -0
  111. package/dist/thread/__tests__/thread-persistence.test.d.ts.map +1 -0
  112. package/dist/thread/__tests__/thread-persistence.test.js +351 -0
  113. package/dist/thread/__tests__/thread.test.js +49 -51
  114. package/dist/thread/thread.d.ts +70 -18
  115. package/dist/thread/thread.d.ts.map +1 -1
  116. package/dist/thread/thread.js +211 -73
  117. package/dist/thread/utils.d.ts +36 -8
  118. package/dist/thread/utils.d.ts.map +1 -1
  119. package/dist/thread/utils.js +52 -8
  120. package/dist/tool/__tests__/fixtures.js +1 -1
  121. package/dist/tool/__tests__/toolkit.test.js +15 -12
  122. package/dist/tool/tool.js +3 -3
  123. package/dist/types/kernl.d.ts +42 -0
  124. package/dist/types/kernl.d.ts.map +1 -0
  125. package/dist/types/thread.d.ts +108 -22
  126. package/dist/types/thread.d.ts.map +1 -1
  127. package/dist/types/thread.js +12 -0
  128. package/package.json +11 -7
  129. package/src/agent/__tests__/concurrency.test.ts +194 -0
  130. package/src/agent/__tests__/run.test.ts +441 -0
  131. package/src/agent/index.ts +0 -0
  132. package/src/agent.ts +141 -24
  133. package/src/api/__tests__/cursor-page.test.ts +512 -0
  134. package/src/api/__tests__/offset-page.test.ts +624 -0
  135. package/src/api/__tests__/threads.test.ts +415 -0
  136. package/src/api/models/index.ts +6 -0
  137. package/src/api/models/thread.ts +138 -0
  138. package/src/api/pagination/base.ts +79 -0
  139. package/src/api/pagination/cursor.ts +86 -0
  140. package/src/api/pagination/offset.ts +89 -0
  141. package/src/api/resources/threads/events.ts +26 -0
  142. package/src/api/resources/threads/index.ts +9 -0
  143. package/src/api/resources/threads/threads.ts +256 -0
  144. package/src/api/resources/threads/types.ts +143 -0
  145. package/src/api/resources/threads/utils.ts +104 -0
  146. package/src/context.ts +10 -1
  147. package/src/index.ts +49 -1
  148. package/src/internal.ts +15 -0
  149. package/src/kernl.ts +86 -17
  150. package/src/mcp/__tests__/integration.test.ts +8 -9
  151. package/src/mcp/__tests__/utils.test.ts +6 -6
  152. package/src/mcp/http.ts +9 -9
  153. package/src/mcp/sse.ts +7 -7
  154. package/src/mcp/utils.ts +6 -5
  155. package/src/storage/__tests__/in-memory.test.ts +534 -0
  156. package/src/storage/base.ts +77 -0
  157. package/src/storage/in-memory.ts +372 -0
  158. package/src/storage/index.ts +21 -0
  159. package/src/storage/thread.ts +141 -0
  160. package/src/task.ts +12 -10
  161. package/src/thread/__tests__/fixtures/mock-model.ts +2 -4
  162. package/src/thread/__tests__/integration.test.ts +111 -10
  163. package/src/thread/__tests__/namespace.test.ts +158 -0
  164. package/src/thread/__tests__/thread-persistence.test.ts +367 -0
  165. package/src/thread/__tests__/thread.test.ts +52 -54
  166. package/src/thread/thread.ts +247 -96
  167. package/src/thread/utils.ts +76 -13
  168. package/src/tool/__tests__/fixtures.ts +1 -1
  169. package/src/tool/__tests__/toolkit.test.ts +15 -12
  170. package/src/tool/tool.ts +3 -3
  171. package/src/types/kernl.ts +51 -0
  172. package/src/types/thread.ts +139 -25
  173. package/vitest.config.ts +1 -0
  174. package/dist/env.d.ts +0 -45
  175. package/dist/env.d.ts.map +0 -1
  176. package/dist/env.js +0 -31
  177. package/dist/error.d.ts +0 -1
  178. package/dist/error.d.ts.map +0 -1
  179. package/dist/kernel.d.ts +0 -7
  180. package/dist/kernel.d.ts.map +0 -1
  181. package/dist/kernel.js +0 -7
  182. package/dist/lib/serde/__tests__/codec.test.d.ts +0 -2
  183. package/dist/lib/serde/__tests__/codec.test.d.ts.map +0 -1
  184. package/dist/lib/serde/__tests__/codec.test.js +0 -75
  185. package/dist/lib/serde/codec.d.ts +0 -12
  186. package/dist/lib/serde/codec.d.ts.map +0 -1
  187. package/dist/lib/serde/codec.js +0 -54
  188. package/dist/lib/serde/thread.d.ts +0 -1
  189. package/dist/lib/serde/thread.d.ts.map +0 -1
  190. package/dist/lib/serde/thread.js +0 -172
  191. package/dist/lib/serde/tool.d.ts +0 -36
  192. package/dist/lib/serde/tool.d.ts.map +0 -1
  193. package/dist/lib/utils.d.ts +0 -19
  194. package/dist/lib/utils.d.ts.map +0 -1
  195. package/dist/lib/utils.js +0 -41
  196. package/dist/logger.d.ts +0 -36
  197. package/dist/logger.d.ts.map +0 -1
  198. package/dist/logger.js +0 -43
  199. package/dist/mcp/__tests__/fixtures/echo-server.d.ts +0 -3
  200. package/dist/mcp/__tests__/fixtures/echo-server.d.ts.map +0 -1
  201. package/dist/mcp/__tests__/fixtures/echo-server.js +0 -92
  202. package/dist/mcp/__tests__/fixtures/math-server.d.ts +0 -3
  203. package/dist/mcp/__tests__/fixtures/math-server.d.ts.map +0 -1
  204. package/dist/mcp/__tests__/fixtures/math-server.js +0 -98
  205. package/dist/mcp/__tests__/fixtures/test-server.d.ts +0 -3
  206. package/dist/mcp/__tests__/fixtures/test-server.d.ts.map +0 -1
  207. package/dist/mcp/__tests__/fixtures/test-server.js +0 -163
  208. package/dist/mcp/__tests__/test-utils.d.ts +0 -17
  209. package/dist/mcp/__tests__/test-utils.d.ts.map +0 -1
  210. package/dist/mcp/__tests__/test-utils.js +0 -42
  211. package/dist/mcp/node.d.ts +0 -60
  212. package/dist/mcp/node.d.ts.map +0 -1
  213. package/dist/mcp/node.js +0 -297
  214. package/dist/model.d.ts +0 -175
  215. package/dist/model.d.ts.map +0 -1
  216. package/dist/providers/ai.d.ts +0 -1
  217. package/dist/providers/ai.d.ts.map +0 -1
  218. package/dist/providers/ai.js +0 -1
  219. package/dist/providers/default.d.ts +0 -16
  220. package/dist/providers/default.d.ts.map +0 -1
  221. package/dist/providers/default.js +0 -17
  222. package/dist/providers/registry.d.ts +0 -1
  223. package/dist/providers/registry.d.ts.map +0 -1
  224. package/dist/providers/registry.js +0 -1
  225. package/dist/sched/scheduler.d.ts +0 -20
  226. package/dist/sched/scheduler.d.ts.map +0 -1
  227. package/dist/sched/task.d.ts +0 -92
  228. package/dist/sched/task.d.ts.map +0 -1
  229. package/dist/sched/task.js +0 -102
  230. package/dist/serde/__tests__/codec.test.d.ts +0 -2
  231. package/dist/serde/__tests__/codec.test.d.ts.map +0 -1
  232. package/dist/serde/__tests__/codec.test.js +0 -75
  233. package/dist/serde/codec.d.ts +0 -12
  234. package/dist/serde/codec.d.ts.map +0 -1
  235. package/dist/serde/codec.js +0 -54
  236. package/dist/serde/json.d.ts +0 -8
  237. package/dist/serde/json.d.ts.map +0 -1
  238. package/dist/serde/json.js +0 -13
  239. package/dist/serde/thread.d.ts +0 -687
  240. package/dist/serde/thread.d.ts.map +0 -1
  241. package/dist/serde/thread.js +0 -158
  242. package/dist/serde/tool.d.ts +0 -36
  243. package/dist/serde/tool.d.ts.map +0 -1
  244. package/dist/session.d.ts +0 -1
  245. package/dist/session.d.ts.map +0 -1
  246. package/dist/session.js +0 -1
  247. package/dist/thread/__tests__/stream.test.d.ts +0 -2
  248. package/dist/thread/__tests__/stream.test.d.ts.map +0 -1
  249. package/dist/thread/__tests__/stream.test.js +0 -244
  250. package/dist/tool/mcp.d.ts +0 -75
  251. package/dist/tool/mcp.d.ts.map +0 -1
  252. package/dist/tool/mcp.js +0 -111
  253. package/dist/tools.d.ts +0 -362
  254. package/dist/tools.d.ts.map +0 -1
  255. package/dist/tools.js +0 -220
  256. package/dist/types/proto.d.ts +0 -1551
  257. package/dist/types/proto.d.ts.map +0 -1
  258. package/dist/types/proto.js +0 -531
  259. package/dist/usage.d.ts +0 -43
  260. package/dist/usage.d.ts.map +0 -1
  261. package/dist/usage.js +0 -61
  262. package/src/lib/serde/thread.ts +0 -188
  263. /package/dist/{error.js → agent/index.js} +0 -0
  264. /package/dist/{lib/serde/tool.js → api/models/index.js} +0 -0
  265. /package/dist/{model.js → api/models/thread.js} +0 -0
  266. /package/dist/{sched/scheduler.js → api/resources/threads/types.js} +0 -0
  267. /package/dist/{serde/tool.js → types/kernl.js} +0 -0
@@ -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
package/src/agent.ts CHANGED
@@ -1,27 +1,37 @@
1
1
  import {
2
+ message,
2
3
  LanguageModel,
4
+ LanguageModelItem,
3
5
  LanguageModelRequestSettings,
4
- message,
5
6
  } from "@kernl-sdk/protocol";
6
7
 
8
+ import { Thread } from "./thread";
9
+ import type { Kernl } from "./kernl";
10
+ import type {
11
+ RThreadsListParams,
12
+ RThreadCreateParams,
13
+ RThreadGetOptions,
14
+ RThreadHistoryParams,
15
+ RThreadUpdateParams,
16
+ } from "@/api/resources/threads/types";
7
17
  import type { Context, UnknownContext } from "./context";
8
- import { InputGuardrail, OutputGuardrail } from "./guardrail";
9
- import { AgentHooks } from "./lifecycle";
10
- import { BaseToolkit } from "./tool/toolkit";
11
18
  import { Tool } from "./tool";
12
- import { Thread } from "./thread";
13
-
14
- import { MisconfiguredError } from "./lib/error";
19
+ import { BaseToolkit } from "./tool/toolkit";
20
+ import {
21
+ InputGuardrail,
22
+ OutputGuardrail,
23
+ type ResolvedAgentResponse,
24
+ } from "./guardrail";
25
+ import { AgentHooks } from "./lifecycle";
15
26
 
27
+ import { MisconfiguredError, RuntimeError } from "./lib/error";
16
28
  import type { AgentConfig, AgentResponseType } from "./types/agent";
17
29
  import type {
18
30
  TextResponse,
19
- ThreadOptions,
31
+ ThreadExecuteOptions,
20
32
  ThreadExecuteResult,
21
33
  ThreadStreamEvent,
22
34
  } from "./types/thread";
23
- import type { Kernl } from "./kernl";
24
- import type { ResolvedAgentResponse } from "./guardrail";
25
35
 
26
36
  export class Agent<
27
37
  TContext = UnknownContext,
@@ -39,6 +49,8 @@ export class Agent<
39
49
  model: LanguageModel;
40
50
  modelSettings: LanguageModelRequestSettings;
41
51
  toolkits: BaseToolkit<TContext>[];
52
+ // actions: ActionSet; /* TODO */
53
+
42
54
  guardrails: {
43
55
  input: InputGuardrail[];
44
56
  output: OutputGuardrail<AgentResponseType>[];
@@ -111,10 +123,13 @@ export class Agent<
111
123
 
112
124
  /**
113
125
  * Blocking execution - spawns or resumes thread and waits for completion
126
+ *
127
+ * @throws {RuntimeError} If the specified thread is already running (concurrent execution not allowed)
128
+ * @throws {MisconfiguredError} If the agent is not bound to a kernl instance
114
129
  */
115
130
  async run(
116
- instructions: string,
117
- options?: ThreadOptions<TContext>,
131
+ input: string | LanguageModelItem[],
132
+ options?: ThreadExecuteOptions<TContext>,
118
133
  ): Promise<ThreadExecuteResult<ResolvedAgentResponse<TResponse>>> {
119
134
  if (!this.kernl) {
120
135
  throw new MisconfiguredError(
@@ -122,17 +137,46 @@ export class Agent<
122
137
  );
123
138
  }
124
139
 
125
- const m = message({ role: "user", text: instructions });
140
+ const items =
141
+ typeof input === "string"
142
+ ? [message({ role: "user", text: input })]
143
+ : input;
126
144
  const tid = options?.threadId;
127
145
 
128
- // NOTE: may end up moving this to the kernl
129
- let thread = tid ? this.kernl.threads.get(tid) : null;
146
+ let thread: Thread<TContext, TResponse> | null = null;
147
+
148
+ if (tid) {
149
+ // no concurrent execution of same thread - correctness contract
150
+ // TODO: race condition - need to check again after async storage.get()
151
+ if (this.kernl.athreads.has(tid)) {
152
+ throw new RuntimeError(`Thread ${tid} is already running.`);
153
+ }
154
+
155
+ // try to resume from storage if available
156
+ if (this.kernl.storage?.threads) {
157
+ thread = (await this.kernl.storage.threads.get(tid, {
158
+ history: true,
159
+ })) as Thread<TContext, TResponse> | null;
160
+ }
161
+ }
162
+
163
+ // create new thread if not found in storage or no tid provided
130
164
  if (!thread) {
131
- thread = new Thread(this.kernl, this, [m], options);
165
+ thread = new Thread({
166
+ agent: this,
167
+ input: items,
168
+ context: options?.context,
169
+ model: options?.model,
170
+ task: options?.task,
171
+ tid: options?.threadId,
172
+ namespace: options?.namespace,
173
+ storage: this.kernl.storage?.threads,
174
+ });
132
175
  return this.kernl.spawn(thread);
133
176
  }
134
177
 
135
- thread.append(m);
178
+ // resume existing thread from storage
179
+ thread.append(...items);
136
180
  return this.kernl.schedule(thread);
137
181
  }
138
182
 
@@ -140,10 +184,13 @@ export class Agent<
140
184
  * Streaming execution - spawns or resumes thread and returns async iterator
141
185
  *
142
186
  * NOTE: streaming probably won't make sense in scheduling contexts so spawnStream etc. won't make sense
187
+ *
188
+ * @throws {RuntimeError} If the specified thread is already running (concurrent execution not allowed)
189
+ * @throws {MisconfiguredError} If the agent is not bound to a kernl instance
143
190
  */
144
191
  async *stream(
145
- instructions: string,
146
- options?: ThreadOptions<TContext>,
192
+ input: string | LanguageModelItem[],
193
+ options?: ThreadExecuteOptions<TContext>,
147
194
  ): AsyncIterable<ThreadStreamEvent> {
148
195
  if (!this.kernl) {
149
196
  throw new MisconfiguredError(
@@ -151,22 +198,53 @@ export class Agent<
151
198
  );
152
199
  }
153
200
 
154
- const m = message({ role: "user", text: instructions });
201
+ const items =
202
+ typeof input === "string"
203
+ ? [message({ role: "user", text: input })]
204
+ : input;
155
205
  const tid = options?.threadId;
156
206
 
157
- // NOTE: may end up moving this to the kernl
158
- let thread = tid ? this.kernl.threads.get(tid) : null;
207
+ let thread: Thread<TContext, TResponse> | null = null;
208
+
209
+ if (tid) {
210
+ // no concurrent execution of same thread - correctness contract
211
+ // TODO: race condition - need to check again after async storage.get()
212
+ if (this.kernl.athreads.has(tid)) {
213
+ throw new RuntimeError(`Thread ${tid} is already running.`);
214
+ }
215
+
216
+ // try to resume from storage if available
217
+ if (this.kernl.storage?.threads) {
218
+ thread = (await this.kernl.storage.threads.get(tid, {
219
+ history: true,
220
+ })) as Thread<TContext, TResponse> | null;
221
+ }
222
+ }
223
+
224
+ // create new thread if not found in storage or no tid provided
159
225
  if (!thread) {
160
- thread = new Thread(this.kernl, this, [m], options);
226
+ thread = new Thread({
227
+ agent: this,
228
+ input: items,
229
+ context: options?.context,
230
+ model: options?.model,
231
+ task: options?.task,
232
+ tid: options?.threadId,
233
+ namespace: options?.namespace,
234
+ storage: this.kernl.storage?.threads,
235
+ });
161
236
  yield* this.kernl.spawnStream(thread);
162
237
  return;
163
238
  }
164
239
 
165
- thread.append(m);
240
+ // resume existing thread from storage
241
+ thread.append(...items);
166
242
  yield* this.kernl.scheduleStream(thread);
167
243
  }
168
244
 
169
245
  /**
246
+ * @internal
247
+ *
170
248
  * Get a specific tool by ID from all toolkits.
171
249
  *
172
250
  * @param id The tool ID to look up
@@ -181,6 +259,8 @@ export class Agent<
181
259
  }
182
260
 
183
261
  /**
262
+ * @internal
263
+ *
184
264
  * Get all tools available from all toolkits for the given context.
185
265
  * Checks for duplicate tool IDs across toolkits and throws an error if found.
186
266
  *
@@ -211,4 +291,41 @@ export class Agent<
211
291
 
212
292
  return allTools;
213
293
  }
294
+
295
+ /**
296
+ * Thread management scoped to this agent.
297
+ *
298
+ * Convenience wrapper around kernl.threads that automatically filters to this agent's threads.
299
+ */
300
+ get threads() {
301
+ if (!this.kernl) {
302
+ throw new MisconfiguredError(
303
+ `Agent ${this.id} not bound to kernl. Call kernl.register(agent) first.`,
304
+ );
305
+ }
306
+
307
+ const agentId = this.id;
308
+ const kthreads = this.kernl.threads;
309
+
310
+ return {
311
+ get: (tid: string, options?: RThreadGetOptions) =>
312
+ kthreads.get(tid, options),
313
+ list: (params: Omit<RThreadsListParams, "agentId"> = {}) =>
314
+ kthreads.list({ ...params, agentId }),
315
+ delete: (tid: string) => kthreads.delete(tid),
316
+ history: (tid: string, params?: RThreadHistoryParams) =>
317
+ kthreads.history(tid, params),
318
+ create: (params: Omit<RThreadCreateParams, "agentId" | "model">) =>
319
+ kthreads.create({
320
+ ...params,
321
+ agentId,
322
+ model: {
323
+ provider: this.model.provider,
324
+ modelId: this.model.modelId,
325
+ },
326
+ }),
327
+ update: (tid: string, patch: RThreadUpdateParams) =>
328
+ kthreads.update(tid, patch),
329
+ };
330
+ }
214
331
  }