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

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 (87) hide show
  1. package/README.md +9 -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 +17 -0
  7. package/docs/guides/connect-whatsapp-zapster.md +16 -0
  8. package/docs/guides/create-agent.md +10 -1
  9. package/docs/guides/run-evals.md +36 -1
  10. package/docs/llms-full.txt +90 -1
  11. package/docs/llms.txt +9 -2
  12. package/package.json +2 -1
  13. package/src/cli/cloud-client.ts +10 -2
  14. package/src/cli/commands/channels.ts +67 -3
  15. package/src/cli/commands/knowledge.ts +136 -0
  16. package/src/cli/deploy-chat-ui.ts +7 -0
  17. package/src/cli/deploy-readiness.ts +19 -0
  18. package/src/cli/help.ts +26 -2
  19. package/src/cli/index.ts +140 -8
  20. package/src/cloud/artifact.ts +92 -1
  21. package/src/cloud/contracts.ts +16 -0
  22. package/src/create-project.ts +38 -6
  23. package/src/index.ts +142 -1
  24. package/src/providers/pi.ts +1 -1
  25. package/src/providers/test.ts +1 -1
  26. package/src/runtime/channel-buffer.ts +30 -0
  27. package/src/runtime/channels.ts +1 -0
  28. package/src/runtime/chat.ts +21 -2
  29. package/src/runtime/config.ts +175 -0
  30. package/src/runtime/core/manifest.ts +37 -0
  31. package/src/runtime/database.ts +93 -2
  32. package/src/runtime/db-commands.ts +9 -0
  33. package/src/runtime/deploy-readiness.ts +12 -0
  34. package/src/runtime/dev-server.ts +201 -11
  35. package/src/runtime/evals.ts +210 -20
  36. package/src/runtime/inspect.ts +39 -0
  37. package/src/runtime/knowledge/chunk.ts +333 -0
  38. package/src/runtime/knowledge/config.ts +135 -0
  39. package/src/runtime/knowledge/embeddings.ts +133 -0
  40. package/src/runtime/knowledge/ingest.ts +521 -0
  41. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  42. package/src/runtime/knowledge/retrieve.ts +283 -0
  43. package/src/runtime/knowledge/schema.ts +56 -0
  44. package/src/runtime/knowledge/tool.ts +64 -0
  45. package/src/runtime/knowledge/vector.ts +258 -0
  46. package/src/runtime/spec.ts +152 -0
  47. package/src/runtime/sync.ts +144 -0
  48. package/src/runtime/targets/cloudflare/build.ts +514 -4
  49. package/src/runtime/tools.ts +121 -1
  50. package/src/runtime/traces.ts +41 -0
  51. package/src/storage/sqlite.ts +141 -0
  52. package/src/templates/blank.ts +16 -5
  53. package/src/templates/dentista.ts +17 -2
  54. package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
  55. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
  56. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  57. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  58. package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
  59. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  60. package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
  61. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
  62. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +37 -0
  63. package/src/templates/skills/agentkit-channels/references/telegram.md +37 -0
  64. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +37 -0
  65. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  66. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  67. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  68. package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
  69. package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
  70. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
  71. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
  72. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
  73. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
  74. package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
  75. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  76. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  77. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  78. package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
  79. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  80. package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
  81. package/src/templates/skills/agentkit-security/SKILL.md +55 -0
  82. package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
  83. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  84. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  85. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  86. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
  87. package/src/templates/support.ts +15 -4
@@ -1,5 +1,6 @@
1
- import { readFile } from "node:fs/promises";
2
- import { relative, resolve } from "node:path";
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import type { Dirent } from "node:fs";
3
+ import { basename, relative, resolve } from "node:path";
3
4
 
4
5
  import type { DatabaseArgs, DatabaseResult, DatabaseRunner, DatabaseStatement } from "../index";
5
6
  import { openCapsuleStore, type SqliteAgentKitStore } from "../storage/sqlite";
@@ -9,6 +10,12 @@ import { AgentKitError } from "./errors";
9
10
  export type LocalDatabasePrepareResult = {
10
11
  store: SqliteAgentKitStore;
11
12
  databasePath: string;
13
+ migrations: {
14
+ configured: boolean;
15
+ path: string | null;
16
+ applied: string[];
17
+ skipped: string[];
18
+ };
12
19
  schema: {
13
20
  configured: boolean;
14
21
  applied: boolean;
@@ -20,10 +27,12 @@ export async function openPreparedCapsuleStore(capsule: LoadedAgentCapsule): Pro
20
27
  const store = await openCapsuleStore(capsule);
21
28
 
22
29
  try {
30
+ const migrations = await applyCapsuleDatabaseMigrations(capsule, store);
23
31
  const schema = await applyCapsuleDatabaseSchema(capsule, store);
24
32
  return {
25
33
  store,
26
34
  databasePath: store.dbPath,
35
+ migrations,
27
36
  schema,
28
37
  };
29
38
  } catch (error) {
@@ -32,6 +41,84 @@ export async function openPreparedCapsuleStore(capsule: LoadedAgentCapsule): Pro
32
41
  }
33
42
  }
34
43
 
44
+ export async function applyCapsuleDatabaseMigrations(
45
+ capsule: LoadedAgentCapsule,
46
+ store: SqliteAgentKitStore,
47
+ ): Promise<LocalDatabasePrepareResult["migrations"]> {
48
+ const database = capsule.config.storage.database;
49
+
50
+ if (!database || database.driver !== "turso") {
51
+ return {
52
+ configured: false,
53
+ path: null,
54
+ applied: [],
55
+ skipped: [],
56
+ };
57
+ }
58
+
59
+ const migrationsPath = resolve(capsule.root, database.migrations ?? "migrations");
60
+ const migrations = await readMigrationFiles(migrationsPath);
61
+
62
+ if (migrations.length === 0) {
63
+ return {
64
+ configured: false,
65
+ path: migrationsPath,
66
+ applied: [],
67
+ skipped: [],
68
+ };
69
+ }
70
+
71
+ try {
72
+ const result = store.applyApplicationMigrations(migrations);
73
+ return {
74
+ configured: true,
75
+ path: migrationsPath,
76
+ applied: result.applied,
77
+ skipped: result.skipped,
78
+ };
79
+ } catch (error) {
80
+ if (error instanceof AgentKitError && error.code === "database_schema_error") {
81
+ throw new AgentKitError(
82
+ "database_schema_error",
83
+ [
84
+ `Could not apply database migrations from ${relative(capsule.root, migrationsPath)}.`,
85
+ "Migrations must be ordered .sql files and should use safe, forward-only changes.",
86
+ error.message,
87
+ ].join(" "),
88
+ { cause: error },
89
+ );
90
+ }
91
+
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ async function readMigrationFiles(directory: string): Promise<Array<{ id: string; sql: string }>> {
97
+ let entries: Dirent[];
98
+
99
+ try {
100
+ entries = await readdir(directory, { withFileTypes: true });
101
+ } catch (error) {
102
+ if (isNodeError(error) && error.code === "ENOENT") {
103
+ return [];
104
+ }
105
+
106
+ throw error;
107
+ }
108
+
109
+ const files = entries
110
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
111
+ .map((entry) => entry.name)
112
+ .sort();
113
+
114
+ return Promise.all(
115
+ files.map(async (file) => ({
116
+ id: basename(file, ".sql"),
117
+ sql: await readFile(resolve(directory, file), "utf8"),
118
+ })),
119
+ );
120
+ }
121
+
35
122
  export async function applyCapsuleDatabaseSchema(
36
123
  capsule: LoadedAgentCapsule,
37
124
  store: SqliteAgentKitStore,
@@ -163,3 +250,7 @@ function validateStatements(statements: DatabaseStatement[]): void {
163
250
  validateSql(statement.sql, statement.args ?? []);
164
251
  }
165
252
  }
253
+
254
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
255
+ return typeof error === "object" && error !== null && "code" in error;
256
+ }
@@ -10,6 +10,9 @@ import { AgentKitError } from "./errors";
10
10
  export type DbMigrateResult = {
11
11
  root: string;
12
12
  databasePath: string;
13
+ migrationsPath: string | null;
14
+ migrationsApplied: string[];
15
+ migrationsSkipped: string[];
13
16
  schemaPath: string | null;
14
17
  schemaApplied: boolean;
15
18
  };
@@ -43,6 +46,9 @@ export async function migrateLocalDatabase(cwd = process.cwd()): Promise<DbMigra
43
46
  return {
44
47
  root: capsule.root,
45
48
  databasePath: prepared.databasePath,
49
+ migrationsPath: prepared.migrations.path,
50
+ migrationsApplied: prepared.migrations.applied,
51
+ migrationsSkipped: prepared.migrations.skipped,
46
52
  schemaPath: prepared.schema.path,
47
53
  schemaApplied: prepared.schema.applied,
48
54
  };
@@ -87,6 +93,9 @@ export async function seedLocalDatabase(cwd = process.cwd(), seedFile = "seed.sq
87
93
  return {
88
94
  root: capsule.root,
89
95
  databasePath: prepared.databasePath,
96
+ migrationsPath: prepared.migrations.path,
97
+ migrationsApplied: prepared.migrations.applied,
98
+ migrationsSkipped: prepared.migrations.skipped,
90
99
  schemaPath: prepared.schema.path,
91
100
  schemaApplied: prepared.schema.applied,
92
101
  seedPath,
@@ -24,6 +24,12 @@ export type DeployReadinessContext = {
24
24
  userManagedSecrets: string[];
25
25
  agentKitManagedSecrets: string[];
26
26
  localSecrets: Record<string, DeployReadinessSecretSource>;
27
+ knowledge: {
28
+ enabled: boolean;
29
+ sourceCount: number;
30
+ embeddingProvider: string;
31
+ databaseDriver: "none" | "turso";
32
+ };
27
33
  };
28
34
 
29
35
  export async function loadDeployReadinessContext(
@@ -70,6 +76,12 @@ export async function loadDeployReadinessContext(
70
76
  userManagedSecrets: requiredSecrets.filter((name) => !managed.has(name)),
71
77
  agentKitManagedSecrets,
72
78
  localSecrets,
79
+ knowledge: {
80
+ enabled: manifest.knowledge.enabled,
81
+ sourceCount: manifest.knowledge.sources.length,
82
+ embeddingProvider: manifest.knowledge.embedding.provider,
83
+ databaseDriver: manifest.storage.database.driver,
84
+ },
73
85
  };
74
86
  }
75
87
 
@@ -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,21 +143,26 @@ 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
+ };
150
+ const capsuleRoot = capsule.root;
128
151
  return createHttpServer((incoming, outgoing) => {
129
- void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
152
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
130
153
  });
131
154
  }
132
155
 
133
156
  async function handleNodeRequest(
134
- capsule: LoadedAgentCapsule,
157
+ capsuleRoot: string,
135
158
  incoming: IncomingMessage,
136
159
  outgoing: ServerResponse,
137
- channelDedupeKeys: Set<string>,
160
+ channelRuntime: DevChannelRuntimeState,
138
161
  ): Promise<void> {
139
162
  try {
163
+ const capsule = await loadAgentCapsule(capsuleRoot);
140
164
  const request = nodeRequestToFetchRequest(incoming);
141
- const response = await handleDevServerRequest(capsule, request, channelDedupeKeys);
165
+ const response = await handleDevServerRequest(capsule, request, channelRuntime);
142
166
  await writeFetchResponse(outgoing, response);
143
167
  } catch (error) {
144
168
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
@@ -197,7 +221,7 @@ async function writeFetchResponse(outgoing: ServerResponse, response: Response):
197
221
  async function handleDevServerRequest(
198
222
  capsule: LoadedAgentCapsule,
199
223
  request: Request,
200
- channelDedupeKeys: Set<string>,
224
+ channelRuntime: DevChannelRuntimeState,
201
225
  ): Promise<Response> {
202
226
  try {
203
227
  const url = new URL(request.url);
@@ -257,7 +281,7 @@ async function handleDevServerRequest(
257
281
  const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
258
282
 
259
283
  if (channelMatch) {
260
- return await handlePortableChannel(capsule, request, channelDedupeKeys, {
284
+ return await handlePortableChannel(capsule, request, channelRuntime, {
261
285
  name: decodeURIComponent(channelMatch[1]),
262
286
  type: channelMatch[2] as ChannelType,
263
287
  provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
@@ -274,6 +298,12 @@ async function handleDevServerRequest(
274
298
  return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
275
299
  }
276
300
 
301
+ const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
302
+
303
+ if (request.method === "GET" && conversationTraceMatch) {
304
+ return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
305
+ }
306
+
277
307
  return jsonError(new AgentKitError("not_found", `Route ${request.method} ${route} was not found.`), 404);
278
308
  } catch (error) {
279
309
  return jsonError(error, statusForError(error));
@@ -283,7 +313,7 @@ async function handleDevServerRequest(
283
313
  async function handlePortableChannel(
284
314
  capsule: LoadedAgentCapsule,
285
315
  request: Request,
286
- dedupeKeys: Set<string>,
316
+ channelRuntime: DevChannelRuntimeState,
287
317
  route: { name: string; type: ChannelType; provider: ChannelProvider },
288
318
  ): Promise<Response> {
289
319
  const channel = (capsule.config.channels ?? []).find(
@@ -323,21 +353,45 @@ async function handlePortableChannel(
323
353
  const deliveries = [];
324
354
 
325
355
  for (const event of events) {
326
- if (dedupeKeys.has(event.dedupeKey)) {
356
+ if (channelRuntime.dedupeKeys.has(event.dedupeKey)) {
327
357
  deliveries.push({ state: "duplicate", dedupeKey: event.dedupeKey });
328
358
  continue;
329
359
  }
330
360
 
331
- dedupeKeys.add(event.dedupeKey);
361
+ channelRuntime.dedupeKeys.add(event.dedupeKey);
332
362
 
333
363
  if (event.kind !== "message" || !event.message) {
334
364
  deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
335
365
  continue;
336
366
  }
337
367
 
368
+ const conversationId = channelConversationId(channel.name, event.externalIdentity.key);
369
+
370
+ if (isChannelBufferEnabled(channel)) {
371
+ const buffer = resolveChannelBuffer(channel);
372
+ if (buffer.mode !== "debounce") {
373
+ throw new AgentKitError("runtime_error", "Unexpected channel buffer mode.");
374
+ }
375
+
376
+ bufferDevChannelMessage(capsule, channelRuntime, {
377
+ bufferKey: `${channel.name}:${event.externalIdentity.key}`,
378
+ conversationId,
379
+ content: event.message.content,
380
+ receivedAt: event.receivedAt,
381
+ buffer,
382
+ });
383
+ deliveries.push({
384
+ state: "buffered",
385
+ dedupeKey: event.dedupeKey,
386
+ conversationId,
387
+ flushAfterMs: buffer.quietWindowMs,
388
+ });
389
+ continue;
390
+ }
391
+
338
392
  const result = await runAgentMessage(capsule, {
339
393
  message: event.message.content,
340
- conversationId: channelConversationId(channel.name, event.externalIdentity.key),
394
+ conversationId,
341
395
  signal: request.signal,
342
396
  });
343
397
  deliveries.push({
@@ -351,6 +405,109 @@ async function handlePortableChannel(
351
405
  return jsonResponse({ deliveries });
352
406
  }
353
407
 
408
+ function bufferDevChannelMessage(
409
+ capsule: LoadedAgentCapsule,
410
+ runtime: DevChannelRuntimeState,
411
+ input: {
412
+ bufferKey: string;
413
+ conversationId: string;
414
+ content: string;
415
+ receivedAt: string;
416
+ buffer: {
417
+ quietWindowMs: number;
418
+ maxWaitMs: number;
419
+ maxMessages: number;
420
+ maxChars: number;
421
+ };
422
+ },
423
+ ): void {
424
+ const now = Date.now();
425
+ const existing = runtime.buffers.get(input.bufferKey);
426
+ const pending: DevChannelBuffer = existing ?? {
427
+ conversationId: input.conversationId,
428
+ messages: [],
429
+ firstReceivedMs: now,
430
+ lastReceivedMs: now,
431
+ };
432
+
433
+ if (existing?.timer) {
434
+ clearTimeout(existing.timer);
435
+ }
436
+
437
+ pending.messages.push({
438
+ content: input.content,
439
+ receivedAt: input.receivedAt,
440
+ });
441
+ pending.lastReceivedMs = now;
442
+ runtime.buffers.set(input.bufferKey, pending);
443
+
444
+ if (shouldFlushDevChannelBuffer(pending, input.buffer)) {
445
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
446
+ return;
447
+ }
448
+
449
+ const elapsedMs = now - pending.firstReceivedMs;
450
+ const delayMs = Math.max(0, Math.min(input.buffer.quietWindowMs, input.buffer.maxWaitMs - elapsedMs));
451
+ pending.timer = setTimeout(() => {
452
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
453
+ }, delayMs);
454
+ unrefTimer(pending.timer);
455
+ }
456
+
457
+ function shouldFlushDevChannelBuffer(
458
+ pending: DevChannelBuffer,
459
+ buffer: {
460
+ maxWaitMs: number;
461
+ maxMessages: number;
462
+ maxChars: number;
463
+ },
464
+ ): boolean {
465
+ const totalChars = pending.messages.reduce((sum, message) => sum + message.content.length, 0);
466
+ return (
467
+ pending.messages.length >= buffer.maxMessages ||
468
+ totalChars >= buffer.maxChars ||
469
+ Date.now() - pending.firstReceivedMs >= buffer.maxWaitMs
470
+ );
471
+ }
472
+
473
+ async function flushDevChannelBuffer(
474
+ capsule: LoadedAgentCapsule,
475
+ runtime: DevChannelRuntimeState,
476
+ bufferKey: string,
477
+ ): Promise<void> {
478
+ const pending = runtime.buffers.get(bufferKey);
479
+
480
+ if (!pending) {
481
+ return;
482
+ }
483
+
484
+ runtime.buffers.delete(bufferKey);
485
+
486
+ if (pending.timer) {
487
+ clearTimeout(pending.timer);
488
+ }
489
+
490
+ const message = renderBufferedChannelMessage(pending.messages);
491
+
492
+ try {
493
+ await runAgentMessage(capsule, {
494
+ message,
495
+ conversationId: pending.conversationId,
496
+ runtime: {
497
+ invocation: "channel",
498
+ },
499
+ });
500
+ } catch {
501
+ // Local channel buffering is best-effort; hosted delivery logs own production failures.
502
+ }
503
+ }
504
+
505
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
506
+ if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
507
+ timer.unref();
508
+ }
509
+ }
510
+
354
511
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
355
512
  const key = normalizeFileKey(rawKey);
356
513
  const root = resolve(capsule.root, ".agentkit/files");
@@ -421,6 +578,32 @@ async function getConversation(capsule: LoadedAgentCapsule, conversationId: stri
421
578
  }
422
579
  }
423
580
 
581
+ async function getConversationTrace(capsule: LoadedAgentCapsule, conversationId: string) {
582
+ if (!conversationId) {
583
+ throw new AgentKitError("validation_error", "Missing conversation id.");
584
+ }
585
+
586
+ const store = await openCapsuleStore(capsule);
587
+
588
+ try {
589
+ const conversation = store.getConversation(conversationId);
590
+
591
+ if (!conversation) {
592
+ throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
593
+ }
594
+
595
+ return {
596
+ ...conversation,
597
+ runs: store.listRunsForConversation(conversation.id).map((run) => ({
598
+ ...run,
599
+ toolCalls: store.listToolCallsForRun(run.id),
600
+ })),
601
+ };
602
+ } finally {
603
+ store.close();
604
+ }
605
+ }
606
+
424
607
  async function readJsonBody(request: Request): Promise<unknown> {
425
608
  const contentType = request.headers.get("content-type") ?? "";
426
609
 
@@ -917,6 +1100,13 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
917
1100
  messages.scrollTop = messages.scrollHeight;
918
1101
  }
919
1102
 
1103
+ textarea.addEventListener("keydown", (event) => {
1104
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
1105
+ event.preventDefault();
1106
+ if (button.disabled) return;
1107
+ form.requestSubmit();
1108
+ });
1109
+
920
1110
  form.addEventListener("submit", async (event) => {
921
1111
  event.preventDefault();
922
1112
  const content = textarea.value.trim();