kernl 0.7.3 → 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.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/dist/agent/__tests__/systools.test.js +13 -9
  4. package/dist/agent/types.d.ts +20 -12
  5. package/dist/agent/types.d.ts.map +1 -1
  6. package/dist/agent.d.ts +12 -8
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +14 -14
  9. package/dist/api/resources/agents/agents.d.ts +5 -5
  10. package/dist/api/resources/agents/agents.d.ts.map +1 -1
  11. package/dist/api/resources/agents/agents.js +1 -1
  12. package/dist/guardrail.d.ts +19 -19
  13. package/dist/guardrail.d.ts.map +1 -1
  14. package/dist/kernl/kernl.d.ts +6 -6
  15. package/dist/kernl/kernl.d.ts.map +1 -1
  16. package/dist/lib/error.d.ts +3 -3
  17. package/dist/lib/error.d.ts.map +1 -1
  18. package/dist/lifecycle.d.ts +6 -6
  19. package/dist/lifecycle.d.ts.map +1 -1
  20. package/dist/memory/__tests__/encoder.test.d.ts +2 -0
  21. package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
  22. package/dist/memory/__tests__/encoder.test.js +120 -0
  23. package/dist/memory/codecs/domain.d.ts +5 -0
  24. package/dist/memory/codecs/domain.d.ts.map +1 -1
  25. package/dist/memory/codecs/domain.js +6 -0
  26. package/dist/memory/encoder.d.ts +25 -2
  27. package/dist/memory/encoder.d.ts.map +1 -1
  28. package/dist/memory/encoder.js +46 -5
  29. package/dist/memory/index.d.ts +2 -2
  30. package/dist/memory/index.d.ts.map +1 -1
  31. package/dist/memory/index.js +1 -1
  32. package/dist/memory/schema.d.ts.map +1 -1
  33. package/dist/memory/schema.js +5 -0
  34. package/dist/memory/types.d.ts +21 -4
  35. package/dist/memory/types.d.ts.map +1 -1
  36. package/dist/storage/in-memory.d.ts.map +1 -1
  37. package/dist/storage/in-memory.js +4 -2
  38. package/dist/thread/__tests__/integration.test.js +1 -1
  39. package/dist/thread/__tests__/thread.test.js +8 -8
  40. package/dist/thread/thread.d.ts +5 -5
  41. package/dist/thread/thread.d.ts.map +1 -1
  42. package/dist/thread/thread.js +13 -2
  43. package/dist/thread/types.d.ts +9 -6
  44. package/dist/thread/types.d.ts.map +1 -1
  45. package/dist/thread/utils.d.ts +7 -6
  46. package/dist/thread/utils.d.ts.map +1 -1
  47. package/dist/thread/utils.js +9 -8
  48. package/dist/tool/sys/memory.d.ts +1 -1
  49. package/dist/tool/sys/memory.d.ts.map +1 -1
  50. package/dist/tool/sys/memory.js +29 -8
  51. package/package.json +4 -3
  52. package/src/agent/__tests__/systools.test.ts +13 -9
  53. package/src/agent/types.ts +25 -29
  54. package/src/agent.ts +29 -28
  55. package/src/api/resources/agents/agents.ts +8 -8
  56. package/src/guardrail.ts +28 -28
  57. package/src/kernl/kernl.ts +12 -12
  58. package/src/lib/error.ts +3 -3
  59. package/src/lifecycle.ts +6 -6
  60. package/src/memory/__tests__/encoder.test.ts +153 -0
  61. package/src/memory/codecs/domain.ts +6 -0
  62. package/src/memory/encoder.ts +51 -6
  63. package/src/memory/index.ts +2 -1
  64. package/src/memory/schema.ts +5 -0
  65. package/src/memory/types.ts +20 -4
  66. package/src/storage/in-memory.ts +6 -2
  67. package/src/thread/__tests__/integration.test.ts +130 -146
  68. package/src/thread/__tests__/thread.test.ts +8 -8
  69. package/src/thread/thread.ts +21 -7
  70. package/src/thread/types.ts +9 -6
  71. package/src/thread/utils.ts +15 -14
  72. package/src/tool/sys/memory.ts +33 -9
package/src/guardrail.ts CHANGED
@@ -4,19 +4,19 @@ import { LanguageModelResponse } from "@kernl-sdk/protocol";
4
4
 
5
5
  import { Agent } from "./agent";
6
6
  import { Context, UnknownContext } from "./context";
7
- import type { AgentResponseType } from "@/agent/types";
8
- import type { TextResponse, ThreadEvent } from "@/thread/types";
7
+ import type { AgentOutputType } from "@/agent/types";
8
+ import type { TextOutput, ThreadEvent } from "@/thread/types";
9
9
 
10
10
  /**
11
11
  * Resolves the agent output type based on the response type.
12
- * - If TResponse is "text" → output is string
13
- * - If TResponse is a ZodType → output is the inferred type from that schema
12
+ * - If TOutput is "text" → output is string
13
+ * - If TOutput is a ZodType → output is the inferred type from that schema
14
14
  */
15
- export type ResolvedAgentResponse<TResponse extends AgentResponseType> =
16
- TResponse extends TextResponse
15
+ export type ResolvedAgentResponse<TOutput extends AgentOutputType> =
16
+ TOutput extends TextOutput
17
17
  ? string
18
- : TResponse extends ZodType
19
- ? z.infer<TResponse>
18
+ : TOutput extends ZodType
19
+ ? z.infer<TOutput>
20
20
  : never;
21
21
 
22
22
  /**
@@ -149,10 +149,10 @@ export function defineInputGuardrail({
149
149
  */
150
150
  export interface OutputGuardrailFunctionArgs<
151
151
  TContext = UnknownContext,
152
- TResponse extends AgentResponseType = TextResponse,
152
+ TOutput extends AgentOutputType = TextOutput,
153
153
  > {
154
154
  agent: Agent<any, any>;
155
- agentOutput: ResolvedAgentResponse<TResponse>; // ??
155
+ agentOutput: ResolvedAgentResponse<TOutput>; // ??
156
156
  context: Context<TContext>;
157
157
  /**
158
158
  * Additional details about the agent output.
@@ -166,16 +166,16 @@ export interface OutputGuardrailFunctionArgs<
166
166
  * A function that takes an output guardrail function arguments and returns a `GuardrailFunctionOutput`.
167
167
  */
168
168
  export type OutputGuardrailFunction<
169
- TResponse extends AgentResponseType = TextResponse,
169
+ TOutput extends AgentOutputType = TextOutput,
170
170
  > = (
171
- args: OutputGuardrailFunctionArgs<UnknownContext, TResponse>,
171
+ args: OutputGuardrailFunctionArgs<UnknownContext, TOutput>,
172
172
  ) => Promise<GuardrailFunctionOutput>;
173
173
 
174
174
  /**
175
175
  * A guardrail that checks the output of the agent.
176
176
  */
177
177
  export interface OutputGuardrail<
178
- TResponse extends AgentResponseType = TextResponse,
178
+ TOutput extends AgentOutputType = TextOutput,
179
179
  > {
180
180
  /**
181
181
  * The name of the guardrail.
@@ -185,7 +185,7 @@ export interface OutputGuardrail<
185
185
  /**
186
186
  * The function that performs the guardrail check.
187
187
  */
188
- execute: OutputGuardrailFunction<TResponse>;
188
+ execute: OutputGuardrailFunction<TOutput>;
189
189
  }
190
190
 
191
191
  /**
@@ -201,7 +201,7 @@ export interface OutputGuardrailMetadata {
201
201
  */
202
202
  export interface OutputGuardrailResult<
203
203
  TMeta = OutputGuardrailMetadata,
204
- TResponse extends AgentResponseType = TextResponse,
204
+ TOutput extends AgentOutputType = TextOutput,
205
205
  > {
206
206
  /**
207
207
  * The metadata of the guardrail.
@@ -211,12 +211,12 @@ export interface OutputGuardrailResult<
211
211
  /**
212
212
  * The output of the agent that ran.
213
213
  */
214
- agentOutput: ResolvedAgentResponse<TResponse>; // ??
214
+ agentOutput: ResolvedAgentResponse<TOutput>; // ??
215
215
 
216
216
  /**
217
217
  * The agent that ran.
218
218
  */
219
- agent: Agent<UnknownContext, TResponse>;
219
+ agent: Agent<UnknownContext, TOutput>;
220
220
 
221
221
  /**
222
222
  * The output of the guardrail.
@@ -229,43 +229,43 @@ export interface OutputGuardrailResult<
229
229
  */
230
230
  export interface OutputGuardrailDefinition<
231
231
  TMeta = OutputGuardrailMetadata,
232
- TResponse extends AgentResponseType = TextResponse,
232
+ TOutput extends AgentOutputType = TextOutput,
233
233
  > extends OutputGuardrailMetadata {
234
- guardrailFunction: OutputGuardrailFunction<TResponse>;
234
+ guardrailFunction: OutputGuardrailFunction<TOutput>;
235
235
  run(
236
- args: OutputGuardrailFunctionArgs<UnknownContext, TResponse>,
237
- ): Promise<OutputGuardrailResult<TMeta, TResponse>>;
236
+ args: OutputGuardrailFunctionArgs<UnknownContext, TOutput>,
237
+ ): Promise<OutputGuardrailResult<TMeta, TOutput>>;
238
238
  }
239
239
 
240
240
  /**
241
241
  * Arguments for defining an output guardrail definition.
242
242
  */
243
243
  export interface DefineOutputGuardrailArgs<
244
- TResponse extends AgentResponseType = TextResponse,
244
+ TOutput extends AgentOutputType = TextOutput,
245
245
  > {
246
246
  name: string;
247
- execute: OutputGuardrailFunction<TResponse>;
247
+ execute: OutputGuardrailFunction<TOutput>;
248
248
  }
249
249
 
250
250
  /**
251
251
  * Creates an output guardrail definition.
252
252
  */
253
253
  export function defineOutputGuardrail<
254
- TResponse extends AgentResponseType = TextResponse,
254
+ TOutput extends AgentOutputType = TextOutput,
255
255
  >({
256
256
  name,
257
257
  execute,
258
- }: DefineOutputGuardrailArgs<TResponse>): OutputGuardrailDefinition<
258
+ }: DefineOutputGuardrailArgs<TOutput>): OutputGuardrailDefinition<
259
259
  OutputGuardrailMetadata,
260
- TResponse
260
+ TOutput
261
261
  > {
262
262
  return {
263
263
  type: "output",
264
264
  name,
265
265
  guardrailFunction: execute,
266
266
  async run(
267
- args: OutputGuardrailFunctionArgs<UnknownContext, TResponse>,
268
- ): Promise<OutputGuardrailResult<OutputGuardrailMetadata, TResponse>> {
267
+ args: OutputGuardrailFunctionArgs<UnknownContext, TOutput>,
268
+ ): Promise<OutputGuardrailResult<OutputGuardrailMetadata, TOutput>> {
269
269
  return {
270
270
  guardrail: { type: "output", name },
271
271
  agent: args.agent,
@@ -17,7 +17,7 @@ import {
17
17
  } from "@/memory";
18
18
 
19
19
  import type { ThreadExecuteResult, ThreadStreamEvent } from "@/thread/types";
20
- import type { AgentResponseType } from "@/agent/types";
20
+ import type { AgentOutputType } from "@/agent/types";
21
21
 
22
22
  import type { KernlOptions } from "./types";
23
23
 
@@ -27,7 +27,7 @@ import type { KernlOptions } from "./types";
27
27
  * Orchestrates agent execution, including guardrails, tool calls, session persistence, and
28
28
  * tracing.
29
29
  */
30
- export class Kernl extends KernlHooks<UnknownContext, AgentResponseType> {
30
+ export class Kernl extends KernlHooks<UnknownContext, AgentOutputType> {
31
31
  private readonly _agents: Map<string, Agent> = new Map();
32
32
  private readonly _models: Map<string, LanguageModel> = new Map();
33
33
 
@@ -94,9 +94,9 @@ export class Kernl extends KernlHooks<UnknownContext, AgentResponseType> {
94
94
  /**
95
95
  * Spawn a new thread - blocking execution
96
96
  */
97
- async spawn<TContext, TResponse extends AgentResponseType>(
98
- thread: Thread<TContext, TResponse>,
99
- ): Promise<ThreadExecuteResult<ResolvedAgentResponse<TResponse>>> {
97
+ async spawn<TContext, TOutput extends AgentOutputType>(
98
+ thread: Thread<TContext, TOutput>,
99
+ ): Promise<ThreadExecuteResult<ResolvedAgentResponse<TOutput>>> {
100
100
  this.athreads.set(thread.tid, thread);
101
101
  try {
102
102
  return await thread.execute();
@@ -110,9 +110,9 @@ export class Kernl extends KernlHooks<UnknownContext, AgentResponseType> {
110
110
  *
111
111
  * NOTE: just blocks for now
112
112
  */
113
- async schedule<TContext, TResponse extends AgentResponseType>(
114
- thread: Thread<TContext, TResponse>,
115
- ): Promise<ThreadExecuteResult<ResolvedAgentResponse<TResponse>>> {
113
+ async schedule<TContext, TOutput extends AgentOutputType>(
114
+ thread: Thread<TContext, TOutput>,
115
+ ): Promise<ThreadExecuteResult<ResolvedAgentResponse<TOutput>>> {
116
116
  this.athreads.set(thread.tid, thread);
117
117
  try {
118
118
  return await thread.execute();
@@ -126,8 +126,8 @@ export class Kernl extends KernlHooks<UnknownContext, AgentResponseType> {
126
126
  *
127
127
  * Spawn a new thread - streaming execution
128
128
  */
129
- async *spawnStream<TContext, TResponse extends AgentResponseType>(
130
- thread: Thread<TContext, TResponse>,
129
+ async *spawnStream<TContext, TOutput extends AgentOutputType>(
130
+ thread: Thread<TContext, TOutput>,
131
131
  ): AsyncIterable<ThreadStreamEvent> {
132
132
  this.athreads.set(thread.tid, thread);
133
133
  try {
@@ -142,8 +142,8 @@ export class Kernl extends KernlHooks<UnknownContext, AgentResponseType> {
142
142
  *
143
143
  * Schedule an existing thread - streaming execution
144
144
  */
145
- async *scheduleStream<TContext, TResponse extends AgentResponseType>(
146
- thread: Thread<TContext, TResponse>,
145
+ async *scheduleStream<TContext, TOutput extends AgentOutputType>(
146
+ thread: Thread<TContext, TOutput>,
147
147
  ): AsyncIterable<ThreadStreamEvent> {
148
148
  this.athreads.set(thread.tid, thread);
149
149
  try {
package/src/lib/error.ts CHANGED
@@ -9,8 +9,8 @@ import { randomID } from "@kernl-sdk/shared/lib";
9
9
  // import { SerializedThread } from "@/lib/serde/thread";
10
10
  type SerializedThread = any;
11
11
 
12
- import { AgentResponseType } from "@/agent/types";
13
- import { TextResponse } from "@/thread/types";
12
+ import { AgentOutputType } from "@/agent/types";
13
+ import { TextOutput } from "@/thread/types";
14
14
 
15
15
  /**
16
16
  * Abstract base class for all `kernl` errors
@@ -144,7 +144,7 @@ export class InputGuardrailTripwireTriggered extends AgentError {
144
144
  */
145
145
  export class OutputGuardrailTripwireTriggered<
146
146
  TMeta extends OutputGuardrailMetadata,
147
- TOutputType extends AgentResponseType = TextResponse,
147
+ TOutputType extends AgentOutputType = TextOutput,
148
148
  > extends AgentError {
149
149
  result: OutputGuardrailResult<TMeta, TOutputType>;
150
150
  constructor(
package/src/lifecycle.ts CHANGED
@@ -5,8 +5,8 @@ import { Context, UnknownContext } from "./context";
5
5
  import { Tool } from "./tool";
6
6
  import type { ToolCall } from "@kernl-sdk/protocol";
7
7
 
8
- import { AgentResponseType } from "@/agent/types";
9
- import { TextResponse } from "@/thread/types";
8
+ import { AgentOutputType } from "@/agent/types";
9
+ import { TextOutput } from "@/thread/types";
10
10
 
11
11
  export type EventEmitterEvents = Record<string, any[]>;
12
12
 
@@ -60,7 +60,7 @@ class TypedEventEmitter<
60
60
 
61
61
  export type AgentHookEvents<
62
62
  TContext = UnknownContext,
63
- TOutput extends AgentResponseType = TextResponse,
63
+ TOutput extends AgentOutputType = TextOutput,
64
64
  > = {
65
65
  /**
66
66
  * @param context - The context of the run
@@ -107,7 +107,7 @@ export type AgentHookEvents<
107
107
  */
108
108
  export class AgentHooks<
109
109
  TContext = UnknownContext,
110
- TOutput extends AgentResponseType = TextResponse,
110
+ TOutput extends AgentOutputType = TextOutput,
111
111
  > extends TypedEventEmitter<AgentHookEvents<TContext, TOutput>> {}
112
112
 
113
113
  /**
@@ -119,7 +119,7 @@ export class AgentHooks<
119
119
  */
120
120
  export type KernlHookEvents<
121
121
  TContext = UnknownContext,
122
- TOutput extends AgentResponseType = TextResponse,
122
+ TOutput extends AgentOutputType = TextOutput,
123
123
  > = {
124
124
  /**
125
125
  * @param context - The context of the run
@@ -177,5 +177,5 @@ export type KernlHookEvents<
177
177
  */
178
178
  export class KernlHooks<
179
179
  TContext = UnknownContext,
180
- TOutput extends AgentResponseType = TextResponse,
180
+ TOutput extends AgentOutputType = TextOutput,
181
181
  > extends TypedEventEmitter<KernlHookEvents<TContext, TOutput>> {}
@@ -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> {
@@ -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
- * Extracts text and computes embeddings for each modality.
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 text = byte.text;
27
- const tvec = text ? await this.embed(text) : undefined;
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
 
@@ -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";
@@ -22,6 +22,7 @@ export type {
22
22
  MemoryKind,
23
23
  NewMemory,
24
24
  AgentMemoryCreate,
25
+ AgentMemoryUpdate,
25
26
  MemoryConfig,
26
27
  MemoryReindexParams,
27
28
  MemoryRecord,
@@ -105,6 +105,11 @@ export function buildMemoryIndexSchema(
105
105
  fts: true,
106
106
  optional: true,
107
107
  },
108
+ objtext: {
109
+ type: "string",
110
+ fts: true,
111
+ optional: true,
112
+ },
108
113
 
109
114
  // vector fields for different modalities
110
115
  tvec: {
@@ -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
@@ -128,7 +129,7 @@ export interface NewMemory {
128
129
  id: string;
129
130
  scope: MemoryScope;
130
131
  kind: MemoryKind;
131
- collection: string;
132
+ collection?: string;
132
133
  content: MemoryByte;
133
134
  wmem?: boolean;
134
135
  smem?: { expiresAt: number | null };
@@ -148,7 +149,7 @@ export interface AgentMemoryCreate {
148
149
  id?: string;
149
150
  namespace?: string;
150
151
  entityId?: string;
151
- collection: string;
152
+ collection?: string;
152
153
  content: MemoryByte;
153
154
  wmem?: boolean;
154
155
  smem?: { expiresAt: number | null };
@@ -156,6 +157,21 @@ export interface AgentMemoryCreate {
156
157
  metadata?: JSONObject | null;
157
158
  }
158
159
 
160
+ /**
161
+ * Simplified input for agent-scoped memory updates.
162
+ *
163
+ * Allows updating content, collection, and memory layer flags.
164
+ * Scope (namespace, entityId, agentId) cannot be changed after creation.
165
+ */
166
+ export interface AgentMemoryUpdate {
167
+ id: string;
168
+ content?: MemoryByte;
169
+ collection?: string;
170
+ wmem?: boolean;
171
+ smem?: { expiresAt: number | null };
172
+ metadata?: JSONObject | null;
173
+ }
174
+
159
175
  /**
160
176
  * Base memory record fields.
161
177
  */
@@ -163,7 +179,7 @@ interface BaseMemoryRecord {
163
179
  id: string;
164
180
  scope: MemoryScope;
165
181
  kind: MemoryKind;
166
- collection: string;
182
+ collection: string | null;
167
183
  content: MemoryByte;
168
184
  wmem: boolean;
169
185
  smem: { expiresAt: number | null };
@@ -260,7 +276,7 @@ export interface IndexMemoryRecord extends IndexableByte {
260
276
  entityId: string | null;
261
277
  agentId: string | null;
262
278
  kind: MemoryKind;
263
- collection: string;
279
+ collection: string | null;
264
280
  timestamp: number;
265
281
  createdAt: number;
266
282
  updatedAt: number;
@@ -415,7 +415,7 @@ export class InMemoryMemoryStore implements MemoryStore {
415
415
  id: memory.id,
416
416
  scope: memory.scope,
417
417
  kind: memory.kind,
418
- collection: memory.collection,
418
+ collection: memory.collection ?? null,
419
419
  content: memory.content,
420
420
  wmem: memory.wmem ?? false,
421
421
  smem: memory.smem ?? { expiresAt: null },
@@ -490,7 +490,11 @@ export class InMemoryMemoryStore implements MemoryStore {
490
490
  }
491
491
 
492
492
  // Filter by collections
493
- if (filter.collections && !filter.collections.includes(record.collection)) {
493
+ if (
494
+ filter.collections &&
495
+ (record.collection === null ||
496
+ !filter.collections.includes(record.collection))
497
+ ) {
494
498
  return false;
495
499
  }
496
500