kernl 0.7.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +27 -1
  3. package/dist/agent/types.d.ts +20 -12
  4. package/dist/agent/types.d.ts.map +1 -1
  5. package/dist/agent.d.ts +7 -7
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +3 -14
  8. package/dist/api/resources/agents/agents.d.ts +5 -5
  9. package/dist/api/resources/agents/agents.d.ts.map +1 -1
  10. package/dist/api/resources/agents/agents.js +1 -1
  11. package/dist/guardrail.d.ts +19 -19
  12. package/dist/guardrail.d.ts.map +1 -1
  13. package/dist/kernl/kernl.d.ts +6 -6
  14. package/dist/kernl/kernl.d.ts.map +1 -1
  15. package/dist/lib/error.d.ts +3 -3
  16. package/dist/lib/error.d.ts.map +1 -1
  17. package/dist/lifecycle.d.ts +6 -6
  18. package/dist/lifecycle.d.ts.map +1 -1
  19. package/dist/memory/__tests__/encoder.test.d.ts +2 -0
  20. package/dist/memory/__tests__/encoder.test.d.ts.map +1 -0
  21. package/dist/memory/__tests__/encoder.test.js +121 -0
  22. package/dist/memory/codecs/domain.d.ts +6 -0
  23. package/dist/memory/codecs/domain.d.ts.map +1 -1
  24. package/dist/memory/codecs/domain.js +6 -0
  25. package/dist/memory/codecs/tpuf.d.ts.map +1 -1
  26. package/dist/memory/codecs/tpuf.js +2 -5
  27. package/dist/memory/encoder.d.ts +25 -2
  28. package/dist/memory/encoder.d.ts.map +1 -1
  29. package/dist/memory/encoder.js +46 -5
  30. package/dist/memory/index.d.ts +1 -1
  31. package/dist/memory/index.d.ts.map +1 -1
  32. package/dist/memory/index.js +1 -1
  33. package/dist/memory/schema.js +5 -4
  34. package/dist/memory/types.d.ts +2 -1
  35. package/dist/memory/types.d.ts.map +1 -1
  36. package/dist/thread/__tests__/integration.test.d.ts +1 -1
  37. package/dist/thread/__tests__/integration.test.d.ts.map +1 -1
  38. package/dist/thread/__tests__/integration.test.js +10 -5
  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/package.json +5 -4
  49. package/src/agent/types.ts +25 -29
  50. package/src/agent.ts +15 -28
  51. package/src/api/resources/agents/agents.ts +8 -8
  52. package/src/guardrail.ts +28 -28
  53. package/src/kernl/kernl.ts +12 -12
  54. package/src/lib/error.ts +3 -3
  55. package/src/lifecycle.ts +6 -6
  56. package/src/memory/__tests__/encoder.test.ts +154 -0
  57. package/src/memory/codecs/domain.ts +6 -0
  58. package/src/memory/codecs/tpuf.ts +2 -5
  59. package/src/memory/encoder.ts +51 -6
  60. package/src/memory/index.ts +1 -1
  61. package/src/memory/schema.ts +5 -5
  62. package/src/memory/types.ts +2 -1
  63. package/src/thread/__tests__/integration.test.ts +130 -146
  64. package/src/thread/__tests__/thread.test.ts +8 -8
  65. package/src/thread/thread.ts +21 -7
  66. package/src/thread/types.ts +9 -6
  67. package/src/thread/utils.ts +15 -14
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,154 @@
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 include metadata (lives in primary DB only)", 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
+ // metadata is not part of IndexableByte - it stays in the primary DB
127
+ expect("metadata" in result).toBe(false);
128
+ });
129
+
130
+ it("returns undefined tvec when no content", async () => {
131
+ const embedder = createMockEmbedder();
132
+ const encoder = new MemoryByteEncoder(embedder);
133
+
134
+ const byte: MemoryByte = {};
135
+ const result = await encoder.encode(byte);
136
+
137
+ expect(result.text).toBeUndefined();
138
+ expect(result.objtext).toBeUndefined();
139
+ expect(result.tvec).toBeUndefined();
140
+ expect(embedder.embed).not.toHaveBeenCalled();
141
+ });
142
+ });
143
+
144
+ describe("embed", () => {
145
+ it("exposes embed method for query embedding", async () => {
146
+ const embedder = createMockEmbedder();
147
+ const encoder = new MemoryByteEncoder(embedder);
148
+
149
+ const vec = await encoder.embed("search query");
150
+
151
+ expect(vec).toEqual([12, 0, 0]); // "search query".length = 12
152
+ });
153
+ });
154
+ });
@@ -58,6 +58,12 @@ 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
+ *
66
+ * Note: metadata is NOT included - it lives in the primary DB only.
61
67
  */
62
68
  export function recordCodec(
63
69
  bytecodec: MemoryByteCodec,
@@ -29,11 +29,8 @@ import type { IndexMemoryRecord } from "../types";
29
29
  */
30
30
  export const TPUF_DOC: Codec<IndexMemoryRecord, UnknownDocument> = {
31
31
  encode(doc: IndexMemoryRecord): UnknownDocument {
32
- const { tvec, ivec, avec, vvec, metadata, ...rest } = doc;
33
- const row: UnknownDocument = {
34
- ...rest,
35
- metadata: metadata as UnknownDocument["metadata"], // metadata is JSONObject | null, cast to FieldValue for UnknownDocument
36
- };
32
+ const { tvec, ivec, avec, vvec, ...rest } = doc;
33
+ const row: UnknownDocument = { ...rest };
37
34
  if (tvec) row.vector = tvec;
38
35
  return row;
39
36
  },
@@ -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";
@@ -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: {
@@ -131,11 +136,6 @@ export function buildMemoryIndexSchema(
131
136
  similarity,
132
137
  optional: true,
133
138
  },
134
-
135
- metadata: {
136
- type: "object",
137
- optional: true,
138
- },
139
139
  };
140
140
 
141
141
  return schema;
@@ -60,14 +60,15 @@ export interface MemoryByte {
60
60
  * Search-ready projection of a MemoryByte.
61
61
  *
62
62
  * Contains canonical text plus embeddings for each modality.
63
+ * Note: metadata is NOT included - it lives in the primary DB only.
63
64
  */
64
65
  export interface IndexableByte {
65
66
  text?: string; // canonical semantic text
67
+ objtext?: string; // string projection of object for indexing (not full JSON)
66
68
  tvec?: number[]; // text embedding
67
69
  ivec?: number[]; // image embedding
68
70
  avec?: number[]; // audio embedding
69
71
  vvec?: number[]; // video embedding
70
- metadata?: JSONObject | null; // structured payload
71
72
  }
72
73
 
73
74
  /**