kernl 0.7.4 → 0.8.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 (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +27 -1
  3. package/dist/agent/types.d.ts +20 -12
  4. package/dist/agent/types.d.ts.map +1 -1
  5. package/dist/agent.d.ts +7 -7
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +3 -14
  8. package/dist/api/resources/agents/agents.d.ts +5 -5
  9. package/dist/api/resources/agents/agents.d.ts.map +1 -1
  10. package/dist/api/resources/agents/agents.js +1 -1
  11. package/dist/guardrail.d.ts +19 -19
  12. package/dist/guardrail.d.ts.map +1 -1
  13. package/dist/kernl/kernl.d.ts +6 -6
  14. package/dist/kernl/kernl.d.ts.map +1 -1
  15. package/dist/lib/error.d.ts +3 -3
  16. package/dist/lib/error.d.ts.map +1 -1
  17. package/dist/lifecycle.d.ts +6 -6
  18. package/dist/lifecycle.d.ts.map +1 -1
  19. package/dist/memory/__tests__/encoder.test.d.ts +2 -0
  20. package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
  21. package/dist/memory/__tests__/encoder.test.js +121 -0
  22. package/dist/memory/codecs/domain.d.ts +6 -0
  23. package/dist/memory/codecs/domain.d.ts.map +1 -1
  24. package/dist/memory/codecs/domain.js +6 -0
  25. package/dist/memory/codecs/tpuf.d.ts.map +1 -1
  26. package/dist/memory/codecs/tpuf.js +2 -5
  27. package/dist/memory/encoder.d.ts +25 -2
  28. package/dist/memory/encoder.d.ts.map +1 -1
  29. package/dist/memory/encoder.js +46 -5
  30. package/dist/memory/index.d.ts +1 -1
  31. package/dist/memory/index.d.ts.map +1 -1
  32. package/dist/memory/index.js +1 -1
  33. package/dist/memory/schema.js +5 -4
  34. package/dist/memory/types.d.ts +2 -1
  35. package/dist/memory/types.d.ts.map +1 -1
  36. package/dist/thread/__tests__/integration.test.d.ts +1 -1
  37. package/dist/thread/__tests__/integration.test.d.ts.map +1 -1
  38. package/dist/thread/__tests__/integration.test.js +10 -5
  39. package/dist/thread/__tests__/thread.test.js +8 -8
  40. package/dist/thread/thread.d.ts +5 -5
  41. package/dist/thread/thread.d.ts.map +1 -1
  42. package/dist/thread/thread.js +13 -2
  43. package/dist/thread/types.d.ts +9 -6
  44. package/dist/thread/types.d.ts.map +1 -1
  45. package/dist/thread/utils.d.ts +7 -6
  46. package/dist/thread/utils.d.ts.map +1 -1
  47. package/dist/thread/utils.js +9 -8
  48. package/package.json +5 -4
  49. package/src/agent/types.ts +25 -29
  50. package/src/agent.ts +15 -28
  51. package/src/api/resources/agents/agents.ts +8 -8
  52. package/src/guardrail.ts +28 -28
  53. package/src/kernl/kernl.ts +12 -12
  54. package/src/lib/error.ts +3 -3
  55. package/src/lifecycle.ts +6 -6
  56. package/src/memory/__tests__/encoder.test.ts +154 -0
  57. package/src/memory/codecs/domain.ts +6 -0
  58. package/src/memory/codecs/tpuf.ts +2 -5
  59. package/src/memory/encoder.ts +51 -6
  60. package/src/memory/index.ts +1 -1
  61. package/src/memory/schema.ts +5 -5
  62. package/src/memory/types.ts +2 -1
  63. package/src/thread/__tests__/integration.test.ts +130 -146
  64. package/src/thread/__tests__/thread.test.ts +8 -8
  65. package/src/thread/thread.ts +21 -7
  66. package/src/thread/types.ts +9 -6
  67. package/src/thread/utils.ts +15 -14
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from "vitest";
2
2
  import { z } from "zod";
3
3
  import { openai } from "@ai-sdk/openai";
4
4
  import { AISDKLanguageModel } from "@kernl-sdk/ai";
5
+ import "@kernl-sdk/ai/openai"; // (TMP)
5
6
 
6
7
  import { Agent } from "@/agent";
7
8
  import { Kernl } from "@/kernl";
@@ -23,21 +24,17 @@ import type { LanguageModelItem } from "@kernl-sdk/protocol";
23
24
 
24
25
  const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
25
26
 
26
- describe.skipIf(SKIP_INTEGRATION_TESTS)(
27
- "Thread streaming integration",
28
- () => {
29
- let kernl: Kernl;
30
- let model: AISDKLanguageModel;
31
-
32
- beforeAll(() => {
33
- kernl = new Kernl();
34
- model = new AISDKLanguageModel(openai("gpt-4o"));
35
- });
36
-
37
- describe("stream()", () => {
38
- it(
39
- "should yield both delta events and complete items",
40
- async () => {
27
+ describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
28
+ let kernl: Kernl;
29
+ let model: AISDKLanguageModel;
30
+
31
+ beforeAll(() => {
32
+ kernl = new Kernl();
33
+ model = new AISDKLanguageModel(openai("gpt-4.1"));
34
+ });
35
+
36
+ describe("stream()", () => {
37
+ it("should yield both delta events and complete items", async () => {
41
38
  const agent = new Agent({
42
39
  id: "test-stream",
43
40
  name: "Test Stream Agent",
@@ -103,13 +100,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
103
100
  // Should have finish event
104
101
  const finishEvents = events.filter((e) => e.kind === "finish");
105
102
  expect(finishEvents.length).toBe(1);
106
- },
107
- 30000,
108
- );
103
+ }, 30000);
109
104
 
110
- it(
111
- "should filter deltas from history but include complete items",
112
- async () => {
105
+ it("should filter deltas from history but include complete items", async () => {
113
106
  const agent = new Agent({
114
107
  id: "test-history",
115
108
  name: "Test History Agent",
@@ -153,8 +146,12 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
153
146
  );
154
147
  expect(streamDeltas.length).toBeGreaterThan(0);
155
148
 
156
- // History should contain the input message
157
- expect(history[0]).toEqual(input[0]);
149
+ // History should contain the input message (with ThreadEvent headers added)
150
+ expect(history[0]).toMatchObject({
151
+ kind: "message",
152
+ role: "user",
153
+ content: [{ kind: "text", text: "Count to 3" }],
154
+ });
158
155
 
159
156
  // History should contain complete Message items
160
157
  const historyMessages = history.filter((e) => e.kind === "message");
@@ -170,11 +167,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
170
167
  );
171
168
  expect(textContent.text).toBeTruthy();
172
169
  expect(textContent.text.length).toBeGreaterThan(0);
173
- },
174
- 30000,
175
- );
170
+ }, 30000);
176
171
 
177
- it("should work with tool calls", async () => {
172
+ it("should work with tool calls", async () => {
178
173
  const addTool = tool({
179
174
  id: "add",
180
175
  name: "add",
@@ -257,122 +252,118 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
257
252
 
258
253
  // Verify the assistant's final response references the correct answer
259
254
  const messages = events.filter((e) => e.kind === "message");
260
- const assistantMessage = messages.find((m: any) => m.role === "assistant");
255
+ const assistantMessage = messages.find(
256
+ (m: any) => m.role === "assistant",
257
+ );
261
258
  expect(assistantMessage).toBeDefined();
262
259
  const textContent = (assistantMessage as any).content.find(
263
260
  (c: any) => c.kind === "text",
264
261
  );
265
262
  expect(textContent).toBeDefined();
266
263
  expect(textContent.text).toContain("42");
264
+ }, 30000);
265
+
266
+ it("should properly encode tool results with matching callIds for multi-turn", async () => {
267
+ const multiplyTool = tool({
268
+ id: "multiply",
269
+ name: "multiply",
270
+ description: "Multiply two numbers",
271
+ parameters: z.object({
272
+ a: z.number().describe("First number"),
273
+ b: z.number().describe("Second number"),
274
+ }),
275
+ execute: async (ctx, { a, b }) => {
276
+ return a * b;
267
277
  },
268
- 30000,
269
- );
278
+ });
270
279
 
271
- it(
272
- "should properly encode tool results with matching callIds for multi-turn",
273
- async () => {
274
- const multiplyTool = tool({
275
- id: "multiply",
276
- name: "multiply",
277
- description: "Multiply two numbers",
278
- parameters: z.object({
279
- a: z.number().describe("First number"),
280
- b: z.number().describe("Second number"),
281
- }),
282
- execute: async (ctx, { a, b }) => {
283
- return a * b;
284
- },
285
- });
286
-
287
- const toolkit = new Toolkit({
288
- id: "math",
289
- tools: [multiplyTool],
290
- });
291
-
292
- const agent = new Agent({
293
- id: "test-multi-turn",
294
- name: "Test Multi-Turn Agent",
295
- instructions: "You are a helpful assistant that can do math.",
296
- model,
297
- toolkits: [toolkit],
298
- });
299
-
300
- const input: LanguageModelItem[] = [
301
- {
302
- kind: "message",
303
- id: "msg-1",
304
- role: "user",
305
- content: [{ kind: "text", text: "What is 7 times 6?" }],
306
- },
307
- ];
308
-
309
- const thread = new Thread({ agent, input });
310
- const events: ThreadStreamEvent[] = [];
311
-
312
- // Collect all events from the stream
313
- for await (const event of thread.stream()) {
314
- events.push(event);
315
- }
316
-
317
- // Find the tool call and result
318
- const toolCalls = events.filter(
319
- (e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
320
- e.kind === "tool-call",
321
- );
322
- const toolResults = events.filter(
323
- (e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
324
- e.kind === "tool-result",
325
- );
326
-
327
- expect(toolCalls.length).toBeGreaterThan(0);
328
- expect(toolResults.length).toBeGreaterThan(0);
329
-
330
- const multiplyCall = toolCalls[0];
331
- const multiplyResult = toolResults[0];
332
-
333
- // Verify callId matches between tool call and result
334
- expect(multiplyCall.callId).toBe(multiplyResult.callId);
335
- expect(multiplyCall.toolId).toBe("multiply");
336
- expect(multiplyResult.toolId).toBe("multiply");
337
-
338
- // Verify the tool result has the correct structure
339
- expect(multiplyResult.callId).toBeDefined();
340
- expect(typeof multiplyResult.callId).toBe("string");
341
- expect(multiplyResult.callId.length).toBeGreaterThan(0);
342
-
343
- // Verify history contains both with matching callIds
344
- const history = (thread as any).history as ThreadEvent[];
345
- const historyToolCall = history.find(
346
- (e) => e.kind === "tool-call" && e.toolId === "multiply",
347
- );
348
- const historyToolResult = history.find(
349
- (e) => e.kind === "tool-result" && e.toolId === "multiply",
350
- );
351
-
352
- expect(historyToolCall).toBeDefined();
353
- expect(historyToolResult).toBeDefined();
354
- expect((historyToolCall as any).callId).toBe(
355
- (historyToolResult as any).callId,
356
- );
357
-
358
- // Verify final response uses the tool result
359
- const messages = events.filter((e) => e.kind === "message");
360
- const assistantMessage = messages.find((m: any) => m.role === "assistant");
361
- expect(assistantMessage).toBeDefined();
362
- const textContent = (assistantMessage as any).content.find(
363
- (c: any) => c.kind === "text",
364
- );
365
- expect(textContent).toBeDefined();
366
- expect(textContent.text).toContain("42");
280
+ const toolkit = new Toolkit({
281
+ id: "math",
282
+ tools: [multiplyTool],
283
+ });
284
+
285
+ const agent = new Agent({
286
+ id: "test-multi-turn",
287
+ name: "Test Multi-Turn Agent",
288
+ instructions: "You are a helpful assistant that can do math.",
289
+ model,
290
+ toolkits: [toolkit],
291
+ });
292
+
293
+ const input: LanguageModelItem[] = [
294
+ {
295
+ kind: "message",
296
+ id: "msg-1",
297
+ role: "user",
298
+ content: [{ kind: "text", text: "What is 7 times 6?" }],
367
299
  },
368
- 30000,
300
+ ];
301
+
302
+ const thread = new Thread({ agent, input });
303
+ const events: ThreadStreamEvent[] = [];
304
+
305
+ // Collect all events from the stream
306
+ for await (const event of thread.stream()) {
307
+ events.push(event);
308
+ }
309
+
310
+ // Find the tool call and result
311
+ const toolCalls = events.filter(
312
+ (e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
313
+ e.kind === "tool-call",
314
+ );
315
+ const toolResults = events.filter(
316
+ (e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
317
+ e.kind === "tool-result",
318
+ );
319
+
320
+ expect(toolCalls.length).toBeGreaterThan(0);
321
+ expect(toolResults.length).toBeGreaterThan(0);
322
+
323
+ const multiplyCall = toolCalls[0];
324
+ const multiplyResult = toolResults[0];
325
+
326
+ // Verify callId matches between tool call and result
327
+ expect(multiplyCall.callId).toBe(multiplyResult.callId);
328
+ expect(multiplyCall.toolId).toBe("multiply");
329
+ expect(multiplyResult.toolId).toBe("multiply");
330
+
331
+ // Verify the tool result has the correct structure
332
+ expect(multiplyResult.callId).toBeDefined();
333
+ expect(typeof multiplyResult.callId).toBe("string");
334
+ expect(multiplyResult.callId.length).toBeGreaterThan(0);
335
+
336
+ // Verify history contains both with matching callIds
337
+ const history = (thread as any).history as ThreadEvent[];
338
+ const historyToolCall = history.find(
339
+ (e) => e.kind === "tool-call" && e.toolId === "multiply",
340
+ );
341
+ const historyToolResult = history.find(
342
+ (e) => e.kind === "tool-result" && e.toolId === "multiply",
369
343
  );
370
- });
371
344
 
372
- describe("execute()", () => {
373
- it(
374
- "should consume stream and return final response",
375
- async () => {
345
+ expect(historyToolCall).toBeDefined();
346
+ expect(historyToolResult).toBeDefined();
347
+ expect((historyToolCall as any).callId).toBe(
348
+ (historyToolResult as any).callId,
349
+ );
350
+
351
+ // Verify final response uses the tool result
352
+ const messages = events.filter((e) => e.kind === "message");
353
+ const assistantMessage = messages.find(
354
+ (m: any) => m.role === "assistant",
355
+ );
356
+ expect(assistantMessage).toBeDefined();
357
+ const textContent = (assistantMessage as any).content.find(
358
+ (c: any) => c.kind === "text",
359
+ );
360
+ expect(textContent).toBeDefined();
361
+ expect(textContent.text).toContain("42");
362
+ }, 30000);
363
+ });
364
+
365
+ describe("execute()", () => {
366
+ it("should consume stream and return final response", async () => {
376
367
  const agent = new Agent({
377
368
  id: "test-blocking",
378
369
  name: "Test Blocking Agent",
@@ -399,14 +390,10 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
399
390
 
400
391
  // Should have final state
401
392
  expect(result.state).toBe("stopped");
402
- },
403
- 30000,
404
- );
393
+ }, 30000);
405
394
 
406
- it(
407
- "should validate structured output in blocking mode",
408
- async () => {
409
- const responseSchema = z.object({
395
+ it("should validate structured output in blocking mode", async () => {
396
+ const PersonSchema = z.object({
410
397
  name: z.string(),
411
398
  age: z.number(),
412
399
  });
@@ -417,7 +404,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
417
404
  instructions:
418
405
  "You are a helpful assistant. Return JSON with name and age fields.",
419
406
  model,
420
- responseType: responseSchema,
407
+ output: PersonSchema,
421
408
  });
422
409
 
423
410
  const input: LanguageModelItem[] = [
@@ -442,9 +429,6 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
442
429
  expect(typeof result.response).toBe("object");
443
430
  expect((result.response as any).name).toBeTruthy();
444
431
  expect(typeof (result.response as any).age).toBe("number");
445
- },
446
- 30000,
447
- );
448
- });
449
- },
450
- );
432
+ }, 30000);
433
+ });
434
+ });
@@ -1061,7 +1061,7 @@ describe("Thread", () => {
1061
1061
  });
1062
1062
 
1063
1063
  describe("Final Output Parsing", () => {
1064
- it("should return text output when responseType is 'text'", async () => {
1064
+ it("should return text output when output is 'text'", async () => {
1065
1065
  const model = createMockModel(async (req: LanguageModelRequest) => {
1066
1066
  return {
1067
1067
  content: [
@@ -1087,7 +1087,7 @@ describe("Thread", () => {
1087
1087
  name: "Test",
1088
1088
  instructions: "Test agent",
1089
1089
  model,
1090
- responseType: "text",
1090
+ output: "text",
1091
1091
  });
1092
1092
 
1093
1093
  const kernl = new Kernl();
@@ -1136,7 +1136,7 @@ describe("Thread", () => {
1136
1136
  name: "Test",
1137
1137
  instructions: "Test agent",
1138
1138
  model,
1139
- responseType: responseSchema,
1139
+ output: responseSchema,
1140
1140
  });
1141
1141
 
1142
1142
  const kernl = new Kernl();
@@ -1186,7 +1186,7 @@ describe("Thread", () => {
1186
1186
  name: "Test",
1187
1187
  instructions: "Test agent",
1188
1188
  model,
1189
- responseType: responseSchema,
1189
+ output: responseSchema,
1190
1190
  });
1191
1191
 
1192
1192
  const kernl = new Kernl();
@@ -1231,7 +1231,7 @@ describe("Thread", () => {
1231
1231
  name: "Test",
1232
1232
  instructions: "Test agent",
1233
1233
  model,
1234
- responseType: responseSchema,
1234
+ output: responseSchema,
1235
1235
  });
1236
1236
 
1237
1237
  const kernl = new Kernl();
@@ -1277,7 +1277,7 @@ describe("Thread", () => {
1277
1277
  name: "Test",
1278
1278
  instructions: "Test agent",
1279
1279
  model,
1280
- responseType: responseSchema,
1280
+ output: responseSchema,
1281
1281
  });
1282
1282
 
1283
1283
  const kernl = new Kernl();
@@ -1336,7 +1336,7 @@ describe("Thread", () => {
1336
1336
  name: "Test",
1337
1337
  instructions: "Test agent",
1338
1338
  model,
1339
- responseType: responseSchema,
1339
+ output: responseSchema,
1340
1340
  });
1341
1341
 
1342
1342
  const kernl = new Kernl();
@@ -1405,7 +1405,7 @@ describe("Thread", () => {
1405
1405
  name: "Test",
1406
1406
  instructions: "Test agent",
1407
1407
  model,
1408
- responseType: "text",
1408
+ output: "text",
1409
1409
  });
1410
1410
 
1411
1411
  const kernl = new Kernl();
@@ -1,4 +1,6 @@
1
1
  import assert from "assert";
2
+ import { ZodType } from "zod";
3
+ import * as z from "zod";
2
4
 
3
5
  import { Agent } from "@/agent";
4
6
  import { Context } from "@/context";
@@ -30,7 +32,8 @@ import type {
30
32
  ThreadExecuteResult,
31
33
  PerformActionsResult,
32
34
  } from "./types";
33
- import type { AgentResponseType } from "@/agent/types";
35
+ import type { AgentOutputType } from "@/agent/types";
36
+ import type { LanguageModelResponseType } from "@kernl-sdk/protocol";
34
37
 
35
38
  import {
36
39
  tevent,
@@ -82,11 +85,11 @@ import {
82
85
  */
83
86
  export class Thread<
84
87
  TContext = unknown,
85
- TResponse extends AgentResponseType = "text",
88
+ TOutput extends AgentOutputType = "text",
86
89
  > {
87
90
  readonly tid: string;
88
91
  readonly namespace: string;
89
- readonly agent: Agent<TContext, TResponse>;
92
+ readonly agent: Agent<TContext, TOutput>;
90
93
  readonly context: Context<TContext>;
91
94
  readonly model: LanguageModel; /* inherited from the agent unless specified */
92
95
  readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
@@ -106,7 +109,7 @@ export class Thread<
106
109
  private abort?: AbortController;
107
110
  private storage?: ThreadStore;
108
111
 
109
- constructor(options: ThreadOptions<TContext, TResponse>) {
112
+ constructor(options: ThreadOptions<TContext, TOutput>) {
110
113
  this.tid = options.tid ?? `tid_${randomID()}`;
111
114
  this.namespace = options.namespace ?? "kernl";
112
115
  this.agent = options.agent;
@@ -142,7 +145,7 @@ export class Thread<
142
145
  * Blocking execution - runs until terminal state or interruption
143
146
  */
144
147
  async execute(): Promise<
145
- ThreadExecuteResult<ResolvedAgentResponse<TResponse>>
148
+ ThreadExecuteResult<ResolvedAgentResponse<TOutput>>
146
149
  > {
147
150
  for await (const _event of this.stream()) {
148
151
  // just consume the stream (already in history in _execute())
@@ -158,7 +161,7 @@ export class Thread<
158
161
 
159
162
  const text = getFinalResponse(items);
160
163
  assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
161
- const parsed = parseFinalResponse(text, this.agent.responseType);
164
+ const parsed = parseFinalResponse(text, this.agent.output);
162
165
 
163
166
  return { response: parsed, state: this.state };
164
167
  }
@@ -405,7 +408,7 @@ export class Thread<
405
408
  e.kind === "tool-result" &&
406
409
  (e.state as any) === "requires_approval" // (TODO): fix this
407
410
  ) {
408
- // Find the original tool call for this pending approval
411
+ // find the original tool call for this pending approval
409
412
  const call = intentions.toolCalls.find((c) => c.callId === e.callId);
410
413
  call && pendingApprovals.push(call);
411
414
  } else {
@@ -508,10 +511,21 @@ export class Thread<
508
511
  );
509
512
  const tools = enabled.map((tool) => tool.serialize());
510
513
 
514
+ // derive responseType from agent.output
515
+ let responseType: LanguageModelResponseType | undefined;
516
+ if (this.agent.output && this.agent.output !== "text") {
517
+ const schema = this.agent.output as ZodType;
518
+ responseType = {
519
+ kind: "json",
520
+ schema: z.toJSONSchema(schema, { target: "draft-7" }) as any,
521
+ };
522
+ }
523
+
511
524
  return {
512
525
  input: filtered,
513
526
  settings,
514
527
  tools,
528
+ responseType,
515
529
  };
516
530
  }
517
531
  }
@@ -15,7 +15,7 @@ import { Task } from "@/task";
15
15
  import { Context } from "@/context";
16
16
  import { Agent } from "@/agent";
17
17
 
18
- import type { AgentResponseType } from "@/agent/types";
18
+ import type { AgentOutputType } from "@/agent/types";
19
19
  import type { ThreadStore } from "@/storage";
20
20
 
21
21
  /**
@@ -23,7 +23,10 @@ import type { ThreadStore } from "@/storage";
23
23
  */
24
24
  export type PublicThreadEvent = LanguageModelItem & ThreadEventBase;
25
25
 
26
- export type TextResponse = "text";
26
+ /**
27
+ * Text output type - indicates the agent returns a plain string.
28
+ */
29
+ export type TextOutput = "text";
27
30
 
28
31
  /**
29
32
  * Thread state values as a const array (for zod schemas).
@@ -61,10 +64,10 @@ export const REQUIRES_APPROVAL = "requires_approval";
61
64
  */
62
65
  export interface IThread<
63
66
  TContext = unknown,
64
- TResponse extends AgentResponseType = "text",
67
+ TOutput extends AgentOutputType = "text",
65
68
  > {
66
69
  tid: string;
67
- agent: Agent<TContext, TResponse>;
70
+ agent: Agent<TContext, TOutput>;
68
71
  model: LanguageModel;
69
72
 
70
73
  context: Context<TContext>;
@@ -148,9 +151,9 @@ export interface ThreadExecuteResult<TResponse = unknown> {
148
151
  */
149
152
  export interface ThreadOptions<
150
153
  TContext = unknown,
151
- TResponse extends AgentResponseType = "text",
154
+ TOutput extends AgentOutputType = "text",
152
155
  > {
153
- agent: Agent<TContext, TResponse>;
156
+ agent: Agent<TContext, TOutput>;
154
157
  input?: LanguageModelItem[];
155
158
  history?: ThreadEvent[];
156
159
  context?: Context<TContext>;
@@ -8,7 +8,7 @@ import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
8
8
  import { ModelBehaviorError } from "@/lib/error";
9
9
 
10
10
  /* types */
11
- import type { AgentResponseType } from "@/agent/types";
11
+ import type { AgentOutputType } from "@/agent/types";
12
12
  import type {
13
13
  ThreadEvent,
14
14
  ThreadEventBase,
@@ -129,30 +129,31 @@ export function getFinalResponse(items: LanguageModelItem[]): string | null {
129
129
  }
130
130
 
131
131
  /**
132
- * (TODO): This should run through the language model's native structured output (if avail)
132
+ * Parse the final response according to the output type schema.
133
133
  *
134
- * Parse the final response according to the response type schema.
135
- * - If responseType is "text", returns the text as-is
136
- * - If responseType is a ZodType, parses and validates the text as JSON
134
+ * This serves as a safety net validation after native structured output from the provider.
135
+ *
136
+ * - If output is "text", returns the text as-is
137
+ * - If output is a ZodType, parses and validates the text as JSON
137
138
  *
138
139
  * @throws {ModelBehaviorError} if structured output validation fails
139
140
  */
140
- export function parseFinalResponse<TResponse extends AgentResponseType>(
141
+ export function parseFinalResponse<TOutput extends AgentOutputType>(
141
142
  text: string,
142
- responseType: TResponse,
143
- ): ResolvedAgentResponse<TResponse> {
144
- if (responseType === "text") {
145
- return text as ResolvedAgentResponse<TResponse>; // text output - return as-is
143
+ output: TOutput,
144
+ ): ResolvedAgentResponse<TOutput> {
145
+ if (output === "text") {
146
+ return text as ResolvedAgentResponse<TOutput>; // text output - return as-is
146
147
  }
147
148
 
148
149
  // structured output - decode JSON and validate with schema
149
- if (responseType && typeof responseType === "object") {
150
+ if (output && typeof output === "object") {
150
151
  // (TODO): prob better way of checking this here
151
- const schema = responseType as ZodType;
152
+ const schema = output as ZodType;
152
153
 
153
154
  try {
154
155
  const validated = json(schema).decode(text); // (TODO): it would be nice if we could use `decodeSafe` here
155
- return validated as ResolvedAgentResponse<TResponse>;
156
+ return validated as ResolvedAgentResponse<TOutput>;
156
157
  } catch (error) {
157
158
  throw new ModelBehaviorError(
158
159
  `Failed to parse structured output: ${error instanceof Error ? error.message : String(error)}`,
@@ -161,5 +162,5 @@ export function parseFinalResponse<TResponse extends AgentResponseType>(
161
162
  }
162
163
 
163
164
  // Fallback - should not reach here
164
- return text as ResolvedAgentResponse<TResponse>;
165
+ return text as ResolvedAgentResponse<TOutput>;
165
166
  }