kernl 0.7.4 → 0.8.0
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 +19 -1
- package/dist/agent/types.d.ts +20 -12
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent.d.ts +7 -7
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -14
- package/dist/api/resources/agents/agents.d.ts +5 -5
- package/dist/api/resources/agents/agents.d.ts.map +1 -1
- package/dist/api/resources/agents/agents.js +1 -1
- package/dist/guardrail.d.ts +19 -19
- package/dist/guardrail.d.ts.map +1 -1
- package/dist/kernl/kernl.d.ts +6 -6
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/lib/error.d.ts +3 -3
- package/dist/lib/error.d.ts.map +1 -1
- package/dist/lifecycle.d.ts +6 -6
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/memory/__tests__/encoder.test.d.ts +2 -0
- package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
- package/dist/memory/__tests__/encoder.test.js +120 -0
- package/dist/memory/codecs/domain.d.ts +5 -0
- package/dist/memory/codecs/domain.d.ts.map +1 -1
- package/dist/memory/codecs/domain.js +6 -0
- package/dist/memory/encoder.d.ts +25 -2
- package/dist/memory/encoder.d.ts.map +1 -1
- package/dist/memory/encoder.js +46 -5
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/schema.d.ts.map +1 -1
- package/dist/memory/schema.js +5 -0
- package/dist/memory/types.d.ts +1 -0
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/thread/__tests__/integration.test.js +1 -1
- package/dist/thread/__tests__/thread.test.js +8 -8
- package/dist/thread/thread.d.ts +5 -5
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +13 -2
- package/dist/thread/types.d.ts +9 -6
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +7 -6
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/utils.js +9 -8
- package/package.json +5 -4
- package/src/agent/types.ts +25 -29
- package/src/agent.ts +15 -28
- package/src/api/resources/agents/agents.ts +8 -8
- package/src/guardrail.ts +28 -28
- package/src/kernl/kernl.ts +12 -12
- package/src/lib/error.ts +3 -3
- package/src/lifecycle.ts +6 -6
- package/src/memory/__tests__/encoder.test.ts +153 -0
- package/src/memory/codecs/domain.ts +6 -0
- package/src/memory/encoder.ts +51 -6
- package/src/memory/index.ts +1 -1
- package/src/memory/schema.ts +5 -0
- package/src/memory/types.ts +1 -0
- package/src/thread/__tests__/integration.test.ts +130 -146
- package/src/thread/__tests__/thread.test.ts +8 -8
- package/src/thread/thread.ts +21 -7
- package/src/thread/types.ts +9 -6
- package/src/thread/utils.ts +15 -14
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { EmbeddingModel } from "@kernl-sdk/protocol";
|
|
3
|
+
|
|
4
|
+
import { MemoryByteEncoder, ObjectTextCodec } from "../encoder";
|
|
5
|
+
import type { MemoryByte } from "../types";
|
|
6
|
+
|
|
7
|
+
// Mock embedder that returns predictable vectors
|
|
8
|
+
function createMockEmbedder(): EmbeddingModel<string> {
|
|
9
|
+
return {
|
|
10
|
+
provider: "test",
|
|
11
|
+
modelId: "test-embedder",
|
|
12
|
+
embed: vi.fn(async ({ values }: { values: string[] }) => ({
|
|
13
|
+
embeddings: values.map((v) => [v.length, 0, 0]), // simple: [length, 0, 0]
|
|
14
|
+
})),
|
|
15
|
+
} as unknown as EmbeddingModel<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("ObjectTextCodec", () => {
|
|
19
|
+
it("encodes simple object to YAML", () => {
|
|
20
|
+
const obj = { name: "Tony", preference: "coffee" };
|
|
21
|
+
const result = ObjectTextCodec.encode(obj);
|
|
22
|
+
|
|
23
|
+
expect(result).toContain("name: Tony");
|
|
24
|
+
expect(result).toContain("preference: coffee");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("sorts keys for determinism", () => {
|
|
28
|
+
const obj1 = { z: 1, a: 2, m: 3 };
|
|
29
|
+
const obj2 = { a: 2, m: 3, z: 1 };
|
|
30
|
+
|
|
31
|
+
expect(ObjectTextCodec.encode(obj1)).toBe(ObjectTextCodec.encode(obj2));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("truncates long objects with ellipsis", () => {
|
|
35
|
+
const longValue = "x".repeat(4000);
|
|
36
|
+
const obj = { data: longValue };
|
|
37
|
+
const result = ObjectTextCodec.encode(obj);
|
|
38
|
+
|
|
39
|
+
expect(result.length).toBeLessThanOrEqual(3005); // 3000 + "\n..."
|
|
40
|
+
expect(result.endsWith("\n...")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles nested objects", () => {
|
|
44
|
+
const obj = {
|
|
45
|
+
user: {
|
|
46
|
+
name: "Tony",
|
|
47
|
+
prefs: { coffee: { shots: 2 } },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const result = ObjectTextCodec.encode(obj);
|
|
51
|
+
|
|
52
|
+
expect(result).toContain("user:");
|
|
53
|
+
expect(result).toContain("name: Tony");
|
|
54
|
+
expect(result).toContain("shots: 2");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles arrays", () => {
|
|
58
|
+
const obj = { items: ["a", "b", "c"] };
|
|
59
|
+
const result = ObjectTextCodec.encode(obj);
|
|
60
|
+
|
|
61
|
+
expect(result).toContain("items:");
|
|
62
|
+
expect(result).toContain("- a");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("MemoryByteEncoder", () => {
|
|
67
|
+
describe("encode", () => {
|
|
68
|
+
it("encodes text-only content", async () => {
|
|
69
|
+
const embedder = createMockEmbedder();
|
|
70
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
71
|
+
|
|
72
|
+
const byte: MemoryByte = { text: "Hello world" };
|
|
73
|
+
const result = await encoder.encode(byte);
|
|
74
|
+
|
|
75
|
+
expect(result.text).toBe("Hello world");
|
|
76
|
+
expect(result.objtext).toBeUndefined();
|
|
77
|
+
expect(result.tvec).toBeDefined();
|
|
78
|
+
expect(embedder.embed).toHaveBeenCalledWith({ values: ["Hello world"] });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("encodes object-only content with projection", async () => {
|
|
82
|
+
const embedder = createMockEmbedder();
|
|
83
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
84
|
+
|
|
85
|
+
const byte: MemoryByte = {
|
|
86
|
+
object: { preference: "coffee", shots: 2 },
|
|
87
|
+
};
|
|
88
|
+
const result = await encoder.encode(byte);
|
|
89
|
+
|
|
90
|
+
// text falls back to objtext projection
|
|
91
|
+
expect(result.text).toContain("preference: coffee");
|
|
92
|
+
expect(result.objtext).toContain("preference: coffee");
|
|
93
|
+
expect(result.tvec).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("combines text and object for embedding", async () => {
|
|
97
|
+
const embedder = createMockEmbedder();
|
|
98
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
99
|
+
|
|
100
|
+
const byte: MemoryByte = {
|
|
101
|
+
text: "Tony likes coffee",
|
|
102
|
+
object: { shots: 2, sugar: false },
|
|
103
|
+
};
|
|
104
|
+
const result = await encoder.encode(byte);
|
|
105
|
+
|
|
106
|
+
expect(result.text).toBe("Tony likes coffee");
|
|
107
|
+
expect(result.objtext).toContain("shots: 2");
|
|
108
|
+
|
|
109
|
+
// embedding should be called with combined text
|
|
110
|
+
const embedCall = (embedder.embed as ReturnType<typeof vi.fn>).mock
|
|
111
|
+
.calls[0][0];
|
|
112
|
+
expect(embedCall.values[0]).toContain("Tony likes coffee");
|
|
113
|
+
expect(embedCall.values[0]).toContain("shots: 2");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not set metadata (handled by domain codec)", async () => {
|
|
117
|
+
const embedder = createMockEmbedder();
|
|
118
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
119
|
+
|
|
120
|
+
const byte: MemoryByte = {
|
|
121
|
+
text: "test",
|
|
122
|
+
object: { key: "value" },
|
|
123
|
+
};
|
|
124
|
+
const result = await encoder.encode(byte);
|
|
125
|
+
|
|
126
|
+
expect(result.metadata).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns undefined tvec when no content", async () => {
|
|
130
|
+
const embedder = createMockEmbedder();
|
|
131
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
132
|
+
|
|
133
|
+
const byte: MemoryByte = {};
|
|
134
|
+
const result = await encoder.encode(byte);
|
|
135
|
+
|
|
136
|
+
expect(result.text).toBeUndefined();
|
|
137
|
+
expect(result.objtext).toBeUndefined();
|
|
138
|
+
expect(result.tvec).toBeUndefined();
|
|
139
|
+
expect(embedder.embed).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("embed", () => {
|
|
144
|
+
it("exposes embed method for query embedding", async () => {
|
|
145
|
+
const embedder = createMockEmbedder();
|
|
146
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
147
|
+
|
|
148
|
+
const vec = await encoder.embed("search query");
|
|
149
|
+
|
|
150
|
+
expect(vec).toEqual([12, 0, 0]); // "search query".length = 12
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -58,6 +58,11 @@ export const MEMORY_FILTER: Codec<MemoryFilter, SearchFilter> = {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Create a codec for MemoryRecord -> IndexMemoryRecord.
|
|
61
|
+
*
|
|
62
|
+
* Combines:
|
|
63
|
+
* - Record scope/timestamps from MemoryRecord
|
|
64
|
+
* - Indexed content (text, object projection, embeddings) from byte codec
|
|
65
|
+
* - User metadata from record.metadata (not from content.object)
|
|
61
66
|
*/
|
|
62
67
|
export function recordCodec(
|
|
63
68
|
bytecodec: MemoryByteCodec,
|
|
@@ -76,6 +81,7 @@ export function recordCodec(
|
|
|
76
81
|
createdAt: record.createdAt,
|
|
77
82
|
updatedAt: record.updatedAt,
|
|
78
83
|
...indexable,
|
|
84
|
+
metadata: record.metadata ?? null, // user metadata, not content.object
|
|
79
85
|
};
|
|
80
86
|
},
|
|
81
87
|
async decode(): Promise<MemoryRecord> {
|
package/src/memory/encoder.ts
CHANGED
|
@@ -2,10 +2,41 @@
|
|
|
2
2
|
* MemoryByte encoder - converts MemoryByte to IndexableByte with embeddings.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { EmbeddingModel } from "@kernl-sdk/protocol";
|
|
5
|
+
import type { EmbeddingModel, JSONObject } from "@kernl-sdk/protocol";
|
|
6
|
+
import { stringify as yamlStringify } from "yaml";
|
|
6
7
|
|
|
7
8
|
import type { MemoryByte, IndexableByte, MemoryByteCodec } from "./types";
|
|
8
9
|
|
|
10
|
+
// ---------------------
|
|
11
|
+
// ObjectTextCodec
|
|
12
|
+
// ---------------------
|
|
13
|
+
|
|
14
|
+
const MAX_OBJECT_TEXT_LENGTH = 3000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Codec for converting JSONObject to a canonical text representation.
|
|
18
|
+
*
|
|
19
|
+
* Uses YAML for human-readable, deterministic output suitable for:
|
|
20
|
+
* - Full-text search indexing
|
|
21
|
+
* - Embedding input (combined with text field)
|
|
22
|
+
*
|
|
23
|
+
* TODO: Allow users to pass custom codec via MemoryOptions.
|
|
24
|
+
*/
|
|
25
|
+
export const ObjectTextCodec = {
|
|
26
|
+
/**
|
|
27
|
+
* Encode a JSONObject to canonical text.
|
|
28
|
+
* Uses YAML with sorted keys for determinism.
|
|
29
|
+
* Truncates at MAX_OBJECT_TEXT_LENGTH chars.
|
|
30
|
+
*/
|
|
31
|
+
encode(obj: JSONObject): string {
|
|
32
|
+
const yaml = yamlStringify(obj, { sortMapEntries: true });
|
|
33
|
+
if (yaml.length <= MAX_OBJECT_TEXT_LENGTH) {
|
|
34
|
+
return yaml;
|
|
35
|
+
}
|
|
36
|
+
return yaml.slice(0, MAX_OBJECT_TEXT_LENGTH) + "\n...";
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
9
40
|
/**
|
|
10
41
|
* Encoder that converts MemoryByte to IndexableByte.
|
|
11
42
|
*
|
|
@@ -20,21 +51,35 @@ export class MemoryByteEncoder implements MemoryByteCodec {
|
|
|
20
51
|
|
|
21
52
|
/**
|
|
22
53
|
* Encode a MemoryByte to IndexableByte.
|
|
23
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* - Produces `objtext` string projection for FTS indexing
|
|
56
|
+
* - Combines text + objtext for embedding input
|
|
57
|
+
* - Returns text (fallback to objtext if no text provided)
|
|
58
|
+
*
|
|
59
|
+
* Note: metadata is NOT set here - it comes from record.metadata
|
|
60
|
+
* via the domain codec, not from MemoryByte.object.
|
|
24
61
|
*/
|
|
25
62
|
async encode(byte: MemoryByte): Promise<IndexableByte> {
|
|
26
|
-
const
|
|
27
|
-
|
|
63
|
+
const objtext = byte.object
|
|
64
|
+
? ObjectTextCodec.encode(byte.object) // encode object as embeddable string
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
// (TODO): this behavior deserves consideration - do we always want to merge text + object?
|
|
68
|
+
//
|
|
69
|
+
// combine text + object for richer embedding
|
|
70
|
+
const combined = [byte.text, objtext].filter(Boolean).join("\n");
|
|
71
|
+
const tvec = combined ? await this.embed(combined) : undefined;
|
|
28
72
|
|
|
29
73
|
// TODO: embed other modalities (image, audio, video)
|
|
74
|
+
//
|
|
30
75
|
// const ivec = byte.image ? await this.embedImage(byte.image) : undefined;
|
|
31
76
|
// const avec = byte.audio ? await this.embedAudio(byte.audio) : undefined;
|
|
32
77
|
// const vvec = byte.video ? await this.embedVideo(byte.video) : undefined;
|
|
33
78
|
|
|
34
79
|
return {
|
|
35
|
-
text,
|
|
80
|
+
text: byte.text ?? objtext, // fallback to projection if no text
|
|
81
|
+
objtext,
|
|
36
82
|
tvec,
|
|
37
|
-
metadata: byte.object ?? null,
|
|
38
83
|
};
|
|
39
84
|
}
|
|
40
85
|
|
package/src/memory/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { Memory } from "./memory";
|
|
6
|
-
export { MemoryByteEncoder } from "./encoder";
|
|
6
|
+
export { MemoryByteEncoder, ObjectTextCodec } from "./encoder";
|
|
7
7
|
export { buildMemoryIndexSchema } from "./schema";
|
|
8
8
|
export { MemoryIndexHandle } from "./handle";
|
|
9
9
|
export type { MemoryIndexHandleConfig } from "./handle";
|
package/src/memory/schema.ts
CHANGED
package/src/memory/types.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface MemoryByte {
|
|
|
63
63
|
*/
|
|
64
64
|
export interface IndexableByte {
|
|
65
65
|
text?: string; // canonical semantic text
|
|
66
|
+
objtext?: string; // string projection of object for indexing (not full JSON)
|
|
66
67
|
tvec?: number[]; // text embedding
|
|
67
68
|
ivec?: number[]; // image embedding
|
|
68
69
|
avec?: number[]; // audio embedding
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from "vitest";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { openai } from "@ai-sdk/openai";
|
|
4
4
|
import { AISDKLanguageModel } from "@kernl-sdk/ai";
|
|
5
|
+
import "@kernl-sdk/ai/openai"; // (TMP)
|
|
5
6
|
|
|
6
7
|
import { Agent } from "@/agent";
|
|
7
8
|
import { Kernl } from "@/kernl";
|
|
@@ -23,21 +24,17 @@ import type { LanguageModelItem } from "@kernl-sdk/protocol";
|
|
|
23
24
|
|
|
24
25
|
const SKIP_INTEGRATION_TESTS = !process.env.OPENAI_API_KEY;
|
|
25
26
|
|
|
26
|
-
describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
describe("stream()", () => {
|
|
38
|
-
it(
|
|
39
|
-
"should yield both delta events and complete items",
|
|
40
|
-
async () => {
|
|
27
|
+
describe.skipIf(SKIP_INTEGRATION_TESTS)("Thread streaming integration", () => {
|
|
28
|
+
let kernl: Kernl;
|
|
29
|
+
let model: AISDKLanguageModel;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
kernl = new Kernl();
|
|
33
|
+
model = new AISDKLanguageModel(openai("gpt-4.1"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("stream()", () => {
|
|
37
|
+
it("should yield both delta events and complete items", async () => {
|
|
41
38
|
const agent = new Agent({
|
|
42
39
|
id: "test-stream",
|
|
43
40
|
name: "Test Stream Agent",
|
|
@@ -103,13 +100,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
103
100
|
// Should have finish event
|
|
104
101
|
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
105
102
|
expect(finishEvents.length).toBe(1);
|
|
106
|
-
|
|
107
|
-
30000,
|
|
108
|
-
);
|
|
103
|
+
}, 30000);
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
"should filter deltas from history but include complete items",
|
|
112
|
-
async () => {
|
|
105
|
+
it("should filter deltas from history but include complete items", async () => {
|
|
113
106
|
const agent = new Agent({
|
|
114
107
|
id: "test-history",
|
|
115
108
|
name: "Test History Agent",
|
|
@@ -153,8 +146,12 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
153
146
|
);
|
|
154
147
|
expect(streamDeltas.length).toBeGreaterThan(0);
|
|
155
148
|
|
|
156
|
-
// History should contain the input message
|
|
157
|
-
expect(history[0]).
|
|
149
|
+
// History should contain the input message (with ThreadEvent headers added)
|
|
150
|
+
expect(history[0]).toMatchObject({
|
|
151
|
+
kind: "message",
|
|
152
|
+
role: "user",
|
|
153
|
+
content: [{ kind: "text", text: "Count to 3" }],
|
|
154
|
+
});
|
|
158
155
|
|
|
159
156
|
// History should contain complete Message items
|
|
160
157
|
const historyMessages = history.filter((e) => e.kind === "message");
|
|
@@ -170,11 +167,9 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
170
167
|
);
|
|
171
168
|
expect(textContent.text).toBeTruthy();
|
|
172
169
|
expect(textContent.text.length).toBeGreaterThan(0);
|
|
173
|
-
|
|
174
|
-
30000,
|
|
175
|
-
);
|
|
170
|
+
}, 30000);
|
|
176
171
|
|
|
177
|
-
|
|
172
|
+
it("should work with tool calls", async () => {
|
|
178
173
|
const addTool = tool({
|
|
179
174
|
id: "add",
|
|
180
175
|
name: "add",
|
|
@@ -257,122 +252,118 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
257
252
|
|
|
258
253
|
// Verify the assistant's final response references the correct answer
|
|
259
254
|
const messages = events.filter((e) => e.kind === "message");
|
|
260
|
-
const assistantMessage = messages.find(
|
|
255
|
+
const assistantMessage = messages.find(
|
|
256
|
+
(m: any) => m.role === "assistant",
|
|
257
|
+
);
|
|
261
258
|
expect(assistantMessage).toBeDefined();
|
|
262
259
|
const textContent = (assistantMessage as any).content.find(
|
|
263
260
|
(c: any) => c.kind === "text",
|
|
264
261
|
);
|
|
265
262
|
expect(textContent).toBeDefined();
|
|
266
263
|
expect(textContent.text).toContain("42");
|
|
264
|
+
}, 30000);
|
|
265
|
+
|
|
266
|
+
it("should properly encode tool results with matching callIds for multi-turn", async () => {
|
|
267
|
+
const multiplyTool = tool({
|
|
268
|
+
id: "multiply",
|
|
269
|
+
name: "multiply",
|
|
270
|
+
description: "Multiply two numbers",
|
|
271
|
+
parameters: z.object({
|
|
272
|
+
a: z.number().describe("First number"),
|
|
273
|
+
b: z.number().describe("Second number"),
|
|
274
|
+
}),
|
|
275
|
+
execute: async (ctx, { a, b }) => {
|
|
276
|
+
return a * b;
|
|
267
277
|
},
|
|
268
|
-
|
|
269
|
-
);
|
|
278
|
+
});
|
|
270
279
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const agent = new Agent({
|
|
293
|
-
id: "test-multi-turn",
|
|
294
|
-
name: "Test Multi-Turn Agent",
|
|
295
|
-
instructions: "You are a helpful assistant that can do math.",
|
|
296
|
-
model,
|
|
297
|
-
toolkits: [toolkit],
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const input: LanguageModelItem[] = [
|
|
301
|
-
{
|
|
302
|
-
kind: "message",
|
|
303
|
-
id: "msg-1",
|
|
304
|
-
role: "user",
|
|
305
|
-
content: [{ kind: "text", text: "What is 7 times 6?" }],
|
|
306
|
-
},
|
|
307
|
-
];
|
|
308
|
-
|
|
309
|
-
const thread = new Thread({ agent, input });
|
|
310
|
-
const events: ThreadStreamEvent[] = [];
|
|
311
|
-
|
|
312
|
-
// Collect all events from the stream
|
|
313
|
-
for await (const event of thread.stream()) {
|
|
314
|
-
events.push(event);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Find the tool call and result
|
|
318
|
-
const toolCalls = events.filter(
|
|
319
|
-
(e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
|
|
320
|
-
e.kind === "tool-call",
|
|
321
|
-
);
|
|
322
|
-
const toolResults = events.filter(
|
|
323
|
-
(e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
|
|
324
|
-
e.kind === "tool-result",
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
expect(toolCalls.length).toBeGreaterThan(0);
|
|
328
|
-
expect(toolResults.length).toBeGreaterThan(0);
|
|
329
|
-
|
|
330
|
-
const multiplyCall = toolCalls[0];
|
|
331
|
-
const multiplyResult = toolResults[0];
|
|
332
|
-
|
|
333
|
-
// Verify callId matches between tool call and result
|
|
334
|
-
expect(multiplyCall.callId).toBe(multiplyResult.callId);
|
|
335
|
-
expect(multiplyCall.toolId).toBe("multiply");
|
|
336
|
-
expect(multiplyResult.toolId).toBe("multiply");
|
|
337
|
-
|
|
338
|
-
// Verify the tool result has the correct structure
|
|
339
|
-
expect(multiplyResult.callId).toBeDefined();
|
|
340
|
-
expect(typeof multiplyResult.callId).toBe("string");
|
|
341
|
-
expect(multiplyResult.callId.length).toBeGreaterThan(0);
|
|
342
|
-
|
|
343
|
-
// Verify history contains both with matching callIds
|
|
344
|
-
const history = (thread as any).history as ThreadEvent[];
|
|
345
|
-
const historyToolCall = history.find(
|
|
346
|
-
(e) => e.kind === "tool-call" && e.toolId === "multiply",
|
|
347
|
-
);
|
|
348
|
-
const historyToolResult = history.find(
|
|
349
|
-
(e) => e.kind === "tool-result" && e.toolId === "multiply",
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
expect(historyToolCall).toBeDefined();
|
|
353
|
-
expect(historyToolResult).toBeDefined();
|
|
354
|
-
expect((historyToolCall as any).callId).toBe(
|
|
355
|
-
(historyToolResult as any).callId,
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
// Verify final response uses the tool result
|
|
359
|
-
const messages = events.filter((e) => e.kind === "message");
|
|
360
|
-
const assistantMessage = messages.find((m: any) => m.role === "assistant");
|
|
361
|
-
expect(assistantMessage).toBeDefined();
|
|
362
|
-
const textContent = (assistantMessage as any).content.find(
|
|
363
|
-
(c: any) => c.kind === "text",
|
|
364
|
-
);
|
|
365
|
-
expect(textContent).toBeDefined();
|
|
366
|
-
expect(textContent.text).toContain("42");
|
|
280
|
+
const toolkit = new Toolkit({
|
|
281
|
+
id: "math",
|
|
282
|
+
tools: [multiplyTool],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const agent = new Agent({
|
|
286
|
+
id: "test-multi-turn",
|
|
287
|
+
name: "Test Multi-Turn Agent",
|
|
288
|
+
instructions: "You are a helpful assistant that can do math.",
|
|
289
|
+
model,
|
|
290
|
+
toolkits: [toolkit],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const input: LanguageModelItem[] = [
|
|
294
|
+
{
|
|
295
|
+
kind: "message",
|
|
296
|
+
id: "msg-1",
|
|
297
|
+
role: "user",
|
|
298
|
+
content: [{ kind: "text", text: "What is 7 times 6?" }],
|
|
367
299
|
},
|
|
368
|
-
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const thread = new Thread({ agent, input });
|
|
303
|
+
const events: ThreadStreamEvent[] = [];
|
|
304
|
+
|
|
305
|
+
// Collect all events from the stream
|
|
306
|
+
for await (const event of thread.stream()) {
|
|
307
|
+
events.push(event);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Find the tool call and result
|
|
311
|
+
const toolCalls = events.filter(
|
|
312
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-call" }> =>
|
|
313
|
+
e.kind === "tool-call",
|
|
314
|
+
);
|
|
315
|
+
const toolResults = events.filter(
|
|
316
|
+
(e): e is Extract<ThreadStreamEvent, { kind: "tool-result" }> =>
|
|
317
|
+
e.kind === "tool-result",
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
321
|
+
expect(toolResults.length).toBeGreaterThan(0);
|
|
322
|
+
|
|
323
|
+
const multiplyCall = toolCalls[0];
|
|
324
|
+
const multiplyResult = toolResults[0];
|
|
325
|
+
|
|
326
|
+
// Verify callId matches between tool call and result
|
|
327
|
+
expect(multiplyCall.callId).toBe(multiplyResult.callId);
|
|
328
|
+
expect(multiplyCall.toolId).toBe("multiply");
|
|
329
|
+
expect(multiplyResult.toolId).toBe("multiply");
|
|
330
|
+
|
|
331
|
+
// Verify the tool result has the correct structure
|
|
332
|
+
expect(multiplyResult.callId).toBeDefined();
|
|
333
|
+
expect(typeof multiplyResult.callId).toBe("string");
|
|
334
|
+
expect(multiplyResult.callId.length).toBeGreaterThan(0);
|
|
335
|
+
|
|
336
|
+
// Verify history contains both with matching callIds
|
|
337
|
+
const history = (thread as any).history as ThreadEvent[];
|
|
338
|
+
const historyToolCall = history.find(
|
|
339
|
+
(e) => e.kind === "tool-call" && e.toolId === "multiply",
|
|
340
|
+
);
|
|
341
|
+
const historyToolResult = history.find(
|
|
342
|
+
(e) => e.kind === "tool-result" && e.toolId === "multiply",
|
|
369
343
|
);
|
|
370
|
-
});
|
|
371
344
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
expect(historyToolCall).toBeDefined();
|
|
346
|
+
expect(historyToolResult).toBeDefined();
|
|
347
|
+
expect((historyToolCall as any).callId).toBe(
|
|
348
|
+
(historyToolResult as any).callId,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Verify final response uses the tool result
|
|
352
|
+
const messages = events.filter((e) => e.kind === "message");
|
|
353
|
+
const assistantMessage = messages.find(
|
|
354
|
+
(m: any) => m.role === "assistant",
|
|
355
|
+
);
|
|
356
|
+
expect(assistantMessage).toBeDefined();
|
|
357
|
+
const textContent = (assistantMessage as any).content.find(
|
|
358
|
+
(c: any) => c.kind === "text",
|
|
359
|
+
);
|
|
360
|
+
expect(textContent).toBeDefined();
|
|
361
|
+
expect(textContent.text).toContain("42");
|
|
362
|
+
}, 30000);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("execute()", () => {
|
|
366
|
+
it("should consume stream and return final response", async () => {
|
|
376
367
|
const agent = new Agent({
|
|
377
368
|
id: "test-blocking",
|
|
378
369
|
name: "Test Blocking Agent",
|
|
@@ -399,14 +390,10 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
399
390
|
|
|
400
391
|
// Should have final state
|
|
401
392
|
expect(result.state).toBe("stopped");
|
|
402
|
-
|
|
403
|
-
30000,
|
|
404
|
-
);
|
|
393
|
+
}, 30000);
|
|
405
394
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
async () => {
|
|
409
|
-
const responseSchema = z.object({
|
|
395
|
+
it("should validate structured output in blocking mode", async () => {
|
|
396
|
+
const PersonSchema = z.object({
|
|
410
397
|
name: z.string(),
|
|
411
398
|
age: z.number(),
|
|
412
399
|
});
|
|
@@ -417,7 +404,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
417
404
|
instructions:
|
|
418
405
|
"You are a helpful assistant. Return JSON with name and age fields.",
|
|
419
406
|
model,
|
|
420
|
-
|
|
407
|
+
output: PersonSchema,
|
|
421
408
|
});
|
|
422
409
|
|
|
423
410
|
const input: LanguageModelItem[] = [
|
|
@@ -442,9 +429,6 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)(
|
|
|
442
429
|
expect(typeof result.response).toBe("object");
|
|
443
430
|
expect((result.response as any).name).toBeTruthy();
|
|
444
431
|
expect(typeof (result.response as any).age).toBe("number");
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
449
|
-
},
|
|
450
|
-
);
|
|
432
|
+
}, 30000);
|
|
433
|
+
});
|
|
434
|
+
});
|