kernl 0.12.0 → 0.12.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 +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/thread/__tests__/mock.d.ts +2 -3
- package/dist/thread/__tests__/mock.d.ts.map +1 -1
- package/dist/thread/thread.d.ts +4 -0
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +43 -75
- package/dist/thread/types.d.ts +18 -3
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +7 -7
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/utils.js +1 -1
- package/package.json +2 -4
- package/src/index.ts +1 -0
- package/src/thread/__tests__/mock.ts +2 -2
- package/src/thread/thread.ts +47 -78
- package/src/thread/types.ts +49 -3
- package/src/thread/utils.ts +14 -7
- package/dist/thread/__tests__/integration.test.d.ts +0 -2
- package/dist/thread/__tests__/integration.test.d.ts.map +0 -1
- package/dist/thread/__tests__/integration.test.js +0 -320
- package/src/thread/__tests__/integration.test.ts +0 -434
package/src/thread/thread.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
LanguageModel,
|
|
20
20
|
LanguageModelItem,
|
|
21
21
|
LanguageModelRequest,
|
|
22
|
+
LanguageModelStreamEvent,
|
|
22
23
|
type LanguageModelUsage,
|
|
23
24
|
type LanguageModelFinishReason,
|
|
24
25
|
} from "@kernl-sdk/protocol";
|
|
@@ -33,6 +34,7 @@ import type {
|
|
|
33
34
|
ThreadStreamEvent,
|
|
34
35
|
ThreadExecuteResult,
|
|
35
36
|
PerformActionsResult,
|
|
37
|
+
PublicThreadEvent,
|
|
36
38
|
} from "./types";
|
|
37
39
|
import type { AgentOutputType } from "@/agent/types";
|
|
38
40
|
import type { LanguageModelResponseType } from "@kernl-sdk/protocol";
|
|
@@ -176,35 +178,15 @@ export class Thread<
|
|
|
176
178
|
|
|
177
179
|
await this.checkpoint(); /* c1: persist RUNNING state + initial input */
|
|
178
180
|
|
|
179
|
-
this.
|
|
180
|
-
kind: "thread.start",
|
|
181
|
-
threadId: this.tid,
|
|
182
|
-
agentId: this.agent.id,
|
|
183
|
-
namespace: this.namespace,
|
|
184
|
-
context: this.context,
|
|
185
|
-
});
|
|
181
|
+
this.emit("thread.start");
|
|
186
182
|
|
|
187
183
|
yield { kind: "stream.start" }; // always yield start immediately
|
|
188
184
|
|
|
189
185
|
try {
|
|
190
186
|
yield* this._execute();
|
|
191
|
-
|
|
192
|
-
this.agent.emit("thread.stop", {
|
|
193
|
-
kind: "thread.stop",
|
|
194
|
-
threadId: this.tid,
|
|
195
|
-
agentId: this.agent.id,
|
|
196
|
-
namespace: this.namespace,
|
|
197
|
-
context: this.context,
|
|
198
|
-
state: STOPPED,
|
|
199
|
-
result: this.tickres,
|
|
200
|
-
});
|
|
187
|
+
this.emit("thread.stop", { state: STOPPED, result: this.tickres });
|
|
201
188
|
} catch (err) {
|
|
202
|
-
this.
|
|
203
|
-
kind: "thread.stop",
|
|
204
|
-
threadId: this.tid,
|
|
205
|
-
agentId: this.agent.id,
|
|
206
|
-
namespace: this.namespace,
|
|
207
|
-
context: this.context,
|
|
189
|
+
this.emit("thread.stop", {
|
|
208
190
|
state: STOPPED,
|
|
209
191
|
error: err instanceof Error ? err.message : String(err),
|
|
210
192
|
});
|
|
@@ -236,12 +218,14 @@ export class Thread<
|
|
|
236
218
|
err = e.error;
|
|
237
219
|
logger.error(e.error); // (TODO): onError callback in options
|
|
238
220
|
}
|
|
239
|
-
//
|
|
221
|
+
// complete items get persisted with seq, deltas are ephemeral
|
|
240
222
|
if (notDelta(e)) {
|
|
241
|
-
|
|
242
|
-
|
|
223
|
+
const [seqd] = this.append(e);
|
|
224
|
+
events.push(seqd);
|
|
225
|
+
yield seqd;
|
|
226
|
+
} else {
|
|
227
|
+
yield e;
|
|
243
228
|
}
|
|
244
|
-
yield e;
|
|
245
229
|
}
|
|
246
230
|
|
|
247
231
|
// if an error event occurred → throw it
|
|
@@ -267,10 +251,10 @@ export class Thread<
|
|
|
267
251
|
const { actions, pendingApprovals } =
|
|
268
252
|
await this.performActions(intentions);
|
|
269
253
|
|
|
270
|
-
// append + yield action events
|
|
254
|
+
// append + yield action events (sequenced)
|
|
271
255
|
for (const a of actions) {
|
|
272
|
-
this.append(a);
|
|
273
|
-
yield
|
|
256
|
+
const [seqd] = this.append(a);
|
|
257
|
+
yield seqd;
|
|
274
258
|
}
|
|
275
259
|
|
|
276
260
|
await this.checkpoint(); /* c3: tick complete */
|
|
@@ -293,23 +277,16 @@ export class Thread<
|
|
|
293
277
|
* NOTE: Streaming structured outputs deferred until concrete use cases emerge.
|
|
294
278
|
* For now, we stream text-delta and tool events, final validation happens in _execute().
|
|
295
279
|
*/
|
|
296
|
-
private async *tick(): AsyncGenerator<
|
|
280
|
+
private async *tick(): AsyncGenerator<LanguageModelStreamEvent> {
|
|
297
281
|
this._tick++;
|
|
298
282
|
|
|
299
283
|
// (TODO): check limits (if this._tick > this.limits.maxTicks)
|
|
300
284
|
// (TODO): run input guardrails on first tick (if this._tick === 1)
|
|
285
|
+
// (TODO): compaction if necessary
|
|
301
286
|
|
|
302
287
|
const req = await this.prepareModelRequest(this.history);
|
|
303
288
|
|
|
304
|
-
this.
|
|
305
|
-
kind: "model.call.start",
|
|
306
|
-
provider: this.model.provider,
|
|
307
|
-
modelId: this.model.modelId,
|
|
308
|
-
settings: req.settings ?? {},
|
|
309
|
-
threadId: this.tid,
|
|
310
|
-
agentId: this.agent.id,
|
|
311
|
-
context: this.context,
|
|
312
|
-
});
|
|
289
|
+
this.emit("model.call.start", { settings: req.settings ?? {} });
|
|
313
290
|
|
|
314
291
|
let usage: LanguageModelUsage | undefined;
|
|
315
292
|
let finishReason: LanguageModelFinishReason = "unknown";
|
|
@@ -333,27 +310,9 @@ export class Thread<
|
|
|
333
310
|
}
|
|
334
311
|
}
|
|
335
312
|
|
|
336
|
-
this.
|
|
337
|
-
kind: "model.call.end",
|
|
338
|
-
provider: this.model.provider,
|
|
339
|
-
modelId: this.model.modelId,
|
|
340
|
-
finishReason,
|
|
341
|
-
usage,
|
|
342
|
-
threadId: this.tid,
|
|
343
|
-
agentId: this.agent.id,
|
|
344
|
-
context: this.context,
|
|
345
|
-
});
|
|
313
|
+
this.emit("model.call.end", { finishReason, usage });
|
|
346
314
|
} catch (error) {
|
|
347
|
-
this.
|
|
348
|
-
kind: "model.call.end",
|
|
349
|
-
provider: this.model.provider,
|
|
350
|
-
modelId: this.model.modelId,
|
|
351
|
-
finishReason: "error",
|
|
352
|
-
threadId: this.tid,
|
|
353
|
-
agentId: this.agent.id,
|
|
354
|
-
context: this.context,
|
|
355
|
-
});
|
|
356
|
-
|
|
315
|
+
this.emit("model.call.end", { finishReason: "error" });
|
|
357
316
|
yield {
|
|
358
317
|
kind: "error",
|
|
359
318
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -439,9 +398,31 @@ export class Thread<
|
|
|
439
398
|
this.abort?.abort();
|
|
440
399
|
}
|
|
441
400
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Emit an agent event with common fields auto-filled.
|
|
403
|
+
*/
|
|
404
|
+
private emit(kind: string, payload?: Record<string, unknown>): void {
|
|
405
|
+
const base = {
|
|
406
|
+
kind,
|
|
407
|
+
threadId: this.tid,
|
|
408
|
+
agentId: this.agent.id,
|
|
409
|
+
context: this.context,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
let auto = {};
|
|
413
|
+
switch (kind) {
|
|
414
|
+
case "thread.start":
|
|
415
|
+
case "thread.stop":
|
|
416
|
+
auto = { namespace: this.namespace };
|
|
417
|
+
break;
|
|
418
|
+
case "model.call.start":
|
|
419
|
+
case "model.call.end":
|
|
420
|
+
auto = { provider: this.model.provider, modelId: this.model.modelId };
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.agent.emit(kind as any, { ...base, ...auto, ...payload } as any);
|
|
425
|
+
}
|
|
445
426
|
|
|
446
427
|
/**
|
|
447
428
|
* Perform the actions returned by the model
|
|
@@ -497,11 +478,7 @@ export class Thread<
|
|
|
497
478
|
calls.map(async (call: ToolCall) => {
|
|
498
479
|
const parsedArgs = JSON.parse(call.arguments || "{}");
|
|
499
480
|
|
|
500
|
-
this.
|
|
501
|
-
kind: "tool.call.start",
|
|
502
|
-
threadId: this.tid,
|
|
503
|
-
agentId: this.agent.id,
|
|
504
|
-
context: this.context,
|
|
481
|
+
this.emit("tool.call.start", {
|
|
505
482
|
toolId: call.toolId,
|
|
506
483
|
callId: call.callId,
|
|
507
484
|
args: parsedArgs,
|
|
@@ -526,11 +503,7 @@ export class Thread<
|
|
|
526
503
|
ctx.approve(call.callId); // mark this call as approved
|
|
527
504
|
const res = await tool.invoke(ctx, call.arguments, call.callId);
|
|
528
505
|
|
|
529
|
-
this.
|
|
530
|
-
kind: "tool.call.end",
|
|
531
|
-
threadId: this.tid,
|
|
532
|
-
agentId: this.agent.id,
|
|
533
|
-
context: this.context,
|
|
506
|
+
this.emit("tool.call.end", {
|
|
534
507
|
toolId: call.toolId,
|
|
535
508
|
callId: call.callId,
|
|
536
509
|
state: res.state,
|
|
@@ -550,11 +523,7 @@ export class Thread<
|
|
|
550
523
|
error: res.error,
|
|
551
524
|
};
|
|
552
525
|
} catch (error) {
|
|
553
|
-
this.
|
|
554
|
-
kind: "tool.call.end",
|
|
555
|
-
threadId: this.tid,
|
|
556
|
-
agentId: this.agent.id,
|
|
557
|
-
context: this.context,
|
|
526
|
+
this.emit("tool.call.end", {
|
|
558
527
|
toolId: call.toolId,
|
|
559
528
|
callId: call.callId,
|
|
560
529
|
state: FAILED,
|
package/src/thread/types.ts
CHANGED
|
@@ -2,13 +2,27 @@ import {
|
|
|
2
2
|
ToolCall,
|
|
3
3
|
LanguageModel,
|
|
4
4
|
LanguageModelItem,
|
|
5
|
-
LanguageModelStreamEvent,
|
|
6
5
|
RUNNING,
|
|
7
6
|
STOPPED,
|
|
8
7
|
INTERRUPTIBLE,
|
|
9
8
|
UNINTERRUPTIBLE,
|
|
10
9
|
ZOMBIE,
|
|
11
10
|
DEAD,
|
|
11
|
+
// Stream event types
|
|
12
|
+
TextStartEvent,
|
|
13
|
+
TextEndEvent,
|
|
14
|
+
TextDeltaEvent,
|
|
15
|
+
ReasoningStartEvent,
|
|
16
|
+
ReasoningEndEvent,
|
|
17
|
+
ReasoningDeltaEvent,
|
|
18
|
+
ToolInputStartEvent,
|
|
19
|
+
ToolInputEndEvent,
|
|
20
|
+
ToolInputDeltaEvent,
|
|
21
|
+
StartEvent,
|
|
22
|
+
FinishEvent,
|
|
23
|
+
AbortEvent,
|
|
24
|
+
ErrorEvent,
|
|
25
|
+
RawEvent,
|
|
12
26
|
} from "@kernl-sdk/protocol";
|
|
13
27
|
|
|
14
28
|
import { Task } from "@/task";
|
|
@@ -128,9 +142,41 @@ export type ThreadEvent =
|
|
|
128
142
|
| ThreadSystemEvent;
|
|
129
143
|
|
|
130
144
|
/**
|
|
131
|
-
*
|
|
145
|
+
* Incremental content chunks (ephemeral, not persisted).
|
|
132
146
|
*/
|
|
133
|
-
export type
|
|
147
|
+
export type StreamDeltaEvent =
|
|
148
|
+
| TextDeltaEvent
|
|
149
|
+
| ReasoningDeltaEvent
|
|
150
|
+
| ToolInputDeltaEvent;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Boundary markers + control flow (ephemeral, not persisted).
|
|
154
|
+
*/
|
|
155
|
+
export type StreamControlEvent =
|
|
156
|
+
| TextStartEvent
|
|
157
|
+
| TextEndEvent
|
|
158
|
+
| ReasoningStartEvent
|
|
159
|
+
| ReasoningEndEvent
|
|
160
|
+
| ToolInputStartEvent
|
|
161
|
+
| ToolInputEndEvent
|
|
162
|
+
| StartEvent
|
|
163
|
+
| FinishEvent
|
|
164
|
+
| AbortEvent
|
|
165
|
+
| ErrorEvent
|
|
166
|
+
| RawEvent;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* All ephemeral stream types (not persisted to history).
|
|
170
|
+
*/
|
|
171
|
+
export type StreamEvent = StreamDeltaEvent | StreamControlEvent;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Thread stream events = sequenced ThreadEvents + ephemeral StreamEvents.
|
|
175
|
+
*
|
|
176
|
+
* Complete items (Message, ToolCall, etc.) are yielded as ThreadEvents with seq.
|
|
177
|
+
* Deltas and control events are yielded as StreamEvents without seq.
|
|
178
|
+
*/
|
|
179
|
+
export type ThreadStreamEvent = ThreadEvent | StreamEvent;
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
182
|
* Result of thread execution
|
package/src/thread/utils.ts
CHANGED
|
@@ -4,7 +4,11 @@ import type { ResolvedAgentResponse } from "@/guardrail";
|
|
|
4
4
|
|
|
5
5
|
/* lib */
|
|
6
6
|
import { json, randomID } from "@kernl-sdk/shared/lib";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ToolCall,
|
|
9
|
+
LanguageModelItem,
|
|
10
|
+
LanguageModelStreamEvent,
|
|
11
|
+
} from "@kernl-sdk/protocol";
|
|
8
12
|
import { ModelBehaviorError } from "@/lib/error";
|
|
9
13
|
|
|
10
14
|
/* types */
|
|
@@ -12,7 +16,6 @@ import type { AgentOutputType } from "@/agent/types";
|
|
|
12
16
|
import type {
|
|
13
17
|
ThreadEvent,
|
|
14
18
|
ThreadEventBase,
|
|
15
|
-
ThreadStreamEvent,
|
|
16
19
|
ActionSet,
|
|
17
20
|
PublicThreadEvent,
|
|
18
21
|
} from "./types";
|
|
@@ -21,7 +24,7 @@ import type {
|
|
|
21
24
|
* Create a ThreadEvent from a LanguageModelItem with thread metadata.
|
|
22
25
|
*
|
|
23
26
|
* @example
|
|
24
|
-
* ```
|
|
27
|
+
* ```ts
|
|
25
28
|
* tevent({
|
|
26
29
|
* kind: "message",
|
|
27
30
|
* seq: 0,
|
|
@@ -57,7 +60,9 @@ export function tevent(event: {
|
|
|
57
60
|
/**
|
|
58
61
|
* Check if an event is a tool call
|
|
59
62
|
*/
|
|
60
|
-
export function isActionIntention(
|
|
63
|
+
export function isActionIntention(
|
|
64
|
+
event: ThreadEvent,
|
|
65
|
+
): event is ToolCall & ThreadEventBase {
|
|
61
66
|
return event.kind === "tool.call";
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -65,7 +70,7 @@ export function isActionIntention(event: LanguageModelItem): event is ToolCall {
|
|
|
65
70
|
* Extract action intentions from a list of events.
|
|
66
71
|
* Returns ActionSet if there are any tool calls, null otherwise.
|
|
67
72
|
*/
|
|
68
|
-
export function getIntentions(events:
|
|
73
|
+
export function getIntentions(events: ThreadEvent[]): ActionSet | null {
|
|
69
74
|
const toolCalls = events.filter(isActionIntention);
|
|
70
75
|
return toolCalls.length > 0 ? { toolCalls } : null;
|
|
71
76
|
}
|
|
@@ -74,7 +79,9 @@ export function getIntentions(events: LanguageModelItem[]): ActionSet | null {
|
|
|
74
79
|
* Check if an event is NOT a delta/start/end event (i.e., a complete item).
|
|
75
80
|
* Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
|
|
76
81
|
*/
|
|
77
|
-
export function notDelta(
|
|
82
|
+
export function notDelta(
|
|
83
|
+
event: LanguageModelStreamEvent,
|
|
84
|
+
): event is LanguageModelItem {
|
|
78
85
|
switch (event.kind) {
|
|
79
86
|
case "message":
|
|
80
87
|
case "reasoning":
|
|
@@ -112,7 +119,7 @@ export function isPublicEvent(event: ThreadEvent): event is PublicThreadEvent {
|
|
|
112
119
|
* Extract the final text response from a list of items.
|
|
113
120
|
* Returns null if no assistant message with text content is found.
|
|
114
121
|
*/
|
|
115
|
-
export function getFinalResponse(items:
|
|
122
|
+
export function getFinalResponse(items: ThreadEvent[]): string | null {
|
|
116
123
|
// scan backwards for the last assistant message
|
|
117
124
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
118
125
|
const item = items[i];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../../src/thread/__tests__/integration.test.ts"],"names":[],"mappings":"AAIA,OAAO,sBAAsB,CAAC"}
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { openai } from "@ai-sdk/openai";
|
|
4
|
-
import { AISDKLanguageModel } from "@kernl-sdk/ai";
|
|
5
|
-
import "@kernl-sdk/ai/openai"; // (TMP)
|
|
6
|
-
import { Agent } from "../../agent.js";
|
|
7
|
-
import { Kernl } from "../../kernl/index.js";
|
|
8
|
-
import { tool, Toolkit } from "../../tool/index.js";
|
|
9
|
-
import { Thread } from "../thread.js";
|
|
10
|
-
/**
|
|
11
|
-
* Integration tests for Thread streaming with real AI SDK providers.
|
|
12
|
-
*
|
|
13
|
-
* These tests require an OPENAI_API_KEY environment variable to be set.
|
|
14
|
-
* They will be skipped if the API key is not available.
|
|
15
|
-
*
|
|
16
|
-
* Run with: OPENAI_API_KEY=your-key pnpm test:run
|
|
17
|
-
*/
|
|
18
|
-
const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
|
|
19
|
-
describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
|
|
20
|
-
let kernl;
|
|
21
|
-
let model;
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
kernl = new Kernl();
|
|
24
|
-
model = new AISDKLanguageModel(openai("gpt-4.1"));
|
|
25
|
-
});
|
|
26
|
-
describe("stream()", () => {
|
|
27
|
-
it("should yield both delta events and complete items", async () => {
|
|
28
|
-
const agent = new Agent({
|
|
29
|
-
id: "test-stream",
|
|
30
|
-
name: "Test Stream Agent",
|
|
31
|
-
instructions: "You are a helpful assistant.",
|
|
32
|
-
model,
|
|
33
|
-
});
|
|
34
|
-
const input = [
|
|
35
|
-
{
|
|
36
|
-
kind: "message",
|
|
37
|
-
id: "msg-1",
|
|
38
|
-
role: "user",
|
|
39
|
-
content: [
|
|
40
|
-
{ kind: "text", text: "Say 'Hello World' and nothing else." },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
const thread = new Thread({ agent, input });
|
|
45
|
-
const events = [];
|
|
46
|
-
for await (const event of thread.stream()) {
|
|
47
|
-
events.push(event);
|
|
48
|
-
}
|
|
49
|
-
expect(events.length).toBeGreaterThan(0);
|
|
50
|
-
// Should have text-delta events (for streaming UX)
|
|
51
|
-
const textDeltas = events.filter((e) => e.kind === "text.delta");
|
|
52
|
-
expect(textDeltas.length).toBeGreaterThan(0);
|
|
53
|
-
// Should have text-start event
|
|
54
|
-
const textStarts = events.filter((e) => e.kind === "text.start");
|
|
55
|
-
expect(textStarts.length).toBeGreaterThan(0);
|
|
56
|
-
// Should have text-end event
|
|
57
|
-
const textEnds = events.filter((e) => e.kind === "text.end");
|
|
58
|
-
expect(textEnds.length).toBeGreaterThan(0);
|
|
59
|
-
// Should have complete Message item (for history)
|
|
60
|
-
const messages = events.filter((e) => e.kind === "message");
|
|
61
|
-
expect(messages.length).toBeGreaterThan(0);
|
|
62
|
-
const assistantMessage = messages.find((m) => m.role === "assistant");
|
|
63
|
-
expect(assistantMessage).toBeDefined();
|
|
64
|
-
expect(assistantMessage.content).toBeDefined();
|
|
65
|
-
expect(assistantMessage.content.length).toBeGreaterThan(0);
|
|
66
|
-
// Message should have accumulated text from all deltas
|
|
67
|
-
const textContent = assistantMessage.content.find((c) => c.kind === "text");
|
|
68
|
-
expect(textContent).toBeDefined();
|
|
69
|
-
expect(textContent.text).toBeDefined();
|
|
70
|
-
expect(textContent.text.length).toBeGreaterThan(0);
|
|
71
|
-
// Verify accumulated text matches concatenated deltas
|
|
72
|
-
const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
|
|
73
|
-
expect(textContent.text).toBe(accumulatedFromDeltas);
|
|
74
|
-
// Should have finish event
|
|
75
|
-
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
76
|
-
expect(finishEvents.length).toBe(1);
|
|
77
|
-
}, 30000);
|
|
78
|
-
it("should filter deltas from history but include complete items", async () => {
|
|
79
|
-
const agent = new Agent({
|
|
80
|
-
id: "test-history",
|
|
81
|
-
name: "Test History Agent",
|
|
82
|
-
instructions: "You are a helpful assistant.",
|
|
83
|
-
model,
|
|
84
|
-
});
|
|
85
|
-
const input = [
|
|
86
|
-
{
|
|
87
|
-
kind: "message",
|
|
88
|
-
id: "msg-1",
|
|
89
|
-
role: "user",
|
|
90
|
-
content: [{ kind: "text", text: "Count to 3" }],
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
const thread = new Thread({ agent, input });
|
|
94
|
-
const streamEvents = [];
|
|
95
|
-
for await (const event of thread.stream()) {
|
|
96
|
-
streamEvents.push(event);
|
|
97
|
-
}
|
|
98
|
-
// Access private history via type assertion for testing
|
|
99
|
-
const history = thread.history;
|
|
100
|
-
// History should only contain complete items (message, reasoning, tool-call, tool-result)
|
|
101
|
-
// TypeScript already enforces this via ThreadEvent type, but let's verify at runtime
|
|
102
|
-
for (const event of history) {
|
|
103
|
-
expect(["message", "reasoning", "tool-call", "tool-result"]).toContain(event.kind);
|
|
104
|
-
}
|
|
105
|
-
// Stream events should include deltas (but history should not)
|
|
106
|
-
const streamDeltas = streamEvents.filter((e) => e.kind === "text.delta" ||
|
|
107
|
-
e.kind === "text.start" ||
|
|
108
|
-
e.kind === "text.end");
|
|
109
|
-
expect(streamDeltas.length).toBeGreaterThan(0);
|
|
110
|
-
// History should contain the input message (with ThreadEvent headers added)
|
|
111
|
-
expect(history[0]).toMatchObject({
|
|
112
|
-
kind: "message",
|
|
113
|
-
role: "user",
|
|
114
|
-
content: [{ kind: "text", text: "Count to 3" }],
|
|
115
|
-
});
|
|
116
|
-
// History should contain complete Message items
|
|
117
|
-
const historyMessages = history.filter((e) => e.kind === "message");
|
|
118
|
-
expect(historyMessages.length).toBeGreaterThan(1); // input + assistant response
|
|
119
|
-
// Verify assistant message has complete text (not deltas)
|
|
120
|
-
const assistantMessage = historyMessages.find((m) => m.role === "assistant");
|
|
121
|
-
expect(assistantMessage).toBeDefined();
|
|
122
|
-
const textContent = assistantMessage.content.find((c) => c.kind === "text");
|
|
123
|
-
expect(textContent.text).toBeTruthy();
|
|
124
|
-
expect(textContent.text.length).toBeGreaterThan(0);
|
|
125
|
-
}, 30000);
|
|
126
|
-
it("should work with tool calls", async () => {
|
|
127
|
-
const addTool = tool({
|
|
128
|
-
id: "add",
|
|
129
|
-
name: "add",
|
|
130
|
-
description: "Add two numbers together",
|
|
131
|
-
parameters: z.object({
|
|
132
|
-
a: z.number().describe("The first number"),
|
|
133
|
-
b: z.number().describe("The second number"),
|
|
134
|
-
}),
|
|
135
|
-
execute: async (ctx, { a, b }) => {
|
|
136
|
-
return a + b;
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
const toolkit = new Toolkit({
|
|
140
|
-
id: "math",
|
|
141
|
-
tools: [addTool],
|
|
142
|
-
});
|
|
143
|
-
const agent = new Agent({
|
|
144
|
-
id: "test-tools",
|
|
145
|
-
name: "Test Tools Agent",
|
|
146
|
-
instructions: "You are a helpful assistant that can do math.",
|
|
147
|
-
model,
|
|
148
|
-
toolkits: [toolkit],
|
|
149
|
-
});
|
|
150
|
-
const input = [
|
|
151
|
-
{
|
|
152
|
-
kind: "message",
|
|
153
|
-
id: "msg-1",
|
|
154
|
-
role: "user",
|
|
155
|
-
content: [{ kind: "text", text: "What is 25 + 17?" }],
|
|
156
|
-
},
|
|
157
|
-
];
|
|
158
|
-
const thread = new Thread({ agent, input });
|
|
159
|
-
const events = [];
|
|
160
|
-
for await (const event of thread.stream()) {
|
|
161
|
-
events.push(event);
|
|
162
|
-
}
|
|
163
|
-
expect(events.length).toBeGreaterThan(0);
|
|
164
|
-
// Should have tool calls
|
|
165
|
-
const toolCalls = events.filter((e) => e.kind === "tool.call");
|
|
166
|
-
expect(toolCalls.length).toBeGreaterThan(0);
|
|
167
|
-
// Verify tool was called with correct parameters
|
|
168
|
-
const addToolCall = toolCalls.find((tc) => tc.toolId === "add");
|
|
169
|
-
expect(addToolCall).toBeDefined();
|
|
170
|
-
expect(JSON.parse(addToolCall.arguments)).toEqual({ a: 25, b: 17 });
|
|
171
|
-
// Should have tool results
|
|
172
|
-
const toolResults = events.filter((e) => e.kind === "tool.result");
|
|
173
|
-
expect(toolResults.length).toBeGreaterThan(0);
|
|
174
|
-
// Verify tool result is correct
|
|
175
|
-
const addToolResult = toolResults.find((tr) => tr.callId === addToolCall.callId);
|
|
176
|
-
expect(addToolResult).toBeDefined();
|
|
177
|
-
expect(addToolResult.result).toBe(42);
|
|
178
|
-
// History should contain tool calls and results
|
|
179
|
-
const history = thread.history;
|
|
180
|
-
const historyToolCalls = history.filter((e) => e.kind === "tool.call");
|
|
181
|
-
const historyToolResults = history.filter((e) => e.kind === "tool.result");
|
|
182
|
-
expect(historyToolCalls.length).toBe(toolCalls.length);
|
|
183
|
-
expect(historyToolResults.length).toBe(toolResults.length);
|
|
184
|
-
// Verify the assistant's final response references the correct answer
|
|
185
|
-
const messages = events.filter((e) => e.kind === "message");
|
|
186
|
-
const assistantMessage = messages.find((m) => m.role === "assistant");
|
|
187
|
-
expect(assistantMessage).toBeDefined();
|
|
188
|
-
const textContent = assistantMessage.content.find((c) => c.kind === "text");
|
|
189
|
-
expect(textContent).toBeDefined();
|
|
190
|
-
expect(textContent.text).toContain("42");
|
|
191
|
-
}, 30000);
|
|
192
|
-
it("should properly encode tool results with matching callIds for multi-turn", async () => {
|
|
193
|
-
const multiplyTool = tool({
|
|
194
|
-
id: "multiply",
|
|
195
|
-
name: "multiply",
|
|
196
|
-
description: "Multiply two numbers",
|
|
197
|
-
parameters: z.object({
|
|
198
|
-
a: z.number().describe("First number"),
|
|
199
|
-
b: z.number().describe("Second number"),
|
|
200
|
-
}),
|
|
201
|
-
execute: async (ctx, { a, b }) => {
|
|
202
|
-
return a * b;
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
const toolkit = new Toolkit({
|
|
206
|
-
id: "math",
|
|
207
|
-
tools: [multiplyTool],
|
|
208
|
-
});
|
|
209
|
-
const agent = new Agent({
|
|
210
|
-
id: "test-multi-turn",
|
|
211
|
-
name: "Test Multi-Turn Agent",
|
|
212
|
-
instructions: "You are a helpful assistant that can do math.",
|
|
213
|
-
model,
|
|
214
|
-
toolkits: [toolkit],
|
|
215
|
-
});
|
|
216
|
-
const input = [
|
|
217
|
-
{
|
|
218
|
-
kind: "message",
|
|
219
|
-
id: "msg-1",
|
|
220
|
-
role: "user",
|
|
221
|
-
content: [{ kind: "text", text: "What is 7 times 6?" }],
|
|
222
|
-
},
|
|
223
|
-
];
|
|
224
|
-
const thread = new Thread({ agent, input });
|
|
225
|
-
const events = [];
|
|
226
|
-
// Collect all events from the stream
|
|
227
|
-
for await (const event of thread.stream()) {
|
|
228
|
-
events.push(event);
|
|
229
|
-
}
|
|
230
|
-
// Find the tool call and result
|
|
231
|
-
const toolCalls = events.filter((e) => e.kind === "tool.call");
|
|
232
|
-
const toolResults = events.filter((e) => e.kind === "tool.result");
|
|
233
|
-
expect(toolCalls.length).toBeGreaterThan(0);
|
|
234
|
-
expect(toolResults.length).toBeGreaterThan(0);
|
|
235
|
-
const multiplyCall = toolCalls[0];
|
|
236
|
-
const multiplyResult = toolResults[0];
|
|
237
|
-
// Verify callId matches between tool call and result
|
|
238
|
-
expect(multiplyCall.callId).toBe(multiplyResult.callId);
|
|
239
|
-
expect(multiplyCall.toolId).toBe("multiply");
|
|
240
|
-
expect(multiplyResult.toolId).toBe("multiply");
|
|
241
|
-
// Verify the tool result has the correct structure
|
|
242
|
-
expect(multiplyResult.callId).toBeDefined();
|
|
243
|
-
expect(typeof multiplyResult.callId).toBe("string");
|
|
244
|
-
expect(multiplyResult.callId.length).toBeGreaterThan(0);
|
|
245
|
-
// Verify history contains both with matching callIds
|
|
246
|
-
const history = thread.history;
|
|
247
|
-
const historyToolCall = history.find((e) => e.kind === "tool.call" && e.toolId === "multiply");
|
|
248
|
-
const historyToolResult = history.find((e) => e.kind === "tool.result" && e.toolId === "multiply");
|
|
249
|
-
expect(historyToolCall).toBeDefined();
|
|
250
|
-
expect(historyToolResult).toBeDefined();
|
|
251
|
-
expect(historyToolCall.callId).toBe(historyToolResult.callId);
|
|
252
|
-
// Verify final response uses the tool result
|
|
253
|
-
const messages = events.filter((e) => e.kind === "message");
|
|
254
|
-
const assistantMessage = messages.find((m) => m.role === "assistant");
|
|
255
|
-
expect(assistantMessage).toBeDefined();
|
|
256
|
-
const textContent = assistantMessage.content.find((c) => c.kind === "text");
|
|
257
|
-
expect(textContent).toBeDefined();
|
|
258
|
-
expect(textContent.text).toContain("42");
|
|
259
|
-
}, 30000);
|
|
260
|
-
});
|
|
261
|
-
describe("execute()", () => {
|
|
262
|
-
it("should consume stream and return final response", async () => {
|
|
263
|
-
const agent = new Agent({
|
|
264
|
-
id: "test-blocking",
|
|
265
|
-
name: "Test Blocking Agent",
|
|
266
|
-
instructions: "You are a helpful assistant.",
|
|
267
|
-
model,
|
|
268
|
-
});
|
|
269
|
-
const input = [
|
|
270
|
-
{
|
|
271
|
-
kind: "message",
|
|
272
|
-
id: "msg-1",
|
|
273
|
-
role: "user",
|
|
274
|
-
content: [{ kind: "text", text: "Say 'Testing' and nothing else." }],
|
|
275
|
-
},
|
|
276
|
-
];
|
|
277
|
-
const thread = new Thread({ agent, input });
|
|
278
|
-
const result = await thread.execute();
|
|
279
|
-
// Should have a response
|
|
280
|
-
expect(result.response).toBeDefined();
|
|
281
|
-
expect(typeof result.response).toBe("string");
|
|
282
|
-
expect(result.response.length).toBeGreaterThan(0);
|
|
283
|
-
// Should have final state
|
|
284
|
-
expect(result.state).toBe("stopped");
|
|
285
|
-
}, 30000);
|
|
286
|
-
it("should validate structured output in blocking mode", async () => {
|
|
287
|
-
const PersonSchema = z.object({
|
|
288
|
-
name: z.string(),
|
|
289
|
-
age: z.number(),
|
|
290
|
-
});
|
|
291
|
-
const agent = new Agent({
|
|
292
|
-
id: "test-structured",
|
|
293
|
-
name: "Test Structured Agent",
|
|
294
|
-
instructions: "You are a helpful assistant. Return JSON with name and age fields.",
|
|
295
|
-
model,
|
|
296
|
-
output: PersonSchema,
|
|
297
|
-
});
|
|
298
|
-
const input = [
|
|
299
|
-
{
|
|
300
|
-
kind: "message",
|
|
301
|
-
id: "msg-1",
|
|
302
|
-
role: "user",
|
|
303
|
-
content: [
|
|
304
|
-
{
|
|
305
|
-
kind: "text",
|
|
306
|
-
text: 'Return a JSON object with name "Alice" and age 30',
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
},
|
|
310
|
-
];
|
|
311
|
-
const thread = new Thread({ agent, input });
|
|
312
|
-
const result = await thread.execute();
|
|
313
|
-
// Response should be validated and parsed
|
|
314
|
-
expect(result.response).toBeDefined();
|
|
315
|
-
expect(typeof result.response).toBe("object");
|
|
316
|
-
expect(result.response.name).toBeTruthy();
|
|
317
|
-
expect(typeof result.response.age).toBe("number");
|
|
318
|
-
}, 30000);
|
|
319
|
-
});
|
|
320
|
-
});
|