@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
@@ -13,10 +13,12 @@ import { websiteChannelAdapter } from "./channels/website";
13
13
  import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
14
14
  import { zapsterWhatsappChannelAdapter } from "./channels/whatsapp-zapster";
15
15
  import { runAgentMessage } from "./chat";
16
+ import { isChannelBufferEnabled, renderBufferedChannelMessage, resolveChannelBuffer } from "./channel-buffer";
16
17
  import { type LoadedAgentCapsule, loadAgentCapsule } from "./config";
17
18
  import { AgentKitError, isAgentKitError } from "./errors";
18
19
  import { loadCapsuleEnv } from "./env";
19
20
  import { buildInspectState } from "./inspect";
21
+ import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
20
22
  import { runToolFromCwd } from "./tool-runner";
21
23
 
22
24
  export type AgentDevServerOptions = {
@@ -47,11 +49,28 @@ export type AgentDevServer = {
47
49
  const DEFAULT_PORT = 4123;
48
50
  const DEFAULT_HOSTNAME = "localhost";
49
51
 
52
+ type DevChannelRuntimeState = {
53
+ dedupeKeys: Set<string>;
54
+ buffers: Map<string, DevChannelBuffer>;
55
+ };
56
+
57
+ type DevChannelBuffer = {
58
+ conversationId: string;
59
+ messages: Array<{
60
+ content: string;
61
+ receivedAt: string;
62
+ }>;
63
+ firstReceivedMs: number;
64
+ lastReceivedMs: number;
65
+ timer?: ReturnType<typeof setTimeout>;
66
+ };
67
+
50
68
  export async function startAgentDevServer(
51
69
  cwd = process.cwd(),
52
70
  options: AgentDevServerOptions = {},
53
71
  ): Promise<AgentDevServer> {
54
72
  const capsule = await loadAgentCapsule(cwd);
73
+ await syncConfiguredKnowledgeSources(capsule);
55
74
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
56
75
  const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
57
76
  const autoPort = options.port === undefined;
@@ -124,9 +143,12 @@ async function listenForDevServer(
124
143
  }
125
144
 
126
145
  function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
127
- const channelDedupeKeys = new Set<string>();
146
+ const channelRuntime: DevChannelRuntimeState = {
147
+ dedupeKeys: new Set<string>(),
148
+ buffers: new Map<string, DevChannelBuffer>(),
149
+ };
128
150
  return createHttpServer((incoming, outgoing) => {
129
- void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
151
+ void handleNodeRequest(capsule, incoming, outgoing, channelRuntime);
130
152
  });
131
153
  }
132
154
 
@@ -134,11 +156,11 @@ async function handleNodeRequest(
134
156
  capsule: LoadedAgentCapsule,
135
157
  incoming: IncomingMessage,
136
158
  outgoing: ServerResponse,
137
- channelDedupeKeys: Set<string>,
159
+ channelRuntime: DevChannelRuntimeState,
138
160
  ): Promise<void> {
139
161
  try {
140
162
  const request = nodeRequestToFetchRequest(incoming);
141
- const response = await handleDevServerRequest(capsule, request, channelDedupeKeys);
163
+ const response = await handleDevServerRequest(capsule, request, channelRuntime);
142
164
  await writeFetchResponse(outgoing, response);
143
165
  } catch (error) {
144
166
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
@@ -197,7 +219,7 @@ async function writeFetchResponse(outgoing: ServerResponse, response: Response):
197
219
  async function handleDevServerRequest(
198
220
  capsule: LoadedAgentCapsule,
199
221
  request: Request,
200
- channelDedupeKeys: Set<string>,
222
+ channelRuntime: DevChannelRuntimeState,
201
223
  ): Promise<Response> {
202
224
  try {
203
225
  const url = new URL(request.url);
@@ -257,7 +279,7 @@ async function handleDevServerRequest(
257
279
  const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
258
280
 
259
281
  if (channelMatch) {
260
- return await handlePortableChannel(capsule, request, channelDedupeKeys, {
282
+ return await handlePortableChannel(capsule, request, channelRuntime, {
261
283
  name: decodeURIComponent(channelMatch[1]),
262
284
  type: channelMatch[2] as ChannelType,
263
285
  provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
@@ -283,7 +305,7 @@ async function handleDevServerRequest(
283
305
  async function handlePortableChannel(
284
306
  capsule: LoadedAgentCapsule,
285
307
  request: Request,
286
- dedupeKeys: Set<string>,
308
+ channelRuntime: DevChannelRuntimeState,
287
309
  route: { name: string; type: ChannelType; provider: ChannelProvider },
288
310
  ): Promise<Response> {
289
311
  const channel = (capsule.config.channels ?? []).find(
@@ -323,21 +345,45 @@ async function handlePortableChannel(
323
345
  const deliveries = [];
324
346
 
325
347
  for (const event of events) {
326
- if (dedupeKeys.has(event.dedupeKey)) {
348
+ if (channelRuntime.dedupeKeys.has(event.dedupeKey)) {
327
349
  deliveries.push({ state: "duplicate", dedupeKey: event.dedupeKey });
328
350
  continue;
329
351
  }
330
352
 
331
- dedupeKeys.add(event.dedupeKey);
353
+ channelRuntime.dedupeKeys.add(event.dedupeKey);
332
354
 
333
355
  if (event.kind !== "message" || !event.message) {
334
356
  deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
335
357
  continue;
336
358
  }
337
359
 
360
+ const conversationId = channelConversationId(channel.name, event.externalIdentity.key);
361
+
362
+ if (isChannelBufferEnabled(channel)) {
363
+ const buffer = resolveChannelBuffer(channel);
364
+ if (buffer.mode !== "debounce") {
365
+ throw new AgentKitError("runtime_error", "Unexpected channel buffer mode.");
366
+ }
367
+
368
+ bufferDevChannelMessage(capsule, channelRuntime, {
369
+ bufferKey: `${channel.name}:${event.externalIdentity.key}`,
370
+ conversationId,
371
+ content: event.message.content,
372
+ receivedAt: event.receivedAt,
373
+ buffer,
374
+ });
375
+ deliveries.push({
376
+ state: "buffered",
377
+ dedupeKey: event.dedupeKey,
378
+ conversationId,
379
+ flushAfterMs: buffer.quietWindowMs,
380
+ });
381
+ continue;
382
+ }
383
+
338
384
  const result = await runAgentMessage(capsule, {
339
385
  message: event.message.content,
340
- conversationId: channelConversationId(channel.name, event.externalIdentity.key),
386
+ conversationId,
341
387
  signal: request.signal,
342
388
  });
343
389
  deliveries.push({
@@ -351,6 +397,109 @@ async function handlePortableChannel(
351
397
  return jsonResponse({ deliveries });
352
398
  }
353
399
 
400
+ function bufferDevChannelMessage(
401
+ capsule: LoadedAgentCapsule,
402
+ runtime: DevChannelRuntimeState,
403
+ input: {
404
+ bufferKey: string;
405
+ conversationId: string;
406
+ content: string;
407
+ receivedAt: string;
408
+ buffer: {
409
+ quietWindowMs: number;
410
+ maxWaitMs: number;
411
+ maxMessages: number;
412
+ maxChars: number;
413
+ };
414
+ },
415
+ ): void {
416
+ const now = Date.now();
417
+ const existing = runtime.buffers.get(input.bufferKey);
418
+ const pending: DevChannelBuffer = existing ?? {
419
+ conversationId: input.conversationId,
420
+ messages: [],
421
+ firstReceivedMs: now,
422
+ lastReceivedMs: now,
423
+ };
424
+
425
+ if (existing?.timer) {
426
+ clearTimeout(existing.timer);
427
+ }
428
+
429
+ pending.messages.push({
430
+ content: input.content,
431
+ receivedAt: input.receivedAt,
432
+ });
433
+ pending.lastReceivedMs = now;
434
+ runtime.buffers.set(input.bufferKey, pending);
435
+
436
+ if (shouldFlushDevChannelBuffer(pending, input.buffer)) {
437
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
438
+ return;
439
+ }
440
+
441
+ const elapsedMs = now - pending.firstReceivedMs;
442
+ const delayMs = Math.max(0, Math.min(input.buffer.quietWindowMs, input.buffer.maxWaitMs - elapsedMs));
443
+ pending.timer = setTimeout(() => {
444
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
445
+ }, delayMs);
446
+ unrefTimer(pending.timer);
447
+ }
448
+
449
+ function shouldFlushDevChannelBuffer(
450
+ pending: DevChannelBuffer,
451
+ buffer: {
452
+ maxWaitMs: number;
453
+ maxMessages: number;
454
+ maxChars: number;
455
+ },
456
+ ): boolean {
457
+ const totalChars = pending.messages.reduce((sum, message) => sum + message.content.length, 0);
458
+ return (
459
+ pending.messages.length >= buffer.maxMessages ||
460
+ totalChars >= buffer.maxChars ||
461
+ Date.now() - pending.firstReceivedMs >= buffer.maxWaitMs
462
+ );
463
+ }
464
+
465
+ async function flushDevChannelBuffer(
466
+ capsule: LoadedAgentCapsule,
467
+ runtime: DevChannelRuntimeState,
468
+ bufferKey: string,
469
+ ): Promise<void> {
470
+ const pending = runtime.buffers.get(bufferKey);
471
+
472
+ if (!pending) {
473
+ return;
474
+ }
475
+
476
+ runtime.buffers.delete(bufferKey);
477
+
478
+ if (pending.timer) {
479
+ clearTimeout(pending.timer);
480
+ }
481
+
482
+ const message = renderBufferedChannelMessage(pending.messages);
483
+
484
+ try {
485
+ await runAgentMessage(capsule, {
486
+ message,
487
+ conversationId: pending.conversationId,
488
+ runtime: {
489
+ invocation: "channel",
490
+ },
491
+ });
492
+ } catch {
493
+ // Local channel buffering is best-effort; hosted delivery logs own production failures.
494
+ }
495
+ }
496
+
497
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
498
+ if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
499
+ timer.unref();
500
+ }
501
+ }
502
+
354
503
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
355
504
  const key = normalizeFileKey(rawKey);
356
505
  const root = resolve(capsule.root, ".agentkit/files");
@@ -4,6 +4,8 @@ import type { AgentChannel, AgentProvider, AgentRuntime, AccessMode } from "../i
4
4
  import type { LoadedAgentCapsule } from "./config";
5
5
  import { loadAgentCapsule } from "./config";
6
6
  import { loadCapsuleEnv } from "./env";
7
+ import { resolveKnowledgeConfig } from "./knowledge/config";
8
+ import { KNOWLEDGE_SEARCH_TOOL_NAME } from "./knowledge/tool";
7
9
 
8
10
  export type SecretState = "set" | "missing";
9
11
 
@@ -14,6 +16,20 @@ export type AgentInspectState = {
14
16
  prompt: string;
15
17
  tools: string[];
16
18
  channels: AgentInspectChannel[];
19
+ knowledge: {
20
+ enabled: boolean;
21
+ sources: string[];
22
+ embedding: {
23
+ provider: string;
24
+ model: string | null;
25
+ dimensions: number | null;
26
+ };
27
+ retrieval: {
28
+ topK: number;
29
+ hybrid: boolean;
30
+ };
31
+ internalTool: string | null;
32
+ };
17
33
  access: {
18
34
  mode: AccessMode;
19
35
  };
@@ -56,6 +72,7 @@ export type AgentInspectChannel = {
56
72
  type: AgentChannel["type"];
57
73
  provider: AgentChannel["provider"];
58
74
  secrets: string[];
75
+ buffer?: AgentChannel["buffer"];
59
76
  };
60
77
 
61
78
  export async function inspectAgentCapsule(
@@ -71,6 +88,7 @@ export function buildInspectState(
71
88
  env: Record<string, string | undefined> = process.env,
72
89
  ): AgentInspectState {
73
90
  const declaredSecrets = new Set(capsule.config.secrets);
91
+ const knowledge = resolveKnowledgeConfig(capsule.config);
74
92
 
75
93
  for (const tool of capsule.config.tools ?? []) {
76
94
  for (const secret of tool.secrets ?? []) {
@@ -84,6 +102,10 @@ export function buildInspectState(
84
102
  }
85
103
  }
86
104
 
105
+ if (knowledge.embedding.secret) {
106
+ declaredSecrets.add(knowledge.embedding.secret);
107
+ }
108
+
87
109
  const managedSecrets = managedCloudSecrets(capsule);
88
110
  const userSecrets = Array.from(declaredSecrets).sort();
89
111
  const secrets: Record<string, SecretState> = {};
@@ -105,7 +127,19 @@ export function buildInspectState(
105
127
  type: channel.type,
106
128
  provider: channel.provider,
107
129
  secrets: [...channel.secrets],
130
+ ...(channel.buffer ? { buffer: channel.buffer } : {}),
108
131
  })),
132
+ knowledge: {
133
+ enabled: knowledge.enabled,
134
+ sources: knowledge.sources.map((source) => source.value),
135
+ embedding: {
136
+ provider: knowledge.embedding.provider,
137
+ model: knowledge.embedding.model,
138
+ dimensions: knowledge.embedding.dimensions,
139
+ },
140
+ retrieval: knowledge.retrieval,
141
+ internalTool: knowledge.enabled ? KNOWLEDGE_SEARCH_TOOL_NAME : null,
142
+ },
109
143
  access: capsule.config.access,
110
144
  storage: capsule.storagePath ? toCapsulePath(capsule.root, capsule.storagePath) : null,
111
145
  database,
@@ -0,0 +1,333 @@
1
+ import { basename, extname } from "node:path";
2
+
3
+ export type KnowledgeChunkMetadata = {
4
+ sourcePath: string;
5
+ title: string;
6
+ section: string | null;
7
+ locator: string | null;
8
+ };
9
+
10
+ export type KnowledgeChunk = {
11
+ ordinal: number;
12
+ content: string;
13
+ tokenCount: number;
14
+ metadata: KnowledgeChunkMetadata;
15
+ };
16
+
17
+ export type ChunkKnowledgeSourceInput = {
18
+ sourcePath: string;
19
+ title?: string | null;
20
+ content: string;
21
+ };
22
+
23
+ const DEFAULT_MAX_WORDS = 720;
24
+ const DEFAULT_OVERLAP_WORDS = 80;
25
+
26
+ export function chunkKnowledgeSource(input: ChunkKnowledgeSourceInput): KnowledgeChunk[] {
27
+ const extension = extname(input.sourcePath).toLowerCase();
28
+
29
+ if (extension === ".csv") {
30
+ return chunkCsv(input);
31
+ }
32
+
33
+ if (extension === ".md" || extension === ".markdown") {
34
+ return chunkMarkdown(input);
35
+ }
36
+
37
+ return chunkText(input);
38
+ }
39
+
40
+ function chunkMarkdown(input: ChunkKnowledgeSourceInput): KnowledgeChunk[] {
41
+ const title = input.title ?? inferTitle(input.sourcePath, input.content);
42
+ const sections = markdownSections(input.content, title);
43
+ const chunks: KnowledgeChunk[] = [];
44
+
45
+ for (const section of sections) {
46
+ const contextualContent = [section.heading, section.body].filter(Boolean).join("\n\n").trim();
47
+ chunks.push(
48
+ ...splitContentIntoChunks({
49
+ sourcePath: input.sourcePath,
50
+ title,
51
+ section: section.heading || null,
52
+ locator: section.heading ? `section:${section.heading}` : null,
53
+ content: contextualContent,
54
+ startingOrdinal: chunks.length,
55
+ }),
56
+ );
57
+ }
58
+
59
+ return ensureAtLeastOneChunk(chunks, input, title);
60
+ }
61
+
62
+ function chunkText(input: ChunkKnowledgeSourceInput): KnowledgeChunk[] {
63
+ const title = input.title ?? inferTitle(input.sourcePath, input.content);
64
+ const paragraphs = input.content
65
+ .split(/\n{2,}/)
66
+ .map((paragraph) => paragraph.trim())
67
+ .filter(Boolean);
68
+ const content = paragraphs.length > 0 ? paragraphs.join("\n\n") : input.content.trim();
69
+
70
+ return ensureAtLeastOneChunk(
71
+ splitContentIntoChunks({
72
+ sourcePath: input.sourcePath,
73
+ title,
74
+ section: null,
75
+ locator: null,
76
+ content,
77
+ startingOrdinal: 0,
78
+ }),
79
+ input,
80
+ title,
81
+ );
82
+ }
83
+
84
+ function chunkCsv(input: ChunkKnowledgeSourceInput): KnowledgeChunk[] {
85
+ const title = input.title ?? inferTitle(input.sourcePath, input.content);
86
+ const rows = parseCsv(input.content);
87
+
88
+ if (rows.length === 0) {
89
+ return ensureAtLeastOneChunk([], input, title);
90
+ }
91
+
92
+ const [headers, ...records] = rows;
93
+
94
+ if (headers.length === 0 || records.length === 0) {
95
+ return chunkText(input);
96
+ }
97
+
98
+ const chunks: KnowledgeChunk[] = [];
99
+
100
+ for (const [index, record] of records.entries()) {
101
+ const content = headers
102
+ .map((header, columnIndex) => {
103
+ const name = header.trim() || `column_${columnIndex + 1}`;
104
+ const value = record[columnIndex]?.trim() ?? "";
105
+ return `${name}: ${value}`;
106
+ })
107
+ .join("\n")
108
+ .trim();
109
+
110
+ if (!content) {
111
+ continue;
112
+ }
113
+
114
+ chunks.push({
115
+ ordinal: chunks.length,
116
+ content,
117
+ tokenCount: estimateTokenCount(content),
118
+ metadata: {
119
+ sourcePath: input.sourcePath,
120
+ title,
121
+ section: null,
122
+ locator: `row:${index + 2}`,
123
+ },
124
+ });
125
+ }
126
+
127
+ return ensureAtLeastOneChunk(chunks, input, title);
128
+ }
129
+
130
+ function markdownSections(content: string, fallbackTitle: string): Array<{ heading: string | null; body: string }> {
131
+ const sections: Array<{ heading: string | null; body: string[] }> = [];
132
+ let current: { heading: string | null; body: string[] } = { heading: null, body: [] };
133
+ const headingStack: string[] = [];
134
+
135
+ for (const line of content.split(/\r?\n/)) {
136
+ const heading = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
137
+
138
+ if (heading) {
139
+ if (current.body.join("\n").trim()) {
140
+ sections.push(current);
141
+ }
142
+
143
+ const level = heading[1].length;
144
+ headingStack.splice(level - 1);
145
+ headingStack[level - 1] = heading[2].trim();
146
+ current = {
147
+ heading: headingStack.filter(Boolean).join(" > "),
148
+ body: [],
149
+ };
150
+ continue;
151
+ }
152
+
153
+ current.body.push(line);
154
+ }
155
+
156
+ if (current.body.join("\n").trim()) {
157
+ sections.push(current);
158
+ }
159
+
160
+ if (sections.length === 0) {
161
+ return [{ heading: fallbackTitle, body: content.trim() }];
162
+ }
163
+
164
+ return sections.map((section) => ({
165
+ heading: section.heading,
166
+ body: section.body.join("\n").trim(),
167
+ }));
168
+ }
169
+
170
+ function splitContentIntoChunks(input: {
171
+ sourcePath: string;
172
+ title: string;
173
+ section: string | null;
174
+ locator: string | null;
175
+ content: string;
176
+ startingOrdinal: number;
177
+ }): KnowledgeChunk[] {
178
+ const words = wordsOf(input.content);
179
+
180
+ if (words.length <= DEFAULT_MAX_WORDS) {
181
+ const content = input.content.trim();
182
+
183
+ if (!content) {
184
+ return [];
185
+ }
186
+
187
+ return [
188
+ {
189
+ ordinal: input.startingOrdinal,
190
+ content,
191
+ tokenCount: estimateTokenCount(content),
192
+ metadata: {
193
+ sourcePath: input.sourcePath,
194
+ title: input.title,
195
+ section: input.section,
196
+ locator: input.locator,
197
+ },
198
+ },
199
+ ];
200
+ }
201
+
202
+ const chunks: KnowledgeChunk[] = [];
203
+ let start = 0;
204
+
205
+ while (start < words.length) {
206
+ const end = Math.min(start + DEFAULT_MAX_WORDS, words.length);
207
+ const content = words.slice(start, end).join(" ");
208
+
209
+ chunks.push({
210
+ ordinal: input.startingOrdinal + chunks.length,
211
+ content,
212
+ tokenCount: estimateTokenCount(content),
213
+ metadata: {
214
+ sourcePath: input.sourcePath,
215
+ title: input.title,
216
+ section: input.section,
217
+ locator: input.locator ? `${input.locator}:part:${chunks.length + 1}` : `part:${chunks.length + 1}`,
218
+ },
219
+ });
220
+
221
+ if (end === words.length) {
222
+ break;
223
+ }
224
+
225
+ start = Math.max(end - DEFAULT_OVERLAP_WORDS, start + 1);
226
+ }
227
+
228
+ return chunks;
229
+ }
230
+
231
+ function ensureAtLeastOneChunk(
232
+ chunks: KnowledgeChunk[],
233
+ input: ChunkKnowledgeSourceInput,
234
+ title: string,
235
+ ): KnowledgeChunk[] {
236
+ if (chunks.length > 0) {
237
+ return chunks.map((chunk, index) => ({ ...chunk, ordinal: index }));
238
+ }
239
+
240
+ const content = input.content.trim();
241
+
242
+ if (!content) {
243
+ return [];
244
+ }
245
+
246
+ return [
247
+ {
248
+ ordinal: 0,
249
+ content,
250
+ tokenCount: estimateTokenCount(content),
251
+ metadata: {
252
+ sourcePath: input.sourcePath,
253
+ title,
254
+ section: null,
255
+ locator: null,
256
+ },
257
+ },
258
+ ];
259
+ }
260
+
261
+ function parseCsv(content: string): string[][] {
262
+ const rows: string[][] = [];
263
+ let row: string[] = [];
264
+ let field = "";
265
+ let quoted = false;
266
+
267
+ for (let index = 0; index < content.length; index += 1) {
268
+ const char = content[index];
269
+ const next = content[index + 1];
270
+
271
+ if (quoted) {
272
+ if (char === '"' && next === '"') {
273
+ field += '"';
274
+ index += 1;
275
+ continue;
276
+ }
277
+
278
+ if (char === '"') {
279
+ quoted = false;
280
+ continue;
281
+ }
282
+
283
+ field += char;
284
+ continue;
285
+ }
286
+
287
+ if (char === '"') {
288
+ quoted = true;
289
+ continue;
290
+ }
291
+
292
+ if (char === ",") {
293
+ row.push(field);
294
+ field = "";
295
+ continue;
296
+ }
297
+
298
+ if (char === "\n") {
299
+ row.push(field.replace(/\r$/, ""));
300
+ rows.push(row);
301
+ row = [];
302
+ field = "";
303
+ continue;
304
+ }
305
+
306
+ field += char;
307
+ }
308
+
309
+ row.push(field.replace(/\r$/, ""));
310
+ if (row.some((value) => value.length > 0)) {
311
+ rows.push(row);
312
+ }
313
+
314
+ return rows;
315
+ }
316
+
317
+ function inferTitle(sourcePath: string, content: string): string {
318
+ const markdownTitle = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
319
+
320
+ if (markdownTitle) {
321
+ return markdownTitle;
322
+ }
323
+
324
+ return basename(sourcePath).replace(/\.[^.]+$/, "") || sourcePath;
325
+ }
326
+
327
+ function wordsOf(content: string): string[] {
328
+ return content.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
329
+ }
330
+
331
+ export function estimateTokenCount(content: string): number {
332
+ return Math.max(1, wordsOf(content).length);
333
+ }