kernl 0.12.4 → 0.12.6
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 +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +29 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/kernl/kernl.d.ts +6 -0
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/kernl/kernl.js +19 -0
- package/dist/kernl/types.d.ts +6 -0
- package/dist/kernl/types.d.ts.map +1 -1
- package/dist/lib/env.d.ts +2 -2
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +1 -5
- package/dist/mcp/sse.d.ts.map +1 -1
- package/dist/mcp/sse.js +1 -5
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -5
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +0 -1
- package/dist/thread/thread.d.ts +5 -4
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +91 -22
- package/dist/thread/types.d.ts +5 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/tracing/__tests__/composite.test.d.ts +2 -0
- package/dist/tracing/__tests__/composite.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/composite.test.js +146 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts +2 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/dispatch.test.js +160 -0
- package/dist/tracing/__tests__/helpers.d.ts +69 -0
- package/dist/tracing/__tests__/helpers.d.ts.map +1 -0
- package/dist/tracing/__tests__/helpers.js +109 -0
- package/dist/tracing/__tests__/integration.test.d.ts +2 -0
- package/dist/tracing/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/integration.test.js +675 -0
- package/dist/tracing/__tests__/span.test.d.ts +2 -0
- package/dist/tracing/__tests__/span.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/span.test.js +188 -0
- package/dist/tracing/dispatch.d.ts +43 -0
- package/dist/tracing/dispatch.d.ts.map +1 -0
- package/dist/tracing/dispatch.js +70 -0
- package/dist/tracing/index.d.ts +8 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +6 -0
- package/dist/tracing/span.d.ts +69 -0
- package/dist/tracing/span.d.ts.map +1 -0
- package/dist/tracing/span.js +64 -0
- package/dist/tracing/subscriber.d.ts +53 -0
- package/dist/tracing/subscriber.d.ts.map +1 -0
- package/dist/tracing/subscriber.js +1 -0
- package/dist/tracing/subscribers/composite.d.ts +26 -0
- package/dist/tracing/subscribers/composite.d.ts.map +1 -0
- package/dist/tracing/subscribers/composite.js +96 -0
- package/dist/tracing/subscribers/console.d.ts +22 -0
- package/dist/tracing/subscribers/console.d.ts.map +1 -0
- package/dist/tracing/subscribers/console.js +82 -0
- package/dist/tracing/types.d.ts +77 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +1 -0
- package/package.json +5 -1
- package/src/agent.ts +2 -0
- package/src/index.ts +1 -0
- package/src/kernl/kernl.ts +21 -0
- package/src/kernl/types.ts +7 -0
- package/src/mcp/http.ts +1 -9
- package/src/mcp/sse.ts +1 -10
- package/src/mcp/stdio.ts +1 -10
- package/src/task.ts +0 -1
- package/src/thread/thread.ts +111 -24
- package/src/thread/types.ts +5 -0
- package/src/tracing/__tests__/composite.test.ts +218 -0
- package/src/tracing/__tests__/dispatch.test.ts +222 -0
- package/src/tracing/__tests__/helpers.ts +138 -0
- package/src/tracing/__tests__/integration.test.ts +808 -0
- package/src/tracing/__tests__/span.test.ts +250 -0
- package/src/tracing/dispatch.ts +114 -0
- package/src/tracing/index.ts +39 -0
- package/src/tracing/span.ts +115 -0
- package/src/tracing/subscriber.ts +62 -0
- package/src/tracing/subscribers/composite.ts +102 -0
- package/src/tracing/subscribers/console.ts +101 -0
- package/src/tracing/types.ts +115 -0
- package/dist/trace/processor.d.ts +0 -1
- package/dist/trace/processor.d.ts.map +0 -1
- package/dist/trace/processor.js +0 -1
- package/dist/trace/traces.d.ts +0 -1
- package/dist/trace/traces.d.ts.map +0 -1
- package/dist/trace/traces.js +0 -73
- package/dist/trace/utils.d.ts +0 -22
- package/dist/trace/utils.d.ts.map +0 -1
- package/dist/trace/utils.js +0 -30
- package/src/trace/processor.ts +0 -0
- package/src/trace/traces.ts +0 -86
- package/src/trace/utils.ts +0 -38
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { IN_PROGRESS, message } from "@kernl-sdk/protocol";
|
|
4
|
+
import { Agent } from "../../agent.js";
|
|
5
|
+
import { Kernl } from "../../kernl/index.js";
|
|
6
|
+
import { Thread } from "../../thread/index.js";
|
|
7
|
+
import { tool, FunctionToolkit } from "../../tool/index.js";
|
|
8
|
+
import { createMockModel } from "../../thread/__tests__/fixtures/mock-model.js";
|
|
9
|
+
import { setSubscriber, clearSubscriber } from "../dispatch.js";
|
|
10
|
+
import { TestSubscriber } from "./helpers.js";
|
|
11
|
+
describe("Tracing Integration", () => {
|
|
12
|
+
let subscriber;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
subscriber = new TestSubscriber();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
clearSubscriber();
|
|
18
|
+
});
|
|
19
|
+
describe("Thread spans", () => {
|
|
20
|
+
it("should create thread span on execution", async () => {
|
|
21
|
+
setSubscriber(subscriber);
|
|
22
|
+
const model = createMockModel(async () => ({
|
|
23
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
24
|
+
finishReason: "stop",
|
|
25
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
26
|
+
warnings: [],
|
|
27
|
+
}));
|
|
28
|
+
const agent = new Agent({
|
|
29
|
+
id: "test-agent",
|
|
30
|
+
name: "Test",
|
|
31
|
+
instructions: "Test",
|
|
32
|
+
model,
|
|
33
|
+
});
|
|
34
|
+
const thread = new Thread({
|
|
35
|
+
agent,
|
|
36
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
37
|
+
});
|
|
38
|
+
await thread.execute();
|
|
39
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
40
|
+
expect(threadSpans).toHaveLength(1);
|
|
41
|
+
expect(threadSpans[0].data).toMatchObject({
|
|
42
|
+
kind: "thread",
|
|
43
|
+
threadId: thread.tid,
|
|
44
|
+
agentId: "test-agent",
|
|
45
|
+
namespace: "kernl",
|
|
46
|
+
});
|
|
47
|
+
expect(threadSpans[0].parent).toBeNull(); // root span
|
|
48
|
+
});
|
|
49
|
+
it("should complete thread span lifecycle", async () => {
|
|
50
|
+
setSubscriber(subscriber);
|
|
51
|
+
const model = createMockModel(async () => ({
|
|
52
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
53
|
+
finishReason: "stop",
|
|
54
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
55
|
+
warnings: [],
|
|
56
|
+
}));
|
|
57
|
+
const agent = new Agent({
|
|
58
|
+
id: "test-agent",
|
|
59
|
+
name: "Test",
|
|
60
|
+
instructions: "Test",
|
|
61
|
+
model,
|
|
62
|
+
});
|
|
63
|
+
const thread = new Thread({
|
|
64
|
+
agent,
|
|
65
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
66
|
+
});
|
|
67
|
+
await thread.execute();
|
|
68
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
69
|
+
const spanId = threadSpans[0].id;
|
|
70
|
+
expect(subscriber.entered.has(spanId)).toBe(true);
|
|
71
|
+
expect(subscriber.closed.has(spanId)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("should record result on successful execution", async () => {
|
|
74
|
+
setSubscriber(subscriber);
|
|
75
|
+
const model = createMockModel(async () => ({
|
|
76
|
+
content: [message({ role: "assistant", text: "Success!" })],
|
|
77
|
+
finishReason: "stop",
|
|
78
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
79
|
+
warnings: [],
|
|
80
|
+
}));
|
|
81
|
+
const agent = new Agent({
|
|
82
|
+
id: "test-agent",
|
|
83
|
+
name: "Test",
|
|
84
|
+
instructions: "Test",
|
|
85
|
+
model,
|
|
86
|
+
});
|
|
87
|
+
const thread = new Thread({
|
|
88
|
+
agent,
|
|
89
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
90
|
+
});
|
|
91
|
+
await thread.execute();
|
|
92
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
93
|
+
const spanId = threadSpans[0].id;
|
|
94
|
+
const recorded = subscriber.getRecorded(spanId);
|
|
95
|
+
expect(recorded.length).toBeGreaterThan(0);
|
|
96
|
+
expect(recorded.some((r) => r.state === "stopped")).toBe(true);
|
|
97
|
+
expect(recorded.some((r) => r.result === "Success!")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it("should record error and emit event on failure", async () => {
|
|
100
|
+
setSubscriber(subscriber);
|
|
101
|
+
const model = createMockModel(async () => {
|
|
102
|
+
throw new Error("Model failed");
|
|
103
|
+
});
|
|
104
|
+
const agent = new Agent({
|
|
105
|
+
id: "test-agent",
|
|
106
|
+
name: "Test",
|
|
107
|
+
instructions: "Test",
|
|
108
|
+
model,
|
|
109
|
+
});
|
|
110
|
+
const thread = new Thread({
|
|
111
|
+
agent,
|
|
112
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
113
|
+
});
|
|
114
|
+
await expect(thread.execute()).rejects.toThrow("Model failed");
|
|
115
|
+
// Check thread span recorded error
|
|
116
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
117
|
+
const spanId = threadSpans[0].id;
|
|
118
|
+
const errors = subscriber.errors.get(spanId);
|
|
119
|
+
expect(errors).toHaveLength(1);
|
|
120
|
+
expect(errors[0].message).toBe("Model failed");
|
|
121
|
+
// Check thread.error event was emitted
|
|
122
|
+
const errorEvents = subscriber.eventsOfKind("thread.error");
|
|
123
|
+
expect(errorEvents).toHaveLength(1);
|
|
124
|
+
expect(errorEvents[0].data.message).toBe("Model failed");
|
|
125
|
+
expect(errorEvents[0].parent).toBeNull(); // parent context not set for events in generators
|
|
126
|
+
});
|
|
127
|
+
it("should include context in thread span", async () => {
|
|
128
|
+
setSubscriber(subscriber);
|
|
129
|
+
const model = createMockModel(async () => ({
|
|
130
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
131
|
+
finishReason: "stop",
|
|
132
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
133
|
+
warnings: [],
|
|
134
|
+
}));
|
|
135
|
+
const agent = new Agent({
|
|
136
|
+
id: "test-agent",
|
|
137
|
+
name: "Test",
|
|
138
|
+
instructions: "Test",
|
|
139
|
+
model,
|
|
140
|
+
});
|
|
141
|
+
// Import Context and pass it properly
|
|
142
|
+
const { Context } = await import("../../context.js");
|
|
143
|
+
const ctx = new Context("kernl", { userId: "user_123" });
|
|
144
|
+
const thread = new Thread({
|
|
145
|
+
agent,
|
|
146
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
147
|
+
context: ctx,
|
|
148
|
+
});
|
|
149
|
+
await thread.execute();
|
|
150
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
151
|
+
// Context is passed from thread.context.context
|
|
152
|
+
expect(threadSpans[0].data.context).toEqual({ userId: "user_123" });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("Model call spans", () => {
|
|
156
|
+
it("should create model.call span nested under thread span", async () => {
|
|
157
|
+
setSubscriber(subscriber);
|
|
158
|
+
const model = createMockModel(async () => ({
|
|
159
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
160
|
+
finishReason: "stop",
|
|
161
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
162
|
+
warnings: [],
|
|
163
|
+
}));
|
|
164
|
+
const agent = new Agent({
|
|
165
|
+
id: "test-agent",
|
|
166
|
+
name: "Test",
|
|
167
|
+
instructions: "Test",
|
|
168
|
+
model,
|
|
169
|
+
});
|
|
170
|
+
const thread = new Thread({
|
|
171
|
+
agent,
|
|
172
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
173
|
+
});
|
|
174
|
+
await thread.execute();
|
|
175
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
176
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
177
|
+
expect(modelSpans).toHaveLength(1);
|
|
178
|
+
expect(modelSpans[0].parent).toBe(threadSpans[0].id);
|
|
179
|
+
});
|
|
180
|
+
it("should include provider and modelId", async () => {
|
|
181
|
+
setSubscriber(subscriber);
|
|
182
|
+
const model = createMockModel(async () => ({
|
|
183
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
184
|
+
finishReason: "stop",
|
|
185
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
186
|
+
warnings: [],
|
|
187
|
+
}));
|
|
188
|
+
const agent = new Agent({
|
|
189
|
+
id: "test-agent",
|
|
190
|
+
name: "Test",
|
|
191
|
+
instructions: "Test",
|
|
192
|
+
model,
|
|
193
|
+
});
|
|
194
|
+
const thread = new Thread({
|
|
195
|
+
agent,
|
|
196
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
197
|
+
});
|
|
198
|
+
await thread.execute();
|
|
199
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
200
|
+
expect(modelSpans[0].data).toMatchObject({
|
|
201
|
+
kind: "model.call",
|
|
202
|
+
provider: "test",
|
|
203
|
+
modelId: "test-model",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
it("should include request in span data", async () => {
|
|
207
|
+
setSubscriber(subscriber);
|
|
208
|
+
const model = createMockModel(async () => ({
|
|
209
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
210
|
+
finishReason: "stop",
|
|
211
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
212
|
+
warnings: [],
|
|
213
|
+
}));
|
|
214
|
+
const agent = new Agent({
|
|
215
|
+
id: "test-agent",
|
|
216
|
+
name: "Test",
|
|
217
|
+
instructions: "Test instructions",
|
|
218
|
+
model,
|
|
219
|
+
});
|
|
220
|
+
const thread = new Thread({
|
|
221
|
+
agent,
|
|
222
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
223
|
+
});
|
|
224
|
+
await thread.execute();
|
|
225
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
226
|
+
expect(modelSpans[0].data.request).toBeDefined();
|
|
227
|
+
expect(modelSpans[0].data.request?.input).toBeDefined();
|
|
228
|
+
});
|
|
229
|
+
it("should record response with usage and finishReason", async () => {
|
|
230
|
+
setSubscriber(subscriber);
|
|
231
|
+
const model = createMockModel(async () => ({
|
|
232
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
233
|
+
finishReason: "stop",
|
|
234
|
+
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
235
|
+
warnings: [],
|
|
236
|
+
}));
|
|
237
|
+
const agent = new Agent({
|
|
238
|
+
id: "test-agent",
|
|
239
|
+
name: "Test",
|
|
240
|
+
instructions: "Test",
|
|
241
|
+
model,
|
|
242
|
+
});
|
|
243
|
+
const thread = new Thread({
|
|
244
|
+
agent,
|
|
245
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
246
|
+
});
|
|
247
|
+
await thread.execute();
|
|
248
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
249
|
+
const spanId = modelSpans[0].id;
|
|
250
|
+
const recorded = subscriber.getRecorded(spanId);
|
|
251
|
+
expect(recorded.length).toBeGreaterThan(0);
|
|
252
|
+
const responseRecord = recorded.find((r) => r.response);
|
|
253
|
+
expect(responseRecord).toBeDefined();
|
|
254
|
+
expect(responseRecord.response.usage).toEqual({
|
|
255
|
+
inputTokens: 10,
|
|
256
|
+
outputTokens: 5,
|
|
257
|
+
totalTokens: 15,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
it("should create multiple model.call spans for multi-turn execution", async () => {
|
|
261
|
+
setSubscriber(subscriber);
|
|
262
|
+
let callCount = 0;
|
|
263
|
+
const model = createMockModel(async () => {
|
|
264
|
+
callCount++;
|
|
265
|
+
if (callCount === 1) {
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
message({ role: "assistant", text: "" }),
|
|
269
|
+
{
|
|
270
|
+
kind: "tool.call",
|
|
271
|
+
toolId: "echo",
|
|
272
|
+
state: IN_PROGRESS,
|
|
273
|
+
callId: "call_1",
|
|
274
|
+
arguments: JSON.stringify({ text: "test" }),
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
finishReason: "stop",
|
|
278
|
+
usage: { inputTokens: 5, outputTokens: 3, totalTokens: 8 },
|
|
279
|
+
warnings: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
284
|
+
finishReason: "stop",
|
|
285
|
+
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
286
|
+
warnings: [],
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
const echoTool = tool({
|
|
290
|
+
id: "echo",
|
|
291
|
+
description: "Echoes input",
|
|
292
|
+
parameters: z.object({ text: z.string() }),
|
|
293
|
+
execute: async (ctx, { text }) => `Echo: ${text}`,
|
|
294
|
+
});
|
|
295
|
+
const agent = new Agent({
|
|
296
|
+
id: "test-agent",
|
|
297
|
+
name: "Test",
|
|
298
|
+
instructions: "Test",
|
|
299
|
+
model,
|
|
300
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [echoTool] })],
|
|
301
|
+
});
|
|
302
|
+
const thread = new Thread({
|
|
303
|
+
agent,
|
|
304
|
+
input: [message({ role: "user", text: "Use echo" })],
|
|
305
|
+
});
|
|
306
|
+
await thread.execute();
|
|
307
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
308
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
309
|
+
expect(modelSpans).toHaveLength(2);
|
|
310
|
+
// Both should be children of the thread span
|
|
311
|
+
expect(modelSpans[0].parent).toBe(threadSpans[0].id);
|
|
312
|
+
expect(modelSpans[1].parent).toBe(threadSpans[0].id);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
describe("Tool call spans", () => {
|
|
316
|
+
it("should create tool.call span nested under thread span", async () => {
|
|
317
|
+
setSubscriber(subscriber);
|
|
318
|
+
let callCount = 0;
|
|
319
|
+
const model = createMockModel(async () => {
|
|
320
|
+
callCount++;
|
|
321
|
+
if (callCount === 1) {
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
message({ role: "assistant", text: "" }),
|
|
325
|
+
{
|
|
326
|
+
kind: "tool.call",
|
|
327
|
+
toolId: "add",
|
|
328
|
+
state: IN_PROGRESS,
|
|
329
|
+
callId: "call_1",
|
|
330
|
+
arguments: JSON.stringify({ a: 5, b: 3 }),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
finishReason: "stop",
|
|
334
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
335
|
+
warnings: [],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
340
|
+
finishReason: "stop",
|
|
341
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
342
|
+
warnings: [],
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
const addTool = tool({
|
|
346
|
+
id: "add",
|
|
347
|
+
description: "Adds numbers",
|
|
348
|
+
parameters: z.object({ a: z.number(), b: z.number() }),
|
|
349
|
+
execute: async (ctx, { a, b }) => a + b,
|
|
350
|
+
});
|
|
351
|
+
const agent = new Agent({
|
|
352
|
+
id: "test-agent",
|
|
353
|
+
name: "Test",
|
|
354
|
+
instructions: "Test",
|
|
355
|
+
model,
|
|
356
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [addTool] })],
|
|
357
|
+
});
|
|
358
|
+
const thread = new Thread({
|
|
359
|
+
agent,
|
|
360
|
+
input: [message({ role: "user", text: "Add" })],
|
|
361
|
+
});
|
|
362
|
+
await thread.execute();
|
|
363
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
364
|
+
const toolSpans = subscriber.spansOfKind("tool.call");
|
|
365
|
+
expect(toolSpans).toHaveLength(1);
|
|
366
|
+
expect(toolSpans[0].parent).toBe(threadSpans[0].id);
|
|
367
|
+
});
|
|
368
|
+
it("should include toolId, callId, and args", async () => {
|
|
369
|
+
setSubscriber(subscriber);
|
|
370
|
+
let callCount = 0;
|
|
371
|
+
const model = createMockModel(async () => {
|
|
372
|
+
callCount++;
|
|
373
|
+
if (callCount === 1) {
|
|
374
|
+
return {
|
|
375
|
+
content: [
|
|
376
|
+
message({ role: "assistant", text: "" }),
|
|
377
|
+
{
|
|
378
|
+
kind: "tool.call",
|
|
379
|
+
toolId: "greet",
|
|
380
|
+
state: IN_PROGRESS,
|
|
381
|
+
callId: "call_xyz",
|
|
382
|
+
arguments: JSON.stringify({ name: "Alice" }),
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
finishReason: "stop",
|
|
386
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
387
|
+
warnings: [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
392
|
+
finishReason: "stop",
|
|
393
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
394
|
+
warnings: [],
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
const greetTool = tool({
|
|
398
|
+
id: "greet",
|
|
399
|
+
description: "Greets someone",
|
|
400
|
+
parameters: z.object({ name: z.string() }),
|
|
401
|
+
execute: async (ctx, { name }) => `Hello, ${name}!`,
|
|
402
|
+
});
|
|
403
|
+
const agent = new Agent({
|
|
404
|
+
id: "test-agent",
|
|
405
|
+
name: "Test",
|
|
406
|
+
instructions: "Test",
|
|
407
|
+
model,
|
|
408
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [greetTool] })],
|
|
409
|
+
});
|
|
410
|
+
const thread = new Thread({
|
|
411
|
+
agent,
|
|
412
|
+
input: [message({ role: "user", text: "Greet" })],
|
|
413
|
+
});
|
|
414
|
+
await thread.execute();
|
|
415
|
+
const toolSpans = subscriber.spansOfKind("tool.call");
|
|
416
|
+
expect(toolSpans[0].data).toMatchObject({
|
|
417
|
+
kind: "tool.call",
|
|
418
|
+
toolId: "greet",
|
|
419
|
+
callId: "call_xyz",
|
|
420
|
+
args: { name: "Alice" },
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
it("should record result and state on success", async () => {
|
|
424
|
+
setSubscriber(subscriber);
|
|
425
|
+
let callCount = 0;
|
|
426
|
+
const model = createMockModel(async () => {
|
|
427
|
+
callCount++;
|
|
428
|
+
if (callCount === 1) {
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
message({ role: "assistant", text: "" }),
|
|
432
|
+
{
|
|
433
|
+
kind: "tool.call",
|
|
434
|
+
toolId: "add",
|
|
435
|
+
state: IN_PROGRESS,
|
|
436
|
+
callId: "call_1",
|
|
437
|
+
arguments: JSON.stringify({ a: 5, b: 3 }),
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
finishReason: "stop",
|
|
441
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
442
|
+
warnings: [],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
447
|
+
finishReason: "stop",
|
|
448
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
449
|
+
warnings: [],
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
const addTool = tool({
|
|
453
|
+
id: "add",
|
|
454
|
+
description: "Adds numbers",
|
|
455
|
+
parameters: z.object({ a: z.number(), b: z.number() }),
|
|
456
|
+
execute: async (ctx, { a, b }) => a + b,
|
|
457
|
+
});
|
|
458
|
+
const agent = new Agent({
|
|
459
|
+
id: "test-agent",
|
|
460
|
+
name: "Test",
|
|
461
|
+
instructions: "Test",
|
|
462
|
+
model,
|
|
463
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [addTool] })],
|
|
464
|
+
});
|
|
465
|
+
const thread = new Thread({
|
|
466
|
+
agent,
|
|
467
|
+
input: [message({ role: "user", text: "Add" })],
|
|
468
|
+
});
|
|
469
|
+
await thread.execute();
|
|
470
|
+
const toolSpans = subscriber.spansOfKind("tool.call");
|
|
471
|
+
const spanId = toolSpans[0].id;
|
|
472
|
+
const recorded = subscriber.getRecorded(spanId);
|
|
473
|
+
expect(recorded.length).toBeGreaterThan(0);
|
|
474
|
+
expect(recorded.some((r) => r.state === "completed")).toBe(true);
|
|
475
|
+
// Tool returns a number (8), which is then stringified by tool result serialization
|
|
476
|
+
expect(recorded.some((r) => r.result === "8" || r.result === 8)).toBe(true);
|
|
477
|
+
});
|
|
478
|
+
it("should record error on tool failure", async () => {
|
|
479
|
+
setSubscriber(subscriber);
|
|
480
|
+
let callCount = 0;
|
|
481
|
+
const model = createMockModel(async () => {
|
|
482
|
+
callCount++;
|
|
483
|
+
if (callCount === 1) {
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
message({ role: "assistant", text: "" }),
|
|
487
|
+
{
|
|
488
|
+
kind: "tool.call",
|
|
489
|
+
toolId: "failing",
|
|
490
|
+
state: IN_PROGRESS,
|
|
491
|
+
callId: "call_1",
|
|
492
|
+
arguments: "{}",
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
finishReason: "stop",
|
|
496
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
497
|
+
warnings: [],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
502
|
+
finishReason: "stop",
|
|
503
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
504
|
+
warnings: [],
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
const failingTool = tool({
|
|
508
|
+
id: "failing",
|
|
509
|
+
description: "Tool that throws",
|
|
510
|
+
parameters: undefined,
|
|
511
|
+
execute: async () => {
|
|
512
|
+
throw new Error("Tool exploded!");
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
const agent = new Agent({
|
|
516
|
+
id: "test-agent",
|
|
517
|
+
name: "Test",
|
|
518
|
+
instructions: "Test",
|
|
519
|
+
model,
|
|
520
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [failingTool] })],
|
|
521
|
+
});
|
|
522
|
+
const thread = new Thread({
|
|
523
|
+
agent,
|
|
524
|
+
input: [message({ role: "user", text: "Fail" })],
|
|
525
|
+
});
|
|
526
|
+
await thread.execute();
|
|
527
|
+
const toolSpans = subscriber.spansOfKind("tool.call");
|
|
528
|
+
const spanId = toolSpans[0].id;
|
|
529
|
+
// Note: tool.invoke() catches execution errors internally and returns a FAILED result,
|
|
530
|
+
// so span.error() is NOT called. The error is recorded via span.record() instead.
|
|
531
|
+
const recorded = subscriber.getRecorded(spanId);
|
|
532
|
+
expect(recorded.some((r) => r.state === "failed")).toBe(true);
|
|
533
|
+
// Error message is wrapped by default error handler, so check for substring
|
|
534
|
+
expect(recorded.some((r) => r.error?.includes("Tool exploded!"))).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
it("should create multiple tool.call spans for parallel tool calls", async () => {
|
|
537
|
+
setSubscriber(subscriber);
|
|
538
|
+
let callCount = 0;
|
|
539
|
+
const model = createMockModel(async () => {
|
|
540
|
+
callCount++;
|
|
541
|
+
if (callCount === 1) {
|
|
542
|
+
return {
|
|
543
|
+
content: [
|
|
544
|
+
message({ role: "assistant", text: "" }),
|
|
545
|
+
{
|
|
546
|
+
kind: "tool.call",
|
|
547
|
+
toolId: "tool1",
|
|
548
|
+
state: IN_PROGRESS,
|
|
549
|
+
callId: "call_1",
|
|
550
|
+
arguments: JSON.stringify({ value: "a" }),
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
kind: "tool.call",
|
|
554
|
+
toolId: "tool2",
|
|
555
|
+
state: IN_PROGRESS,
|
|
556
|
+
callId: "call_2",
|
|
557
|
+
arguments: JSON.stringify({ value: "b" }),
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
finishReason: "stop",
|
|
561
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
562
|
+
warnings: [],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
content: [message({ role: "assistant", text: "Done" })],
|
|
567
|
+
finishReason: "stop",
|
|
568
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
569
|
+
warnings: [],
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
const tool1 = tool({
|
|
573
|
+
id: "tool1",
|
|
574
|
+
description: "Tool 1",
|
|
575
|
+
parameters: z.object({ value: z.string() }),
|
|
576
|
+
execute: async (ctx, { value }) => `T1: ${value}`,
|
|
577
|
+
});
|
|
578
|
+
const tool2 = tool({
|
|
579
|
+
id: "tool2",
|
|
580
|
+
description: "Tool 2",
|
|
581
|
+
parameters: z.object({ value: z.string() }),
|
|
582
|
+
execute: async (ctx, { value }) => `T2: ${value}`,
|
|
583
|
+
});
|
|
584
|
+
const agent = new Agent({
|
|
585
|
+
id: "test-agent",
|
|
586
|
+
name: "Test",
|
|
587
|
+
instructions: "Test",
|
|
588
|
+
model,
|
|
589
|
+
toolkits: [new FunctionToolkit({ id: "tools", tools: [tool1, tool2] })],
|
|
590
|
+
});
|
|
591
|
+
const thread = new Thread({
|
|
592
|
+
agent,
|
|
593
|
+
input: [message({ role: "user", text: "Use tools" })],
|
|
594
|
+
});
|
|
595
|
+
await thread.execute();
|
|
596
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
597
|
+
const toolSpans = subscriber.spansOfKind("tool.call");
|
|
598
|
+
expect(toolSpans).toHaveLength(2);
|
|
599
|
+
// Both should be children of the thread span
|
|
600
|
+
expect(toolSpans[0].parent).toBe(threadSpans[0].id);
|
|
601
|
+
expect(toolSpans[1].parent).toBe(threadSpans[0].id);
|
|
602
|
+
// Verify both tool IDs are present
|
|
603
|
+
const toolIds = toolSpans.map((s) => s.data.toolId);
|
|
604
|
+
expect(toolIds).toContain("tool1");
|
|
605
|
+
expect(toolIds).toContain("tool2");
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
describe("No subscriber configured", () => {
|
|
609
|
+
it("should execute without errors when no subscriber is set", async () => {
|
|
610
|
+
// Don't set subscriber
|
|
611
|
+
const model = createMockModel(async () => ({
|
|
612
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
613
|
+
finishReason: "stop",
|
|
614
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
615
|
+
warnings: [],
|
|
616
|
+
}));
|
|
617
|
+
const agent = new Agent({
|
|
618
|
+
id: "test-agent",
|
|
619
|
+
name: "Test",
|
|
620
|
+
instructions: "Test",
|
|
621
|
+
model,
|
|
622
|
+
});
|
|
623
|
+
const thread = new Thread({
|
|
624
|
+
agent,
|
|
625
|
+
input: [message({ role: "user", text: "Hi" })],
|
|
626
|
+
});
|
|
627
|
+
// Should not throw
|
|
628
|
+
const result = await thread.execute();
|
|
629
|
+
expect(result.response).toBe("Hello");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
describe("Kernl-level tracer config", () => {
|
|
633
|
+
it("should use tracer from Kernl options", async () => {
|
|
634
|
+
const kernl = new Kernl({ tracer: subscriber });
|
|
635
|
+
const model = createMockModel(async () => ({
|
|
636
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
637
|
+
finishReason: "stop",
|
|
638
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
639
|
+
warnings: [],
|
|
640
|
+
}));
|
|
641
|
+
const agent = new Agent({
|
|
642
|
+
id: "test-agent",
|
|
643
|
+
name: "Test",
|
|
644
|
+
instructions: "Test",
|
|
645
|
+
model,
|
|
646
|
+
});
|
|
647
|
+
kernl.register(agent);
|
|
648
|
+
await agent.run("Hi");
|
|
649
|
+
const threadSpans = subscriber.spansOfKind("thread");
|
|
650
|
+
expect(threadSpans).toHaveLength(1);
|
|
651
|
+
// Clean up
|
|
652
|
+
await kernl.shutdown();
|
|
653
|
+
});
|
|
654
|
+
it("should flush and shutdown subscriber on kernl.shutdown", async () => {
|
|
655
|
+
const kernl = new Kernl({ tracer: subscriber });
|
|
656
|
+
const model = createMockModel(async () => ({
|
|
657
|
+
content: [message({ role: "assistant", text: "Hello" })],
|
|
658
|
+
finishReason: "stop",
|
|
659
|
+
usage: { inputTokens: 2, outputTokens: 2, totalTokens: 4 },
|
|
660
|
+
warnings: [],
|
|
661
|
+
}));
|
|
662
|
+
const agent = new Agent({
|
|
663
|
+
id: "test-agent",
|
|
664
|
+
name: "Test",
|
|
665
|
+
instructions: "Test",
|
|
666
|
+
model,
|
|
667
|
+
});
|
|
668
|
+
kernl.register(agent);
|
|
669
|
+
await agent.run("Hi");
|
|
670
|
+
await kernl.shutdown();
|
|
671
|
+
expect(subscriber.calls.some((c) => c.method === "flush")).toBe(true);
|
|
672
|
+
expect(subscriber.calls.some((c) => c.method === "shutdown")).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.test.d.ts","sourceRoot":"","sources":["../../../src/tracing/__tests__/span.test.ts"],"names":[],"mappings":""}
|