kernl 0.8.2 → 0.8.4
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 +4 -5
- package/CHANGELOG.md +12 -0
- 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 +9 -0
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/kernl/kernl.js +56 -22
- package/dist/kernl/types.d.ts +3 -2
- package/dist/kernl/types.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +10 -27
- 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/package.json +1 -2
- package/src/kernl/__tests__/memory-config.test.ts +203 -0
- package/src/kernl/kernl.ts +72 -30
- package/src/kernl/types.ts +3 -2
- package/src/lib/logger.ts +10 -31
- 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/.turbo/turbo-check-types.log +0 -4
- package/dist/memory/codec.d.ts +0 -32
- package/dist/memory/codec.d.ts.map +0 -1
- package/dist/memory/codec.js +0 -97
- package/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kernl",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "A modern AI agent framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kernl",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
36
|
-
"pino": "^9.6.0",
|
|
37
36
|
"yaml": "^2.8.2",
|
|
38
37
|
"zod": "^4.1.12",
|
|
39
38
|
"@kernl-sdk/protocol": "0.2.8",
|
|
@@ -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
|
@@ -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.
|
|
@@ -34,6 +35,14 @@ export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
|
|
|
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,38 +50,15 @@ 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
|
/**
|
|
@@ -82,6 +68,25 @@ export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
|
|
|
82
68
|
this._agents.set(agent.id, agent);
|
|
83
69
|
agent.bind(this);
|
|
84
70
|
|
|
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
|
+
|
|
85
90
|
// (TODO): implement exhaustive model registry in protocol/ package
|
|
86
91
|
//
|
|
87
92
|
// auto-populate model registry for storage hydration
|
|
@@ -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
|
@@ -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.
|
package/src/lib/logger.ts
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
import pino from "pino";
|
|
2
|
-
|
|
3
1
|
import { env } from "./env";
|
|
4
2
|
|
|
5
|
-
/**
|
|
6
|
-
* By default we don't log LLM inputs/outputs, to prevent exposing sensitive data.
|
|
7
|
-
* Set KERNL_LOG_MODEL_DATA=true to enable.
|
|
8
|
-
*/
|
|
9
|
-
const dontLogModelData = !env.KERNL_LOG_MODEL_DATA;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* By default we don't log tool inputs/outputs, to prevent exposing sensitive data.
|
|
13
|
-
* Set KERNL_LOG_TOOL_DATA=true to enable.
|
|
14
|
-
*/
|
|
15
|
-
const dontLogToolData = !env.KERNL_LOG_TOOL_DATA;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Base pino logger instance
|
|
19
|
-
*/
|
|
20
|
-
const base = pino({
|
|
21
|
-
level: env.LOG_LEVEL,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
3
|
/**
|
|
25
4
|
* A logger instance with namespace support and sensitive data flags.
|
|
26
5
|
*/
|
|
@@ -52,21 +31,21 @@ export type Logger = {
|
|
|
52
31
|
* Get a logger for a given namespace.
|
|
53
32
|
*
|
|
54
33
|
* @param namespace - the namespace to use for the logger (e.g., 'kernl:core', 'kernl:agent').
|
|
55
|
-
* @returns A logger object with
|
|
34
|
+
* @returns A logger object with console-based logging and sensitive data flags.
|
|
56
35
|
*/
|
|
57
36
|
export function getLogger(namespace: string = "kernl"): Logger {
|
|
58
|
-
const
|
|
37
|
+
const prefix = `[${namespace}]`;
|
|
59
38
|
|
|
60
39
|
return {
|
|
61
40
|
namespace,
|
|
62
|
-
trace:
|
|
63
|
-
debug:
|
|
64
|
-
info:
|
|
65
|
-
warn:
|
|
66
|
-
error:
|
|
67
|
-
fatal:
|
|
68
|
-
dontLogModelData,
|
|
69
|
-
dontLogToolData,
|
|
41
|
+
trace: (msg, ...args) => console.debug(prefix, msg, ...args),
|
|
42
|
+
debug: (msg, ...args) => console.debug(prefix, msg, ...args),
|
|
43
|
+
info: (msg, ...args) => console.info(prefix, msg, ...args),
|
|
44
|
+
warn: (msg, ...args) => console.warn(prefix, msg, ...args),
|
|
45
|
+
error: (msg, ...args) => console.error(prefix, msg, ...args),
|
|
46
|
+
fatal: (msg, ...args) => console.error(prefix, "[FATAL]", msg, ...args),
|
|
47
|
+
dontLogModelData: !env.KERNL_LOG_MODEL_DATA,
|
|
48
|
+
dontLogToolData: !env.KERNL_LOG_TOOL_DATA,
|
|
70
49
|
};
|
|
71
50
|
}
|
|
72
51
|
|
|
@@ -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
|
}
|
package/dist/memory/codec.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory codecs.
|
|
3
|
-
*/
|
|
4
|
-
import type { Codec, AsyncCodec } from "@kernl-sdk/shared/lib";
|
|
5
|
-
import type { Filter as SearchFilter } from "@kernl-sdk/retrieval";
|
|
6
|
-
import type { MemoryFilter, MemoryRecord, MemoryRecordUpdate, IndexMemoryRecord, IndexMemoryRecordPatch, MemoryByteCodec } from "./types.js";
|
|
7
|
-
/**
|
|
8
|
-
* Codec for converting MemoryFilter to retrieval Filter.
|
|
9
|
-
*
|
|
10
|
-
* - scope.namespace → namespace
|
|
11
|
-
* - scope.entityId → entityId
|
|
12
|
-
* - scope.agentId → agentId
|
|
13
|
-
* - collections → collection: { $in: [...] }
|
|
14
|
-
* - after/before → timestamp: { $gt/$lt }
|
|
15
|
-
*
|
|
16
|
-
* Content field filtering (text, metadata, kind) is not currently supported.
|
|
17
|
-
* Text relevance is handled via vector similarity in the query, not filters.
|
|
18
|
-
*/
|
|
19
|
-
export declare const MEMORY_FILTER: Codec<MemoryFilter, SearchFilter>;
|
|
20
|
-
/**
|
|
21
|
-
* Create a codec for MemoryRecord -> IndexMemoryRecord.
|
|
22
|
-
*/
|
|
23
|
-
export declare function recordCodec(bytecodec: MemoryByteCodec): AsyncCodec<MemoryRecord, IndexMemoryRecord>;
|
|
24
|
-
/**
|
|
25
|
-
* Codec for converting MemoryRecordUpdate to IndexMemoryRecordPatch.
|
|
26
|
-
*
|
|
27
|
-
* Maps patchable fields from domain update to index patch format.
|
|
28
|
-
* wmem/smem are store-only fields and are not included.
|
|
29
|
-
* content changes require full re-index, not a patch.
|
|
30
|
-
*/
|
|
31
|
-
export declare const PATCH_CODEC: Codec<MemoryRecordUpdate, IndexMemoryRecordPatch>;
|
|
32
|
-
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/memory/codec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,YAAY,EAAE,YAAY,CA0B3D,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,eAAe,GACzB,UAAU,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAqB7C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,kBAAkB,EAAE,sBAAsB,CAqBzE,CAAC"}
|