kernl 0.1.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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +53 -0
- package/LICENSE +201 -0
- package/dist/agent.d.ts +43 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +130 -0
- package/dist/context.d.ts +70 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +111 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +31 -0
- package/dist/error.d.ts +1 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +1 -0
- package/dist/guardrail.d.ts +178 -0
- package/dist/guardrail.d.ts.map +1 -0
- package/dist/guardrail.js +34 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/kernel.d.ts +7 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +7 -0
- package/dist/kernl.d.ts +18 -0
- package/dist/kernl.d.ts.map +1 -0
- package/dist/kernl.js +16 -0
- package/dist/lib/env.d.ts +43 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +29 -0
- package/dist/lib/error.d.ts +88 -0
- package/dist/lib/error.d.ts.map +1 -0
- package/dist/lib/error.js +117 -0
- package/dist/lib/logger.d.ts +36 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +43 -0
- package/dist/lib/serde/__tests__/codec.test.d.ts +2 -0
- package/dist/lib/serde/__tests__/codec.test.d.ts.map +1 -0
- package/dist/lib/serde/__tests__/codec.test.js +75 -0
- package/dist/lib/serde/codec.d.ts +12 -0
- package/dist/lib/serde/codec.d.ts.map +1 -0
- package/dist/lib/serde/codec.js +54 -0
- package/dist/lib/serde/json.d.ts +8 -0
- package/dist/lib/serde/json.d.ts.map +1 -0
- package/dist/lib/serde/json.js +13 -0
- package/dist/lib/serde/thread.d.ts +1 -0
- package/dist/lib/serde/thread.d.ts.map +1 -0
- package/dist/lib/serde/thread.js +172 -0
- package/dist/lib/serde/tool.d.ts +36 -0
- package/dist/lib/serde/tool.d.ts.map +1 -0
- package/dist/lib/serde/tool.js +1 -0
- package/dist/lib/utils.d.ts +19 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +41 -0
- package/dist/lifecycle.d.ts +133 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +29 -0
- package/dist/logger.d.ts +36 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +43 -0
- package/dist/mcp/__tests__/base.test.d.ts +2 -0
- package/dist/mcp/__tests__/base.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/base.test.js +268 -0
- package/dist/mcp/__tests__/fixtures/echo-server.d.ts +3 -0
- package/dist/mcp/__tests__/fixtures/echo-server.d.ts.map +1 -0
- package/dist/mcp/__tests__/fixtures/echo-server.js +92 -0
- package/dist/mcp/__tests__/fixtures/math-server.d.ts +3 -0
- package/dist/mcp/__tests__/fixtures/math-server.d.ts.map +1 -0
- package/dist/mcp/__tests__/fixtures/math-server.js +98 -0
- package/dist/mcp/__tests__/fixtures/server.d.ts +3 -0
- package/dist/mcp/__tests__/fixtures/server.d.ts.map +1 -0
- package/dist/mcp/__tests__/fixtures/server.js +162 -0
- package/dist/mcp/__tests__/fixtures/test-server.d.ts +3 -0
- package/dist/mcp/__tests__/fixtures/test-server.d.ts.map +1 -0
- package/dist/mcp/__tests__/fixtures/test-server.js +163 -0
- package/dist/mcp/__tests__/fixtures/utils.d.ts +17 -0
- package/dist/mcp/__tests__/fixtures/utils.d.ts.map +1 -0
- package/dist/mcp/__tests__/fixtures/utils.js +42 -0
- package/dist/mcp/__tests__/integration.test.d.ts +2 -0
- package/dist/mcp/__tests__/integration.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/integration.test.js +360 -0
- package/dist/mcp/__tests__/stdio.test.d.ts +2 -0
- package/dist/mcp/__tests__/stdio.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/stdio.test.js +180 -0
- package/dist/mcp/__tests__/test-utils.d.ts +17 -0
- package/dist/mcp/__tests__/test-utils.d.ts.map +1 -0
- package/dist/mcp/__tests__/test-utils.js +42 -0
- package/dist/mcp/__tests__/utils.test.d.ts +2 -0
- package/dist/mcp/__tests__/utils.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/utils.test.js +300 -0
- package/dist/mcp/base.d.ts +88 -0
- package/dist/mcp/base.d.ts.map +1 -0
- package/dist/mcp/base.js +68 -0
- package/dist/mcp/http.d.ts +34 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +100 -0
- package/dist/mcp/node.d.ts +60 -0
- package/dist/mcp/node.d.ts.map +1 -0
- package/dist/mcp/node.js +297 -0
- package/dist/mcp/sse.d.ts +34 -0
- package/dist/mcp/sse.d.ts.map +1 -0
- package/dist/mcp/sse.js +97 -0
- package/dist/mcp/stdio.d.ts +32 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +96 -0
- package/dist/mcp/types.d.ts +172 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +16 -0
- package/dist/mcp/utils.d.ts +23 -0
- package/dist/mcp/utils.d.ts.map +1 -0
- package/dist/mcp/utils.js +44 -0
- package/dist/model.d.ts +175 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1 -0
- package/dist/providers/ai.d.ts +1 -0
- package/dist/providers/ai.d.ts.map +1 -0
- package/dist/providers/ai.js +1 -0
- package/dist/providers/default.d.ts +16 -0
- package/dist/providers/default.d.ts.map +1 -0
- package/dist/providers/default.js +17 -0
- package/dist/providers/registry.d.ts +1 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +1 -0
- package/dist/sched/scheduler.d.ts +20 -0
- package/dist/sched/scheduler.d.ts.map +1 -0
- package/dist/sched/scheduler.js +1 -0
- package/dist/sched/task.d.ts +92 -0
- package/dist/sched/task.d.ts.map +1 -0
- package/dist/sched/task.js +102 -0
- package/dist/serde/__tests__/codec.test.d.ts +2 -0
- package/dist/serde/__tests__/codec.test.d.ts.map +1 -0
- package/dist/serde/__tests__/codec.test.js +75 -0
- package/dist/serde/codec.d.ts +12 -0
- package/dist/serde/codec.d.ts.map +1 -0
- package/dist/serde/codec.js +54 -0
- package/dist/serde/json.d.ts +8 -0
- package/dist/serde/json.d.ts.map +1 -0
- package/dist/serde/json.js +13 -0
- package/dist/serde/thread.d.ts +687 -0
- package/dist/serde/thread.d.ts.map +1 -0
- package/dist/serde/thread.js +158 -0
- package/dist/serde/tool.d.ts +36 -0
- package/dist/serde/tool.d.ts.map +1 -0
- package/dist/serde/tool.js +1 -0
- package/dist/session.d.ts +1 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1 -0
- package/dist/task.d.ts +87 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +97 -0
- package/dist/thread/__tests__/mock.d.ts +28 -0
- package/dist/thread/__tests__/mock.d.ts.map +1 -0
- package/dist/thread/__tests__/mock.js +74 -0
- package/dist/thread/__tests__/thread.test.d.ts +2 -0
- package/dist/thread/__tests__/thread.test.d.ts.map +1 -0
- package/dist/thread/__tests__/thread.test.js +1412 -0
- package/dist/thread/index.d.ts +2 -0
- package/dist/thread/index.d.ts.map +1 -0
- package/dist/thread/index.js +1 -0
- package/dist/thread/thread.d.ts +66 -0
- package/dist/thread/thread.d.ts.map +1 -0
- package/dist/thread/thread.js +472 -0
- package/dist/thread/utils.d.ts +19 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/utils.js +50 -0
- package/dist/tool/__tests__/fixtures.d.ts +45 -0
- package/dist/tool/__tests__/fixtures.d.ts.map +1 -0
- package/dist/tool/__tests__/fixtures.js +97 -0
- package/dist/tool/__tests__/tool.test.d.ts +2 -0
- package/dist/tool/__tests__/tool.test.d.ts.map +1 -0
- package/dist/tool/__tests__/tool.test.js +172 -0
- package/dist/tool/__tests__/toolkit.test.d.ts +2 -0
- package/dist/tool/__tests__/toolkit.test.d.ts.map +1 -0
- package/dist/tool/__tests__/toolkit.test.js +134 -0
- package/dist/tool/index.d.ts +4 -0
- package/dist/tool/index.d.ts.map +1 -0
- package/dist/tool/index.js +2 -0
- package/dist/tool/mcp.d.ts +75 -0
- package/dist/tool/mcp.d.ts.map +1 -0
- package/dist/tool/mcp.js +111 -0
- package/dist/tool/tool.d.ts +95 -0
- package/dist/tool/tool.d.ts.map +1 -0
- package/dist/tool/tool.js +176 -0
- package/dist/tool/toolkit.d.ts +121 -0
- package/dist/tool/toolkit.d.ts.map +1 -0
- package/dist/tool/toolkit.js +180 -0
- package/dist/tool/types.d.ts +187 -0
- package/dist/tool/types.d.ts.map +1 -0
- package/dist/tool/types.js +1 -0
- package/dist/tools.d.ts +362 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +220 -0
- package/dist/trace/processor.d.ts +1 -0
- package/dist/trace/processor.d.ts.map +1 -0
- package/dist/trace/processor.js +1 -0
- package/dist/trace/traces.d.ts +1 -0
- package/dist/trace/traces.d.ts.map +1 -0
- package/dist/trace/traces.js +73 -0
- package/dist/trace/utils.d.ts +22 -0
- package/dist/trace/utils.d.ts.map +1 -0
- package/dist/trace/utils.js +30 -0
- package/dist/types/agent.d.ts +91 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/proto.d.ts +1551 -0
- package/dist/types/proto.d.ts.map +1 -0
- package/dist/types/proto.js +531 -0
- package/dist/types/thread.d.ts +71 -0
- package/dist/types/thread.d.ts.map +1 -0
- package/dist/types/thread.js +5 -0
- package/dist/usage.d.ts +43 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +61 -0
- package/package.json +52 -0
- package/src/agent.ts +203 -0
- package/src/context.ts +265 -0
- package/src/guardrail.ts +277 -0
- package/src/index.ts +3 -0
- package/src/kernl.ts +22 -0
- package/src/lib/env.ts +36 -0
- package/src/lib/error.ts +158 -0
- package/src/lib/logger.ts +78 -0
- package/src/lib/serde/json.ts +18 -0
- package/src/lib/serde/thread.ts +188 -0
- package/src/lifecycle.ts +181 -0
- package/src/mcp/__tests__/base.test.ts +344 -0
- package/src/mcp/__tests__/fixtures/server.ts +179 -0
- package/src/mcp/__tests__/fixtures/utils.ts +58 -0
- package/src/mcp/__tests__/integration.test.ts +447 -0
- package/src/mcp/__tests__/stdio.test.ts +236 -0
- package/src/mcp/__tests__/utils.test.ts +360 -0
- package/src/mcp/base.ts +162 -0
- package/src/mcp/http.ts +147 -0
- package/src/mcp/sse.ts +137 -0
- package/src/mcp/stdio.ts +136 -0
- package/src/mcp/types.ts +202 -0
- package/src/mcp/utils.ts +62 -0
- package/src/task.ts +119 -0
- package/src/thread/__tests__/mock.ts +95 -0
- package/src/thread/__tests__/thread.test.ts +1574 -0
- package/src/thread/index.ts +1 -0
- package/src/thread/thread.ts +611 -0
- package/src/thread/utils.ts +67 -0
- package/src/tool/__tests__/fixtures.ts +106 -0
- package/src/tool/__tests__/tool.test.ts +235 -0
- package/src/tool/__tests__/toolkit.test.ts +174 -0
- package/src/tool/index.ts +10 -0
- package/src/tool/tool.ts +264 -0
- package/src/tool/toolkit.ts +234 -0
- package/src/tool/types.ts +243 -0
- package/src/trace/processor.ts +0 -0
- package/src/trace/traces.ts +86 -0
- package/src/trace/utils.ts +38 -0
- package/src/types/agent.ts +145 -0
- package/src/types/thread.ts +86 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { LanguageModel, LanguageModelRequest } from "@kernl/protocol";
|
|
5
|
+
import { IN_PROGRESS, COMPLETED, FAILED } from "@kernl/protocol";
|
|
6
|
+
|
|
7
|
+
import { Thread } from "../thread";
|
|
8
|
+
import { Agent } from "@/agent";
|
|
9
|
+
import { Kernl } from "@/kernl";
|
|
10
|
+
import { Context } from "@/context";
|
|
11
|
+
import { tool, FunctionToolkit } from "@/tool";
|
|
12
|
+
import { ModelBehaviorError } from "@/lib/error";
|
|
13
|
+
|
|
14
|
+
import type { ThreadEvent } from "@/types/thread";
|
|
15
|
+
|
|
16
|
+
describe("Thread", () => {
|
|
17
|
+
describe("Basic Execution", () => {
|
|
18
|
+
it("should execute single turn and terminate with exact history", async () => {
|
|
19
|
+
const model: LanguageModel = {
|
|
20
|
+
spec: "1.0" as const,
|
|
21
|
+
provider: "test",
|
|
22
|
+
modelId: "test-model",
|
|
23
|
+
async generate(req: LanguageModelRequest) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
kind: "message" as const,
|
|
28
|
+
id: "msg_1",
|
|
29
|
+
role: "assistant" as const,
|
|
30
|
+
content: [{ kind: "text" as const, text: "Hello world" }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
finishReason: "stop",
|
|
34
|
+
usage: {
|
|
35
|
+
inputTokens: 2,
|
|
36
|
+
outputTokens: 2,
|
|
37
|
+
totalTokens: 4,
|
|
38
|
+
},
|
|
39
|
+
warnings: [],
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
stream: async function* () {
|
|
43
|
+
throw new Error("Not implemented");
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const agent = new Agent({
|
|
48
|
+
id: "test",
|
|
49
|
+
name: "Test",
|
|
50
|
+
instructions: "Test agent",
|
|
51
|
+
model,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const kernl = new Kernl();
|
|
55
|
+
const thread = new Thread(kernl, agent, "Hello world");
|
|
56
|
+
|
|
57
|
+
const result = await thread.execute();
|
|
58
|
+
|
|
59
|
+
// Access private history via type assertion for testing
|
|
60
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
61
|
+
|
|
62
|
+
expect(history).toEqual([
|
|
63
|
+
{
|
|
64
|
+
kind: "message",
|
|
65
|
+
id: expect.any(String),
|
|
66
|
+
role: "user",
|
|
67
|
+
content: [{ kind: "text", text: "Hello world" }],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: "message",
|
|
71
|
+
id: "msg_1",
|
|
72
|
+
role: "assistant",
|
|
73
|
+
content: [{ kind: "text", text: "Hello world" }],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
expect(result.state.tick).toBe(1);
|
|
78
|
+
expect(result.state.modelResponses).toHaveLength(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should convert string input to UserMessage", async () => {
|
|
82
|
+
const model: LanguageModel = {
|
|
83
|
+
spec: "1.0" as const,
|
|
84
|
+
provider: "test",
|
|
85
|
+
modelId: "test-model",
|
|
86
|
+
async generate(req: LanguageModelRequest) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
kind: "message" as const,
|
|
91
|
+
id: "msg_1",
|
|
92
|
+
role: "assistant" as const,
|
|
93
|
+
content: [{ kind: "text" as const, text: "Response" }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
finishReason: "stop",
|
|
97
|
+
usage: {
|
|
98
|
+
inputTokens: 2,
|
|
99
|
+
outputTokens: 2,
|
|
100
|
+
totalTokens: 4,
|
|
101
|
+
},
|
|
102
|
+
warnings: [],
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
stream: async function* () {
|
|
106
|
+
throw new Error("Not implemented");
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const agent = new Agent({
|
|
111
|
+
id: "test",
|
|
112
|
+
name: "Test",
|
|
113
|
+
instructions: "Test agent",
|
|
114
|
+
model,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const kernl = new Kernl();
|
|
118
|
+
const thread = new Thread(kernl, agent, "Test input");
|
|
119
|
+
|
|
120
|
+
await thread.execute();
|
|
121
|
+
|
|
122
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
123
|
+
const firstMessage = history[0];
|
|
124
|
+
|
|
125
|
+
expect(firstMessage).toEqual({
|
|
126
|
+
kind: "message",
|
|
127
|
+
id: expect.any(String),
|
|
128
|
+
role: "user",
|
|
129
|
+
content: [{ kind: "text", text: "Test input" }],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should use array input as-is", async () => {
|
|
134
|
+
const model: LanguageModel = {
|
|
135
|
+
spec: "1.0" as const,
|
|
136
|
+
provider: "test",
|
|
137
|
+
modelId: "test-model",
|
|
138
|
+
async generate(req: LanguageModelRequest) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
kind: "message" as const,
|
|
143
|
+
id: "msg_1",
|
|
144
|
+
role: "assistant" as const,
|
|
145
|
+
content: [{ kind: "text" as const, text: "Response" }],
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
finishReason: "stop",
|
|
149
|
+
usage: {
|
|
150
|
+
inputTokens: 2,
|
|
151
|
+
outputTokens: 2,
|
|
152
|
+
totalTokens: 4,
|
|
153
|
+
},
|
|
154
|
+
warnings: [],
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
stream: async function* () {
|
|
158
|
+
throw new Error("Not implemented");
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const agent = new Agent({
|
|
163
|
+
id: "test",
|
|
164
|
+
name: "Test",
|
|
165
|
+
instructions: "Test agent",
|
|
166
|
+
model,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const events: ThreadEvent[] = [
|
|
170
|
+
{
|
|
171
|
+
kind: "message",
|
|
172
|
+
id: "custom_msg",
|
|
173
|
+
role: "user",
|
|
174
|
+
content: [{ kind: "text", text: "Custom message" }],
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const kernl = new Kernl();
|
|
179
|
+
const thread = new Thread(kernl, agent, events);
|
|
180
|
+
|
|
181
|
+
await thread.execute();
|
|
182
|
+
|
|
183
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
184
|
+
const firstMessage = history[0];
|
|
185
|
+
|
|
186
|
+
expect(firstMessage).toEqual(events[0]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("Multi-Turn Execution", () => {
|
|
191
|
+
it("should execute multi-turn with tool call and exact history", async () => {
|
|
192
|
+
let callCount = 0;
|
|
193
|
+
|
|
194
|
+
const model: LanguageModel = {
|
|
195
|
+
spec: "1.0" as const,
|
|
196
|
+
provider: "test",
|
|
197
|
+
modelId: "test-model",
|
|
198
|
+
async generate(req: LanguageModelRequest) {
|
|
199
|
+
callCount++;
|
|
200
|
+
|
|
201
|
+
// First call: return tool call
|
|
202
|
+
if (callCount === 1) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
kind: "message" as const,
|
|
207
|
+
id: "msg_1",
|
|
208
|
+
role: "assistant" as const,
|
|
209
|
+
content: [],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
kind: "tool-call" as const,
|
|
213
|
+
toolId: "echo",
|
|
214
|
+
state: IN_PROGRESS,
|
|
215
|
+
callId: "call_1",
|
|
216
|
+
arguments: JSON.stringify({ text: "test" }),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
finishReason: "stop",
|
|
220
|
+
usage: {
|
|
221
|
+
inputTokens: 2,
|
|
222
|
+
outputTokens: 2,
|
|
223
|
+
totalTokens: 4,
|
|
224
|
+
},
|
|
225
|
+
warnings: [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Second call: return final message
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
kind: "message" as const,
|
|
234
|
+
id: "msg_2",
|
|
235
|
+
role: "assistant" as const,
|
|
236
|
+
content: [{ kind: "text" as const, text: "Done!" }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
finishReason: "stop",
|
|
240
|
+
usage: {
|
|
241
|
+
inputTokens: 4,
|
|
242
|
+
outputTokens: 2,
|
|
243
|
+
totalTokens: 6,
|
|
244
|
+
},
|
|
245
|
+
warnings: [],
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
stream: async function* () {
|
|
249
|
+
throw new Error("Not implemented");
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const echoTool = tool({
|
|
254
|
+
id: "echo",
|
|
255
|
+
description: "Echoes input",
|
|
256
|
+
parameters: z.object({ text: z.string() }),
|
|
257
|
+
execute: async (ctx, { text }) => `Echo: ${text}`,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const agent = new Agent({
|
|
261
|
+
id: "test",
|
|
262
|
+
name: "Test",
|
|
263
|
+
instructions: "Test agent",
|
|
264
|
+
model,
|
|
265
|
+
toolkits: [
|
|
266
|
+
new FunctionToolkit({ id: "test-tools", tools: [echoTool] }),
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const kernl = new Kernl();
|
|
271
|
+
const thread = new Thread(kernl, agent, "Use the echo tool");
|
|
272
|
+
|
|
273
|
+
const result = await thread.execute();
|
|
274
|
+
|
|
275
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
276
|
+
|
|
277
|
+
expect(history).toEqual([
|
|
278
|
+
// Initial user message
|
|
279
|
+
{
|
|
280
|
+
kind: "message",
|
|
281
|
+
id: expect.any(String),
|
|
282
|
+
role: "user",
|
|
283
|
+
content: [{ kind: "text", text: "Use the echo tool" }],
|
|
284
|
+
},
|
|
285
|
+
// Assistant message (tick 1)
|
|
286
|
+
{
|
|
287
|
+
kind: "message",
|
|
288
|
+
id: "msg_1",
|
|
289
|
+
role: "assistant",
|
|
290
|
+
content: [],
|
|
291
|
+
},
|
|
292
|
+
// Tool call (tick 1)
|
|
293
|
+
{
|
|
294
|
+
kind: "tool-call",
|
|
295
|
+
id: "echo",
|
|
296
|
+
callId: "call_1",
|
|
297
|
+
name: "echo",
|
|
298
|
+
arguments: JSON.stringify({ text: "test" }),
|
|
299
|
+
},
|
|
300
|
+
// Tool result (executed after tick 1)
|
|
301
|
+
{
|
|
302
|
+
kind: "tool-result",
|
|
303
|
+
callId: "call_1",
|
|
304
|
+
name: "echo",
|
|
305
|
+
state: COMPLETED,
|
|
306
|
+
result: "Echo: test",
|
|
307
|
+
error: null,
|
|
308
|
+
},
|
|
309
|
+
// Final assistant message (tick 2)
|
|
310
|
+
{
|
|
311
|
+
kind: "message",
|
|
312
|
+
id: "msg_2",
|
|
313
|
+
role: "assistant",
|
|
314
|
+
content: [{ kind: "text", text: "Done!" }],
|
|
315
|
+
},
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
expect(result.state.tick).toBe(2);
|
|
319
|
+
expect(result.state.modelResponses).toHaveLength(2);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should accumulate history across multiple turns", async () => {
|
|
323
|
+
let callCount = 0;
|
|
324
|
+
|
|
325
|
+
const model: LanguageModel = {
|
|
326
|
+
spec: "1.0" as const,
|
|
327
|
+
provider: "test",
|
|
328
|
+
modelId: "test-model",
|
|
329
|
+
async generate(req: LanguageModelRequest) {
|
|
330
|
+
callCount++;
|
|
331
|
+
|
|
332
|
+
if (callCount === 1) {
|
|
333
|
+
return {
|
|
334
|
+
content: [
|
|
335
|
+
{
|
|
336
|
+
kind: "message" as const,
|
|
337
|
+
id: "msg_1",
|
|
338
|
+
role: "assistant" as const,
|
|
339
|
+
content: [],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
kind: "tool-call" as const,
|
|
343
|
+
toolId: "simple",
|
|
344
|
+
state: IN_PROGRESS,
|
|
345
|
+
callId: "call_1",
|
|
346
|
+
arguments: "first",
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
finishReason: "stop",
|
|
350
|
+
usage: {
|
|
351
|
+
inputTokens: 2,
|
|
352
|
+
outputTokens: 2,
|
|
353
|
+
totalTokens: 4,
|
|
354
|
+
},
|
|
355
|
+
warnings: [],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (callCount === 2) {
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
kind: "message" as const,
|
|
364
|
+
id: "msg_2",
|
|
365
|
+
role: "assistant" as const,
|
|
366
|
+
content: [],
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
kind: "tool-call" as const,
|
|
370
|
+
toolId: "simple",
|
|
371
|
+
state: IN_PROGRESS,
|
|
372
|
+
callId: "call_2",
|
|
373
|
+
arguments: "second",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
finishReason: "stop",
|
|
377
|
+
usage: {
|
|
378
|
+
inputTokens: 3,
|
|
379
|
+
outputTokens: 2,
|
|
380
|
+
totalTokens: 5,
|
|
381
|
+
},
|
|
382
|
+
warnings: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
kind: "message" as const,
|
|
390
|
+
id: "msg_3",
|
|
391
|
+
role: "assistant" as const,
|
|
392
|
+
content: [{ kind: "text" as const, text: "All done" }],
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
finishReason: "stop",
|
|
396
|
+
usage: {
|
|
397
|
+
inputTokens: 4,
|
|
398
|
+
outputTokens: 2,
|
|
399
|
+
totalTokens: 6,
|
|
400
|
+
},
|
|
401
|
+
warnings: [],
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
stream: async function* () {
|
|
405
|
+
throw new Error("Not implemented");
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const simpleTool = tool({
|
|
410
|
+
id: "simple",
|
|
411
|
+
description: "Simple tool",
|
|
412
|
+
parameters: undefined,
|
|
413
|
+
execute: async (ctx, input: string) => `Result: ${input}`,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const agent = new Agent({
|
|
417
|
+
id: "test",
|
|
418
|
+
name: "Test",
|
|
419
|
+
instructions: "Test agent",
|
|
420
|
+
model,
|
|
421
|
+
toolkits: [
|
|
422
|
+
new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
|
|
423
|
+
],
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const kernl = new Kernl();
|
|
427
|
+
const thread = new Thread(kernl, agent, "Start");
|
|
428
|
+
|
|
429
|
+
const result = await thread.execute();
|
|
430
|
+
|
|
431
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
432
|
+
|
|
433
|
+
// Should have: 1 user msg + 3 assistant msgs + 2 tool calls + 2 tool results = 8 events
|
|
434
|
+
expect(history).toHaveLength(8);
|
|
435
|
+
expect(result.state.tick).toBe(3);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("Tool Execution", () => {
|
|
440
|
+
it("should handle tool not found with exact error in history", async () => {
|
|
441
|
+
let callCount = 0;
|
|
442
|
+
|
|
443
|
+
const model: LanguageModel = {
|
|
444
|
+
spec: "1.0" as const,
|
|
445
|
+
provider: "test",
|
|
446
|
+
modelId: "test-model",
|
|
447
|
+
async generate(req: LanguageModelRequest) {
|
|
448
|
+
callCount++;
|
|
449
|
+
|
|
450
|
+
// First call: return tool call
|
|
451
|
+
if (callCount === 1) {
|
|
452
|
+
return {
|
|
453
|
+
content: [
|
|
454
|
+
{
|
|
455
|
+
kind: "message" as const,
|
|
456
|
+
id: "msg_1",
|
|
457
|
+
role: "assistant" as const,
|
|
458
|
+
content: [],
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
kind: "tool-call" as const,
|
|
462
|
+
toolId: "nonexistent",
|
|
463
|
+
state: IN_PROGRESS,
|
|
464
|
+
callId: "call_1",
|
|
465
|
+
arguments: "{}",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
finishReason: "stop",
|
|
469
|
+
usage: {
|
|
470
|
+
inputTokens: 2,
|
|
471
|
+
outputTokens: 2,
|
|
472
|
+
totalTokens: 4,
|
|
473
|
+
},
|
|
474
|
+
warnings: [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Second call: return terminal message
|
|
479
|
+
return {
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
kind: "message" as const,
|
|
483
|
+
id: "msg_2",
|
|
484
|
+
role: "assistant" as const,
|
|
485
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
finishReason: "stop",
|
|
489
|
+
usage: {
|
|
490
|
+
inputTokens: 2,
|
|
491
|
+
outputTokens: 2,
|
|
492
|
+
totalTokens: 4,
|
|
493
|
+
},
|
|
494
|
+
warnings: [],
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
stream: async function* () {
|
|
498
|
+
throw new Error("Not implemented");
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const agent = new Agent({
|
|
503
|
+
id: "test",
|
|
504
|
+
name: "Test",
|
|
505
|
+
instructions: "Test agent",
|
|
506
|
+
model,
|
|
507
|
+
toolkits: [], // No tools available
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const kernl = new Kernl();
|
|
511
|
+
const thread = new Thread(kernl, agent, "test");
|
|
512
|
+
|
|
513
|
+
await thread.execute();
|
|
514
|
+
|
|
515
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
516
|
+
|
|
517
|
+
// Check that the tool result is an error
|
|
518
|
+
const toolResult = history.find((e) => e.kind === "tool-result");
|
|
519
|
+
expect(toolResult).toEqual({
|
|
520
|
+
kind: "tool-result",
|
|
521
|
+
callId: "call_1",
|
|
522
|
+
toolId: "nonexistent",
|
|
523
|
+
state: FAILED,
|
|
524
|
+
result: undefined,
|
|
525
|
+
error: "Tool nonexistent not found",
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should handle tool execution error", async () => {
|
|
530
|
+
let callCount = 0;
|
|
531
|
+
|
|
532
|
+
const model: LanguageModel = {
|
|
533
|
+
spec: "1.0" as const,
|
|
534
|
+
provider: "test",
|
|
535
|
+
modelId: "test-model",
|
|
536
|
+
async generate(req: LanguageModelRequest) {
|
|
537
|
+
callCount++;
|
|
538
|
+
|
|
539
|
+
// First call: return tool call
|
|
540
|
+
if (callCount === 1) {
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
kind: "message" as const,
|
|
545
|
+
id: "msg_1",
|
|
546
|
+
role: "assistant" as const,
|
|
547
|
+
content: [],
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
kind: "tool-call" as const,
|
|
551
|
+
toolId: "failing",
|
|
552
|
+
state: IN_PROGRESS,
|
|
553
|
+
callId: "call_1",
|
|
554
|
+
arguments: "{}",
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
finishReason: "stop",
|
|
558
|
+
usage: {
|
|
559
|
+
inputTokens: 2,
|
|
560
|
+
outputTokens: 2,
|
|
561
|
+
totalTokens: 4,
|
|
562
|
+
},
|
|
563
|
+
warnings: [],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Second call: return terminal message
|
|
568
|
+
return {
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
kind: "message" as const,
|
|
572
|
+
id: "msg_2",
|
|
573
|
+
role: "assistant" as const,
|
|
574
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
finishReason: "stop",
|
|
578
|
+
usage: {
|
|
579
|
+
inputTokens: 2,
|
|
580
|
+
outputTokens: 2,
|
|
581
|
+
totalTokens: 4,
|
|
582
|
+
},
|
|
583
|
+
warnings: [],
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
stream: async function* () {
|
|
587
|
+
throw new Error("Not implemented");
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const failingTool = tool({
|
|
592
|
+
id: "failing",
|
|
593
|
+
description: "Tool that throws",
|
|
594
|
+
parameters: undefined,
|
|
595
|
+
execute: async () => {
|
|
596
|
+
throw new Error("Execution failed!");
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const agent = new Agent({
|
|
601
|
+
id: "test",
|
|
602
|
+
name: "Test",
|
|
603
|
+
instructions: "Test agent",
|
|
604
|
+
model,
|
|
605
|
+
toolkits: [
|
|
606
|
+
new FunctionToolkit({ id: "test-tools", tools: [failingTool] }),
|
|
607
|
+
],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const kernl = new Kernl();
|
|
611
|
+
const thread = new Thread(kernl, agent, "test");
|
|
612
|
+
|
|
613
|
+
await thread.execute();
|
|
614
|
+
|
|
615
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
616
|
+
|
|
617
|
+
const toolResult = history.find((e) => e.kind === "tool-result");
|
|
618
|
+
expect(toolResult).toMatchObject({
|
|
619
|
+
kind: "tool-result",
|
|
620
|
+
callId: "call_1",
|
|
621
|
+
toolId: "failing",
|
|
622
|
+
state: FAILED,
|
|
623
|
+
result: undefined,
|
|
624
|
+
});
|
|
625
|
+
expect((toolResult as any).error).toContain("Execution failed!");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("should execute tool successfully with result in history", async () => {
|
|
629
|
+
let callCount = 0;
|
|
630
|
+
|
|
631
|
+
const model: LanguageModel = {
|
|
632
|
+
spec: "1.0" as const,
|
|
633
|
+
provider: "test",
|
|
634
|
+
modelId: "test-model",
|
|
635
|
+
async generate(req: LanguageModelRequest) {
|
|
636
|
+
callCount++;
|
|
637
|
+
|
|
638
|
+
// First call: return tool call
|
|
639
|
+
if (callCount === 1) {
|
|
640
|
+
return {
|
|
641
|
+
content: [
|
|
642
|
+
{
|
|
643
|
+
kind: "message" as const,
|
|
644
|
+
id: "msg_1",
|
|
645
|
+
role: "assistant" as const,
|
|
646
|
+
content: [],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
kind: "tool-call" as const,
|
|
650
|
+
toolId: "add",
|
|
651
|
+
state: IN_PROGRESS,
|
|
652
|
+
callId: "call_1",
|
|
653
|
+
arguments: JSON.stringify({ a: 5, b: 3 }),
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
finishReason: "stop",
|
|
657
|
+
usage: {
|
|
658
|
+
inputTokens: 2,
|
|
659
|
+
outputTokens: 2,
|
|
660
|
+
totalTokens: 4,
|
|
661
|
+
},
|
|
662
|
+
warnings: [],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Second call: return terminal message
|
|
667
|
+
return {
|
|
668
|
+
content: [
|
|
669
|
+
{
|
|
670
|
+
kind: "message" as const,
|
|
671
|
+
id: "msg_2",
|
|
672
|
+
role: "assistant" as const,
|
|
673
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
finishReason: "stop",
|
|
677
|
+
usage: {
|
|
678
|
+
inputTokens: 2,
|
|
679
|
+
outputTokens: 2,
|
|
680
|
+
totalTokens: 4,
|
|
681
|
+
},
|
|
682
|
+
warnings: [],
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
stream: async function* () {
|
|
686
|
+
throw new Error("Not implemented");
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const addTool = tool({
|
|
691
|
+
id: "add",
|
|
692
|
+
description: "Adds two numbers",
|
|
693
|
+
parameters: z.object({ a: z.number(), b: z.number() }),
|
|
694
|
+
execute: async (ctx, { a, b }) => a + b,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const agent = new Agent({
|
|
698
|
+
id: "test",
|
|
699
|
+
name: "Test",
|
|
700
|
+
instructions: "Test agent",
|
|
701
|
+
model,
|
|
702
|
+
toolkits: [new FunctionToolkit({ id: "test-tools", tools: [addTool] })],
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const kernl = new Kernl();
|
|
706
|
+
const thread = new Thread(kernl, agent, "Add 5 and 3");
|
|
707
|
+
|
|
708
|
+
await thread.execute();
|
|
709
|
+
|
|
710
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
711
|
+
|
|
712
|
+
const toolResult = history.find((e) => e.kind === "tool-result");
|
|
713
|
+
expect(toolResult).toEqual({
|
|
714
|
+
kind: "tool-result",
|
|
715
|
+
callId: "call_1",
|
|
716
|
+
toolId: "add",
|
|
717
|
+
state: COMPLETED,
|
|
718
|
+
result: 8,
|
|
719
|
+
error: null,
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
describe("Parallel Tool Execution", () => {
|
|
725
|
+
it("should execute multiple tools in parallel with exact history", async () => {
|
|
726
|
+
let callCount = 0;
|
|
727
|
+
|
|
728
|
+
const model: LanguageModel = {
|
|
729
|
+
spec: "1.0" as const,
|
|
730
|
+
provider: "test",
|
|
731
|
+
modelId: "test-model",
|
|
732
|
+
async generate(req: LanguageModelRequest) {
|
|
733
|
+
callCount++;
|
|
734
|
+
|
|
735
|
+
// First call: return multiple tool calls
|
|
736
|
+
if (callCount === 1) {
|
|
737
|
+
return {
|
|
738
|
+
content: [
|
|
739
|
+
{
|
|
740
|
+
kind: "message" as const,
|
|
741
|
+
id: "msg_1",
|
|
742
|
+
role: "assistant" as const,
|
|
743
|
+
content: [],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
kind: "tool-call" as const,
|
|
747
|
+
toolId: "tool1",
|
|
748
|
+
state: IN_PROGRESS,
|
|
749
|
+
callId: "call_1",
|
|
750
|
+
arguments: JSON.stringify({ value: "a" }),
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
kind: "tool-call" as const,
|
|
754
|
+
toolId: "tool2",
|
|
755
|
+
state: IN_PROGRESS,
|
|
756
|
+
callId: "call_2",
|
|
757
|
+
arguments: JSON.stringify({ value: "b" }),
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
finishReason: "stop",
|
|
761
|
+
usage: {
|
|
762
|
+
inputTokens: 2,
|
|
763
|
+
outputTokens: 2,
|
|
764
|
+
totalTokens: 4,
|
|
765
|
+
},
|
|
766
|
+
warnings: [],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Second call: return terminal message
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
kind: "message" as const,
|
|
775
|
+
id: "msg_2",
|
|
776
|
+
role: "assistant" as const,
|
|
777
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
778
|
+
},
|
|
779
|
+
],
|
|
780
|
+
finishReason: "stop",
|
|
781
|
+
usage: {
|
|
782
|
+
inputTokens: 2,
|
|
783
|
+
outputTokens: 2,
|
|
784
|
+
totalTokens: 4,
|
|
785
|
+
},
|
|
786
|
+
warnings: [],
|
|
787
|
+
};
|
|
788
|
+
},
|
|
789
|
+
stream: async function* () {
|
|
790
|
+
throw new Error("Not implemented");
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const tool1 = tool({
|
|
795
|
+
id: "tool1",
|
|
796
|
+
description: "Tool 1",
|
|
797
|
+
parameters: z.object({ value: z.string() }),
|
|
798
|
+
execute: async (ctx, { value }) => `Tool1: ${value}`,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const tool2 = tool({
|
|
802
|
+
id: "tool2",
|
|
803
|
+
description: "Tool 2",
|
|
804
|
+
parameters: z.object({ value: z.string() }),
|
|
805
|
+
execute: async (ctx, { value }) => `Tool2: ${value}`,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const agent = new Agent({
|
|
809
|
+
id: "test",
|
|
810
|
+
name: "Test",
|
|
811
|
+
instructions: "Test agent",
|
|
812
|
+
model,
|
|
813
|
+
toolkits: [
|
|
814
|
+
new FunctionToolkit({ id: "test-tools", tools: [tool1, tool2] }),
|
|
815
|
+
],
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const kernl = new Kernl();
|
|
819
|
+
const thread = new Thread(kernl, agent, "test");
|
|
820
|
+
|
|
821
|
+
await thread.execute();
|
|
822
|
+
|
|
823
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
824
|
+
|
|
825
|
+
// Should have both tool results in history
|
|
826
|
+
const toolResults = history.filter((e) => e.kind === "tool-result");
|
|
827
|
+
expect(toolResults).toHaveLength(2);
|
|
828
|
+
expect(toolResults).toEqual(
|
|
829
|
+
expect.arrayContaining([
|
|
830
|
+
{
|
|
831
|
+
kind: "tool-result",
|
|
832
|
+
callId: "call_1",
|
|
833
|
+
toolId: "tool1",
|
|
834
|
+
state: COMPLETED,
|
|
835
|
+
result: "Tool1: a",
|
|
836
|
+
error: null,
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
kind: "tool-result",
|
|
840
|
+
callId: "call_2",
|
|
841
|
+
toolId: "tool2",
|
|
842
|
+
state: COMPLETED,
|
|
843
|
+
result: "Tool2: b",
|
|
844
|
+
error: null,
|
|
845
|
+
},
|
|
846
|
+
]),
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe("State Management", () => {
|
|
852
|
+
it("should track tick counter correctly", async () => {
|
|
853
|
+
let callCount = 0;
|
|
854
|
+
|
|
855
|
+
const model: LanguageModel = {
|
|
856
|
+
spec: "1.0" as const,
|
|
857
|
+
provider: "test",
|
|
858
|
+
modelId: "test-model",
|
|
859
|
+
async generate(req: LanguageModelRequest) {
|
|
860
|
+
callCount++;
|
|
861
|
+
|
|
862
|
+
if (callCount < 3) {
|
|
863
|
+
return {
|
|
864
|
+
content: [
|
|
865
|
+
{
|
|
866
|
+
kind: "message" as const,
|
|
867
|
+
id: `msg_${callCount}`,
|
|
868
|
+
role: "assistant" as const,
|
|
869
|
+
content: [],
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
kind: "tool-call" as const,
|
|
873
|
+
toolId: "simple",
|
|
874
|
+
state: IN_PROGRESS,
|
|
875
|
+
callId: `call_${callCount}`,
|
|
876
|
+
arguments: "{}",
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
finishReason: "stop",
|
|
880
|
+
usage: {
|
|
881
|
+
inputTokens: 2,
|
|
882
|
+
outputTokens: 2,
|
|
883
|
+
totalTokens: 4,
|
|
884
|
+
},
|
|
885
|
+
warnings: [],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
content: [
|
|
891
|
+
{
|
|
892
|
+
kind: "message" as const,
|
|
893
|
+
id: "msg_final",
|
|
894
|
+
role: "assistant" as const,
|
|
895
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
896
|
+
},
|
|
897
|
+
],
|
|
898
|
+
finishReason: "stop",
|
|
899
|
+
usage: {
|
|
900
|
+
inputTokens: 2,
|
|
901
|
+
outputTokens: 2,
|
|
902
|
+
totalTokens: 4,
|
|
903
|
+
},
|
|
904
|
+
warnings: [],
|
|
905
|
+
};
|
|
906
|
+
},
|
|
907
|
+
stream: async function* () {
|
|
908
|
+
throw new Error("Not implemented");
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const simpleTool = tool({
|
|
913
|
+
id: "simple",
|
|
914
|
+
description: "Simple tool",
|
|
915
|
+
parameters: undefined,
|
|
916
|
+
execute: async () => "result",
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const agent = new Agent({
|
|
920
|
+
id: "test",
|
|
921
|
+
name: "Test",
|
|
922
|
+
instructions: "Test agent",
|
|
923
|
+
model,
|
|
924
|
+
toolkits: [
|
|
925
|
+
new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
|
|
926
|
+
],
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const kernl = new Kernl();
|
|
930
|
+
const thread = new Thread(kernl, agent, "test");
|
|
931
|
+
|
|
932
|
+
const result = await thread.execute();
|
|
933
|
+
|
|
934
|
+
expect(result.state.tick).toBe(3);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("should accumulate model responses", async () => {
|
|
938
|
+
let callCount = 0;
|
|
939
|
+
|
|
940
|
+
const model: LanguageModel = {
|
|
941
|
+
spec: "1.0" as const,
|
|
942
|
+
provider: "test",
|
|
943
|
+
modelId: "test-model",
|
|
944
|
+
async generate(req: LanguageModelRequest) {
|
|
945
|
+
callCount++;
|
|
946
|
+
|
|
947
|
+
if (callCount === 1) {
|
|
948
|
+
return {
|
|
949
|
+
content: [
|
|
950
|
+
{
|
|
951
|
+
kind: "message" as const,
|
|
952
|
+
id: "msg_1",
|
|
953
|
+
role: "assistant" as const,
|
|
954
|
+
content: [],
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
kind: "tool-call" as const,
|
|
958
|
+
toolId: "simple",
|
|
959
|
+
state: IN_PROGRESS,
|
|
960
|
+
callId: "call_1",
|
|
961
|
+
arguments: "{}",
|
|
962
|
+
},
|
|
963
|
+
],
|
|
964
|
+
finishReason: "stop",
|
|
965
|
+
usage: {
|
|
966
|
+
inputTokens: 10,
|
|
967
|
+
outputTokens: 5,
|
|
968
|
+
totalTokens: 15,
|
|
969
|
+
},
|
|
970
|
+
warnings: [],
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
content: [
|
|
976
|
+
{
|
|
977
|
+
kind: "message" as const,
|
|
978
|
+
id: "msg_2",
|
|
979
|
+
role: "assistant" as const,
|
|
980
|
+
content: [{ kind: "text" as const, text: "Done" }],
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
finishReason: "stop",
|
|
984
|
+
usage: {
|
|
985
|
+
inputTokens: 20,
|
|
986
|
+
outputTokens: 10,
|
|
987
|
+
totalTokens: 30,
|
|
988
|
+
},
|
|
989
|
+
warnings: [],
|
|
990
|
+
};
|
|
991
|
+
},
|
|
992
|
+
stream: async function* () {
|
|
993
|
+
throw new Error("Not implemented");
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
const simpleTool = tool({
|
|
998
|
+
id: "simple",
|
|
999
|
+
description: "Simple tool",
|
|
1000
|
+
parameters: undefined,
|
|
1001
|
+
execute: async () => "result",
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const agent = new Agent({
|
|
1005
|
+
id: "test",
|
|
1006
|
+
name: "Test",
|
|
1007
|
+
instructions: "Test agent",
|
|
1008
|
+
model,
|
|
1009
|
+
toolkits: [
|
|
1010
|
+
new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
|
|
1011
|
+
],
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const kernl = new Kernl();
|
|
1015
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1016
|
+
|
|
1017
|
+
const result = await thread.execute();
|
|
1018
|
+
|
|
1019
|
+
expect(result.state.modelResponses).toHaveLength(2);
|
|
1020
|
+
expect(result.state.modelResponses[0].usage.inputTokens).toBe(10);
|
|
1021
|
+
expect(result.state.modelResponses[1].usage.inputTokens).toBe(20);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
describe("Terminal State Detection", () => {
|
|
1026
|
+
it("should terminate when assistant message has no tool calls", async () => {
|
|
1027
|
+
const model: LanguageModel = {
|
|
1028
|
+
spec: "1.0" as const,
|
|
1029
|
+
provider: "test",
|
|
1030
|
+
modelId: "test-model",
|
|
1031
|
+
async generate(req: LanguageModelRequest) {
|
|
1032
|
+
return {
|
|
1033
|
+
content: [
|
|
1034
|
+
{
|
|
1035
|
+
kind: "message" as const,
|
|
1036
|
+
id: "msg_1",
|
|
1037
|
+
role: "assistant" as const,
|
|
1038
|
+
content: [{ kind: "text" as const, text: "Final response" }],
|
|
1039
|
+
},
|
|
1040
|
+
],
|
|
1041
|
+
finishReason: "stop",
|
|
1042
|
+
usage: {
|
|
1043
|
+
inputTokens: 2,
|
|
1044
|
+
outputTokens: 2,
|
|
1045
|
+
totalTokens: 4,
|
|
1046
|
+
},
|
|
1047
|
+
warnings: [],
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
stream: async function* () {
|
|
1051
|
+
throw new Error("Not implemented");
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const agent = new Agent({
|
|
1056
|
+
id: "test",
|
|
1057
|
+
name: "Test",
|
|
1058
|
+
instructions: "Test agent",
|
|
1059
|
+
model,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const kernl = new Kernl();
|
|
1063
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1064
|
+
|
|
1065
|
+
const result = await thread.execute();
|
|
1066
|
+
|
|
1067
|
+
expect(result.state.tick).toBe(1);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it("should continue when assistant message has tool calls", async () => {
|
|
1071
|
+
let callCount = 0;
|
|
1072
|
+
|
|
1073
|
+
const model: LanguageModel = {
|
|
1074
|
+
spec: "1.0" as const,
|
|
1075
|
+
provider: "test",
|
|
1076
|
+
modelId: "test-model",
|
|
1077
|
+
async generate(req: LanguageModelRequest) {
|
|
1078
|
+
callCount++;
|
|
1079
|
+
|
|
1080
|
+
if (callCount === 1) {
|
|
1081
|
+
return {
|
|
1082
|
+
content: [
|
|
1083
|
+
{
|
|
1084
|
+
kind: "message" as const,
|
|
1085
|
+
id: "msg_1",
|
|
1086
|
+
role: "assistant" as const,
|
|
1087
|
+
content: [
|
|
1088
|
+
{ kind: "text" as const, text: "Let me use a tool" },
|
|
1089
|
+
],
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
kind: "tool-call" as const,
|
|
1093
|
+
toolId: "simple",
|
|
1094
|
+
state: IN_PROGRESS,
|
|
1095
|
+
callId: "call_1",
|
|
1096
|
+
arguments: "{}",
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
finishReason: "stop",
|
|
1100
|
+
usage: {
|
|
1101
|
+
inputTokens: 2,
|
|
1102
|
+
outputTokens: 2,
|
|
1103
|
+
totalTokens: 4,
|
|
1104
|
+
},
|
|
1105
|
+
warnings: [],
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
content: [
|
|
1111
|
+
{
|
|
1112
|
+
kind: "message" as const,
|
|
1113
|
+
id: "msg_2",
|
|
1114
|
+
role: "assistant" as const,
|
|
1115
|
+
content: [{ kind: "text" as const, text: "Done now" }],
|
|
1116
|
+
},
|
|
1117
|
+
],
|
|
1118
|
+
finishReason: "stop",
|
|
1119
|
+
usage: {
|
|
1120
|
+
inputTokens: 3,
|
|
1121
|
+
outputTokens: 2,
|
|
1122
|
+
totalTokens: 5,
|
|
1123
|
+
},
|
|
1124
|
+
warnings: [],
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
stream: async function* () {
|
|
1128
|
+
throw new Error("Not implemented");
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const simpleTool = tool({
|
|
1133
|
+
id: "simple",
|
|
1134
|
+
description: "Simple tool",
|
|
1135
|
+
parameters: undefined,
|
|
1136
|
+
execute: async () => "result",
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const agent = new Agent({
|
|
1140
|
+
id: "test",
|
|
1141
|
+
name: "Test",
|
|
1142
|
+
instructions: "Test agent",
|
|
1143
|
+
model,
|
|
1144
|
+
toolkits: [
|
|
1145
|
+
new FunctionToolkit({ id: "test-tools", tools: [simpleTool] }),
|
|
1146
|
+
],
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
const kernl = new Kernl();
|
|
1150
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1151
|
+
|
|
1152
|
+
const result = await thread.execute();
|
|
1153
|
+
|
|
1154
|
+
// Should have made 2 calls - first with tool, second without
|
|
1155
|
+
expect(result.state.tick).toBe(2);
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
describe("Final Output Parsing", () => {
|
|
1160
|
+
it("should return text output when responseType is 'text'", async () => {
|
|
1161
|
+
const model: LanguageModel = {
|
|
1162
|
+
spec: "1.0" as const,
|
|
1163
|
+
provider: "test",
|
|
1164
|
+
modelId: "test-model",
|
|
1165
|
+
async generate(req: LanguageModelRequest) {
|
|
1166
|
+
return {
|
|
1167
|
+
content: [
|
|
1168
|
+
{
|
|
1169
|
+
kind: "message" as const,
|
|
1170
|
+
id: "msg_1",
|
|
1171
|
+
role: "assistant" as const,
|
|
1172
|
+
content: [{ kind: "text" as const, text: "Hello, world!" }],
|
|
1173
|
+
},
|
|
1174
|
+
],
|
|
1175
|
+
finishReason: "stop",
|
|
1176
|
+
usage: {
|
|
1177
|
+
inputTokens: 2,
|
|
1178
|
+
outputTokens: 2,
|
|
1179
|
+
totalTokens: 4,
|
|
1180
|
+
},
|
|
1181
|
+
warnings: [],
|
|
1182
|
+
};
|
|
1183
|
+
},
|
|
1184
|
+
stream: async function* () {
|
|
1185
|
+
throw new Error("Not implemented");
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const agent = new Agent({
|
|
1190
|
+
id: "test",
|
|
1191
|
+
name: "Test",
|
|
1192
|
+
instructions: "Test agent",
|
|
1193
|
+
model,
|
|
1194
|
+
responseType: "text",
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const kernl = new Kernl();
|
|
1198
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1199
|
+
|
|
1200
|
+
const result = await thread.execute();
|
|
1201
|
+
|
|
1202
|
+
expect(result.response).toBe("Hello, world!");
|
|
1203
|
+
expect(result.state.tick).toBe(1);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("should parse and validate structured output with valid JSON", async () => {
|
|
1207
|
+
const responseSchema = z.object({
|
|
1208
|
+
name: z.string(),
|
|
1209
|
+
age: z.number(),
|
|
1210
|
+
email: z.string().email(),
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
const model: LanguageModel = {
|
|
1214
|
+
spec: "1.0" as const,
|
|
1215
|
+
provider: "test",
|
|
1216
|
+
modelId: "test-model",
|
|
1217
|
+
async generate(req: LanguageModelRequest) {
|
|
1218
|
+
return {
|
|
1219
|
+
content: [
|
|
1220
|
+
{
|
|
1221
|
+
kind: "message" as const,
|
|
1222
|
+
id: "msg_1",
|
|
1223
|
+
role: "assistant" as const,
|
|
1224
|
+
content: [
|
|
1225
|
+
{
|
|
1226
|
+
kind: "text" as const,
|
|
1227
|
+
text: '{"name": "Alice", "age": 30, "email": "alice@example.com"}',
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
finishReason: "stop",
|
|
1233
|
+
usage: {
|
|
1234
|
+
inputTokens: 2,
|
|
1235
|
+
outputTokens: 2,
|
|
1236
|
+
totalTokens: 4,
|
|
1237
|
+
},
|
|
1238
|
+
warnings: [],
|
|
1239
|
+
};
|
|
1240
|
+
},
|
|
1241
|
+
stream: async function* () {
|
|
1242
|
+
throw new Error("Not implemented");
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
const agent = new Agent({
|
|
1247
|
+
id: "test",
|
|
1248
|
+
name: "Test",
|
|
1249
|
+
instructions: "Test agent",
|
|
1250
|
+
model,
|
|
1251
|
+
responseType: responseSchema,
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
const kernl = new Kernl();
|
|
1255
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1256
|
+
|
|
1257
|
+
const result = await thread.execute();
|
|
1258
|
+
|
|
1259
|
+
expect(result.response).toEqual({
|
|
1260
|
+
name: "Alice",
|
|
1261
|
+
age: 30,
|
|
1262
|
+
email: "alice@example.com",
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it("should throw ModelBehaviorError for invalid JSON syntax", async () => {
|
|
1267
|
+
const responseSchema = z.object({
|
|
1268
|
+
name: z.string(),
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
const model: LanguageModel = {
|
|
1272
|
+
spec: "1.0" as const,
|
|
1273
|
+
provider: "test",
|
|
1274
|
+
modelId: "test-model",
|
|
1275
|
+
async generate(req: LanguageModelRequest) {
|
|
1276
|
+
return {
|
|
1277
|
+
content: [
|
|
1278
|
+
{
|
|
1279
|
+
kind: "message" as const,
|
|
1280
|
+
id: "msg_1",
|
|
1281
|
+
role: "assistant" as const,
|
|
1282
|
+
content: [
|
|
1283
|
+
{
|
|
1284
|
+
kind: "text" as const,
|
|
1285
|
+
text: '{"name": "Alice"', // Invalid JSON - missing closing brace
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
finishReason: "stop",
|
|
1291
|
+
usage: {
|
|
1292
|
+
inputTokens: 2,
|
|
1293
|
+
outputTokens: 2,
|
|
1294
|
+
totalTokens: 4,
|
|
1295
|
+
},
|
|
1296
|
+
warnings: [],
|
|
1297
|
+
};
|
|
1298
|
+
},
|
|
1299
|
+
stream: async function* () {
|
|
1300
|
+
throw new Error("Not implemented");
|
|
1301
|
+
},
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
const agent = new Agent({
|
|
1305
|
+
id: "test",
|
|
1306
|
+
name: "Test",
|
|
1307
|
+
instructions: "Test agent",
|
|
1308
|
+
model,
|
|
1309
|
+
responseType: responseSchema,
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const kernl = new Kernl();
|
|
1313
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1314
|
+
|
|
1315
|
+
await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it("should throw ModelBehaviorError when JSON doesn't match schema", async () => {
|
|
1319
|
+
const responseSchema = z.object({
|
|
1320
|
+
name: z.string(),
|
|
1321
|
+
age: z.number(),
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
const model: LanguageModel = {
|
|
1325
|
+
spec: "1.0" as const,
|
|
1326
|
+
provider: "test",
|
|
1327
|
+
modelId: "test-model",
|
|
1328
|
+
async generate(req: LanguageModelRequest) {
|
|
1329
|
+
return {
|
|
1330
|
+
content: [
|
|
1331
|
+
{
|
|
1332
|
+
kind: "message" as const,
|
|
1333
|
+
id: "msg_1",
|
|
1334
|
+
role: "assistant" as const,
|
|
1335
|
+
content: [
|
|
1336
|
+
{
|
|
1337
|
+
kind: "text" as const,
|
|
1338
|
+
text: '{"name": "Alice", "age": "thirty"}', // age is string instead of number
|
|
1339
|
+
},
|
|
1340
|
+
],
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
finishReason: "stop",
|
|
1344
|
+
usage: {
|
|
1345
|
+
inputTokens: 2,
|
|
1346
|
+
outputTokens: 2,
|
|
1347
|
+
totalTokens: 4,
|
|
1348
|
+
},
|
|
1349
|
+
warnings: [],
|
|
1350
|
+
};
|
|
1351
|
+
},
|
|
1352
|
+
stream: async function* () {
|
|
1353
|
+
throw new Error("Not implemented");
|
|
1354
|
+
},
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const agent = new Agent({
|
|
1358
|
+
id: "test",
|
|
1359
|
+
name: "Test",
|
|
1360
|
+
instructions: "Test agent",
|
|
1361
|
+
model,
|
|
1362
|
+
responseType: responseSchema,
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
const kernl = new Kernl();
|
|
1366
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1367
|
+
|
|
1368
|
+
await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
it("should throw ModelBehaviorError when required fields are missing", async () => {
|
|
1372
|
+
const responseSchema = z.object({
|
|
1373
|
+
name: z.string(),
|
|
1374
|
+
age: z.number(),
|
|
1375
|
+
email: z.string(),
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
const model: LanguageModel = {
|
|
1379
|
+
spec: "1.0" as const,
|
|
1380
|
+
provider: "test",
|
|
1381
|
+
modelId: "test-model",
|
|
1382
|
+
async generate(req: LanguageModelRequest) {
|
|
1383
|
+
return {
|
|
1384
|
+
content: [
|
|
1385
|
+
{
|
|
1386
|
+
kind: "message" as const,
|
|
1387
|
+
id: "msg_1",
|
|
1388
|
+
role: "assistant" as const,
|
|
1389
|
+
content: [
|
|
1390
|
+
{
|
|
1391
|
+
kind: "text" as const,
|
|
1392
|
+
text: '{"name": "Alice", "age": 30}', // missing email
|
|
1393
|
+
},
|
|
1394
|
+
],
|
|
1395
|
+
},
|
|
1396
|
+
],
|
|
1397
|
+
finishReason: "stop",
|
|
1398
|
+
usage: {
|
|
1399
|
+
inputTokens: 2,
|
|
1400
|
+
outputTokens: 2,
|
|
1401
|
+
totalTokens: 4,
|
|
1402
|
+
},
|
|
1403
|
+
warnings: [],
|
|
1404
|
+
};
|
|
1405
|
+
},
|
|
1406
|
+
stream: async function* () {
|
|
1407
|
+
throw new Error("Not implemented");
|
|
1408
|
+
},
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
const agent = new Agent({
|
|
1412
|
+
id: "test",
|
|
1413
|
+
name: "Test",
|
|
1414
|
+
instructions: "Test agent",
|
|
1415
|
+
model,
|
|
1416
|
+
responseType: responseSchema,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
const kernl = new Kernl();
|
|
1420
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1421
|
+
|
|
1422
|
+
await expect(thread.execute()).rejects.toThrow(ModelBehaviorError);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it("should handle nested structured output", async () => {
|
|
1426
|
+
const responseSchema = z.object({
|
|
1427
|
+
user: z.object({
|
|
1428
|
+
name: z.string(),
|
|
1429
|
+
profile: z.object({
|
|
1430
|
+
bio: z.string(),
|
|
1431
|
+
age: z.number(),
|
|
1432
|
+
}),
|
|
1433
|
+
}),
|
|
1434
|
+
metadata: z.object({
|
|
1435
|
+
timestamp: z.string(),
|
|
1436
|
+
}),
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
const model: LanguageModel = {
|
|
1440
|
+
spec: "1.0" as const,
|
|
1441
|
+
provider: "test",
|
|
1442
|
+
modelId: "test-model",
|
|
1443
|
+
async generate(req: LanguageModelRequest) {
|
|
1444
|
+
return {
|
|
1445
|
+
content: [
|
|
1446
|
+
{
|
|
1447
|
+
kind: "message" as const,
|
|
1448
|
+
id: "msg_1",
|
|
1449
|
+
role: "assistant" as const,
|
|
1450
|
+
content: [
|
|
1451
|
+
{
|
|
1452
|
+
kind: "text" as const,
|
|
1453
|
+
text: JSON.stringify({
|
|
1454
|
+
user: {
|
|
1455
|
+
name: "Bob",
|
|
1456
|
+
profile: { bio: "Engineer", age: 25 },
|
|
1457
|
+
},
|
|
1458
|
+
metadata: { timestamp: "2024-01-01" },
|
|
1459
|
+
}),
|
|
1460
|
+
},
|
|
1461
|
+
],
|
|
1462
|
+
},
|
|
1463
|
+
],
|
|
1464
|
+
finishReason: "stop",
|
|
1465
|
+
usage: {
|
|
1466
|
+
inputTokens: 2,
|
|
1467
|
+
outputTokens: 2,
|
|
1468
|
+
totalTokens: 4,
|
|
1469
|
+
},
|
|
1470
|
+
warnings: [],
|
|
1471
|
+
};
|
|
1472
|
+
},
|
|
1473
|
+
stream: async function* () {
|
|
1474
|
+
throw new Error("Not implemented");
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
const agent = new Agent({
|
|
1479
|
+
id: "test",
|
|
1480
|
+
name: "Test",
|
|
1481
|
+
instructions: "Test agent",
|
|
1482
|
+
model,
|
|
1483
|
+
responseType: responseSchema,
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const kernl = new Kernl();
|
|
1487
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1488
|
+
|
|
1489
|
+
const result = await thread.execute();
|
|
1490
|
+
|
|
1491
|
+
expect(result.response).toEqual({
|
|
1492
|
+
user: {
|
|
1493
|
+
name: "Bob",
|
|
1494
|
+
profile: { bio: "Engineer", age: 25 },
|
|
1495
|
+
},
|
|
1496
|
+
metadata: { timestamp: "2024-01-01" },
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
it("should continue loop when no text in assistant message", async () => {
|
|
1501
|
+
let callCount = 0;
|
|
1502
|
+
|
|
1503
|
+
const model: LanguageModel = {
|
|
1504
|
+
spec: "1.0" as const,
|
|
1505
|
+
provider: "test",
|
|
1506
|
+
modelId: "test-model",
|
|
1507
|
+
async generate(req: LanguageModelRequest) {
|
|
1508
|
+
callCount++;
|
|
1509
|
+
|
|
1510
|
+
// First call: return empty message (no text)
|
|
1511
|
+
if (callCount === 1) {
|
|
1512
|
+
return {
|
|
1513
|
+
content: [
|
|
1514
|
+
{
|
|
1515
|
+
kind: "message" as const,
|
|
1516
|
+
id: "msg_1",
|
|
1517
|
+
role: "assistant" as const,
|
|
1518
|
+
content: [], // No content
|
|
1519
|
+
},
|
|
1520
|
+
],
|
|
1521
|
+
finishReason: "stop",
|
|
1522
|
+
usage: {
|
|
1523
|
+
inputTokens: 2,
|
|
1524
|
+
outputTokens: 2,
|
|
1525
|
+
totalTokens: 4,
|
|
1526
|
+
},
|
|
1527
|
+
warnings: [],
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Second call: return message with text
|
|
1532
|
+
return {
|
|
1533
|
+
content: [
|
|
1534
|
+
{
|
|
1535
|
+
kind: "message" as const,
|
|
1536
|
+
id: "msg_2",
|
|
1537
|
+
role: "assistant" as const,
|
|
1538
|
+
content: [{ kind: "text" as const, text: "Now I have text" }],
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
finishReason: "stop",
|
|
1542
|
+
usage: {
|
|
1543
|
+
inputTokens: 2,
|
|
1544
|
+
outputTokens: 2,
|
|
1545
|
+
totalTokens: 4,
|
|
1546
|
+
},
|
|
1547
|
+
warnings: [],
|
|
1548
|
+
};
|
|
1549
|
+
},
|
|
1550
|
+
stream: async function* () {
|
|
1551
|
+
throw new Error("Not implemented");
|
|
1552
|
+
},
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const agent = new Agent({
|
|
1556
|
+
id: "test",
|
|
1557
|
+
name: "Test",
|
|
1558
|
+
instructions: "Test agent",
|
|
1559
|
+
model,
|
|
1560
|
+
responseType: "text",
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
const kernl = new Kernl();
|
|
1564
|
+
const thread = new Thread(kernl, agent, "test");
|
|
1565
|
+
|
|
1566
|
+
const result = await thread.execute();
|
|
1567
|
+
|
|
1568
|
+
// Should have made 2 calls
|
|
1569
|
+
expect(callCount).toBe(2);
|
|
1570
|
+
expect(result.response).toBe("Now I have text");
|
|
1571
|
+
expect(result.state.tick).toBe(2);
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
});
|