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,86 +1,144 @@
1
1
  import assert from "assert";
2
2
 
3
- import { Kernl } from "@/kernl";
4
3
  import { Agent } from "@/agent";
5
4
  import { Context } from "@/context";
6
5
  import type { Task } from "@/task";
6
+ import type { ResolvedAgentResponse } from "@/guardrail";
7
+ import type { ThreadStore } from "@/storage";
8
+
9
+ import { logger } from "@/lib/logger";
7
10
 
8
11
  import {
9
- ToolCall,
10
- LanguageModel,
11
- LanguageModelRequest,
12
- LanguageModelItem,
13
12
  FAILED,
14
13
  RUNNING,
15
14
  STOPPED,
15
+ message,
16
+ ToolCall,
17
+ LanguageModel,
18
+ LanguageModelItem,
19
+ LanguageModelRequest,
16
20
  } from "@kernl-sdk/protocol";
17
21
  import { randomID, filter } from "@kernl-sdk/shared/lib";
18
22
 
19
23
  import type {
20
24
  ActionSet,
21
25
  ThreadEvent,
26
+ ThreadState,
22
27
  ThreadOptions,
28
+ ThreadEventInner,
29
+ ThreadStreamEvent,
23
30
  ThreadExecuteResult,
24
31
  PerformActionsResult,
25
- ThreadState,
26
- ThreadStreamEvent,
27
32
  } from "@/types/thread";
28
33
  import type { AgentResponseType } from "@/types/agent";
29
- import type { ResolvedAgentResponse } from "@/guardrail";
30
34
 
31
35
  import {
36
+ tevent,
32
37
  notDelta,
33
- getFinalResponse,
34
38
  getIntentions,
39
+ getFinalResponse,
35
40
  parseFinalResponse,
36
41
  } from "./utils";
37
42
 
38
43
  /**
39
44
  * A thread drives the execution loop for an agent.
45
+ *
46
+ * Ground principles:
47
+ *
48
+ * 1) Event log is source of truth.
49
+ * - Persistent storage (e.g. Postgres) is treated as an append-only per-thread log of `ThreadEvent`s:
50
+ * monotonic `seq`, no gaps, no updates/deletes.
51
+ * - `Thread.state`, `tick`, etc. are projections of that log, not an alternative source of truth.
52
+ *
53
+ * 2) Single writer per thread.
54
+ * - At most one executor is allowed for a given `tid` at a time.
55
+ * - Callers are responsible for enforcing this (e.g. locking/versioning) so two processes cannot
56
+ * interleave or race on `seq` or state.
57
+ *
58
+ * 3) Persist before use / observation.
59
+ * - Before an event can:
60
+ * - influence a future tick (i.e. be part of `history` fed back into the model), or
61
+ * - be considered “delivered” to a client,
62
+ * it SHOULD be durably written to storage when storage is configured.
63
+ *
64
+ * 4) Transaction boundaries match semantic steps.
65
+ * - The intended strategy is to buffer within a tick, then atomically persist all new events + state
66
+ * at the end of `tick()`.
67
+ * - After a crash, you only ever see whole ticks or none, never half a tick, from the store’s
68
+ * point of view.
69
+ *
70
+ * 5) Recovery is replay.
71
+ * - On restart, callers rebuild a `Thread` from the stored event log (plus optional snapshots).
72
+ * - Any incomplete tick or pending tool call is handled by a clear, deterministic policy at a
73
+ * higher layer (e.g. re-run, mark failed, or require manual intervention).
74
+ *
75
+ * On storage failures:
76
+ *
77
+ * “If storage is configured, it is authoritative” → fail hard on persist errors rather than
78
+ * treating persistence as best-effort.
79
+ *
80
+ * If a storage implementation is present, `persist(...)` is expected to throw on failure, and
81
+ * that error should bubble out of `_execute()` / `stream()` and stop the thread.
40
82
  */
41
83
  export class Thread<
42
84
  TContext = unknown,
43
85
  TResponse extends AgentResponseType = "text",
44
86
  > {
45
- private kernl: Kernl;
46
-
47
- readonly id: string;
87
+ readonly tid: string;
88
+ readonly namespace: string;
48
89
  readonly agent: Agent<TContext, TResponse>;
49
90
  readonly context: Context<TContext>;
50
91
  readonly model: LanguageModel; /* inherited from the agent unless specified */
51
92
  readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
52
- readonly mode: "blocking" | "stream"; /* TODO */
53
- readonly input: ThreadEvent[]; /* the initial input for the thread */
93
+ readonly createdAt: Date;
94
+ readonly updatedAt: Date;
95
+ readonly metadata: Record<string, unknown> | null;
54
96
  // readonly stats: ThreadMetrics;
55
97
 
56
98
  /* state */
57
- _tick: number;
99
+ _tick: number; /* number of LLM roundtrips */
100
+ _seq: number; /* monotonic event sequence */
58
101
  state: ThreadState;
59
- private history: ThreadEvent[] /* events generated during this thread's execution */;
102
+ private cpbuf: ThreadEvent[]; /* checkpoint buffer - events pending persistence */
103
+ private persisted: boolean; /* indicates thread was hydrated from storage */
104
+ private history: ThreadEvent[] /* history representing the event log for the thread */;
105
+
60
106
  private abort?: AbortController;
107
+ private storage?: ThreadStore;
108
+
109
+ constructor(options: ThreadOptions<TContext, TResponse>) {
110
+ this.tid = options.tid ?? `tid_${randomID()}`;
111
+ this.namespace = options.namespace ?? "kernl";
112
+ this.agent = options.agent;
113
+ this.context =
114
+ options.context ?? new Context<TContext>(this.namespace, {} as TContext);
115
+ this.parent = options.task ?? null;
116
+ this.model = options.model ?? options.agent.model;
117
+ this.storage = options.storage;
118
+ this.createdAt = options.createdAt ?? new Date();
119
+ this.updatedAt = options.updatedAt ?? new Date();
120
+ this.metadata = options.metadata ?? null;
121
+
122
+ this._tick = options.tick ?? 0;
123
+ this._seq = -1;
124
+ this.state = options.state ?? STOPPED;
125
+ this.cpbuf = [];
126
+ this.persisted = options.persisted ?? false;
127
+ this.history = options.history ?? [];
128
+
129
+ // seek to latest seq (not persisted)
130
+ if (this.history.length > 0) {
131
+ this._seq = Math.max(...this.history.map((e) => e.seq));
132
+ }
61
133
 
62
- constructor(
63
- kernl: Kernl,
64
- agent: Agent<TContext, TResponse>,
65
- input: ThreadEvent[],
66
- options?: ThreadOptions<TContext>,
67
- ) {
68
- this.id = `tid_${randomID()}`;
69
- this.agent = agent;
70
- this.context = options?.context ?? new Context<TContext>();
71
- this.kernl = kernl;
72
- this.parent = options?.task ?? null;
73
- this.model = options?.model ?? agent.model;
74
- this.mode = "blocking"; // (TODO): add streaming
75
- this.input = input;
76
-
77
- this._tick = 0;
78
- this.state = STOPPED;
79
- this.history = input;
134
+ // append initial input if provided (for new threads)
135
+ if (options.input && options.input.length > 0) {
136
+ this.append(...options.input);
137
+ }
80
138
  }
81
139
 
82
140
  /**
83
- * Blocking execution loop - runs until terminal state or interruption
141
+ * Blocking execution - runs until terminal state or interruption
84
142
  */
85
143
  async execute(): Promise<
86
144
  ThreadExecuteResult<ResolvedAgentResponse<TResponse>>
@@ -89,10 +147,16 @@ export class Thread<
89
147
  // just consume the stream (already in history in _execute())
90
148
  }
91
149
 
92
- // extract final response from accumulated history
93
- const text = getFinalResponse(this.history);
94
- assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
150
+ // filter for language model items
151
+ const items = this.history
152
+ .filter((e) => e.kind !== "system")
153
+ .map((e) => {
154
+ const { tid, seq, timestamp, metadata, ...item } = e;
155
+ return item as LanguageModelItem;
156
+ });
95
157
 
158
+ const text = getFinalResponse(items);
159
+ assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
96
160
  const parsed = parseFinalResponse(text, this.agent.responseType);
97
161
 
98
162
  return { response: parsed, state: this.state };
@@ -109,6 +173,10 @@ export class Thread<
109
173
  this.state = RUNNING;
110
174
  this.abort = new AbortController();
111
175
 
176
+ await this.checkpoint(); /* c1: persist RUNNING state + initial input */
177
+
178
+ yield { kind: "stream-start" }; // always yield start immediately
179
+
112
180
  try {
113
181
  yield* this._execute();
114
182
  } catch (err) {
@@ -116,66 +184,68 @@ export class Thread<
116
184
  } finally {
117
185
  this.state = STOPPED;
118
186
  this.abort = undefined;
187
+ await this.checkpoint(); /* c4: final checkpoint - persist STOPPED state */
119
188
  }
120
189
  }
121
190
 
122
191
  /**
123
- * Cancel the running thread
124
- */
125
- cancel() {
126
- this.abort?.abort();
127
- }
128
-
129
- /**
130
- * Append a new event to the thread history
131
- */
132
- append(event: ThreadEvent): void {
133
- this.history.push(event);
134
- }
135
-
136
- /**
137
- * Main execution loop - always yields events, callers can propagate or discard (as in execute())
192
+ * Main execution loop - always yields events, callers can propagate or discard.
138
193
  *
139
194
  * NOTE: Streaming structured output deferred for now. Prioritizing correctness + simplicity,
140
195
  * and unclear what use cases there would actually be for streaming a structured output (other than maybe gen UI).
141
196
  */
142
197
  private async *_execute(): AsyncGenerator<ThreadStreamEvent, void> {
143
198
  for (;;) {
199
+ let err = false;
200
+
144
201
  if (this.abort?.signal.aborted) {
145
202
  return;
146
203
  }
147
204
 
148
205
  const events = [];
149
206
  for await (const e of this.tick()) {
207
+ if (e.kind === "error") {
208
+ err = true;
209
+ logger.error(e.error); // (TODO): onError callback in options
210
+ }
150
211
  // we don't want deltas in the history
151
212
  if (notDelta(e)) {
152
213
  events.push(e);
153
- this.history.push(e);
214
+ this.append(e);
154
215
  }
155
216
  yield e;
156
217
  }
157
218
 
158
- // if model returns a message with no action intentions -> terminal state
219
+ // if an error event occurred terminate
220
+ if (err) {
221
+ return;
222
+ }
223
+
224
+ // if model returns a message with no action intentions → terminal state
159
225
  const intentions = getIntentions(events);
160
226
  if (!intentions) {
161
227
  const text = getFinalResponse(events);
162
228
  if (!text) continue; // run again, policy-dependent? (how to ensure no infinite loop here?)
163
229
 
230
+ await this.checkpoint(); /* c2: terminal tick - no tool calls */
231
+
164
232
  // await this.agent.runOutputGuardails(context, state);
165
233
  // this.kernl.emit("thread.terminated", context, output);
166
234
  return;
167
235
  }
168
236
 
169
- // perform actions intended by the model
237
+ // perform intended actions
170
238
  const { actions, pendingApprovals } =
171
239
  await this.performActions(intentions);
172
240
 
173
- // yield action events
241
+ // append + yield action events
174
242
  for (const a of actions) {
175
- this.history.push(a);
243
+ this.append(a);
176
244
  yield a;
177
245
  }
178
246
 
247
+ await this.checkpoint(); /* c3: tick complete */
248
+
179
249
  if (pendingApprovals.length > 0) {
180
250
  // publish a batch approval request containing all of them
181
251
  //
@@ -202,22 +272,108 @@ export class Thread<
202
272
 
203
273
  const req = await this.prepareModelRequest(this.history);
204
274
 
205
- // try to stream if model supports it
206
- if (this.model.stream) {
207
- const stream = this.model.stream(req);
208
- for await (const event of stream) {
209
- yield event; // [text-delta, tool-call, message, reasoning, ...]
210
- }
211
- } else {
212
- // fallback: blocking generate, yield events as batch
213
- const res = await this.model.generate(req);
214
- for (const event of res.content) {
215
- yield event;
275
+ try {
276
+ if (this.model.stream) {
277
+ const stream = this.model.stream(req);
278
+ for await (const event of stream) {
279
+ yield event; // [text-delta, tool-call, message, reasoning, ...]
280
+ }
281
+ } else {
282
+ // fallback: blocking generate, yield events as batch
283
+ const res = await this.model.generate(req);
284
+ for (const event of res.content) {
285
+ yield event;
286
+ }
287
+ // (TODO): this.stats.usage.add(res.usage)
216
288
  }
217
- // (TODO): track usage (this.stats.usage.add(res.usage))
289
+ } catch (error) {
290
+ yield {
291
+ kind: "error",
292
+ error: error instanceof Error ? error : new Error(String(error)),
293
+ };
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Persist current thread state to storage.
299
+ *
300
+ * - If storage is configured, it is authoritative - failures throw and halt execution.
301
+ * - No-op if storage is not configured.
302
+ */
303
+ private async checkpoint(): Promise<void> {
304
+ if (!this.storage) {
305
+ logger.warn(
306
+ "thread: storage is not configured, thread will not be persisted",
307
+ );
308
+ return;
218
309
  }
310
+
311
+ // insert thread record on first persist for new threads
312
+ if (!this.persisted) {
313
+ await this.storage.insert({
314
+ id: this.tid,
315
+ namespace: this.namespace,
316
+ agentId: this.agent.id,
317
+ parentTaskId: this.parent?.id ?? null,
318
+ model: `${this.model.provider}/${this.model.modelId}`,
319
+ context: this.context.context,
320
+ tick: this._tick,
321
+ state: this.state,
322
+ metadata: this.metadata,
323
+ });
324
+ this.persisted = true;
325
+ }
326
+
327
+ // append + drain events from checkpoint buffer
328
+ if (this.cpbuf.length > 0) {
329
+ await this.storage.append(this.cpbuf);
330
+ this.cpbuf = [];
331
+ }
332
+
333
+ // update thread state
334
+ await this.storage.update(this.tid, {
335
+ state: this.state,
336
+ tick: this._tick,
337
+ context: this.context,
338
+ metadata: this.metadata,
339
+ });
219
340
  }
220
341
 
342
+ /**
343
+ * Append one or more items to history + enrich w/ runtime headers.
344
+ *
345
+ * Core rule:
346
+ *
347
+ * > An event becomes a ThreadEvent (and gets seq/timestamp) exactly when it is appended to history. <
348
+ */
349
+ append(...items: ThreadEventInner[]): ThreadEvent[] {
350
+ const events: ThreadEvent[] = [];
351
+ for (const item of items) {
352
+ const seq = ++this._seq;
353
+ const e = tevent({
354
+ tid: this.tid,
355
+ seq,
356
+ kind: item.kind,
357
+ data: item,
358
+ });
359
+ this.history.push(e);
360
+ this.cpbuf.push(e);
361
+ events.push(e);
362
+ }
363
+ return events;
364
+ }
365
+
366
+ /**
367
+ * Cancel the running thread
368
+ */
369
+ cancel() {
370
+ this.abort?.abort();
371
+ }
372
+
373
+ // ----------------------------
374
+ // utils
375
+ // ----------------------------
376
+
221
377
  /**
222
378
  * Perform the actions returned by the model
223
379
  */
@@ -239,7 +395,7 @@ export class Thread<
239
395
  const toolEvents = await this.executeTools(intentions.toolCalls);
240
396
  // const mcpEvents = await this.executeMCPRequests(actions.mcpRequests);
241
397
 
242
- const actions: ThreadEvent[] = [];
398
+ const actions: ThreadEventInner[] = [];
243
399
  const pendingApprovals: ToolCall[] = [];
244
400
 
245
401
  // (TODO): clean this - approval tracking should be handled differently
@@ -249,12 +405,8 @@ export class Thread<
249
405
  (e.state as any) === "requires_approval" // (TODO): fix this
250
406
  ) {
251
407
  // Find the original tool call for this pending approval
252
- const originalCall = intentions.toolCalls.find(
253
- (call) => call.callId === e.callId,
254
- );
255
- if (originalCall) {
256
- pendingApprovals.push(originalCall);
257
- }
408
+ const call = intentions.toolCalls.find((c) => c.callId === e.callId);
409
+ call && pendingApprovals.push(call);
258
410
  } else {
259
411
  actions.push(e);
260
412
  }
@@ -271,7 +423,7 @@ export class Thread<
271
423
  *
272
424
  * TODO: refactor into actions system
273
425
  */
274
- private async executeTools(calls: ToolCall[]): Promise<ThreadEvent[]> {
426
+ private async executeTools(calls: ToolCall[]): Promise<ThreadEventInner[]> {
275
427
  return await Promise.all(
276
428
  calls.map(async (call: ToolCall) => {
277
429
  try {
@@ -288,7 +440,7 @@ export class Thread<
288
440
 
289
441
  // (TMP) - passing the approval status through the context until actions system
290
442
  // is refined
291
- const ctx = new Context(this.context.context);
443
+ const ctx = new Context(this.namespace, this.context.context);
292
444
  ctx.approve(call.callId); // mark this call as approved
293
445
  const res = await tool.invoke(ctx, call.arguments, call.callId);
294
446
 
@@ -301,7 +453,6 @@ export class Thread<
301
453
  error: res.error,
302
454
  };
303
455
  } catch (error) {
304
- // Handles both tool not found AND any execution errors
305
456
  return {
306
457
  kind: "tool-result" as const,
307
458
  callId: call.callId,
@@ -325,32 +476,32 @@ export class Thread<
325
476
  ...this.agent.modelSettings,
326
477
  };
327
478
 
328
- // // TODO: what do we want to do with this?
479
+ // (TODO): what do we want to do with this?
329
480
  // settings = maybeResetToolChoice(this.agent, this.state.toolUse, settings);
330
481
 
331
482
  const system = await this.agent.instructions(this.context);
483
+
484
+ // filter for model items + strip event headers
485
+ const items = history
486
+ .filter((e) => e.kind !== "system") // system events are not sent to model
487
+ .map((event) => {
488
+ const { id, tid, seq, timestamp, metadata, ...item } = event;
489
+ return item as LanguageModelItem;
490
+ });
491
+
332
492
  const input: LanguageModelItem[] = system
333
- ? [
334
- // (TODO): add message(role, text) helper
335
- {
336
- kind: "message",
337
- id: randomID(),
338
- role: "system",
339
- content: [{ kind: "text", text: system }],
340
- },
341
- ...history, // (TODO): filter for LanguageModelItem specifically - there may be other thread events
342
- ]
343
- : history;
344
-
345
- // TODO: apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
493
+ ? [message({ role: "system", text: system }), ...items]
494
+ : items;
495
+
496
+ // (TODO): apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
346
497
  // const filtered = await applyInputFilters(inputWithSystem, context);
347
498
 
348
499
  const filtered = input;
349
500
 
350
501
  // serialize action repertoire
351
- const allTools = await this.agent.tools(this.context);
502
+ const all = await this.agent.tools(this.context);
352
503
  const enabled = await filter(
353
- allTools,
504
+ all,
354
505
  async (tool) => await tool.isEnabled(this.context, this.agent),
355
506
  );
356
507
  const tools = enabled.map((tool) => tool.serialize());
@@ -3,18 +3,61 @@ import { ZodType } from "zod";
3
3
  import type { ResolvedAgentResponse } from "@/guardrail";
4
4
 
5
5
  /* lib */
6
- import { json } from "@kernl-sdk/shared/lib";
7
- import { ToolCall } from "@kernl-sdk/protocol";
6
+ import { json, randomID } from "@kernl-sdk/shared/lib";
7
+ import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
8
8
  import { ModelBehaviorError } from "@/lib/error";
9
9
 
10
10
  /* types */
11
11
  import type { AgentResponseType } from "@/types/agent";
12
- import type { ThreadEvent, ThreadStreamEvent, ActionSet } from "@/types/thread";
12
+ import type {
13
+ ThreadEvent,
14
+ ThreadEventBase,
15
+ ThreadStreamEvent,
16
+ ActionSet,
17
+ PublicThreadEvent,
18
+ } from "@/types/thread";
13
19
 
14
20
  /**
15
- * Check if an event represents an intention (action to be performed)
21
+ * Create a ThreadEvent from a LanguageModelItem with thread metadata.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * tevent({
26
+ * kind: "message",
27
+ * seq: 0,
28
+ * tid: "tid_123",
29
+ * data: message({role: "user", text: "hello"}),
30
+ * })
31
+ * // → {kind: "message", role: "user", content: [...], id: "message:msg_xyz", tid: "tid_123", seq: 0, timestamp: Date}
32
+ * ```
33
+ */
34
+ export function tevent(event: {
35
+ seq: number;
36
+ tid: string;
37
+ kind: ThreadEvent["kind"];
38
+ data: LanguageModelItem | null; // null for system events
39
+ id?: string;
40
+ timestamp?: Date;
41
+ metadata?: Record<string, unknown>;
42
+ }): ThreadEvent {
43
+ const iid = event.data ? event.data.id : undefined;
44
+ const defaultId = iid ? `${event.kind}:${iid}` : randomID();
45
+
46
+ return {
47
+ ...(event.data || {}),
48
+ kind: event.kind,
49
+ id: event.id ?? defaultId,
50
+ tid: event.tid,
51
+ seq: event.seq,
52
+ timestamp: event.timestamp ?? new Date(),
53
+ metadata: event.metadata ?? {},
54
+ } as ThreadEvent;
55
+ }
56
+
57
+ /**
58
+ * Check if an event is a tool call
16
59
  */
17
- export function isActionIntention(event: ThreadEvent): event is ToolCall {
60
+ export function isActionIntention(event: LanguageModelItem): event is ToolCall {
18
61
  return event.kind === "tool-call";
19
62
  }
20
63
 
@@ -22,7 +65,7 @@ export function isActionIntention(event: ThreadEvent): event is ToolCall {
22
65
  * Extract action intentions from a list of events.
23
66
  * Returns ActionSet if there are any tool calls, null otherwise.
24
67
  */
25
- export function getIntentions(events: ThreadEvent[]): ActionSet | null {
68
+ export function getIntentions(events: LanguageModelItem[]): ActionSet | null {
26
69
  const toolCalls = events.filter(isActionIntention);
27
70
  return toolCalls.length > 0 ? { toolCalls } : null;
28
71
  }
@@ -31,7 +74,7 @@ export function getIntentions(events: ThreadEvent[]): ActionSet | null {
31
74
  * Check if an event is NOT a delta/start/end event (i.e., a complete item).
32
75
  * Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
33
76
  */
34
- export function notDelta(event: ThreadStreamEvent): event is ThreadEvent {
77
+ export function notDelta(event: ThreadStreamEvent): event is LanguageModelItem {
35
78
  switch (event.kind) {
36
79
  case "message":
37
80
  case "reasoning":
@@ -46,16 +89,36 @@ export function notDelta(event: ThreadStreamEvent): event is ThreadEvent {
46
89
  }
47
90
 
48
91
  /**
49
- * Extract the final text response from a list of events.
92
+ * Check if an event is public/client-facing (not internal).
93
+ * Filters out internal system events that clients don't need.
94
+ */
95
+ export function isPublicEvent(event: ThreadEvent): event is PublicThreadEvent {
96
+ switch (event.kind) {
97
+ case "message":
98
+ case "reasoning":
99
+ case "tool-call":
100
+ case "tool-result":
101
+ return true;
102
+
103
+ case "system":
104
+ return false;
105
+
106
+ default:
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Extract the final text response from a list of items.
50
113
  * Returns null if no assistant message with text content is found.
51
114
  */
52
- export function getFinalResponse(events: ThreadEvent[]): string | null {
115
+ export function getFinalResponse(items: LanguageModelItem[]): string | null {
53
116
  // Scan backwards for the last assistant message
54
- for (let i = events.length - 1; i >= 0; i--) {
55
- const event = events[i];
56
- if (event.kind === "message" && event.role === "assistant") {
117
+ for (let i = items.length - 1; i >= 0; i--) {
118
+ const item = items[i];
119
+ if (item.kind === "message" && item.role === "assistant") {
57
120
  // Extract text from content parts
58
- for (const part of event.content) {
121
+ for (const part of item.content) {
59
122
  if (part.kind === "text") {
60
123
  return part.text;
61
124
  }
@@ -6,7 +6,7 @@ import { tool, HostedTool } from "../tool";
6
6
  * Create a minimal mock context for testing
7
7
  */
8
8
  export const mockContext = <T = any>(data?: T): Context<T> => {
9
- return new Context<T>(data ?? ({} as T));
9
+ return new Context<T>("test-namespace", data ?? ({} as T));
10
10
  };
11
11
 
12
12
  /**