kernl 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/dist/agent.d.ts +20 -3
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +61 -41
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/kernl.d.ts +27 -1
  10. package/dist/kernl.d.ts.map +1 -1
  11. package/dist/kernl.js +36 -2
  12. package/dist/mcp/__tests__/integration.test.js +16 -0
  13. package/dist/thread/__tests__/fixtures/mock-model.d.ts +7 -0
  14. package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -0
  15. package/dist/thread/__tests__/fixtures/mock-model.js +59 -0
  16. package/dist/thread/__tests__/integration.test.d.ts +2 -0
  17. package/dist/thread/__tests__/integration.test.d.ts.map +1 -0
  18. package/dist/thread/__tests__/integration.test.js +315 -0
  19. package/dist/thread/__tests__/stream.test.d.ts +2 -0
  20. package/dist/thread/__tests__/stream.test.d.ts.map +1 -0
  21. package/dist/thread/__tests__/stream.test.js +244 -0
  22. package/dist/thread/__tests__/thread.test.js +612 -763
  23. package/dist/thread/thread.d.ts +30 -25
  24. package/dist/thread/thread.d.ts.map +1 -1
  25. package/dist/thread/thread.js +114 -314
  26. package/dist/thread/utils.d.ts +16 -1
  27. package/dist/thread/utils.d.ts.map +1 -1
  28. package/dist/thread/utils.js +30 -0
  29. package/dist/tool/index.d.ts +1 -1
  30. package/dist/tool/index.d.ts.map +1 -1
  31. package/dist/tool/index.js +1 -1
  32. package/dist/tool/tool.d.ts.map +1 -1
  33. package/dist/tool/tool.js +6 -2
  34. package/dist/tool/toolkit.d.ts +7 -3
  35. package/dist/tool/toolkit.d.ts.map +1 -1
  36. package/dist/tool/toolkit.js +7 -3
  37. package/dist/types/agent.d.ts +5 -5
  38. package/dist/types/agent.d.ts.map +1 -1
  39. package/dist/types/thread.d.ts +10 -16
  40. package/dist/types/thread.d.ts.map +1 -1
  41. package/package.json +7 -5
  42. package/src/agent.ts +99 -86
  43. package/src/index.ts +1 -1
  44. package/src/kernl.ts +51 -2
  45. package/src/mcp/__tests__/integration.test.ts +17 -0
  46. package/src/thread/__tests__/fixtures/mock-model.ts +71 -0
  47. package/src/thread/__tests__/integration.test.ts +449 -0
  48. package/src/thread/__tests__/thread.test.ts +625 -775
  49. package/src/thread/thread.ts +134 -381
  50. package/src/thread/utils.ts +36 -1
  51. package/src/tool/index.ts +1 -1
  52. package/src/tool/tool.ts +6 -2
  53. package/src/tool/toolkit.ts +10 -3
  54. package/src/types/agent.ts +9 -6
  55. package/src/types/thread.ts +25 -17
@@ -9,9 +9,10 @@ import {
9
9
  ToolCall,
10
10
  LanguageModel,
11
11
  LanguageModelRequest,
12
- LanguageModelResponse,
13
12
  LanguageModelItem,
14
13
  FAILED,
14
+ RUNNING,
15
+ STOPPED,
15
16
  } from "@kernl-sdk/protocol";
16
17
  import { randomID, filter } from "@kernl-sdk/shared/lib";
17
18
 
@@ -21,12 +22,18 @@ import type {
21
22
  ThreadOptions,
22
23
  ThreadExecuteResult,
23
24
  PerformActionsResult,
24
- TickResult,
25
+ ThreadState,
26
+ ThreadStreamEvent,
25
27
  } from "@/types/thread";
26
28
  import type { AgentResponseType } from "@/types/agent";
27
29
  import type { ResolvedAgentResponse } from "@/guardrail";
28
30
 
29
- import { getFinalResponse, parseFinalResponse } from "./utils";
31
+ import {
32
+ notDelta,
33
+ getFinalResponse,
34
+ getIntentions,
35
+ parseFinalResponse,
36
+ } from "./utils";
30
37
 
31
38
  /**
32
39
  * A thread drives the execution loop for an agent.
@@ -43,16 +50,19 @@ export class Thread<
43
50
  readonly model: LanguageModel; /* inherited from the agent unless specified */
44
51
  readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
45
52
  readonly mode: "blocking" | "stream"; /* TODO */
53
+ readonly input: ThreadEvent[]; /* the initial input for the thread */
54
+ // readonly stats: ThreadMetrics;
46
55
 
47
56
  /* state */
48
- readonly state: ThreadState;
49
- readonly input: ThreadEvent[] | string; /* the initial input for the thread */
57
+ _tick: number;
58
+ state: ThreadState;
50
59
  private history: ThreadEvent[] /* events generated during this thread's execution */;
60
+ private abort?: AbortController;
51
61
 
52
62
  constructor(
53
63
  kernl: Kernl,
54
64
  agent: Agent<TContext, TResponse>,
55
- input: ThreadEvent[] | string,
65
+ input: ThreadEvent[],
56
66
  options?: ThreadOptions<TContext>,
57
67
  ) {
58
68
  this.id = `tid_${randomID()}`;
@@ -61,69 +71,110 @@ export class Thread<
61
71
  this.kernl = kernl;
62
72
  this.parent = options?.task ?? null;
63
73
  this.model = options?.model ?? agent.model;
64
- this.state = new ThreadState(); // (TODO): checkpoint ?? new ThreadState()
65
74
  this.mode = "blocking"; // (TODO): add streaming
66
75
  this.input = input;
67
76
 
68
- // Convert string input to user message and initialize history
69
- if (typeof input === "string") {
70
- this.history = [
71
- {
72
- kind: "message",
73
- id: `msg_${randomID()}`,
74
- role: "user",
75
- content: [
76
- {
77
- kind: "text",
78
- text: input,
79
- },
80
- ],
81
- },
82
- ];
83
- } else {
84
- this.history = input;
85
- }
77
+ this._tick = 0;
78
+ this.state = STOPPED;
79
+ this.history = input;
86
80
  }
87
81
 
88
82
  /**
89
- * Main thread execution loop - runs until terminal state or interruption
83
+ * Blocking execution loop - runs until terminal state or interruption
90
84
  */
91
85
  async execute(): Promise<
92
86
  ThreadExecuteResult<ResolvedAgentResponse<TResponse>>
93
87
  > {
94
- while (true) {
95
- const { events, intentions } = await this.tick(); // actions: { syscalls, functions, mcpApprovalRequests }
96
-
97
- this.history.push(...events);
98
-
99
- // // priority 1: syscalls first - these override all other actions
100
- // if (actions.syscalls.length > 0) {
101
- // switch (actions.syscalls.kind) { // is it possible to have more than one?
102
- // case SYS_WAIT:
103
- // return this.state;
104
- // case SYS_EXIT:
105
- // return { state: this.state, output: this.output }
106
- // default:
107
- // }
108
- // }
109
-
110
- // if model returns a message with no actions intentions -> terminal state
88
+ for await (const _event of this.stream()) {
89
+ // just consume the stream (already in history in _execute())
90
+ }
91
+
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
95
+
96
+ const parsed = parseFinalResponse(text, this.agent.responseType);
97
+
98
+ return { response: parsed, state: this.state };
99
+ }
100
+
101
+ /**
102
+ * Streaming execution - returns async iterator of events
103
+ */
104
+ async *stream(): AsyncIterable<ThreadStreamEvent> {
105
+ if (this.state === RUNNING && this.abort) {
106
+ throw new Error("thread already running");
107
+ }
108
+
109
+ this.state = RUNNING;
110
+ this.abort = new AbortController();
111
+
112
+ try {
113
+ yield* this._execute();
114
+ } catch (err) {
115
+ throw err;
116
+ } finally {
117
+ this.state = STOPPED;
118
+ this.abort = undefined;
119
+ }
120
+ }
121
+
122
+ /**
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())
138
+ *
139
+ * NOTE: Streaming structured output deferred for now. Prioritizing correctness + simplicity,
140
+ * and unclear what use cases there would actually be for streaming a structured output (other than maybe gen UI).
141
+ */
142
+ private async *_execute(): AsyncGenerator<ThreadStreamEvent, void> {
143
+ for (;;) {
144
+ if (this.abort?.signal.aborted) {
145
+ return;
146
+ }
147
+
148
+ const events = [];
149
+ for await (const e of this.tick()) {
150
+ // we don't want deltas in the history
151
+ if (notDelta(e)) {
152
+ events.push(e);
153
+ this.history.push(e);
154
+ }
155
+ yield e;
156
+ }
157
+
158
+ // if model returns a message with no action intentions -> terminal state
159
+ const intentions = getIntentions(events);
111
160
  if (!intentions) {
112
161
  const text = getFinalResponse(events);
113
- if (!text) continue; // run again, policy-dependent?
114
-
115
- const parsed = parseFinalResponse(text, this.agent.responseType);
162
+ if (!text) continue; // run again, policy-dependent? (how to ensure no infinite loop here?)
116
163
 
117
164
  // await this.agent.runOutputGuardails(context, state);
118
165
  // this.kernl.emit("thread.terminated", context, output);
119
- return { response: parsed, state: this.state };
166
+ return;
120
167
  }
121
168
 
122
- // perform the actions intended by the model
169
+ // perform actions intended by the model
123
170
  const { actions, pendingApprovals } =
124
171
  await this.performActions(intentions);
125
172
 
126
- this.history.push(...actions);
173
+ // yield action events
174
+ for (const a of actions) {
175
+ this.history.push(a);
176
+ yield a;
177
+ }
127
178
 
128
179
  if (pendingApprovals.length > 0) {
129
180
  // publish a batch approval request containing all of them
@@ -137,49 +188,34 @@ export class Thread<
137
188
  }
138
189
  }
139
190
 
140
- // ----------------------
141
- // Internal helpers
142
- // ----------------------
143
-
144
191
  /**
145
- * A single tick of the thread's execution.
192
+ * A single tick - calls model and yields events as they arrive
146
193
  *
147
- * Prepares the input for the model, gets the response, and then parses into a TickResult
148
- * with the events generated and the model's intentions (actions).
194
+ * NOTE: Streaming structured outputs deferred until concrete use cases emerge.
195
+ * For now, we stream text-delta and tool events, final validation happens in _execute().
149
196
  */
150
- private async tick(): Promise<TickResult> {
151
- this.state.tick++;
197
+ private async *tick(): AsyncGenerator<ThreadStreamEvent> {
198
+ this._tick++;
152
199
 
153
- // // check limits
154
- // if (this.state.tick > this.limits.maxTicks) {
155
- // throw new RuntimeError("resource_limit:max_ticks_exceeded");
156
- // }
200
+ // (TODO): check limits (if this._tick > this.limits.maxTicks)
201
+ // (TODO): run input guardrails on first tick (if this._tick === 1)
157
202
 
158
- // run guardrails on the first tick
159
- if (this.state.tick === 1) {
160
- // await this.agent.runInputGuardrails(this.context, ...?);
161
- }
203
+ const req = await this.prepareModelRequest(this.history);
162
204
 
163
- const req = await this.prepareModelRequest(this.history); // (TODO): how to get input for this tick?
164
-
165
- // if (this.mode === "stream") {
166
- // const stream = this.model.stream(input, {
167
- // system: systemPrompt,
168
- // tools: this.agent.tools /* [systools, tools] */,
169
- // settings: this.agent.modelSettings,
170
- // responseSchema: this.agent.responseType,
171
- // });
172
- // for await (const event of stream) {
173
- // // handle streaming events
174
- // }
175
- // response = stream.collect(); // something like this
176
- // } else {
177
- const res = await this.model.generate(req);
178
-
179
- this.state.modelResponses.push(res);
180
- // this.stats.usage.add(response.usage);
181
-
182
- return this.parseModelResponse(res);
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;
216
+ }
217
+ // (TODO): track usage (this.stats.usage.add(res.usage))
218
+ }
183
219
  }
184
220
 
185
221
  /**
@@ -188,11 +224,21 @@ export class Thread<
188
224
  private async performActions(
189
225
  intentions: ActionSet,
190
226
  ): Promise<PerformActionsResult> {
227
+ // // priority 1: syscalls first - these override all other actions
228
+ // if (actions.syscalls.length > 0) {
229
+ // switch (actions.syscalls.kind) { // is it possible to have more than one?
230
+ // case SYS_WAIT:
231
+ // return this.state;
232
+ // case SYS_EXIT:
233
+ // return { state: this.state, output: this.output }
234
+ // default:
235
+ // }
236
+ // }
237
+
191
238
  // (TODO): refactor into a general actions system - probably shouldn't be handled by Thread
192
239
  const toolEvents = await this.executeTools(intentions.toolCalls);
193
240
  // const mcpEvents = await this.executeMCPRequests(actions.mcpRequests);
194
241
 
195
- // Separate events and pending approvals
196
242
  const actions: ThreadEvent[] = [];
197
243
  const pendingApprovals: ToolCall[] = [];
198
244
 
@@ -200,7 +246,7 @@ export class Thread<
200
246
  for (const e of toolEvents) {
201
247
  if (
202
248
  e.kind === "tool-result" &&
203
- (e.state as any) === "requires_approval"
249
+ (e.state as any) === "requires_approval" // (TODO): fix this
204
250
  ) {
205
251
  // Find the original tool call for this pending approval
206
252
  const originalCall = intentions.toolCalls.find(
@@ -315,297 +361,4 @@ export class Thread<
315
361
  tools,
316
362
  };
317
363
  }
318
-
319
- /**
320
- * @internal
321
- * Parses the model's response into events (for history) and actions (for execution).
322
- */
323
- private parseModelResponse(res: LanguageModelResponse): TickResult {
324
- const events: ThreadEvent[] = [];
325
- const toolCalls: ToolCall[] = [];
326
-
327
- for (const event of res.content) {
328
- switch (event.kind) {
329
- case "tool-call":
330
- // Add to both actions (for execution) and events (for history)
331
- toolCalls.push(event);
332
- // fallthrough
333
- default:
334
- events.push(event);
335
- break;
336
- }
337
- }
338
-
339
- return {
340
- events,
341
- intentions: toolCalls.length > 0 ? { toolCalls } : null,
342
- };
343
- }
344
- }
345
-
346
- /**
347
- * ThreadState tracks the execution state of a single thread.
348
- *
349
- * A thread is created each time a task is scheduled and executes
350
- * the main tick() loop until terminal state.
351
- */
352
- export class ThreadState {
353
- tick: number /* current tick number (starts at 0, increments on each model call) */;
354
- modelResponses: LanguageModelResponse[] /* all model responses received during this thread's execution */;
355
-
356
- constructor() {
357
- this.tick = 0;
358
- this.modelResponses = [];
359
- }
360
-
361
- // /**
362
- // * Check if the thread is in a terminal state - true when last event is an assistant
363
- // * message with no tool calls
364
- // */
365
- // isTerminal(): boolean {
366
- // if (this.history.length === 0) return false;
367
-
368
- // const lastEvent = this.history[this.history.length - 1];
369
- // return lastEvent.kind === "message" && lastEvent.role === "assistant";
370
- // }
371
364
  }
372
-
373
- /**
374
- * Common thread options shared between streaming and non-streaming execution pathways.
375
- */
376
- type SharedThreadOptions<TContext = undefined> = {
377
- context?: TContext | Context<TContext>;
378
- maxTurns?: number;
379
- abort?: AbortSignal;
380
- conversationId?: string;
381
- // sessionInputCallback?: SessionInputCallback;
382
- // callModelInputFilter?: CallModelInputFilter;
383
- };
384
-
385
- // /**
386
- // * The result of an agent run in streaming mode.
387
- // */
388
- // export class StreamedRunResult<
389
- // TContext,
390
- // TAgent extends Agent<TContext, AgentResponseType>,
391
- // >
392
- // extends RunResultBase<TContext, TAgent>
393
- // implements AsyncIterable<ThreadStreamEvent>
394
- // {
395
- // /**
396
- // * The current agent that is running
397
- // */
398
- // public get currentAgent(): TAgent | undefined {
399
- // return this.lastAgent;
400
- // }
401
-
402
- // /**
403
- // * The current turn number
404
- // */
405
- // public currentTurn: number = 0;
406
-
407
- // /**
408
- // * The maximum number of turns that can be run
409
- // */
410
- // public maxTurns: number | undefined;
411
-
412
- // #error: unknown = null;
413
- // #signal?: AbortSignal;
414
- // #readableController:
415
- // | ReadableStreamDefaultController<ThreadStreamEvent>
416
- // | undefined;
417
- // #readableStream: ReadableStream<ThreadStreamEvent>;
418
- // #completedPromise: Promise<void>;
419
- // #completedPromiseResolve: (() => void) | undefined;
420
- // #completedPromiseReject: ((err: unknown) => void) | undefined;
421
- // #cancelled: boolean = false;
422
- // #streamLoopPromise: Promise<void> | undefined;
423
-
424
- // constructor(
425
- // result: {
426
- // state: ThreadState<TContext, TAgent>;
427
- // signal?: AbortSignal;
428
- // } = {} as any,
429
- // ) {
430
- // super(result.state);
431
-
432
- // this.#signal = result.signal;
433
-
434
- // this.#readableStream = new ReadableStream<ThreadStreamEvent>({
435
- // start: (controller) => {
436
- // this.#readableController = controller;
437
- // },
438
- // cancel: () => {
439
- // this.#cancelled = true;
440
- // },
441
- // });
442
-
443
- // this.#completedPromise = new Promise((resolve, reject) => {
444
- // this.#completedPromiseResolve = resolve;
445
- // this.#completedPromiseReject = reject;
446
- // });
447
-
448
- // if (this.#signal) {
449
- // const handleAbort = () => {
450
- // if (this.#cancelled) {
451
- // return;
452
- // }
453
-
454
- // this.#cancelled = true;
455
-
456
- // const controller = this.#readableController;
457
- // this.#readableController = undefined;
458
-
459
- // if (this.#readableStream.locked) {
460
- // if (controller) {
461
- // try {
462
- // controller.close();
463
- // } catch (err) {
464
- // logger.debug(`Failed to close readable stream on abort: ${err}`);
465
- // }
466
- // }
467
- // } else {
468
- // void this.#readableStream
469
- // .cancel(this.#signal?.reason)
470
- // .catch((err) => {
471
- // logger.debug(`Failed to cancel readable stream on abort: ${err}`);
472
- // });
473
- // }
474
-
475
- // this.#completedPromiseResolve?.();
476
- // };
477
-
478
- // if (this.#signal.aborted) {
479
- // handleAbort();
480
- // } else {
481
- // this.#signal.addEventListener("abort", handleAbort, { once: true });
482
- // }
483
- // }
484
- // }
485
-
486
- // /**
487
- // * @internal
488
- // * Adds an item to the stream of output items
489
- // */
490
- // _addItem(item: ThreadStreamEvent) {
491
- // if (!this.cancelled) {
492
- // this.#readableController?.enqueue(item);
493
- // }
494
- // }
495
-
496
- // /**
497
- // * @internal
498
- // * Indicates that the stream has been completed
499
- // */
500
- // _done() {
501
- // if (!this.cancelled && this.#readableController) {
502
- // this.#readableController.close();
503
- // this.#readableController = undefined;
504
- // this.#completedPromiseResolve?.();
505
- // }
506
- // }
507
-
508
- // /**
509
- // * @internal
510
- // * Handles an error in the stream loop.
511
- // */
512
- // _raiseError(err: unknown) {
513
- // if (!this.cancelled && this.#readableController) {
514
- // this.#readableController.error(err);
515
- // this.#readableController = undefined;
516
- // }
517
- // this.#error = err;
518
- // this.#completedPromiseReject?.(err);
519
- // this.#completedPromise.catch((e) => {
520
- // logger.debug(`Resulted in an error: ${e}`);
521
- // });
522
- // }
523
-
524
- // /**
525
- // * Returns true if the stream has been cancelled.
526
- // */
527
- // get cancelled(): boolean {
528
- // return this.#cancelled;
529
- // }
530
-
531
- // /**
532
- // * Returns the underlying readable stream.
533
- // * @returns A readable stream of the agent run.
534
- // */
535
- // toStream(): ReadableStream<ThreadStreamEvent> {
536
- // return this.#readableStream as ReadableStream<ThreadStreamEvent>;
537
- // }
538
-
539
- // /**
540
- // * Await this promise to ensure that the stream has been completed if you are not consuming the
541
- // * stream directly.
542
- // */
543
- // get completed() {
544
- // return this.#completedPromise;
545
- // }
546
-
547
- // /**
548
- // * Error thrown during the run, if any.
549
- // */
550
- // get error() {
551
- // return this.#error;
552
- // }
553
-
554
- // /**
555
- // * Returns a readable stream of the final text output of the agent run.
556
- // *
557
- // * @returns A readable stream of the final output of the agent run.
558
- // * @remarks Pass `{ compatibleWithNodeStreams: true }` to receive a Node.js compatible stream
559
- // * instance.
560
- // */
561
- // toTextStream(): ReadableStream<string>;
562
- // toTextStream(options?: { compatibleWithNodeStreams: true }): Readable;
563
- // toTextStream(options?: {
564
- // compatibleWithNodeStreams?: false;
565
- // }): ReadableStream<string>;
566
- // toTextStream(
567
- // options: { compatibleWithNodeStreams?: boolean } = {},
568
- // ): Readable | ReadableStream<string> {
569
- // const stream = this.#readableStream.pipeThrough(
570
- // new TransformStream<ThreadStreamEvent, string>({
571
- // transform(event, controller) {
572
- // if (
573
- // event.kind === "raw_model_stream_event" && // (TODO): what to do here?
574
- // event.data.kind === "text-delta"
575
- // ) {
576
- // const item = TextDeltaEvent.parse(event); // ??
577
- // controller.enqueue(item.text); // (TODO): is it just the text that we want to return here?
578
- // }
579
- // },
580
- // }),
581
- // );
582
-
583
- // if (options.compatibleWithNodeStreams) {
584
- // return Readable.fromWeb(stream);
585
- // }
586
-
587
- // return stream as ReadableStream<string>;
588
- // }
589
-
590
- // [Symbol.asyncIterator](): AsyncIterator<ThreadStreamEvent> {
591
- // return this.#readableStream[Symbol.asyncIterator]();
592
- // }
593
-
594
- // /**
595
- // * @internal
596
- // * Sets the stream loop promise that completes when the internal stream loop finishes.
597
- // * This is used to defer trace end until all agent work is complete.
598
- // */
599
- // _setStreamLoopPromise(promise: Promise<void>) {
600
- // this.#streamLoopPromise = promise;
601
- // }
602
-
603
- // /**
604
- // * @internal
605
- // * Returns a promise that resolves when the stream loop completes.
606
- // * This is used by the tracing system to wait for all agent work before ending the trace.
607
- // */
608
- // _getStreamLoopPromise(): Promise<void> | undefined {
609
- // return this.#streamLoopPromise;
610
- // }
611
- // }
@@ -4,11 +4,46 @@ import type { ResolvedAgentResponse } from "@/guardrail";
4
4
 
5
5
  /* lib */
6
6
  import { json } from "@kernl-sdk/shared/lib";
7
+ import { ToolCall } from "@kernl-sdk/protocol";
7
8
  import { ModelBehaviorError } from "@/lib/error";
8
9
 
9
10
  /* types */
10
11
  import type { AgentResponseType } from "@/types/agent";
11
- import type { ThreadEvent } from "@/types/thread";
12
+ import type { ThreadEvent, ThreadStreamEvent, ActionSet } from "@/types/thread";
13
+
14
+ /**
15
+ * Check if an event represents an intention (action to be performed)
16
+ */
17
+ export function isActionIntention(event: ThreadEvent): event is ToolCall {
18
+ return event.kind === "tool-call";
19
+ }
20
+
21
+ /**
22
+ * Extract action intentions from a list of events.
23
+ * Returns ActionSet if there are any tool calls, null otherwise.
24
+ */
25
+ export function getIntentions(events: ThreadEvent[]): ActionSet | null {
26
+ const toolCalls = events.filter(isActionIntention);
27
+ return toolCalls.length > 0 ? { toolCalls } : null;
28
+ }
29
+
30
+ /**
31
+ * Check if an event is NOT a delta/start/end event (i.e., a complete item).
32
+ * Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
33
+ */
34
+ export function notDelta(event: ThreadStreamEvent): event is ThreadEvent {
35
+ switch (event.kind) {
36
+ case "message":
37
+ case "reasoning":
38
+ case "tool-call":
39
+ case "tool-result":
40
+ return true;
41
+
42
+ // all other events are streaming deltas/control events
43
+ default:
44
+ return false;
45
+ }
46
+ }
12
47
 
13
48
  /**
14
49
  * Extract the final text response from a list of events.
package/src/tool/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { BaseTool, FunctionTool, HostedTool, tool } from "./tool";
2
- export { Toolkit, FunctionToolkit, MCPToolkit } from "./toolkit";
2
+ export { BaseToolkit, Toolkit, FunctionToolkit, MCPToolkit } from "./toolkit";
3
3
  export type {
4
4
  Tool,
5
5
  ToolResult,
package/src/tool/tool.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { Context, UnknownContext } from "@/context";
2
3
 
3
4
  import { ModelBehaviorError } from "@/lib/error";
@@ -167,10 +168,11 @@ export class FunctionTool<
167
168
  }
168
169
  }
169
170
 
170
- // Check if approval is required
171
+ // check if approval is required
171
172
  const needsApproval = await this.requiresApproval(context, parsed, callId);
172
173
  const approvalStatus = callId ? context.approvals.get(callId) : undefined;
173
174
 
175
+ // (TODO): this will become a more detailed action.approval event
174
176
  if (needsApproval && approvalStatus !== "approved") {
175
177
  return {
176
178
  state: INTERRUPTIBLE,
@@ -195,7 +197,9 @@ export class FunctionTool<
195
197
  kind: "function",
196
198
  name: this.id,
197
199
  description: this.description,
198
- parameters: this.parameters as any, // TODO: convert Zod to JSON Schema
200
+ parameters: (this.parameters
201
+ ? z.toJSONSchema(this.parameters, { target: "draft-7" })
202
+ : {}) as any, // JSONSchema7 - target: 'draft-7' produces this
199
203
  };
200
204
  }
201
205
  }