kernl 0.8.3 → 0.9.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 +21 -0
- package/dist/agent/base.d.ts +73 -0
- package/dist/agent/base.d.ts.map +1 -0
- package/dist/agent/base.js +137 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +2 -1
- package/dist/agent/types.d.ts +4 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent.d.ts +10 -90
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +5 -171
- package/dist/api/resources/agents/agents.d.ts +11 -7
- package/dist/api/resources/agents/agents.d.ts.map +1 -1
- package/dist/api/resources/agents/agents.js +14 -8
- package/dist/kernl/__tests__/memory-config.test.d.ts +2 -0
- package/dist/kernl/__tests__/memory-config.test.d.ts.map +1 -0
- package/dist/kernl/__tests__/memory-config.test.js +157 -0
- package/dist/kernl/kernl.d.ts +11 -2
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/kernl/kernl.js +62 -28
- package/dist/kernl/types.d.ts +6 -5
- package/dist/kernl/types.d.ts.map +1 -1
- package/dist/lib/env.d.ts +2 -2
- package/dist/mcp/__tests__/utils.test.js +4 -2
- package/dist/mcp/utils.d.ts +1 -1
- package/dist/mcp/utils.js +1 -1
- package/dist/memory/__tests__/encoder.test.js +46 -0
- package/dist/memory/codecs/domain.js +1 -2
- package/dist/memory/encoder.d.ts +7 -7
- package/dist/memory/encoder.d.ts.map +1 -1
- package/dist/memory/encoder.js +15 -7
- package/dist/memory/memory.js +1 -1
- package/dist/memory/types.d.ts +6 -2
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/realtime/agent.d.ts +17 -0
- package/dist/realtime/agent.d.ts.map +1 -0
- package/dist/realtime/agent.js +17 -0
- package/dist/realtime/channel.d.ts +30 -0
- package/dist/realtime/channel.d.ts.map +1 -0
- package/dist/realtime/channel.js +1 -0
- package/dist/realtime/index.d.ts +5 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/session.d.ts +98 -0
- package/dist/realtime/session.d.ts.map +1 -0
- package/dist/realtime/session.js +203 -0
- package/dist/realtime/types.d.ts +58 -0
- package/dist/realtime/types.d.ts.map +1 -0
- package/dist/realtime/types.js +1 -0
- package/dist/storage/in-memory.d.ts.map +1 -1
- package/dist/storage/in-memory.js +5 -1
- package/dist/tool/__tests__/toolkit.test.js +2 -2
- package/dist/tool/tool.d.ts +2 -1
- package/dist/tool/tool.d.ts.map +1 -1
- package/dist/tool/toolkit.d.ts +4 -4
- package/dist/tool/toolkit.d.ts.map +1 -1
- package/dist/tool/toolkit.js +2 -1
- package/dist/tool/types.d.ts +4 -4
- package/dist/tool/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agent/base.ts +220 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/types.ts +5 -0
- package/src/agent.ts +12 -231
- package/src/api/resources/agents/agents.ts +19 -13
- package/src/kernl/__tests__/memory-config.test.ts +203 -0
- package/src/kernl/kernl.ts +81 -39
- package/src/kernl/types.ts +6 -5
- package/src/mcp/__tests__/utils.test.ts +4 -2
- package/src/mcp/utils.ts +1 -1
- package/src/memory/__tests__/encoder.test.ts +63 -0
- package/src/memory/codecs/domain.ts +1 -1
- package/src/memory/encoder.ts +18 -10
- package/src/memory/memory.ts +1 -1
- package/src/memory/types.ts +6 -2
- package/src/realtime/agent.ts +24 -0
- package/src/realtime/channel.ts +32 -0
- package/src/realtime/index.ts +4 -0
- package/src/realtime/session.ts +259 -0
- package/src/realtime/types.ts +73 -0
- package/src/storage/in-memory.ts +9 -1
- package/src/tool/__tests__/toolkit.test.ts +2 -2
- package/src/tool/tool.ts +2 -1
- package/src/tool/toolkit.ts +6 -5
- package/src/tool/types.ts +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Agent } from "@/agent";
|
|
2
|
+
import type { BaseAgent } from "@/agent/base";
|
|
2
3
|
import type { AgentOutputType } from "@/agent/types";
|
|
3
4
|
import type { UnknownContext } from "@/context";
|
|
4
5
|
import type { TextOutput } from "@/thread/types";
|
|
@@ -9,13 +10,19 @@ import type { TextOutput } from "@/thread/types";
|
|
|
9
10
|
* Thin facade over the in-process agent registry, returning live Agent instances.
|
|
10
11
|
*
|
|
11
12
|
* Note: agents are code, not persisted data; this is process-local.
|
|
13
|
+
*
|
|
14
|
+
* Currently only exposes LLM agents (kind: "llm"). RealtimeAgents are stored
|
|
15
|
+
* in the internal registry but not returned by these methods. If public access
|
|
16
|
+
* to realtime agents is needed, add a separate `kernl.realtimeAgents` resource.
|
|
12
17
|
*/
|
|
13
18
|
export class RAgents {
|
|
14
|
-
constructor(private readonly registry: Map<string,
|
|
19
|
+
constructor(private readonly registry: Map<string, BaseAgent>) {}
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Get a live Agent instance by id.
|
|
18
23
|
*
|
|
24
|
+
* Only returns LLM agents. Returns undefined for realtime agents.
|
|
25
|
+
*
|
|
19
26
|
* Callers are expected to know the concrete TContext/TOutput types
|
|
20
27
|
* for their own agents and can specify them via generics.
|
|
21
28
|
*/
|
|
@@ -24,27 +31,26 @@ export class RAgents {
|
|
|
24
31
|
TOutput extends AgentOutputType = TextOutput,
|
|
25
32
|
>(id: string): Agent<TContext, TOutput> | undefined {
|
|
26
33
|
const agent = this.registry.get(id);
|
|
27
|
-
|
|
34
|
+
if (agent?.kind === "llm") {
|
|
35
|
+
return agent as Agent<TContext, TOutput>;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/**
|
|
31
|
-
* Check if an agent with the given id is registered.
|
|
41
|
+
* Check if an LLM agent with the given id is registered.
|
|
32
42
|
*/
|
|
33
43
|
has(id: string): boolean {
|
|
34
|
-
return this.registry.
|
|
44
|
+
return this.registry.get(id)?.kind === "llm";
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/**
|
|
38
|
-
* List all registered agents as live instances.
|
|
39
|
-
*
|
|
40
|
-
* Since this is a heterogeneous collection, we expose the widest safe
|
|
41
|
-
* type parameters here.
|
|
48
|
+
* List all registered LLM agents as live instances.
|
|
42
49
|
*/
|
|
43
50
|
list(): Agent<UnknownContext, AgentOutputType>[] {
|
|
44
|
-
return Array.from(this.registry.values())
|
|
45
|
-
UnknownContext,
|
|
46
|
-
|
|
47
|
-
>[];
|
|
51
|
+
return Array.from(this.registry.values()).filter(
|
|
52
|
+
(a): a is Agent<UnknownContext, AgentOutputType> => a.kind === "llm",
|
|
53
|
+
);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { LanguageModel, EmbeddingModel } from "@kernl-sdk/protocol";
|
|
3
|
+
import type { SearchIndex } from "@kernl-sdk/retrieval";
|
|
4
|
+
|
|
5
|
+
import { Kernl } from "../kernl";
|
|
6
|
+
import { Agent } from "@/agent";
|
|
7
|
+
import { logger } from "@/lib/logger";
|
|
8
|
+
|
|
9
|
+
function createMockLanguageModel(): LanguageModel {
|
|
10
|
+
return {
|
|
11
|
+
spec: "1.0" as const,
|
|
12
|
+
provider: "test",
|
|
13
|
+
modelId: "test-model",
|
|
14
|
+
} as unknown as LanguageModel;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createMockEmbeddingModel(): EmbeddingModel<string> {
|
|
18
|
+
return {
|
|
19
|
+
provider: "test",
|
|
20
|
+
modelId: "test-embedder",
|
|
21
|
+
embed: vi.fn(async ({ values }: { values: string[] }) => ({
|
|
22
|
+
embeddings: values.map((v) => [v.length, 0, 0]),
|
|
23
|
+
})),
|
|
24
|
+
} as unknown as EmbeddingModel<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createMockVectorIndex(): SearchIndex {
|
|
28
|
+
return {
|
|
29
|
+
id: "mock",
|
|
30
|
+
capabilities: () => ({
|
|
31
|
+
namespacing: false,
|
|
32
|
+
filtering: { basic: true },
|
|
33
|
+
hybrid: false,
|
|
34
|
+
}),
|
|
35
|
+
createIndex: vi.fn(),
|
|
36
|
+
deleteIndex: vi.fn(),
|
|
37
|
+
upsert: vi.fn(),
|
|
38
|
+
query: vi.fn(),
|
|
39
|
+
delete: vi.fn(),
|
|
40
|
+
} as unknown as SearchIndex;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("Kernl memory config warnings", () => {
|
|
44
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
warnSpy.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("warns when agent enables memory but no embedding configured", () => {
|
|
55
|
+
const kernl = new Kernl({
|
|
56
|
+
storage: {
|
|
57
|
+
vector: createMockVectorIndex(),
|
|
58
|
+
},
|
|
59
|
+
// no memory.embedding
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const agent = new Agent({
|
|
63
|
+
id: "test-agent",
|
|
64
|
+
name: "Test Agent",
|
|
65
|
+
instructions: "test",
|
|
66
|
+
model: createMockLanguageModel(),
|
|
67
|
+
memory: { enabled: true },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
kernl.register(agent);
|
|
71
|
+
|
|
72
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
73
|
+
expect.stringContaining("Embeddings are not configured"),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("warns when agent enables memory but no vector storage configured", () => {
|
|
78
|
+
const kernl = new Kernl({
|
|
79
|
+
memory: {
|
|
80
|
+
embedding: createMockEmbeddingModel(),
|
|
81
|
+
},
|
|
82
|
+
// no storage.vector
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const agent = new Agent({
|
|
86
|
+
id: "test-agent",
|
|
87
|
+
name: "Test Agent",
|
|
88
|
+
instructions: "test",
|
|
89
|
+
model: createMockLanguageModel(),
|
|
90
|
+
memory: { enabled: true },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
kernl.register(agent);
|
|
94
|
+
|
|
95
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
96
|
+
expect.stringContaining("No vector storage configured"),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("warns for both missing embedding and vector storage", () => {
|
|
101
|
+
const kernl = new Kernl({});
|
|
102
|
+
|
|
103
|
+
const agent = new Agent({
|
|
104
|
+
id: "test-agent",
|
|
105
|
+
name: "Test Agent",
|
|
106
|
+
instructions: "test",
|
|
107
|
+
model: createMockLanguageModel(),
|
|
108
|
+
memory: { enabled: true },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
kernl.register(agent);
|
|
112
|
+
|
|
113
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
115
|
+
expect.stringContaining("Embeddings are not configured"),
|
|
116
|
+
);
|
|
117
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
118
|
+
expect.stringContaining("No vector storage configured"),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("warns only once across multiple agents", () => {
|
|
123
|
+
const kernl = new Kernl({});
|
|
124
|
+
|
|
125
|
+
const agent1 = new Agent({
|
|
126
|
+
id: "agent-1",
|
|
127
|
+
name: "Agent 1",
|
|
128
|
+
instructions: "test",
|
|
129
|
+
model: createMockLanguageModel(),
|
|
130
|
+
memory: { enabled: true },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const agent2 = new Agent({
|
|
134
|
+
id: "agent-2",
|
|
135
|
+
name: "Agent 2",
|
|
136
|
+
instructions: "test",
|
|
137
|
+
model: createMockLanguageModel(),
|
|
138
|
+
memory: { enabled: true },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
kernl.register(agent1);
|
|
142
|
+
kernl.register(agent2);
|
|
143
|
+
|
|
144
|
+
// Should only warn twice total (once for embedding, once for vector)
|
|
145
|
+
// not four times (twice per agent)
|
|
146
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("does not warn when memory config is complete", () => {
|
|
150
|
+
const kernl = new Kernl({
|
|
151
|
+
storage: {
|
|
152
|
+
vector: createMockVectorIndex(),
|
|
153
|
+
},
|
|
154
|
+
memory: {
|
|
155
|
+
embedding: createMockEmbeddingModel(),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const agent = new Agent({
|
|
160
|
+
id: "test-agent",
|
|
161
|
+
name: "Test Agent",
|
|
162
|
+
instructions: "test",
|
|
163
|
+
model: createMockLanguageModel(),
|
|
164
|
+
memory: { enabled: true },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
kernl.register(agent);
|
|
168
|
+
|
|
169
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("does not warn when agent does not enable memory", () => {
|
|
173
|
+
const kernl = new Kernl({});
|
|
174
|
+
|
|
175
|
+
const agent = new Agent({
|
|
176
|
+
id: "test-agent",
|
|
177
|
+
name: "Test Agent",
|
|
178
|
+
instructions: "test",
|
|
179
|
+
model: createMockLanguageModel(),
|
|
180
|
+
memory: { enabled: false },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
kernl.register(agent);
|
|
184
|
+
|
|
185
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("does not warn when agent has no memory config", () => {
|
|
189
|
+
const kernl = new Kernl({});
|
|
190
|
+
|
|
191
|
+
const agent = new Agent({
|
|
192
|
+
id: "test-agent",
|
|
193
|
+
name: "Test Agent",
|
|
194
|
+
instructions: "test",
|
|
195
|
+
model: createMockLanguageModel(),
|
|
196
|
+
// no memory config at all
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
kernl.register(agent);
|
|
200
|
+
|
|
201
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
});
|
package/src/kernl/kernl.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LanguageModel } from "@kernl-sdk/protocol";
|
|
2
2
|
import { resolveEmbeddingModel } from "@kernl-sdk/retrieval";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { BaseAgent } from "@/agent/base";
|
|
5
5
|
import { UnknownContext } from "@/context";
|
|
6
6
|
import { KernlHooks } from "@/lifecycle";
|
|
7
7
|
import type { Thread } from "@/thread";
|
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
buildMemoryIndexSchema,
|
|
17
17
|
} from "@/memory";
|
|
18
18
|
|
|
19
|
+
import { logger } from "@/lib/logger";
|
|
20
|
+
|
|
19
21
|
import type { ThreadExecuteResult, ThreadStreamEvent } from "@/thread/types";
|
|
20
22
|
import type { AgentOutputType } from "@/agent/types";
|
|
21
|
-
|
|
22
|
-
import type { KernlOptions } from "./types";
|
|
23
|
+
import type { KernlOptions, MemoryOptions, StorageOptions } from "./types";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The kernl - manages agent processes, scheduling, and task lifecycle.
|
|
@@ -28,12 +29,20 @@ import type { KernlOptions } from "./types";
|
|
|
28
29
|
* tracing.
|
|
29
30
|
*/
|
|
30
31
|
export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
|
|
31
|
-
private readonly _agents: Map<string,
|
|
32
|
+
private readonly _agents: Map<string, BaseAgent> = new Map();
|
|
32
33
|
private readonly _models: Map<string, LanguageModel> = new Map();
|
|
33
34
|
|
|
34
35
|
readonly storage: KernlStorage;
|
|
35
36
|
athreads: Map<string, Thread<any, any>> = new Map(); /* active threads */
|
|
36
37
|
|
|
38
|
+
private readonly _memopts: MemoryOptions | undefined;
|
|
39
|
+
private readonly _storopts: StorageOptions | undefined;
|
|
40
|
+
|
|
41
|
+
private warnings = {
|
|
42
|
+
embedding: false, // "Embeddings are not configured. If you want memories to auto-embed text content..."
|
|
43
|
+
vector: false, // "No vector storage configured. The memories.search() function will not be..."
|
|
44
|
+
}; /* tracks warnings that have been logged */
|
|
45
|
+
|
|
37
46
|
// --- public API ---
|
|
38
47
|
readonly threads: RThreads;
|
|
39
48
|
readonly agents: RAgents;
|
|
@@ -41,53 +50,49 @@ export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
|
|
|
41
50
|
|
|
42
51
|
constructor(options: KernlOptions = {}) {
|
|
43
52
|
super();
|
|
53
|
+
|
|
54
|
+
this._memopts = options.memory;
|
|
55
|
+
this._storopts = options.storage;
|
|
56
|
+
|
|
44
57
|
this.storage = options.storage?.db ?? new InMemoryStorage();
|
|
45
58
|
this.storage.bind({ agents: this._agents, models: this._models });
|
|
46
59
|
this.threads = new RThreads(this.storage.threads);
|
|
47
60
|
this.agents = new RAgents(this._agents);
|
|
48
|
-
|
|
49
|
-
// initialize memory
|
|
50
|
-
const embeddingModel =
|
|
51
|
-
options.memory?.embeddingModel ?? "openai/text-embedding-3-small";
|
|
52
|
-
const embedder =
|
|
53
|
-
typeof embeddingModel === "string"
|
|
54
|
-
? resolveEmbeddingModel<string>(embeddingModel)
|
|
55
|
-
: embeddingModel;
|
|
56
|
-
const encoder = new MemoryByteEncoder(embedder);
|
|
57
|
-
|
|
58
|
-
const vector = options.storage?.vector;
|
|
59
|
-
const indexId = options.memory?.indexId ?? "memories_sindex";
|
|
60
|
-
const dimensions = options.memory?.dimensions ?? 1536;
|
|
61
|
-
const providerOptions = options.memory?.indexProviderOptions ?? { schema: "kernl" };
|
|
62
|
-
|
|
63
|
-
this.memories = new Memory({
|
|
64
|
-
store: this.storage.memories,
|
|
65
|
-
search:
|
|
66
|
-
vector !== undefined
|
|
67
|
-
? new MemoryIndexHandle({
|
|
68
|
-
index: vector,
|
|
69
|
-
indexId,
|
|
70
|
-
schema: buildMemoryIndexSchema({ dimensions }),
|
|
71
|
-
providerOptions,
|
|
72
|
-
})
|
|
73
|
-
: undefined,
|
|
74
|
-
encoder,
|
|
75
|
-
});
|
|
61
|
+
this.memories = this.initializeMemory();
|
|
76
62
|
}
|
|
77
63
|
|
|
78
64
|
/**
|
|
79
65
|
* Registers a new agent with the kernl instance.
|
|
80
66
|
*/
|
|
81
|
-
register(agent:
|
|
67
|
+
register(agent: BaseAgent): void {
|
|
82
68
|
this._agents.set(agent.id, agent);
|
|
83
69
|
agent.bind(this);
|
|
84
70
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
// memory config warnings (log once)
|
|
72
|
+
if (agent.memory.enabled) {
|
|
73
|
+
if (!this._memopts?.embedding && !this.warnings.embedding) {
|
|
74
|
+
logger.warn(
|
|
75
|
+
"Embeddings are not configured. If you want memories to auto-embed text content, " +
|
|
76
|
+
"pass an embedding model into the memory config in new Kernl()",
|
|
77
|
+
);
|
|
78
|
+
this.warnings.embedding = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!this._storopts?.vector && !this.warnings.vector) {
|
|
82
|
+
logger.warn(
|
|
83
|
+
"No vector storage configured. The memories.search() function will not be " +
|
|
84
|
+
"available. To enable memory search, pass storage.vector in new Kernl()",
|
|
85
|
+
);
|
|
86
|
+
this.warnings.vector = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// auto-populate model registry for storage hydration (llm agents only - for now)
|
|
91
|
+
if (agent.kind === "llm") {
|
|
92
|
+
const key = `${agent.model.provider}/${agent.model.modelId}`;
|
|
93
|
+
if (!this._models.has(key)) {
|
|
94
|
+
this._models.set(key, agent.model as LanguageModel);
|
|
95
|
+
}
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -152,4 +157,41 @@ export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
|
|
|
152
157
|
this.athreads.delete(thread.tid);
|
|
153
158
|
}
|
|
154
159
|
}
|
|
160
|
+
|
|
161
|
+
// --- private utils ---
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @internal
|
|
165
|
+
*
|
|
166
|
+
* Initialize the memory system based on the storage + memory configuration.
|
|
167
|
+
*/
|
|
168
|
+
private initializeMemory(): Memory {
|
|
169
|
+
const embeddingModel = this._memopts?.embedding;
|
|
170
|
+
const embedder = embeddingModel
|
|
171
|
+
? typeof embeddingModel === "string"
|
|
172
|
+
? resolveEmbeddingModel<string>(embeddingModel)
|
|
173
|
+
: embeddingModel
|
|
174
|
+
: undefined;
|
|
175
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
176
|
+
|
|
177
|
+
const vector = this._storopts?.vector;
|
|
178
|
+
const indexId = this._memopts?.indexId ?? "memories_sindex";
|
|
179
|
+
const dimensions = this._memopts?.dimensions ?? 1536;
|
|
180
|
+
const providerOptions = this._memopts?.indexProviderOptions ?? {
|
|
181
|
+
schema: "kernl",
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return new Memory({
|
|
185
|
+
store: this.storage.memories,
|
|
186
|
+
search: vector
|
|
187
|
+
? new MemoryIndexHandle({
|
|
188
|
+
index: vector,
|
|
189
|
+
indexId,
|
|
190
|
+
schema: buildMemoryIndexSchema({ dimensions }),
|
|
191
|
+
providerOptions,
|
|
192
|
+
})
|
|
193
|
+
: undefined,
|
|
194
|
+
encoder,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
155
197
|
}
|
package/src/kernl/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LanguageModel, EmbeddingModel } from "@kernl-sdk/protocol";
|
|
2
2
|
import { SearchIndex } from "@kernl-sdk/retrieval";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { BaseAgent } from "@/agent/base";
|
|
5
5
|
import { KernlStorage } from "@/storage";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -35,9 +35,10 @@ export interface MemoryOptions {
|
|
|
35
35
|
* - A string like "openai/text-embedding-3-small" (resolved via provider registry)
|
|
36
36
|
* - An EmbeddingModel instance
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* If not provided, memories will not auto-embed text content and
|
|
39
|
+
* semantic search will not be available.
|
|
39
40
|
*/
|
|
40
|
-
|
|
41
|
+
embedding?: string | EmbeddingModel<string>;
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Logical index ID used by the search backend.
|
|
@@ -86,10 +87,10 @@ export interface KernlOptions {
|
|
|
86
87
|
/**
|
|
87
88
|
* Agent registry interface.
|
|
88
89
|
*
|
|
89
|
-
* Satisfied by Map<string,
|
|
90
|
+
* Satisfied by Map<string, BaseAgent>.
|
|
90
91
|
*/
|
|
91
92
|
export interface AgentRegistry {
|
|
92
|
-
get(id: string):
|
|
93
|
+
get(id: string): BaseAgent<any> | undefined;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/**
|
|
@@ -63,7 +63,7 @@ describe("mcpToFunctionTool", () => {
|
|
|
63
63
|
expect(functionTool.parameters).toBeDefined();
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it("should handle tools without inputSchema (
|
|
66
|
+
it("should handle tools without inputSchema (empty object parameters)", () => {
|
|
67
67
|
const server = createMockServer();
|
|
68
68
|
// In practice, MCP SDK tools require inputSchema, but our function handles
|
|
69
69
|
// the case where it might not be present. We use 'as any' to test this edge case.
|
|
@@ -75,7 +75,9 @@ describe("mcpToFunctionTool", () => {
|
|
|
75
75
|
const functionTool = mcpToFunctionTool(server, mcpTool);
|
|
76
76
|
|
|
77
77
|
expect(functionTool.id).toBe("no_params");
|
|
78
|
-
|
|
78
|
+
// When no inputSchema, we use an empty z.object({}) to match AI SDK behavior
|
|
79
|
+
expect(functionTool.parameters).toBeDefined();
|
|
80
|
+
expect(functionTool.parameters?.def.type).toBe("object");
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
it("should invoke server.callTool with correct params", async () => {
|
package/src/mcp/utils.ts
CHANGED
|
@@ -150,5 +150,68 @@ describe("MemoryByteEncoder", () => {
|
|
|
150
150
|
|
|
151
151
|
expect(vec).toEqual([12, 0, 0]); // "search query".length = 12
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
it("returns null when no embedder configured", async () => {
|
|
155
|
+
const encoder = new MemoryByteEncoder(); // no embedder
|
|
156
|
+
|
|
157
|
+
const vec = await encoder.embed("search query");
|
|
158
|
+
|
|
159
|
+
expect(vec).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("throws when embedder returns empty embedding", async () => {
|
|
163
|
+
const embedder = {
|
|
164
|
+
provider: "test",
|
|
165
|
+
modelId: "test-embedder",
|
|
166
|
+
embed: vi.fn(async () => ({
|
|
167
|
+
embeddings: [[]], // empty embedding
|
|
168
|
+
})),
|
|
169
|
+
} as unknown as EmbeddingModel<string>;
|
|
170
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
171
|
+
|
|
172
|
+
await expect(encoder.embed("test")).rejects.toThrow(
|
|
173
|
+
"Embedder returned empty embedding",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("throws when embedder returns undefined embedding", async () => {
|
|
178
|
+
const embedder = {
|
|
179
|
+
provider: "test",
|
|
180
|
+
modelId: "test-embedder",
|
|
181
|
+
embed: vi.fn(async () => ({
|
|
182
|
+
embeddings: [], // no embeddings at all
|
|
183
|
+
})),
|
|
184
|
+
} as unknown as EmbeddingModel<string>;
|
|
185
|
+
const encoder = new MemoryByteEncoder(embedder);
|
|
186
|
+
|
|
187
|
+
await expect(encoder.embed("test")).rejects.toThrow(
|
|
188
|
+
"Embedder returned empty embedding",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("without embedder", () => {
|
|
194
|
+
it("returns undefined tvec when encoding with no embedder", async () => {
|
|
195
|
+
const encoder = new MemoryByteEncoder(); // no embedder
|
|
196
|
+
|
|
197
|
+
const byte: MemoryByte = { text: "Hello world" };
|
|
198
|
+
const result = await encoder.encode(byte);
|
|
199
|
+
|
|
200
|
+
expect(result.text).toBe("Hello world");
|
|
201
|
+
expect(result.tvec).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("still produces objtext projection without embedder", async () => {
|
|
205
|
+
const encoder = new MemoryByteEncoder(); // no embedder
|
|
206
|
+
|
|
207
|
+
const byte: MemoryByte = {
|
|
208
|
+
object: { preference: "coffee", shots: 2 },
|
|
209
|
+
};
|
|
210
|
+
const result = await encoder.encode(byte);
|
|
211
|
+
|
|
212
|
+
expect(result.text).toContain("preference: coffee");
|
|
213
|
+
expect(result.objtext).toContain("preference: coffee");
|
|
214
|
+
expect(result.tvec).toBeUndefined();
|
|
215
|
+
});
|
|
153
216
|
});
|
|
154
217
|
});
|
|
@@ -110,7 +110,7 @@ export const PATCH_CODEC: Codec<MemoryRecordUpdate, IndexMemoryRecordPatch> = {
|
|
|
110
110
|
if (update.collection !== undefined) patch.collection = update.collection;
|
|
111
111
|
if (update.timestamp !== undefined) patch.timestamp = update.timestamp;
|
|
112
112
|
if (update.updatedAt !== undefined) patch.updatedAt = update.updatedAt;
|
|
113
|
-
|
|
113
|
+
// metadata is stored in primary DB only, not indexed
|
|
114
114
|
|
|
115
115
|
return patch;
|
|
116
116
|
},
|
package/src/memory/encoder.ts
CHANGED
|
@@ -41,11 +41,12 @@ export const ObjectTextCodec = {
|
|
|
41
41
|
* Encoder that converts MemoryByte to IndexableByte.
|
|
42
42
|
*
|
|
43
43
|
* Extracts canonical text from content and computes embeddings.
|
|
44
|
+
* If no embedder is provided, skips embedding and tvec will be undefined.
|
|
44
45
|
*/
|
|
45
46
|
export class MemoryByteEncoder implements MemoryByteCodec {
|
|
46
|
-
private readonly embedder
|
|
47
|
+
private readonly embedder?: EmbeddingModel<string>;
|
|
47
48
|
|
|
48
|
-
constructor(embedder
|
|
49
|
+
constructor(embedder?: EmbeddingModel<string>) {
|
|
49
50
|
this.embedder = embedder;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -55,9 +56,6 @@ export class MemoryByteEncoder implements MemoryByteCodec {
|
|
|
55
56
|
* - Produces `objtext` string projection for FTS indexing
|
|
56
57
|
* - Combines text + objtext for embedding input
|
|
57
58
|
* - 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.
|
|
61
59
|
*/
|
|
62
60
|
async encode(byte: MemoryByte): Promise<IndexableByte> {
|
|
63
61
|
const objtext = byte.object
|
|
@@ -67,8 +65,9 @@ export class MemoryByteEncoder implements MemoryByteCodec {
|
|
|
67
65
|
// (TODO): this behavior deserves consideration - do we always want to merge text + object?
|
|
68
66
|
//
|
|
69
67
|
// combine text + object for richer embedding
|
|
68
|
+
// skip embedding if no embedder configured (embed returns null)
|
|
70
69
|
const combined = [byte.text, objtext].filter(Boolean).join("\n");
|
|
71
|
-
const tvec = combined ? await this.embed(combined) :
|
|
70
|
+
const tvec = combined ? await this.embed(combined) : null;
|
|
72
71
|
|
|
73
72
|
// TODO: embed other modalities (image, audio, video)
|
|
74
73
|
//
|
|
@@ -79,7 +78,7 @@ export class MemoryByteEncoder implements MemoryByteCodec {
|
|
|
79
78
|
return {
|
|
80
79
|
text: byte.text ?? objtext, // fallback to projection if no text
|
|
81
80
|
objtext,
|
|
82
|
-
tvec,
|
|
81
|
+
tvec: tvec ?? undefined,
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -92,10 +91,19 @@ export class MemoryByteEncoder implements MemoryByteCodec {
|
|
|
92
91
|
|
|
93
92
|
/**
|
|
94
93
|
* Embed a text string.
|
|
95
|
-
*
|
|
94
|
+
*
|
|
95
|
+
* @returns Embedding vector, or null if no embedder configured.
|
|
96
|
+
* @throws If embedder returns empty embedding.
|
|
96
97
|
*/
|
|
97
|
-
async embed(text: string): Promise<number[]> {
|
|
98
|
+
async embed(text: string): Promise<number[] | null> {
|
|
99
|
+
if (!this.embedder) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
98
102
|
const result = await this.embedder.embed({ values: [text] });
|
|
99
|
-
|
|
103
|
+
const embedding = result.embeddings[0];
|
|
104
|
+
if (!embedding || embedding.length === 0) {
|
|
105
|
+
throw new Error("Embedder returned empty embedding");
|
|
106
|
+
}
|
|
107
|
+
return embedding;
|
|
100
108
|
}
|
|
101
109
|
}
|
package/src/memory/memory.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class Memory {
|
|
|
95
95
|
const tvec = await this.encoder.embed(q.query);
|
|
96
96
|
|
|
97
97
|
return this._search.query({
|
|
98
|
-
query: [{ text: q.query, tvec }],
|
|
98
|
+
query: [{ text: q.query, tvec: tvec ?? undefined }],
|
|
99
99
|
filter: q.filter ? MEMORY_FILTER.encode(q.filter) : undefined,
|
|
100
100
|
topK: q.limit ?? 20,
|
|
101
101
|
});
|
package/src/memory/types.ts
CHANGED
|
@@ -75,7 +75,12 @@ export interface IndexableByte {
|
|
|
75
75
|
* Encoder that converts MemoryByte to IndexableByte with embeddings.
|
|
76
76
|
*/
|
|
77
77
|
export interface MemoryByteCodec extends AsyncCodec<MemoryByte, IndexableByte> {
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Embed a text string.
|
|
80
|
+
*
|
|
81
|
+
* @returns Embedding vector, or null if no embedder configured.
|
|
82
|
+
*/
|
|
83
|
+
embed(text: string): Promise<number[] | null>;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
// -------------------
|
|
@@ -294,5 +299,4 @@ export interface IndexMemoryRecordPatch {
|
|
|
294
299
|
collection?: string;
|
|
295
300
|
timestamp?: number;
|
|
296
301
|
updatedAt?: number;
|
|
297
|
-
metadata?: JSONObject | null;
|
|
298
302
|
}
|