@andreprado/agentkit 0.1.0-alpha.5 → 0.1.0-alpha.6

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 (74) hide show
  1. package/README.md +5 -0
  2. package/docs/guides/add-channel.md +25 -0
  3. package/docs/guides/add-knowledge.md +134 -0
  4. package/docs/guides/agentkit-skills-architecture.md +471 -0
  5. package/docs/guides/channels-production-handoff.md +2 -0
  6. package/docs/guides/connect-telegram.md +15 -0
  7. package/docs/guides/connect-whatsapp-zapster.md +16 -0
  8. package/docs/guides/create-agent.md +1 -1
  9. package/docs/llms-full.txt +90 -1
  10. package/docs/llms.txt +9 -2
  11. package/package.json +2 -1
  12. package/src/cli/commands/channels.ts +39 -1
  13. package/src/cli/commands/knowledge.ts +136 -0
  14. package/src/cli/deploy-readiness.ts +19 -0
  15. package/src/cli/help.ts +8 -0
  16. package/src/cli/index.ts +16 -4
  17. package/src/cloud/artifact.ts +92 -1
  18. package/src/cloud/contracts.ts +16 -0
  19. package/src/create-project.ts +38 -6
  20. package/src/index.ts +98 -0
  21. package/src/runtime/channel-buffer.ts +30 -0
  22. package/src/runtime/channels.ts +1 -0
  23. package/src/runtime/chat.ts +21 -2
  24. package/src/runtime/config.ts +167 -0
  25. package/src/runtime/core/manifest.ts +37 -0
  26. package/src/runtime/deploy-readiness.ts +12 -0
  27. package/src/runtime/dev-server.ts +159 -10
  28. package/src/runtime/inspect.ts +34 -0
  29. package/src/runtime/knowledge/chunk.ts +333 -0
  30. package/src/runtime/knowledge/config.ts +135 -0
  31. package/src/runtime/knowledge/embeddings.ts +133 -0
  32. package/src/runtime/knowledge/ingest.ts +521 -0
  33. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  34. package/src/runtime/knowledge/retrieve.ts +283 -0
  35. package/src/runtime/knowledge/schema.ts +56 -0
  36. package/src/runtime/knowledge/tool.ts +64 -0
  37. package/src/runtime/knowledge/vector.ts +258 -0
  38. package/src/runtime/targets/cloudflare/build.ts +469 -4
  39. package/src/storage/sqlite.ts +5 -0
  40. package/src/templates/blank.ts +8 -4
  41. package/src/templates/dentista.ts +3 -1
  42. package/src/templates/skills/agentkit-build-agent/SKILL.md +49 -0
  43. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
  44. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  45. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  46. package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
  47. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  48. package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
  49. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
  50. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +37 -0
  51. package/src/templates/skills/agentkit-channels/references/telegram.md +37 -0
  52. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +37 -0
  53. package/src/templates/skills/agentkit-database/SKILL.md +42 -0
  54. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  55. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  56. package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
  57. package/src/templates/skills/agentkit-evals/SKILL.md +31 -0
  58. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
  59. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
  60. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
  61. package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
  62. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  63. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  64. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  65. package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
  66. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  67. package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
  68. package/src/templates/skills/agentkit-security/SKILL.md +55 -0
  69. package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
  70. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  71. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  72. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  73. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
  74. package/src/templates/support.ts +8 -4
@@ -0,0 +1,283 @@
1
+ import type { DatabaseRunner, DatabaseRow } from "../../index";
2
+ import { createLocalDatabaseRunner, openPreparedCapsuleStore } from "../database";
3
+ import { loadCapsuleEnv } from "../env";
4
+ import { AgentKitError } from "../errors";
5
+ import { loadAgentCapsule, type LoadedAgentCapsule } from "../config";
6
+ import { resolveKnowledgeConfig } from "./config";
7
+ import { cosineSimilarity, createEmbeddingProvider } from "./embeddings";
8
+ import { searchLocalKnowledgeVectorRows } from "./vector";
9
+
10
+ export type KnowledgeSearchResult = {
11
+ chunkId: string;
12
+ content: string;
13
+ score: number;
14
+ source: {
15
+ id: string;
16
+ path: string;
17
+ title: string | null;
18
+ section: string | null;
19
+ locator: string | null;
20
+ };
21
+ match: {
22
+ lexical: number;
23
+ semantic: number | null;
24
+ };
25
+ };
26
+
27
+ export async function searchKnowledgeFromCwd(
28
+ cwd: string,
29
+ query: string,
30
+ options: { topK?: number } = {},
31
+ ): Promise<{ databasePath: string; results: KnowledgeSearchResult[] }> {
32
+ const capsule = await loadAgentCapsule(cwd);
33
+ const { store, databasePath } = await openPreparedCapsuleStore(capsule);
34
+
35
+ try {
36
+ const db = createLocalDatabaseRunner(store);
37
+ return {
38
+ databasePath,
39
+ results: await searchKnowledge(capsule, db, query, options),
40
+ };
41
+ } finally {
42
+ store.close();
43
+ }
44
+ }
45
+
46
+ export async function searchKnowledge(
47
+ capsule: LoadedAgentCapsule,
48
+ db: DatabaseRunner,
49
+ query: string,
50
+ options: { topK?: number } = {},
51
+ ): Promise<KnowledgeSearchResult[]> {
52
+ const normalizedQuery = query.trim();
53
+
54
+ if (!normalizedQuery) {
55
+ throw new AgentKitError("knowledge_query_invalid", "Knowledge search query must be a non-empty string.");
56
+ }
57
+
58
+ const knowledge = resolveKnowledgeConfig(capsule.config);
59
+ const topK = options.topK ?? knowledge.retrieval.topK;
60
+ const env = await loadCapsuleEnv(capsule.root);
61
+ const embeddingProvider = createEmbeddingProvider({
62
+ config: knowledge.embedding,
63
+ env,
64
+ });
65
+ const lexical = knowledge.retrieval.hybrid || !embeddingProvider
66
+ ? await searchLexical(db, normalizedQuery, Math.max(topK * 4, topK))
67
+ : [];
68
+ const semantic = embeddingProvider
69
+ ? await searchSemantic(capsule, db, normalizedQuery, topK * 4, embeddingProvider)
70
+ : [];
71
+
72
+ return combineResults(lexical, semantic, topK);
73
+ }
74
+
75
+ async function searchLexical(db: DatabaseRunner, query: string, limit: number): Promise<KnowledgeSearchResult[]> {
76
+ const tokens = queryTokens(query);
77
+
78
+ if (tokens.length === 0) {
79
+ return [];
80
+ }
81
+
82
+ try {
83
+ const result = await db.query(
84
+ `
85
+ SELECT
86
+ c.id AS chunk_id,
87
+ c.source_id AS source_id,
88
+ c.content AS content,
89
+ c.title AS title,
90
+ c.section AS section,
91
+ c.locator AS locator,
92
+ s.source_key AS source_key,
93
+ rank
94
+ FROM agentkit_knowledge_fts
95
+ JOIN agentkit_knowledge_chunks c ON c.id = agentkit_knowledge_fts.chunk_id
96
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
97
+ WHERE agentkit_knowledge_fts MATCH ?
98
+ ORDER BY rank ASC
99
+ LIMIT ?
100
+ `,
101
+ [tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR "), limit],
102
+ );
103
+
104
+ return result.rows.map((row, index) => knowledgeSearchResultFromRow(row, lexicalScore(index), null));
105
+ } catch {
106
+ return searchLexicalFallback(db, query, limit);
107
+ }
108
+ }
109
+
110
+ async function searchLexicalFallback(db: DatabaseRunner, query: string, limit: number): Promise<KnowledgeSearchResult[]> {
111
+ const like = `%${query.toLowerCase()}%`;
112
+ const result = await db.query(
113
+ `
114
+ SELECT
115
+ c.id AS chunk_id,
116
+ c.source_id AS source_id,
117
+ c.content AS content,
118
+ c.title AS title,
119
+ c.section AS section,
120
+ c.locator AS locator,
121
+ s.source_key AS source_key
122
+ FROM agentkit_knowledge_chunks c
123
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
124
+ WHERE lower(c.content) LIKE ?
125
+ OR lower(COALESCE(c.title, '')) LIKE ?
126
+ OR lower(COALESCE(c.section, '')) LIKE ?
127
+ ORDER BY c.created_at DESC, c.ordinal ASC
128
+ LIMIT ?
129
+ `,
130
+ [like, like, like, limit],
131
+ );
132
+
133
+ return result.rows.map((row, index) => knowledgeSearchResultFromRow(row, lexicalScore(index), null));
134
+ }
135
+
136
+ async function searchSemantic(
137
+ capsule: LoadedAgentCapsule,
138
+ db: DatabaseRunner,
139
+ query: string,
140
+ limit: number,
141
+ embeddingProvider: NonNullable<ReturnType<typeof createEmbeddingProvider>>,
142
+ ): Promise<KnowledgeSearchResult[]> {
143
+ const queryEmbedding = await embeddingProvider.embed(query);
144
+
145
+ if (capsule.storagePath) {
146
+ try {
147
+ const vectorRows = await searchLocalKnowledgeVectorRows({
148
+ databasePath: capsule.storagePath,
149
+ queryEmbedding,
150
+ limit,
151
+ });
152
+
153
+ return vectorRows
154
+ .map((row) => {
155
+ const distance = Number(row.vector_distance);
156
+ const semantic = Number.isFinite(distance) ? 1 - distance : 0;
157
+ return knowledgeSearchResultFromRow(row, 0, semantic);
158
+ })
159
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
160
+ .slice(0, limit);
161
+ } catch {
162
+ // Older local databases and non-libSQL environments use the JSON fallback below.
163
+ }
164
+ }
165
+
166
+ const result = await db.query(
167
+ `
168
+ SELECT
169
+ c.id AS chunk_id,
170
+ c.source_id AS source_id,
171
+ c.content AS content,
172
+ c.title AS title,
173
+ c.section AS section,
174
+ c.locator AS locator,
175
+ c.embedding_json AS embedding_json,
176
+ s.source_key AS source_key
177
+ FROM agentkit_knowledge_chunks c
178
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
179
+ WHERE c.embedding_json IS NOT NULL
180
+ ORDER BY c.created_at DESC, c.ordinal ASC
181
+ `,
182
+ );
183
+
184
+ return result.rows
185
+ .map((row) => {
186
+ const embedding = parseEmbedding(row.embedding_json);
187
+ const semantic = embedding ? cosineSimilarity(queryEmbedding, embedding) : 0;
188
+ return knowledgeSearchResultFromRow(row, 0, semantic);
189
+ })
190
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
191
+ .sort((left, right) => (right.match.semantic ?? 0) - (left.match.semantic ?? 0))
192
+ .slice(0, limit);
193
+ }
194
+
195
+ function combineResults(
196
+ lexical: KnowledgeSearchResult[],
197
+ semantic: KnowledgeSearchResult[],
198
+ topK: number,
199
+ ): KnowledgeSearchResult[] {
200
+ const byChunk = new Map<string, KnowledgeSearchResult>();
201
+
202
+ for (const result of lexical) {
203
+ byChunk.set(result.chunkId, result);
204
+ }
205
+
206
+ for (const result of semantic) {
207
+ const existing = byChunk.get(result.chunkId);
208
+
209
+ if (!existing) {
210
+ byChunk.set(result.chunkId, {
211
+ ...result,
212
+ score: combinedScore(0, result.match.semantic),
213
+ });
214
+ continue;
215
+ }
216
+
217
+ byChunk.set(result.chunkId, {
218
+ ...existing,
219
+ score: combinedScore(existing.match.lexical, result.match.semantic),
220
+ match: {
221
+ lexical: existing.match.lexical,
222
+ semantic: result.match.semantic,
223
+ },
224
+ });
225
+ }
226
+
227
+ return Array.from(byChunk.values())
228
+ .map((result) => ({
229
+ ...result,
230
+ score: result.score || combinedScore(result.match.lexical, result.match.semantic),
231
+ }))
232
+ .sort((left, right) => right.score - left.score || left.source.path.localeCompare(right.source.path))
233
+ .slice(0, topK);
234
+ }
235
+
236
+ function knowledgeSearchResultFromRow(
237
+ row: DatabaseRow,
238
+ lexical: number,
239
+ semantic: number | null,
240
+ ): KnowledgeSearchResult {
241
+ return {
242
+ chunkId: String(row.chunk_id),
243
+ content: String(row.content),
244
+ score: combinedScore(lexical, semantic),
245
+ source: {
246
+ id: String(row.source_id),
247
+ path: String(row.source_key),
248
+ title: typeof row.title === "string" ? row.title : null,
249
+ section: typeof row.section === "string" ? row.section : null,
250
+ locator: typeof row.locator === "string" ? row.locator : null,
251
+ },
252
+ match: {
253
+ lexical,
254
+ semantic,
255
+ },
256
+ };
257
+ }
258
+
259
+ function combinedScore(lexical: number, semantic: number | null): number {
260
+ const semanticScore = semantic === null ? 0 : Math.max(0, (semantic + 1) / 2);
261
+ return Number((lexical * 0.6 + semanticScore * 0.4).toFixed(6));
262
+ }
263
+
264
+ function lexicalScore(index: number): number {
265
+ return Number((1 / (index + 1)).toFixed(6));
266
+ }
267
+
268
+ function queryTokens(query: string): string[] {
269
+ return Array.from(new Set(query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [])).slice(0, 12);
270
+ }
271
+
272
+ function parseEmbedding(value: unknown): number[] | null {
273
+ if (typeof value !== "string") {
274
+ return null;
275
+ }
276
+
277
+ try {
278
+ const parsed = JSON.parse(value);
279
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === "number") ? parsed : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
@@ -0,0 +1,56 @@
1
+ export const KNOWLEDGE_MIGRATION_ID = "0006_create_knowledge_tables";
2
+
3
+ export const KNOWLEDGE_SCHEMA_SQL = `
4
+ CREATE TABLE IF NOT EXISTS agentkit_knowledge_sources (
5
+ id TEXT PRIMARY KEY,
6
+ source_key TEXT NOT NULL UNIQUE,
7
+ kind TEXT NOT NULL,
8
+ path TEXT,
9
+ url TEXT,
10
+ title TEXT,
11
+ content_sha256 TEXT,
12
+ status TEXT NOT NULL DEFAULT 'pending',
13
+ chunk_count INTEGER NOT NULL DEFAULT 0,
14
+ metadata_json TEXT NOT NULL DEFAULT '{}',
15
+ created_at TEXT NOT NULL,
16
+ updated_at TEXT NOT NULL,
17
+ indexed_at TEXT,
18
+ error_message TEXT
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS agentkit_knowledge_sources_status_idx
22
+ ON agentkit_knowledge_sources(status, updated_at);
23
+
24
+ CREATE TABLE IF NOT EXISTS agentkit_knowledge_chunks (
25
+ id TEXT PRIMARY KEY,
26
+ source_id TEXT NOT NULL REFERENCES agentkit_knowledge_sources(id) ON DELETE CASCADE,
27
+ ordinal INTEGER NOT NULL,
28
+ content TEXT NOT NULL,
29
+ token_count INTEGER NOT NULL,
30
+ title TEXT,
31
+ section TEXT,
32
+ locator TEXT,
33
+ content_sha256 TEXT NOT NULL,
34
+ embedding_json TEXT,
35
+ embedding_hash TEXT,
36
+ embedding_provider TEXT,
37
+ embedding_model TEXT,
38
+ embedding_dimensions INTEGER,
39
+ metadata_json TEXT NOT NULL DEFAULT '{}',
40
+ created_at TEXT NOT NULL,
41
+ UNIQUE(source_id, ordinal)
42
+ );
43
+
44
+ CREATE INDEX IF NOT EXISTS agentkit_knowledge_chunks_source_ordinal_idx
45
+ ON agentkit_knowledge_chunks(source_id, ordinal);
46
+
47
+ CREATE VIRTUAL TABLE IF NOT EXISTS agentkit_knowledge_fts
48
+ USING fts5(
49
+ chunk_id UNINDEXED,
50
+ source_id UNINDEXED,
51
+ title,
52
+ section,
53
+ content,
54
+ tokenize='unicode61'
55
+ );
56
+ `;
@@ -0,0 +1,64 @@
1
+ import type { AgentTool } from "../../index";
2
+ import type { LoadedAgentCapsule } from "../config";
3
+ import { resolveKnowledgeConfig } from "./config";
4
+ import { searchKnowledge } from "./retrieve";
5
+
6
+ export const KNOWLEDGE_SEARCH_TOOL_NAME = "agentkit_search_knowledge";
7
+
8
+ export function createKnowledgeSearchTool(capsule: LoadedAgentCapsule): AgentTool | null {
9
+ const knowledge = resolveKnowledgeConfig(capsule.config);
10
+
11
+ if (!knowledge.enabled) {
12
+ return null;
13
+ }
14
+
15
+ return {
16
+ name: KNOWLEDGE_SEARCH_TOOL_NAME,
17
+ description: "Search the Agent Capsule's configured Knowledge sources for business-specific facts.",
18
+ visibility: "internal",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ query: {
23
+ type: "string",
24
+ },
25
+ topK: {
26
+ type: "integer",
27
+ },
28
+ },
29
+ required: ["query"],
30
+ additionalProperties: false,
31
+ },
32
+ outputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ results: {
36
+ type: "array",
37
+ items: {
38
+ type: "object",
39
+ properties: {
40
+ content: { type: "string" },
41
+ score: { type: "number" },
42
+ source: { type: "object" },
43
+ },
44
+ required: ["content", "score", "source"],
45
+ },
46
+ },
47
+ },
48
+ required: ["results"],
49
+ additionalProperties: false,
50
+ },
51
+ async execute(input, ctx) {
52
+ const typed = input as { query: string; topK?: number };
53
+ const results = await searchKnowledge(capsule, ctx.db, typed.query, { topK: typed.topK });
54
+
55
+ return {
56
+ results: results.map((result) => ({
57
+ content: result.content,
58
+ score: result.score,
59
+ source: result.source,
60
+ })),
61
+ };
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,258 @@
1
+ import { access } from "node:fs/promises";
2
+ import { basename, dirname, extname, join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { createClient } from "@libsql/client";
6
+
7
+ import type { DatabaseRow } from "../../index";
8
+
9
+ export const KNOWLEDGE_VECTOR_TABLE = "agentkit_knowledge_vectors";
10
+ export const KNOWLEDGE_VECTOR_INDEX = "agentkit_knowledge_vectors_embedding_idx";
11
+ const KNOWLEDGE_VECTOR_SOURCE_INDEX = "agentkit_knowledge_vectors_source_idx";
12
+
13
+ export type LocalKnowledgeVectorStatus = {
14
+ enabled: boolean;
15
+ provider: "libsql" | "none" | "json_fallback";
16
+ chunksIndexed: number;
17
+ message: string;
18
+ };
19
+
20
+ export function localKnowledgeVectorDatabasePath(databasePath: string): string {
21
+ const extension = extname(databasePath);
22
+ const stem = extension ? basename(databasePath, extension) : basename(databasePath);
23
+ return join(dirname(databasePath), `${stem}.vectors.db`);
24
+ }
25
+
26
+ export async function localKnowledgeVectorDatabaseExists(databasePath: string): Promise<boolean> {
27
+ return access(localKnowledgeVectorDatabasePath(databasePath)).then(
28
+ () => true,
29
+ () => false,
30
+ );
31
+ }
32
+
33
+ export async function rebuildLocalKnowledgeVectorIndex(options: {
34
+ databasePath: string;
35
+ dimensions: number | null;
36
+ }): Promise<LocalKnowledgeVectorStatus> {
37
+ if (!isPositiveInteger(options.dimensions)) {
38
+ return {
39
+ enabled: false,
40
+ provider: "none",
41
+ chunksIndexed: 0,
42
+ message: "Knowledge embeddings are disabled, so no local vector index was created.",
43
+ };
44
+ }
45
+
46
+ const mainClient = createClient({ url: pathToFileURL(options.databasePath).href });
47
+ const vectorClient = createClient({ url: pathToFileURL(localKnowledgeVectorDatabasePath(options.databasePath)).href });
48
+ const dimensions = options.dimensions;
49
+
50
+ try {
51
+ await vectorClient.batch(
52
+ [
53
+ `DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_INDEX}`,
54
+ `DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_SOURCE_INDEX}`,
55
+ `DROP TABLE IF EXISTS ${KNOWLEDGE_VECTOR_TABLE}`,
56
+ `
57
+ CREATE TABLE ${KNOWLEDGE_VECTOR_TABLE} (
58
+ id INTEGER PRIMARY KEY,
59
+ chunk_id TEXT NOT NULL UNIQUE,
60
+ source_id TEXT NOT NULL,
61
+ source_key TEXT NOT NULL,
62
+ content TEXT NOT NULL,
63
+ title TEXT,
64
+ section TEXT,
65
+ locator TEXT,
66
+ embedding F32_BLOB(${dimensions}) NOT NULL,
67
+ embedding_hash TEXT NOT NULL,
68
+ created_at TEXT NOT NULL
69
+ )
70
+ `,
71
+ `CREATE INDEX ${KNOWLEDGE_VECTOR_SOURCE_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(source_id)`,
72
+ `CREATE INDEX ${KNOWLEDGE_VECTOR_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(libsql_vector_idx(embedding, 'metric=cosine'))`,
73
+ ],
74
+ "write",
75
+ );
76
+
77
+ const rows = await mainClient.execute({
78
+ sql: `
79
+ SELECT
80
+ c.id AS chunk_id,
81
+ c.source_id,
82
+ c.content,
83
+ c.title,
84
+ c.section,
85
+ c.locator,
86
+ c.embedding_json,
87
+ c.embedding_hash,
88
+ c.created_at,
89
+ s.source_key
90
+ FROM agentkit_knowledge_chunks c
91
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
92
+ WHERE c.embedding_json IS NOT NULL
93
+ AND c.embedding_hash IS NOT NULL
94
+ AND c.embedding_dimensions = ?
95
+ ORDER BY c.created_at DESC, c.ordinal ASC
96
+ `,
97
+ args: [dimensions],
98
+ });
99
+
100
+ let chunksIndexed = 0;
101
+
102
+ for (const row of rows.rows) {
103
+ const chunkId = readString(row, "chunk_id");
104
+ const sourceId = readString(row, "source_id");
105
+ const sourceKey = readString(row, "source_key");
106
+ const content = readString(row, "content");
107
+ const embeddingJson = readString(row, "embedding_json");
108
+ const embeddingHash = readString(row, "embedding_hash");
109
+ const createdAt = readString(row, "created_at");
110
+
111
+ if (!chunkId || !sourceId || !sourceKey || !content || !embeddingJson || !embeddingHash || !createdAt) {
112
+ continue;
113
+ }
114
+
115
+ await vectorClient.execute({
116
+ sql: `
117
+ INSERT INTO ${KNOWLEDGE_VECTOR_TABLE} (
118
+ chunk_id,
119
+ source_id,
120
+ source_key,
121
+ content,
122
+ title,
123
+ section,
124
+ locator,
125
+ embedding,
126
+ embedding_hash,
127
+ created_at
128
+ )
129
+ VALUES (?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?)
130
+ `,
131
+ args: [
132
+ chunkId,
133
+ sourceId,
134
+ sourceKey,
135
+ content,
136
+ readNullableString(row, "title"),
137
+ readNullableString(row, "section"),
138
+ readNullableString(row, "locator"),
139
+ embeddingJson,
140
+ embeddingHash,
141
+ createdAt,
142
+ ],
143
+ });
144
+ chunksIndexed += 1;
145
+ }
146
+
147
+ await vectorClient.execute("PRAGMA wal_checkpoint(TRUNCATE)").catch(() => undefined);
148
+
149
+ return {
150
+ enabled: true,
151
+ provider: "libsql",
152
+ chunksIndexed,
153
+ message: `Local libSQL vector database rebuilt with ${chunksIndexed} chunk${chunksIndexed === 1 ? "" : "s"}.`,
154
+ };
155
+ } catch (error) {
156
+ return {
157
+ enabled: false,
158
+ provider: "json_fallback",
159
+ chunksIndexed: 0,
160
+ message: `Local libSQL vector index unavailable; using JSON embedding fallback. ${formatVectorError(error)}`,
161
+ };
162
+ } finally {
163
+ mainClient.close();
164
+ vectorClient.close();
165
+ }
166
+ }
167
+
168
+ export async function searchLocalKnowledgeVectorRows(options: {
169
+ databasePath: string;
170
+ queryEmbedding: number[];
171
+ limit: number;
172
+ }): Promise<DatabaseRow[]> {
173
+ const vectorDatabasePath = localKnowledgeVectorDatabasePath(options.databasePath);
174
+
175
+ await access(vectorDatabasePath);
176
+
177
+ const client = createClient({ url: pathToFileURL(vectorDatabasePath).href });
178
+ const vector = JSON.stringify(options.queryEmbedding);
179
+
180
+ try {
181
+ const result = await client.execute({
182
+ sql: `
183
+ SELECT
184
+ c.id AS chunk_id,
185
+ c.source_id AS source_id,
186
+ c.content AS content,
187
+ c.title AS title,
188
+ c.section AS section,
189
+ c.locator AS locator,
190
+ c.source_key AS source_key,
191
+ vector_distance_cos(c.embedding, vector32(?)) AS vector_distance
192
+ FROM vector_top_k('${KNOWLEDGE_VECTOR_INDEX}', vector32(?), ?) nearest
193
+ JOIN ${KNOWLEDGE_VECTOR_TABLE} c ON c.id = nearest.id
194
+ ORDER BY vector_distance ASC
195
+ `,
196
+ args: [vector, vector, options.limit],
197
+ });
198
+
199
+ return result.rows.map(normalizeLibsqlRow);
200
+ } finally {
201
+ client.close();
202
+ }
203
+ }
204
+
205
+ function normalizeLibsqlRow(row: unknown): DatabaseRow {
206
+ const result: DatabaseRow = {};
207
+
208
+ if (!row || typeof row !== "object") {
209
+ return result;
210
+ }
211
+
212
+ for (const [key, value] of Object.entries(row)) {
213
+ if (key === "length" || /^\d+$/.test(key)) {
214
+ continue;
215
+ }
216
+
217
+ if (typeof value === "bigint") {
218
+ result[key] = Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
219
+ continue;
220
+ }
221
+
222
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
223
+ result[key] = value;
224
+ continue;
225
+ }
226
+
227
+ result[key] = String(value);
228
+ }
229
+
230
+ return result;
231
+ }
232
+
233
+ function isPositiveInteger(value: number | null): value is number {
234
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
235
+ }
236
+
237
+ function readString(row: unknown, key: string): string | null {
238
+ if (!row || typeof row !== "object" || !(key in row)) {
239
+ return null;
240
+ }
241
+
242
+ const value = (row as Record<string, unknown>)[key];
243
+ return typeof value === "string" && value.length > 0 ? value : null;
244
+ }
245
+
246
+ function readNullableString(row: unknown, key: string): string | null {
247
+ if (!row || typeof row !== "object" || !(key in row)) {
248
+ return null;
249
+ }
250
+
251
+ const value = (row as Record<string, unknown>)[key];
252
+ return typeof value === "string" ? value : null;
253
+ }
254
+
255
+ function formatVectorError(error: unknown): string {
256
+ const message = error instanceof Error ? error.message : String(error);
257
+ return message.length <= 240 ? message : `${message.slice(0, 237)}...`;
258
+ }