kernl 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lifecycle.ts CHANGED
@@ -1,131 +1,307 @@
1
1
  import { Emitter } from "@kernl-sdk/shared";
2
2
 
3
- import { Agent } from "./agent";
4
- import { Context, UnknownContext } from "./context";
5
- import { Tool } from "./tool";
6
- import type { ToolCall } from "@kernl-sdk/protocol";
7
-
8
- import { AgentOutputType } from "@/agent/types";
9
- import { TextOutput } from "@/thread/types";
10
-
11
- export type AgentHookEvents<
12
- TContext = UnknownContext,
13
- TOutput extends AgentOutputType = TextOutput,
14
- > = {
15
- /**
16
- * @param context - The context of the run
17
- */
18
- agent_start: [context: Context<TContext>, agent: Agent<TContext, TOutput>];
19
- /**
20
- * @param context - The context of the run
21
- * @param output - The output of the agent
22
- */
23
- agent_end: [context: Context<TContext>, output: string];
24
- // /**
25
- // * @param context - The context of the run
26
- // * @param agent - The agent that is handing off
27
- // * @param nextAgent - The next agent to run
28
- // */
29
- // agent_handoff: [context: Context<TContext>, nextAgent: Agent<any, any>];
30
- /**
31
- * @param context - The context of the run
32
- * @param agent - The agent that is starting a tool
33
- * @param tool - The tool that is starting
34
- */
35
- agent_tool_start: [
36
- context: Context<TContext>,
37
- tool: Tool<any>,
38
- details: { toolCall: ToolCall },
39
- ];
40
- /**
41
- * @param context - The context of the run
42
- * @param agent - The agent that is ending a tool
43
- * @param tool - The tool that is ending
44
- * @param result - The result of the tool
45
- */
46
- agent_tool_end: [
47
- context: Context<TContext>,
48
- tool: Tool<any>,
49
- result: string,
50
- details: { toolCall: ToolCall },
51
- ];
52
- };
3
+ import type {
4
+ LanguageModelUsage,
5
+ LanguageModelFinishReason,
6
+ LanguageModelRequestSettings,
7
+ ToolCallState,
8
+ } from "@kernl-sdk/protocol";
9
+ import type { Context } from "@/context";
10
+ import type { ThreadState } from "@/thread/types";
11
+
12
+ // --- Thread Events ---
53
13
 
54
14
  /**
55
- * Event emitter that every Agent instance inherits from and that emits events for the lifecycle
56
- * of the agent.
15
+ * Emitted when a thread starts execution.
57
16
  */
58
- export class AgentHooks<
59
- TContext = UnknownContext,
60
- TOutput extends AgentOutputType = TextOutput,
61
- > extends Emitter<AgentHookEvents<TContext, TOutput>> {}
17
+ export interface ThreadStartEvent<TContext = unknown> {
18
+ readonly kind: "thread.start";
19
+
20
+ /**
21
+ * The thread ID.
22
+ */
23
+ threadId: string;
24
+
25
+ /**
26
+ * The agent executing this thread.
27
+ */
28
+ agentId: string;
29
+
30
+ /**
31
+ * The namespace of the thread.
32
+ */
33
+ namespace: string;
34
+
35
+ /**
36
+ * The context for this execution.
37
+ *
38
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
39
+ */
40
+ context: Context<TContext>;
41
+ }
62
42
 
63
43
  /**
64
- * Events emitted by the kernl during execution.
65
- *
66
- * Unlike AgentHookEvents (which are emitted by individual agents with implicit context),
67
- * KernlHookEvents explicitly include the agent reference in all events since it needs to
68
- * coordinate multiple agents and listeners need to know which agent triggered each event.
44
+ * Emitted when a thread stops execution.
69
45
  */
70
- export type KernlHookEvents<
71
- TContext = UnknownContext,
72
- TOutput extends AgentOutputType = TextOutput,
73
- > = {
74
- /**
75
- * @param context - The context of the run
76
- * @param agent - The agent that is starting
77
- */
78
- agent_start: [context: Context<TContext>, agent: Agent<TContext, TOutput>];
79
- /**
80
- * @param context - The context of the run
81
- * @param agent - The agent that is ending
82
- * @param output - The output of the agent
83
- */
84
- agent_end: [
85
- context: Context<TContext>,
86
- agent: Agent<TContext, TOutput>,
87
- output: string,
88
- ];
89
- /**
90
- * @param context - The context of the run
91
- * @param fromAgent - The agent that is handing off
92
- * @param toAgent - The next agent to run
93
- */
94
- agent_handoff: [
95
- context: Context<TContext>,
96
- fromAgent: Agent<any, any>,
97
- toAgent: Agent<any, any>,
98
- ];
99
- /**
100
- * @param context - The context of the run
101
- * @param agent - The agent that is starting a tool
102
- * @param tool - The tool that is starting
103
- */
104
- agent_tool_start: [
105
- context: Context<TContext>,
106
- agent: Agent<TContext, TOutput>,
107
- tool: Tool,
108
- details: { toolCall: ToolCall },
109
- ];
110
- /**
111
- * @param context - The context of the run
112
- * @param agent - The agent that is ending a tool
113
- * @param tool - The tool that is ending
114
- * @param result - The result of the tool
115
- */
116
- agent_tool_end: [
117
- context: Context<TContext>,
118
- agent: Agent<TContext, TOutput>,
119
- tool: Tool,
120
- result: string,
121
- details: { toolCall: ToolCall },
122
- ];
46
+ export interface ThreadStopEvent<TContext = unknown, TOutput = unknown> {
47
+ readonly kind: "thread.stop";
48
+
49
+ /**
50
+ * The thread ID.
51
+ */
52
+ threadId: string;
53
+
54
+ /**
55
+ * The agent that executed this thread.
56
+ */
57
+ agentId: string;
58
+
59
+ /**
60
+ * The namespace of the thread.
61
+ */
62
+ namespace: string;
63
+
64
+ /**
65
+ * The context for this execution.
66
+ *
67
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
68
+ */
69
+ context: Context<TContext>;
70
+
71
+ /**
72
+ * Final state of the thread.
73
+ */
74
+ state: ThreadState;
75
+
76
+ /**
77
+ * The outcome of the execution.
78
+ */
79
+ outcome: "success" | "error" | "cancelled";
80
+
81
+ /**
82
+ * The result if outcome is "success".
83
+ */
84
+ result?: TOutput;
85
+
86
+ /**
87
+ * Error message if outcome is "error".
88
+ */
89
+ error?: string;
90
+ }
91
+
92
+ // --- Model Events ---
93
+
94
+ /**
95
+ * Emitted when a model call starts.
96
+ */
97
+ export interface ModelCallStartEvent<TContext = unknown> {
98
+ readonly kind: "model.call.start";
99
+
100
+ /**
101
+ * The model provider.
102
+ */
103
+ provider: string;
104
+
105
+ /**
106
+ * The model ID.
107
+ */
108
+ modelId: string;
109
+
110
+ /**
111
+ * Request settings passed to the model.
112
+ */
113
+ settings: LanguageModelRequestSettings;
114
+
115
+ /**
116
+ * Thread ID if called within a thread context.
117
+ */
118
+ threadId?: string;
119
+
120
+ /**
121
+ * Agent ID if called within an agent context.
122
+ */
123
+ agentId?: string;
124
+
125
+ /**
126
+ * Execution context if available.
127
+ *
128
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
129
+ */
130
+ context?: Context<TContext>;
131
+ }
132
+
133
+ /**
134
+ * Emitted when a model call ends.
135
+ */
136
+ export interface ModelCallEndEvent<TContext = unknown> {
137
+ readonly kind: "model.call.end";
138
+
139
+ /**
140
+ * The model provider.
141
+ */
142
+ provider: string;
143
+
144
+ /**
145
+ * The model ID.
146
+ */
147
+ modelId: string;
148
+
149
+ /**
150
+ * Reason the model stopped generating.
151
+ */
152
+ finishReason: LanguageModelFinishReason;
153
+
154
+ /**
155
+ * Token usage for this call.
156
+ */
157
+ usage?: LanguageModelUsage;
158
+
159
+ /**
160
+ * Thread ID if called within a thread context.
161
+ */
162
+ threadId?: string;
163
+
164
+ /**
165
+ * Agent ID if called within an agent context.
166
+ */
167
+ agentId?: string;
168
+
169
+ /**
170
+ * Execution context if available.
171
+ *
172
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
173
+ */
174
+ context?: Context<TContext>;
175
+ }
176
+
177
+ // --- Tool Events ---
178
+
179
+ /**
180
+ * Emitted when a tool call starts.
181
+ */
182
+ export interface ToolCallStartEvent<TContext = unknown> {
183
+ readonly kind: "tool.call.start";
184
+
185
+ /**
186
+ * The thread ID.
187
+ */
188
+ threadId: string;
189
+
190
+ /**
191
+ * The agent executing this tool.
192
+ */
193
+ agentId: string;
194
+
195
+ /**
196
+ * The context for this execution.
197
+ *
198
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
199
+ */
200
+ context: Context<TContext>;
201
+
202
+ /**
203
+ * The tool being called.
204
+ */
205
+ toolId: string;
206
+
207
+ /**
208
+ * Unique identifier for this call.
209
+ */
210
+ callId: string;
211
+
212
+ /**
213
+ * Arguments passed to the tool (parsed JSON).
214
+ */
215
+ args: Record<string, unknown>;
216
+ }
217
+
218
+ /**
219
+ * Emitted when a tool call ends.
220
+ */
221
+ export interface ToolCallEndEvent<TContext = unknown> {
222
+ readonly kind: "tool.call.end";
223
+
224
+ /**
225
+ * The thread ID.
226
+ */
227
+ threadId: string;
228
+
229
+ /**
230
+ * The agent that executed this tool.
231
+ */
232
+ agentId: string;
233
+
234
+ /**
235
+ * The context for this execution.
236
+ *
237
+ * NOTE: Includes `context.agent` reference for tools - may be optimized in future.
238
+ */
239
+ context: Context<TContext>;
240
+
241
+ /**
242
+ * The tool that was called.
243
+ */
244
+ toolId: string;
245
+
246
+ /**
247
+ * Unique identifier for this call.
248
+ */
249
+ callId: string;
250
+
251
+ /**
252
+ * Final state of the tool call.
253
+ */
254
+ state: ToolCallState;
255
+
256
+ /**
257
+ * Result if state is "completed".
258
+ */
259
+ result?: string;
260
+
261
+ /**
262
+ * Error message if state is "failed", null if successful.
263
+ */
264
+ error: string | null;
265
+ }
266
+
267
+ // --- Union ---
268
+
269
+ export type LifecycleEvent<TContext = unknown, TOutput = unknown> =
270
+ | ThreadStartEvent<TContext>
271
+ | ThreadStopEvent<TContext, TOutput>
272
+ | ModelCallStartEvent<TContext>
273
+ | ModelCallEndEvent<TContext>
274
+ | ToolCallStartEvent<TContext>
275
+ | ToolCallEndEvent<TContext>;
276
+
277
+ // --- Event Maps ---
278
+
279
+ /**
280
+ * Event map for agent-level lifecycle hooks (typed).
281
+ */
282
+ export type AgentHookEvents<TContext = unknown, TOutput = unknown> = {
283
+ "thread.start": [event: ThreadStartEvent<TContext>];
284
+ "thread.stop": [event: ThreadStopEvent<TContext, TOutput>];
285
+ "model.call.start": [event: ModelCallStartEvent<TContext>];
286
+ "model.call.end": [event: ModelCallEndEvent<TContext>];
287
+ "tool.call.start": [event: ToolCallStartEvent<TContext>];
288
+ "tool.call.end": [event: ToolCallEndEvent<TContext>];
123
289
  };
124
290
 
125
291
  /**
126
- * Event emitter that the kernl uses to emit events for the lifecycle of every agent run.
292
+ * Event map for Kernl-level lifecycle hooks (untyped).
293
+ */
294
+ export type KernlHookEvents = AgentHookEvents<unknown, unknown>;
295
+
296
+ /**
297
+ * Event emitter for agent-level lifecycle events.
298
+ */
299
+ export class AgentHooks<
300
+ TContext = unknown,
301
+ TOutput = unknown,
302
+ > extends Emitter<AgentHookEvents<TContext, TOutput>> {}
303
+
304
+ /**
305
+ * Event emitter for Kernl-level lifecycle events.
127
306
  */
128
- export class KernlHooks<
129
- TContext = UnknownContext,
130
- TOutput extends AgentOutputType = TextOutput,
131
- > extends Emitter<KernlHookEvents<TContext, TOutput>> {}
307
+ export class KernlHooks extends Emitter<KernlHookEvents> {}
@@ -316,7 +316,7 @@ describe("Thread", () => {
316
316
  toolId: "simple",
317
317
  state: IN_PROGRESS,
318
318
  callId: "call_1",
319
- arguments: "first",
319
+ arguments: JSON.stringify({ value: "first" }),
320
320
  },
321
321
  ],
322
322
  finishReason: "stop",
@@ -343,7 +343,7 @@ describe("Thread", () => {
343
343
  toolId: "simple",
344
344
  state: IN_PROGRESS,
345
345
  callId: "call_2",
346
- arguments: "second",
346
+ arguments: JSON.stringify({ value: "second" }),
347
347
  },
348
348
  ],
349
349
  finishReason: "stop",
@@ -19,6 +19,8 @@ import {
19
19
  LanguageModel,
20
20
  LanguageModelItem,
21
21
  LanguageModelRequest,
22
+ type LanguageModelUsage,
23
+ type LanguageModelFinishReason,
22
24
  } from "@kernl-sdk/protocol";
23
25
  import { randomID, filter } from "@kernl-sdk/shared/lib";
24
26
 
@@ -90,8 +92,8 @@ export class Thread<
90
92
  readonly tid: string;
91
93
  readonly namespace: string;
92
94
  readonly agent: Agent<TContext, TOutput>;
93
- readonly context: Context<TContext>;
94
- readonly model: LanguageModel; /* inherited from the agent unless specified */
95
+ context: Context<TContext>;
96
+ model: LanguageModel; /* inherited from the agent unless specified */
95
97
  readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
96
98
  readonly createdAt: Date;
97
99
  readonly updatedAt: Date;
@@ -200,7 +202,7 @@ export class Thread<
200
202
  */
201
203
  private async *_execute(): AsyncGenerator<ThreadStreamEvent, void> {
202
204
  for (;;) {
203
- let err = false;
205
+ let err: Error | undefined = undefined;
204
206
 
205
207
  if (this.abort?.signal.aborted) {
206
208
  return;
@@ -209,7 +211,7 @@ export class Thread<
209
211
  const events = [];
210
212
  for await (const e of this.tick()) {
211
213
  if (e.kind === "error") {
212
- err = true;
214
+ err = e.error;
213
215
  logger.error(e.error); // (TODO): onError callback in options
214
216
  }
215
217
  // we don't want deltas in the history
@@ -220,9 +222,9 @@ export class Thread<
220
222
  yield e;
221
223
  }
222
224
 
223
- // if an error event occurred → terminate
225
+ // if an error event occurred → throw it
224
226
  if (err) {
225
- return;
227
+ throw err;
226
228
  }
227
229
 
228
230
  // if model returns a message with no action intentions → terminal state
@@ -276,21 +278,59 @@ export class Thread<
276
278
 
277
279
  const req = await this.prepareModelRequest(this.history);
278
280
 
281
+ this.agent.emit("model.call.start", {
282
+ kind: "model.call.start",
283
+ provider: this.model.provider,
284
+ modelId: this.model.modelId,
285
+ settings: req.settings ?? {},
286
+ threadId: this.tid,
287
+ agentId: this.agent.id,
288
+ context: this.context,
289
+ });
290
+
291
+ let usage: LanguageModelUsage | undefined;
292
+ let finishReason: LanguageModelFinishReason = "unknown";
293
+
279
294
  try {
280
295
  if (this.model.stream) {
281
- const stream = this.model.stream(req);
282
- for await (const event of stream) {
283
- yield event; // [text-delta, tool-call, message, reasoning, ...]
296
+ for await (const event of this.model.stream(req)) {
297
+ if (event.kind === "finish") {
298
+ usage = event.usage;
299
+ finishReason = event.finishReason;
300
+ }
301
+ yield event;
284
302
  }
285
303
  } else {
286
304
  // fallback: blocking generate, yield events as batch
287
305
  const res = await this.model.generate(req);
306
+ usage = res.usage;
307
+ finishReason = res.finishReason;
288
308
  for (const event of res.content) {
289
309
  yield event;
290
310
  }
291
- // (TODO): this.stats.usage.add(res.usage)
292
311
  }
312
+
313
+ this.agent.emit("model.call.end", {
314
+ kind: "model.call.end",
315
+ provider: this.model.provider,
316
+ modelId: this.model.modelId,
317
+ finishReason,
318
+ usage,
319
+ threadId: this.tid,
320
+ agentId: this.agent.id,
321
+ context: this.context,
322
+ });
293
323
  } catch (error) {
324
+ this.agent.emit("model.call.end", {
325
+ kind: "model.call.end",
326
+ provider: this.model.provider,
327
+ modelId: this.model.modelId,
328
+ finishReason: "error",
329
+ threadId: this.tid,
330
+ agentId: this.agent.id,
331
+ context: this.context,
332
+ });
333
+
294
334
  yield {
295
335
  kind: "error",
296
336
  error: error instanceof Error ? error : new Error(String(error)),
@@ -369,6 +409,8 @@ export class Thread<
369
409
 
370
410
  /**
371
411
  * Cancel the running thread
412
+ *
413
+ * TODO: Emit thread.stop with outcome: 'cancelled' when cancelled
372
414
  */
373
415
  cancel() {
374
416
  this.abort?.abort();
@@ -430,6 +472,18 @@ export class Thread<
430
472
  private async executeTools(calls: ToolCall[]): Promise<ThreadEventInner[]> {
431
473
  return await Promise.all(
432
474
  calls.map(async (call: ToolCall) => {
475
+ const parsedArgs = JSON.parse(call.arguments || "{}");
476
+
477
+ this.agent.emit("tool.call.start", {
478
+ kind: "tool.call.start",
479
+ threadId: this.tid,
480
+ agentId: this.agent.id,
481
+ context: this.context,
482
+ toolId: call.toolId,
483
+ callId: call.callId,
484
+ args: parsedArgs,
485
+ });
486
+
433
487
  try {
434
488
  const tool = this.agent.tool(call.toolId);
435
489
  if (!tool) {
@@ -449,6 +503,21 @@ export class Thread<
449
503
  ctx.approve(call.callId); // mark this call as approved
450
504
  const res = await tool.invoke(ctx, call.arguments, call.callId);
451
505
 
506
+ this.agent.emit("tool.call.end", {
507
+ kind: "tool.call.end",
508
+ threadId: this.tid,
509
+ agentId: this.agent.id,
510
+ context: this.context,
511
+ toolId: call.toolId,
512
+ callId: call.callId,
513
+ state: res.state,
514
+ result:
515
+ typeof res.result === "string"
516
+ ? res.result
517
+ : JSON.stringify(res.result),
518
+ error: res.error,
519
+ });
520
+
452
521
  return {
453
522
  kind: "tool-result" as const,
454
523
  callId: call.callId,
@@ -458,6 +527,17 @@ export class Thread<
458
527
  error: res.error,
459
528
  };
460
529
  } catch (error) {
530
+ this.agent.emit("tool.call.end", {
531
+ kind: "tool.call.end",
532
+ threadId: this.tid,
533
+ agentId: this.agent.id,
534
+ context: this.context,
535
+ toolId: call.toolId,
536
+ callId: call.callId,
537
+ state: FAILED,
538
+ error: error instanceof Error ? error.message : String(error),
539
+ });
540
+
461
541
  return {
462
542
  kind: "tool-result" as const,
463
543
  callId: call.callId,
@@ -179,7 +179,7 @@ export interface ThreadOptions<
179
179
  * Options passed to agent.execute() and agent.stream().
180
180
  */
181
181
  export interface ThreadExecuteOptions<TContext> {
182
- context?: Context<TContext>;
182
+ context?: TContext;
183
183
  model?: LanguageModel;
184
184
  task?: Task<TContext>;
185
185
  threadId?: string;