kernl 0.2.1 → 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 +1 -1
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/CHANGELOG.md +138 -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 +101 -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 +6 -6
  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 +139 -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 +13 -12
  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
@@ -1,48 +1,111 @@
1
1
  import assert from "assert";
2
2
  import { Context } from "../context";
3
- import { FAILED, RUNNING, STOPPED, } from "@kernl-sdk/protocol";
3
+ import { logger } from "../lib/logger";
4
+ import { FAILED, RUNNING, STOPPED, message, } from "@kernl-sdk/protocol";
4
5
  import { randomID, filter } from "@kernl-sdk/shared/lib";
5
- import { notDelta, getFinalResponse, getIntentions, parseFinalResponse, } from "./utils";
6
+ import { tevent, notDelta, getIntentions, getFinalResponse, parseFinalResponse, } from "./utils";
6
7
  /**
7
8
  * A thread drives the execution loop for an agent.
9
+ *
10
+ * Ground principles:
11
+ *
12
+ * 1) Event log is source of truth.
13
+ * - Persistent storage (e.g. Postgres) is treated as an append-only per-thread log of `ThreadEvent`s:
14
+ * monotonic `seq`, no gaps, no updates/deletes.
15
+ * - `Thread.state`, `tick`, etc. are projections of that log, not an alternative source of truth.
16
+ *
17
+ * 2) Single writer per thread.
18
+ * - At most one executor is allowed for a given `tid` at a time.
19
+ * - Callers are responsible for enforcing this (e.g. locking/versioning) so two processes cannot
20
+ * interleave or race on `seq` or state.
21
+ *
22
+ * 3) Persist before use / observation.
23
+ * - Before an event can:
24
+ * - influence a future tick (i.e. be part of `history` fed back into the model), or
25
+ * - be considered “delivered” to a client,
26
+ * it SHOULD be durably written to storage when storage is configured.
27
+ *
28
+ * 4) Transaction boundaries match semantic steps.
29
+ * - The intended strategy is to buffer within a tick, then atomically persist all new events + state
30
+ * at the end of `tick()`.
31
+ * - After a crash, you only ever see whole ticks or none, never half a tick, from the store’s
32
+ * point of view.
33
+ *
34
+ * 5) Recovery is replay.
35
+ * - On restart, callers rebuild a `Thread` from the stored event log (plus optional snapshots).
36
+ * - Any incomplete tick or pending tool call is handled by a clear, deterministic policy at a
37
+ * higher layer (e.g. re-run, mark failed, or require manual intervention).
38
+ *
39
+ * On storage failures:
40
+ *
41
+ * “If storage is configured, it is authoritative” → fail hard on persist errors rather than
42
+ * treating persistence as best-effort.
43
+ *
44
+ * If a storage implementation is present, `persist(...)` is expected to throw on failure, and
45
+ * that error should bubble out of `_execute()` / `stream()` and stop the thread.
8
46
  */
9
47
  export class Thread {
10
- kernl;
11
- id;
48
+ tid;
49
+ namespace;
12
50
  agent;
13
51
  context;
14
52
  model; /* inherited from the agent unless specified */
15
53
  parent; /* parent task which spawned this thread */
16
- mode; /* TODO */
17
- input; /* the initial input for the thread */
54
+ createdAt;
55
+ updatedAt;
56
+ metadata;
18
57
  // readonly stats: ThreadMetrics;
19
58
  /* state */
20
- _tick;
59
+ _tick; /* number of LLM roundtrips */
60
+ _seq; /* monotonic event sequence */
21
61
  state;
62
+ cpbuf; /* checkpoint buffer - events pending persistence */
63
+ persisted; /* indicates thread was hydrated from storage */
22
64
  history;
23
65
  abort;
24
- constructor(kernl, agent, input, options) {
25
- this.id = `tid_${randomID()}`;
26
- this.agent = agent;
27
- this.context = options?.context ?? new Context();
28
- this.kernl = kernl;
29
- this.parent = options?.task ?? null;
30
- this.model = options?.model ?? agent.model;
31
- this.mode = "blocking"; // (TODO): add streaming
32
- this.input = input;
33
- this._tick = 0;
34
- this.state = STOPPED;
35
- this.history = input;
66
+ storage;
67
+ constructor(options) {
68
+ this.tid = options.tid ?? `tid_${randomID()}`;
69
+ this.namespace = options.namespace ?? "kernl";
70
+ this.agent = options.agent;
71
+ this.context =
72
+ options.context ?? new Context(this.namespace, {});
73
+ this.parent = options.task ?? null;
74
+ this.model = options.model ?? options.agent.model;
75
+ this.storage = options.storage;
76
+ this.createdAt = options.createdAt ?? new Date();
77
+ this.updatedAt = options.updatedAt ?? new Date();
78
+ this.metadata = options.metadata ?? null;
79
+ this._tick = options.tick ?? 0;
80
+ this._seq = -1;
81
+ this.state = options.state ?? STOPPED;
82
+ this.cpbuf = [];
83
+ this.persisted = options.persisted ?? false;
84
+ this.history = options.history ?? [];
85
+ // seek to latest seq (not persisted)
86
+ if (this.history.length > 0) {
87
+ this._seq = Math.max(...this.history.map((e) => e.seq));
88
+ }
89
+ // append initial input if provided (for new threads)
90
+ if (options.input && options.input.length > 0) {
91
+ this.append(...options.input);
92
+ }
36
93
  }
37
94
  /**
38
- * Blocking execution loop - runs until terminal state or interruption
95
+ * Blocking execution - runs until terminal state or interruption
39
96
  */
40
97
  async execute() {
41
98
  for await (const _event of this.stream()) {
42
99
  // just consume the stream (already in history in _execute())
43
100
  }
44
- // extract final response from accumulated history
45
- const text = getFinalResponse(this.history);
101
+ // filter for language model items
102
+ const items = this.history
103
+ .filter((e) => e.kind !== "system")
104
+ .map((e) => {
105
+ const { tid, seq, timestamp, metadata, ...item } = e;
106
+ return item;
107
+ });
108
+ const text = getFinalResponse(items);
46
109
  assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
47
110
  const parsed = parseFinalResponse(text, this.agent.responseType);
48
111
  return { response: parsed, state: this.state };
@@ -56,6 +119,8 @@ export class Thread {
56
119
  }
57
120
  this.state = RUNNING;
58
121
  this.abort = new AbortController();
122
+ await this.checkpoint(); /* c1: persist RUNNING state + initial input */
123
+ yield { kind: "stream-start" }; // always yield start immediately
59
124
  try {
60
125
  yield* this._execute();
61
126
  }
@@ -65,57 +130,57 @@ export class Thread {
65
130
  finally {
66
131
  this.state = STOPPED;
67
132
  this.abort = undefined;
133
+ await this.checkpoint(); /* c4: final checkpoint - persist STOPPED state */
68
134
  }
69
135
  }
70
136
  /**
71
- * Cancel the running thread
72
- */
73
- cancel() {
74
- this.abort?.abort();
75
- }
76
- /**
77
- * Append a new event to the thread history
78
- */
79
- append(event) {
80
- this.history.push(event);
81
- }
82
- /**
83
- * Main execution loop - always yields events, callers can propagate or discard (as in execute())
137
+ * Main execution loop - always yields events, callers can propagate or discard.
84
138
  *
85
139
  * NOTE: Streaming structured output deferred for now. Prioritizing correctness + simplicity,
86
140
  * and unclear what use cases there would actually be for streaming a structured output (other than maybe gen UI).
87
141
  */
88
142
  async *_execute() {
89
143
  for (;;) {
144
+ let err = false;
90
145
  if (this.abort?.signal.aborted) {
91
146
  return;
92
147
  }
93
148
  const events = [];
94
149
  for await (const e of this.tick()) {
150
+ if (e.kind === "error") {
151
+ err = true;
152
+ logger.error(e.error); // (TODO): onError callback in options
153
+ }
95
154
  // we don't want deltas in the history
96
155
  if (notDelta(e)) {
97
156
  events.push(e);
98
- this.history.push(e);
157
+ this.append(e);
99
158
  }
100
159
  yield e;
101
160
  }
102
- // if model returns a message with no action intentions -> terminal state
161
+ // if an error event occurred terminate
162
+ if (err) {
163
+ return;
164
+ }
165
+ // if model returns a message with no action intentions → terminal state
103
166
  const intentions = getIntentions(events);
104
167
  if (!intentions) {
105
168
  const text = getFinalResponse(events);
106
169
  if (!text)
107
170
  continue; // run again, policy-dependent? (how to ensure no infinite loop here?)
171
+ await this.checkpoint(); /* c2: terminal tick - no tool calls */
108
172
  // await this.agent.runOutputGuardails(context, state);
109
173
  // this.kernl.emit("thread.terminated", context, output);
110
174
  return;
111
175
  }
112
- // perform actions intended by the model
176
+ // perform intended actions
113
177
  const { actions, pendingApprovals } = await this.performActions(intentions);
114
- // yield action events
178
+ // append + yield action events
115
179
  for (const a of actions) {
116
- this.history.push(a);
180
+ this.append(a);
117
181
  yield a;
118
182
  }
183
+ await this.checkpoint(); /* c3: tick complete */
119
184
  if (pendingApprovals.length > 0) {
120
185
  // publish a batch approval request containing all of them
121
186
  //
@@ -138,22 +203,100 @@ export class Thread {
138
203
  // (TODO): check limits (if this._tick > this.limits.maxTicks)
139
204
  // (TODO): run input guardrails on first tick (if this._tick === 1)
140
205
  const req = await this.prepareModelRequest(this.history);
141
- // try to stream if model supports it
142
- if (this.model.stream) {
143
- const stream = this.model.stream(req);
144
- for await (const event of stream) {
145
- yield event; // [text-delta, tool-call, message, reasoning, ...]
206
+ try {
207
+ if (this.model.stream) {
208
+ const stream = this.model.stream(req);
209
+ for await (const event of stream) {
210
+ yield event; // [text-delta, tool-call, message, reasoning, ...]
211
+ }
146
212
  }
147
- }
148
- else {
149
- // fallback: blocking generate, yield events as batch
150
- const res = await this.model.generate(req);
151
- for (const event of res.content) {
152
- yield event;
213
+ else {
214
+ // fallback: blocking generate, yield events as batch
215
+ const res = await this.model.generate(req);
216
+ for (const event of res.content) {
217
+ yield event;
218
+ }
219
+ // (TODO): this.stats.usage.add(res.usage)
153
220
  }
154
- // (TODO): track usage (this.stats.usage.add(res.usage))
221
+ }
222
+ catch (error) {
223
+ yield {
224
+ kind: "error",
225
+ error: error instanceof Error ? error : new Error(String(error)),
226
+ };
155
227
  }
156
228
  }
229
+ /**
230
+ * Persist current thread state to storage.
231
+ *
232
+ * - If storage is configured, it is authoritative - failures throw and halt execution.
233
+ * - No-op if storage is not configured.
234
+ */
235
+ async checkpoint() {
236
+ if (!this.storage) {
237
+ logger.warn("thread: storage is not configured, thread will not be persisted");
238
+ return;
239
+ }
240
+ // insert thread record on first persist for new threads
241
+ if (!this.persisted) {
242
+ await this.storage.insert({
243
+ id: this.tid,
244
+ namespace: this.namespace,
245
+ agentId: this.agent.id,
246
+ parentTaskId: this.parent?.id ?? null,
247
+ model: `${this.model.provider}/${this.model.modelId}`,
248
+ context: this.context.context,
249
+ tick: this._tick,
250
+ state: this.state,
251
+ metadata: this.metadata,
252
+ });
253
+ this.persisted = true;
254
+ }
255
+ // append + drain events from checkpoint buffer
256
+ if (this.cpbuf.length > 0) {
257
+ await this.storage.append(this.cpbuf);
258
+ this.cpbuf = [];
259
+ }
260
+ // update thread state
261
+ await this.storage.update(this.tid, {
262
+ state: this.state,
263
+ tick: this._tick,
264
+ context: this.context,
265
+ metadata: this.metadata,
266
+ });
267
+ }
268
+ /**
269
+ * Append one or more items to history + enrich w/ runtime headers.
270
+ *
271
+ * Core rule:
272
+ *
273
+ * > An event becomes a ThreadEvent (and gets seq/timestamp) exactly when it is appended to history. <
274
+ */
275
+ append(...items) {
276
+ const events = [];
277
+ for (const item of items) {
278
+ const seq = ++this._seq;
279
+ const e = tevent({
280
+ tid: this.tid,
281
+ seq,
282
+ kind: item.kind,
283
+ data: item,
284
+ });
285
+ this.history.push(e);
286
+ this.cpbuf.push(e);
287
+ events.push(e);
288
+ }
289
+ return events;
290
+ }
291
+ /**
292
+ * Cancel the running thread
293
+ */
294
+ cancel() {
295
+ this.abort?.abort();
296
+ }
297
+ // ----------------------------
298
+ // utils
299
+ // ----------------------------
157
300
  /**
158
301
  * Perform the actions returned by the model
159
302
  */
@@ -179,10 +322,8 @@ export class Thread {
179
322
  e.state === "requires_approval" // (TODO): fix this
180
323
  ) {
181
324
  // Find the original tool call for this pending approval
182
- const originalCall = intentions.toolCalls.find((call) => call.callId === e.callId);
183
- if (originalCall) {
184
- pendingApprovals.push(originalCall);
185
- }
325
+ const call = intentions.toolCalls.find((c) => c.callId === e.callId);
326
+ call && pendingApprovals.push(call);
186
327
  }
187
328
  else {
188
329
  actions.push(e);
@@ -209,7 +350,7 @@ export class Thread {
209
350
  assert(tool.type === "function", `Tool ${call.id} is a hosted tool and should not be executed locally`);
210
351
  // (TMP) - passing the approval status through the context until actions system
211
352
  // is refined
212
- const ctx = new Context(this.context.context);
353
+ const ctx = new Context(this.namespace, this.context.context);
213
354
  ctx.approve(call.callId); // mark this call as approved
214
355
  const res = await tool.invoke(ctx, call.arguments, call.callId);
215
356
  return {
@@ -222,7 +363,6 @@ export class Thread {
222
363
  };
223
364
  }
224
365
  catch (error) {
225
- // Handles both tool not found AND any execution errors
226
366
  return {
227
367
  kind: "tool-result",
228
368
  callId: call.callId,
@@ -241,27 +381,25 @@ export class Thread {
241
381
  let settings = {
242
382
  ...this.agent.modelSettings,
243
383
  };
244
- // // TODO: what do we want to do with this?
384
+ // (TODO): what do we want to do with this?
245
385
  // settings = maybeResetToolChoice(this.agent, this.state.toolUse, settings);
246
386
  const system = await this.agent.instructions(this.context);
387
+ // filter for model items + strip event headers
388
+ const items = history
389
+ .filter((e) => e.kind !== "system") // system events are not sent to model
390
+ .map((event) => {
391
+ const { id, tid, seq, timestamp, metadata, ...item } = event;
392
+ return item;
393
+ });
247
394
  const input = system
248
- ? [
249
- // (TODO): add message(role, text) helper
250
- {
251
- kind: "message",
252
- id: randomID(),
253
- role: "system",
254
- content: [{ kind: "text", text: system }],
255
- },
256
- ...history, // (TODO): filter for LanguageModelItem specifically - there may be other thread events
257
- ]
258
- : history;
259
- // TODO: apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
395
+ ? [message({ role: "system", text: system }), ...items]
396
+ : items;
397
+ // (TODO): apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
260
398
  // const filtered = await applyInputFilters(inputWithSystem, context);
261
399
  const filtered = input;
262
400
  // serialize action repertoire
263
- const allTools = await this.agent.tools(this.context);
264
- const enabled = await filter(allTools, async (tool) => await tool.isEnabled(this.context, this.agent));
401
+ const all = await this.agent.tools(this.context);
402
+ const enabled = await filter(all, async (tool) => await tool.isEnabled(this.context, this.agent));
265
403
  const tools = enabled.map((tool) => tool.serialize());
266
404
  return {
267
405
  input: filtered,
@@ -1,26 +1,54 @@
1
1
  import type { ResolvedAgentResponse } from "../guardrail";
2
- import { ToolCall } from "@kernl-sdk/protocol";
2
+ import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
3
3
  import type { AgentResponseType } from "../types/agent";
4
- import type { ThreadEvent, ThreadStreamEvent, ActionSet } from "../types/thread";
4
+ import type { ThreadEvent, ThreadStreamEvent, ActionSet, PublicThreadEvent } from "../types/thread";
5
5
  /**
6
- * Check if an event represents an intention (action to be performed)
6
+ * Create a ThreadEvent from a LanguageModelItem with thread metadata.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * tevent({
11
+ * kind: "message",
12
+ * seq: 0,
13
+ * tid: "tid_123",
14
+ * data: message({role: "user", text: "hello"}),
15
+ * })
16
+ * // → {kind: "message", role: "user", content: [...], id: "message:msg_xyz", tid: "tid_123", seq: 0, timestamp: Date}
17
+ * ```
18
+ */
19
+ export declare function tevent(event: {
20
+ seq: number;
21
+ tid: string;
22
+ kind: ThreadEvent["kind"];
23
+ data: LanguageModelItem | null;
24
+ id?: string;
25
+ timestamp?: Date;
26
+ metadata?: Record<string, unknown>;
27
+ }): ThreadEvent;
28
+ /**
29
+ * Check if an event is a tool call
7
30
  */
8
- export declare function isActionIntention(event: ThreadEvent): event is ToolCall;
31
+ export declare function isActionIntention(event: LanguageModelItem): event is ToolCall;
9
32
  /**
10
33
  * Extract action intentions from a list of events.
11
34
  * Returns ActionSet if there are any tool calls, null otherwise.
12
35
  */
13
- export declare function getIntentions(events: ThreadEvent[]): ActionSet | null;
36
+ export declare function getIntentions(events: LanguageModelItem[]): ActionSet | null;
14
37
  /**
15
38
  * Check if an event is NOT a delta/start/end event (i.e., a complete item).
16
39
  * Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
17
40
  */
18
- export declare function notDelta(event: ThreadStreamEvent): event is ThreadEvent;
41
+ export declare function notDelta(event: ThreadStreamEvent): event is LanguageModelItem;
42
+ /**
43
+ * Check if an event is public/client-facing (not internal).
44
+ * Filters out internal system events that clients don't need.
45
+ */
46
+ export declare function isPublicEvent(event: ThreadEvent): event is PublicThreadEvent;
19
47
  /**
20
- * Extract the final text response from a list of events.
48
+ * Extract the final text response from a list of items.
21
49
  * Returns null if no assistant message with text content is found.
22
50
  */
23
- export declare function getFinalResponse(events: ThreadEvent[]): string | null;
51
+ export declare function getFinalResponse(items: LanguageModelItem[]): string | null;
24
52
  /**
25
53
  * (TODO): This should run through the language model's native structured output (if avail)
26
54
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/thread/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAIzD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,KAAK,IAAI,QAAQ,CAEvE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,GAAG,IAAI,CAGrE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,IAAI,WAAW,CAYvE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAcrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,SAAS,iBAAiB,EACpE,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,GACtB,qBAAqB,CAAC,SAAS,CAAC,CAsBlC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/thread/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAIzD,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAIlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EACV,WAAW,EAEX,iBAAiB,EACjB,SAAS,EACT,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,WAAW,CAad;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,IAAI,QAAQ,CAE7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAAG,IAAI,CAG3E;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,IAAI,iBAAiB,CAY7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,KAAK,IAAI,iBAAiB,CAc5E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,MAAM,GAAG,IAAI,CAc1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,SAAS,iBAAiB,EACpE,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,GACtB,qBAAqB,CAAC,SAAS,CAAC,CAsBlC"}
@@ -1,8 +1,35 @@
1
1
  /* lib */
2
- import { json } from "@kernl-sdk/shared/lib";
2
+ import { json, randomID } from "@kernl-sdk/shared/lib";
3
3
  import { ModelBehaviorError } from "../lib/error";
4
4
  /**
5
- * Check if an event represents an intention (action to be performed)
5
+ * Create a ThreadEvent from a LanguageModelItem with thread metadata.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * tevent({
10
+ * kind: "message",
11
+ * seq: 0,
12
+ * tid: "tid_123",
13
+ * data: message({role: "user", text: "hello"}),
14
+ * })
15
+ * // → {kind: "message", role: "user", content: [...], id: "message:msg_xyz", tid: "tid_123", seq: 0, timestamp: Date}
16
+ * ```
17
+ */
18
+ export function tevent(event) {
19
+ const iid = event.data ? event.data.id : undefined;
20
+ const defaultId = iid ? `${event.kind}:${iid}` : randomID();
21
+ return {
22
+ ...(event.data || {}),
23
+ kind: event.kind,
24
+ id: event.id ?? defaultId,
25
+ tid: event.tid,
26
+ seq: event.seq,
27
+ timestamp: event.timestamp ?? new Date(),
28
+ metadata: event.metadata ?? {},
29
+ };
30
+ }
31
+ /**
32
+ * Check if an event is a tool call
6
33
  */
7
34
  export function isActionIntention(event) {
8
35
  return event.kind === "tool-call";
@@ -32,16 +59,33 @@ export function notDelta(event) {
32
59
  }
33
60
  }
34
61
  /**
35
- * Extract the final text response from a list of events.
62
+ * Check if an event is public/client-facing (not internal).
63
+ * Filters out internal system events that clients don't need.
64
+ */
65
+ export function isPublicEvent(event) {
66
+ switch (event.kind) {
67
+ case "message":
68
+ case "reasoning":
69
+ case "tool-call":
70
+ case "tool-result":
71
+ return true;
72
+ case "system":
73
+ return false;
74
+ default:
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Extract the final text response from a list of items.
36
80
  * Returns null if no assistant message with text content is found.
37
81
  */
38
- export function getFinalResponse(events) {
82
+ export function getFinalResponse(items) {
39
83
  // Scan backwards for the last assistant message
40
- for (let i = events.length - 1; i >= 0; i--) {
41
- const event = events[i];
42
- if (event.kind === "message" && event.role === "assistant") {
84
+ for (let i = items.length - 1; i >= 0; i--) {
85
+ const item = items[i];
86
+ if (item.kind === "message" && item.role === "assistant") {
43
87
  // Extract text from content parts
44
- for (const part of event.content) {
88
+ for (const part of item.content) {
45
89
  if (part.kind === "text") {
46
90
  return part.text;
47
91
  }
@@ -5,7 +5,7 @@ import { tool, HostedTool } from "../tool";
5
5
  * Create a minimal mock context for testing
6
6
  */
7
7
  export const mockContext = (data) => {
8
- return new Context(data ?? {});
8
+ return new Context("test-namespace", data ?? {});
9
9
  };
10
10
  /**
11
11
  * Simple string tool with no parameters
@@ -83,22 +83,22 @@ describe("FunctionToolkit", () => {
83
83
  const toolkit = new FunctionToolkit({ id: "test", tools: [simpleStringTool] });
84
84
  const serialized = (await toolkit.list()).map((tool) => tool.serialize());
85
85
  expect(serialized).toHaveLength(1);
86
- expect(serialized[0]).toEqual({
87
- type: "function",
88
- name: simpleStringTool.name,
86
+ expect(serialized[0]).toMatchObject({
87
+ kind: "function",
88
+ name: simpleStringTool.id,
89
89
  description: simpleStringTool.description,
90
- parameters: simpleStringTool.parameters,
91
90
  });
91
+ expect(serialized[0].parameters).toBeDefined();
92
92
  });
93
93
  it("should serialize hosted tools correctly", async () => {
94
94
  const toolkit = new FunctionToolkit({ id: "test", tools: [mockHostedTool] });
95
95
  const serialized = (await toolkit.list()).map((tool) => tool.serialize());
96
96
  expect(serialized).toHaveLength(1);
97
97
  expect(serialized[0]).toEqual({
98
- type: "hosted-tool",
98
+ kind: "provider-defined",
99
99
  id: mockHostedTool.id,
100
100
  name: mockHostedTool.name,
101
- providerData: mockHostedTool.providerData,
101
+ args: mockHostedTool.providerData,
102
102
  });
103
103
  });
104
104
  it("should serialize both function and hosted tools", async () => {
@@ -107,17 +107,20 @@ describe("FunctionToolkit", () => {
107
107
  expect(serialized).toHaveLength(2);
108
108
  // Check both tools are present (order not guaranteed with Map)
109
109
  expect(serialized).toContainEqual({
110
- type: "hosted-tool",
110
+ kind: "provider-defined",
111
111
  id: mockHostedTool.id,
112
112
  name: mockHostedTool.name,
113
- providerData: mockHostedTool.providerData,
113
+ args: mockHostedTool.providerData,
114
114
  });
115
- expect(serialized).toContainEqual({
116
- type: "function",
117
- name: simpleStringTool.name,
115
+ // Check that function tool is present with correct structure
116
+ const functionTool = serialized.find((t) => t.kind === "function");
117
+ expect(functionTool).toBeDefined();
118
+ expect(functionTool).toMatchObject({
119
+ kind: "function",
120
+ name: simpleStringTool.id,
118
121
  description: simpleStringTool.description,
119
- parameters: simpleStringTool.parameters,
120
122
  });
123
+ expect(functionTool.parameters).toBeDefined();
121
124
  });
122
125
  it("should handle hosted tools without providerData", async () => {
123
126
  const toolkit = new FunctionToolkit({ id: "test", tools: [anotherHostedTool] });
package/dist/tool/tool.js CHANGED
@@ -120,9 +120,9 @@ export class FunctionTool extends BaseTool {
120
120
  kind: "function",
121
121
  name: this.id,
122
122
  description: this.description,
123
- parameters: (this.parameters
124
- ? z.toJSONSchema(this.parameters, { target: "draft-7" })
125
- : {}), // JSONSchema7 - target: 'draft-7' produces this
123
+ parameters: z.toJSONSchema(this.parameters ?? z.object({}), {
124
+ target: "draft-7",
125
+ }), // Use empty object if no parameters (matches AI SDK)
126
126
  };
127
127
  }
128
128
  }