@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (135) hide show
  1. package/README.md +67 -6
  2. package/docs/guides/add-channel.md +118 -7
  3. package/docs/guides/add-knowledge.md +144 -0
  4. package/docs/guides/add-managed-composio.md +163 -0
  5. package/docs/guides/add-tool.md +1 -1
  6. package/docs/guides/channel-security.md +97 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +78 -1
  10. package/docs/guides/connect-whatsapp-zapster.md +112 -8
  11. package/docs/guides/create-agent.md +45 -4
  12. package/docs/guides/debug-channel.md +147 -0
  13. package/docs/guides/improve-from-production.md +151 -0
  14. package/docs/guides/prepare-deploy.md +47 -17
  15. package/docs/guides/replay-production-traces.md +72 -0
  16. package/docs/guides/run-evals.md +147 -20
  17. package/docs/guides/security-rules.md +7 -6
  18. package/docs/guides/send-feedback.md +135 -0
  19. package/docs/guides/use-provider.md +27 -3
  20. package/docs/llms-full.txt +303 -55
  21. package/docs/llms.txt +57 -7
  22. package/package.json +2 -5
  23. package/src/cli/args.ts +57 -0
  24. package/src/cli/cloud-client.ts +377 -0
  25. package/src/cli/commands/channels.ts +1315 -0
  26. package/src/cli/commands/feedback.ts +438 -0
  27. package/src/cli/commands/knowledge.ts +136 -0
  28. package/src/cli/commands/transcribe.ts +171 -0
  29. package/src/cli/constants.ts +4 -0
  30. package/src/cli/deploy-chat-ui.ts +535 -0
  31. package/src/cli/deploy-readiness.ts +481 -0
  32. package/src/cli/flags.ts +162 -0
  33. package/src/cli/help.ts +236 -0
  34. package/src/cli/index.ts +1167 -1005
  35. package/src/cli/process.ts +31 -0
  36. package/src/cloud/artifact.ts +139 -0
  37. package/src/cloud/client.ts +80 -0
  38. package/src/cloud/contracts.ts +63 -0
  39. package/src/cloud/index.ts +3 -0
  40. package/src/create-project.ts +21 -6
  41. package/src/index.ts +479 -7
  42. package/src/providers/pi.ts +70 -16
  43. package/src/providers/test.ts +88 -1
  44. package/src/providers/types.ts +7 -0
  45. package/src/runtime/channel-buffer.ts +30 -0
  46. package/src/runtime/channel-test-harness.ts +8 -1
  47. package/src/runtime/channels/discord.ts +896 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +466 -23
  50. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  51. package/src/runtime/channels/whatsapp-zapster.ts +677 -40
  52. package/src/runtime/channels.ts +86 -3
  53. package/src/runtime/chat.ts +130 -38
  54. package/src/runtime/config.ts +483 -18
  55. package/src/runtime/core/manifest.ts +103 -5
  56. package/src/runtime/core/targets.ts +5 -5
  57. package/src/runtime/database.ts +93 -2
  58. package/src/runtime/db-commands.ts +9 -0
  59. package/src/runtime/deploy-readiness.ts +46 -4
  60. package/src/runtime/deploy.ts +1 -1
  61. package/src/runtime/dev-server.ts +759 -41
  62. package/src/runtime/env.ts +8 -3
  63. package/src/runtime/evals.ts +589 -43
  64. package/src/runtime/improve.ts +868 -0
  65. package/src/runtime/inspect.ts +194 -4
  66. package/src/runtime/integrations/composio.ts +423 -0
  67. package/src/runtime/knowledge/chunk.ts +333 -0
  68. package/src/runtime/knowledge/config.ts +135 -0
  69. package/src/runtime/knowledge/embeddings.ts +133 -0
  70. package/src/runtime/knowledge/ingest.ts +521 -0
  71. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  72. package/src/runtime/knowledge/retrieve.ts +303 -0
  73. package/src/runtime/knowledge/schema.ts +100 -0
  74. package/src/runtime/knowledge/tool.ts +64 -0
  75. package/src/runtime/knowledge/vector.ts +258 -0
  76. package/src/runtime/prompt-context.ts +141 -0
  77. package/src/runtime/runtime-contract.ts +86 -8
  78. package/src/runtime/skills.ts +95 -0
  79. package/src/runtime/spec.ts +152 -0
  80. package/src/runtime/sync.ts +144 -0
  81. package/src/runtime/targets/cloudflare/build.ts +1430 -185
  82. package/src/runtime/targets/container/server.ts +1 -1
  83. package/src/runtime/targets/vps/deploy.ts +26 -9
  84. package/src/runtime/tool-runner.ts +9 -1
  85. package/src/runtime/tools.ts +128 -2
  86. package/src/runtime/traces.ts +41 -0
  87. package/src/runtime/transcription.ts +483 -0
  88. package/src/storage/sqlite.ts +149 -3
  89. package/src/templates/blank.ts +76 -17
  90. package/src/templates/dentista.ts +1011 -0
  91. package/src/templates/index.ts +2 -0
  92. package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
  93. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
  94. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  95. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  96. package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
  97. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  98. package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
  99. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
  100. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
  101. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  102. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  103. package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
  104. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
  105. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  106. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  107. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  108. package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
  109. package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
  110. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
  111. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
  112. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
  113. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
  114. package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
  115. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  116. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  117. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  118. package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
  119. package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
  120. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  121. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  122. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  123. package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
  124. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  125. package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
  126. package/src/templates/skills/agentkit-security/SKILL.md +56 -0
  127. package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
  128. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  129. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  130. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  131. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
  132. package/src/templates/support.ts +77 -18
  133. package/docs/guides/channels-production-handoff.md +0 -99
  134. package/docs/portable-deploy-release-checklist.md +0 -41
  135. package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
4
  import { createServer as createNetServer } from "node:net";
@@ -6,27 +7,39 @@ import { Readable } from "node:stream";
6
7
 
7
8
  import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
8
9
  import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
9
- import type { ChannelAdapter, RawWebhookEvent } from "./channels";
10
+ import type { ChannelAdapter, NormalizedChannelMessage, RawWebhookEvent } from "./channels";
11
+ import { discordChannelAdapter } from "./channels/discord";
12
+ import { slackChannelAdapter } from "./channels/slack";
10
13
  import { telegramChannelAdapter } from "./channels/telegram";
11
14
  import { websiteChannelAdapter } from "./channels/website";
12
15
  import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
13
16
  import { zapsterWhatsappChannelAdapter } from "./channels/whatsapp-zapster";
14
17
  import { runAgentMessage } from "./chat";
18
+ import { isChannelBufferEnabled, renderBufferedChannelMessage, resolveChannelBuffer } from "./channel-buffer";
15
19
  import { type LoadedAgentCapsule, loadAgentCapsule } from "./config";
16
20
  import { AgentKitError, isAgentKitError } from "./errors";
17
21
  import { loadCapsuleEnv } from "./env";
18
22
  import { buildInspectState } from "./inspect";
23
+ import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
19
24
  import { runToolFromCwd } from "./tool-runner";
25
+ import { resolveTranscriptionConfig, transcribeAudio } from "./transcription";
20
26
 
21
27
  export type AgentDevServerOptions = {
22
28
  port?: number;
23
29
  hostname?: string;
30
+ basePort?: number;
31
+ access?: AgentDevServerAccessPolicy;
24
32
  };
25
33
 
26
34
  export type AgentDevServer = {
27
35
  capsule: LoadedAgentCapsule;
28
36
  hostname: string;
37
+ requestedPort: number | null;
29
38
  port: number;
39
+ portConflict: {
40
+ port: number;
41
+ agent?: string;
42
+ } | null;
30
43
  urls: {
31
44
  chat: string;
32
45
  api: string;
@@ -39,50 +52,133 @@ export type AgentDevServer = {
39
52
 
40
53
  const DEFAULT_PORT = 4123;
41
54
  const DEFAULT_HOSTNAME = "localhost";
55
+ const CHANNEL_DEDUPE_TTL_MS = 6 * 60 * 60 * 1000;
56
+ const CHANNEL_DEDUPE_MAX_KEYS = 10_000;
57
+
58
+ export type AgentDevServerAccessPolicy = "local" | "enforce";
59
+
60
+ type DevHttpServerOptions = {
61
+ access: AgentDevServerAccessPolicy;
62
+ };
63
+
64
+ type DevChannelRuntimeState = {
65
+ dedupeKeys: BoundedDedupeCache;
66
+ buffers: Map<string, DevChannelBuffer>;
67
+ };
68
+
69
+ type DevChannelBuffer = {
70
+ conversationId: string;
71
+ messages: Array<{
72
+ content: string;
73
+ receivedAt: string;
74
+ }>;
75
+ firstReceivedMs: number;
76
+ lastReceivedMs: number;
77
+ timer?: ReturnType<typeof setTimeout>;
78
+ };
42
79
 
43
80
  export async function startAgentDevServer(
44
81
  cwd = process.cwd(),
45
82
  options: AgentDevServerOptions = {},
46
83
  ): Promise<AgentDevServer> {
47
84
  const capsule = await loadAgentCapsule(cwd);
85
+ await syncConfiguredKnowledgeSources(capsule);
48
86
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
49
- const port = options.port === 0 ? await findAvailablePort() : options.port ?? DEFAULT_PORT;
50
- const channelDedupeKeys = new Set<string>();
51
- const server = createHttpServer((incoming, outgoing) => {
52
- void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
87
+ const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
88
+ const autoPort = options.port === undefined;
89
+ const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort, {
90
+ access: options.access ?? "local",
53
91
  });
54
- const actualPort = await listen(server, hostname, port);
55
92
 
56
- if (actualPort === undefined) {
93
+ if (start.actualPort === undefined) {
57
94
  throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
58
95
  }
59
96
 
97
+ const actualPort = start.actualPort;
98
+
60
99
  return {
61
100
  capsule,
62
101
  hostname,
102
+ requestedPort: options.port === undefined || options.port === 0 ? null : options.port,
63
103
  port: actualPort,
104
+ portConflict: start.portConflict,
64
105
  urls: {
65
106
  chat: `http://${hostname}:${actualPort}`,
66
107
  api: `http://${hostname}:${actualPort}/v1/chat`,
67
108
  inspect: `http://${hostname}:${actualPort}/_agentkit`,
68
109
  },
69
110
  storage: capsule.storagePath ? relativePath(capsule.root, capsule.storagePath) : null,
70
- server,
111
+ server: start.server,
71
112
  async stop() {
72
- await closeServer(server);
113
+ await closeServer(start.server);
73
114
  },
74
115
  };
75
116
  }
76
117
 
77
- async function handleNodeRequest(
118
+ async function listenForDevServer(
78
119
  capsule: LoadedAgentCapsule,
120
+ hostname: string,
121
+ requestedPort: number,
122
+ autoPort: boolean,
123
+ serverOptions: DevHttpServerOptions,
124
+ ): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
125
+ if (requestedPort === 0) {
126
+ const server = createDevHttpServer(capsule, serverOptions);
127
+ const actualPort = await listen(server, hostname, await findAvailablePort());
128
+ return { server, actualPort, portConflict: null };
129
+ }
130
+
131
+ let firstConflict: AgentDevServer["portConflict"] = null;
132
+ const lastPort = autoPort ? requestedPort + 50 : requestedPort;
133
+
134
+ for (let port = requestedPort; port <= lastPort; port += 1) {
135
+ const server = createDevHttpServer(capsule, serverOptions);
136
+
137
+ try {
138
+ const actualPort = await listen(server, hostname, port);
139
+ return { server, actualPort, portConflict: port === requestedPort ? null : firstConflict };
140
+ } catch (error) {
141
+ await closeServerAfterListenError(server);
142
+
143
+ if (!autoPort || !isAddressInUseError(error)) {
144
+ throw error;
145
+ }
146
+
147
+ firstConflict ??= {
148
+ port,
149
+ agent: await detectAgentKitOccupant(hostname, port),
150
+ };
151
+ }
152
+ }
153
+
154
+ throw new AgentKitError(
155
+ "runtime_error",
156
+ `Could not allocate a local AgentKit dev server port after ${requestedPort}-${lastPort}.`,
157
+ );
158
+ }
159
+
160
+ function createDevHttpServer(capsule: LoadedAgentCapsule, options: DevHttpServerOptions): Server {
161
+ const channelRuntime: DevChannelRuntimeState = {
162
+ dedupeKeys: new BoundedDedupeCache(CHANNEL_DEDUPE_MAX_KEYS, CHANNEL_DEDUPE_TTL_MS),
163
+ buffers: new Map<string, DevChannelBuffer>(),
164
+ };
165
+ const capsuleRoot = capsule.root;
166
+ return createHttpServer((incoming, outgoing) => {
167
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime, options);
168
+ });
169
+ }
170
+
171
+ async function handleNodeRequest(
172
+ capsuleRoot: string,
79
173
  incoming: IncomingMessage,
80
174
  outgoing: ServerResponse,
81
- channelDedupeKeys: Set<string>,
175
+ channelRuntime: DevChannelRuntimeState,
176
+ options: DevHttpServerOptions,
82
177
  ): Promise<void> {
83
178
  try {
179
+ const capsule = await loadAgentCapsule(capsuleRoot);
84
180
  const request = nodeRequestToFetchRequest(incoming);
85
- const response = await handleDevServerRequest(capsule, request, channelDedupeKeys);
181
+ const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
86
182
  await writeFetchResponse(outgoing, response);
87
183
  } catch (error) {
88
184
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
@@ -141,31 +237,48 @@ async function writeFetchResponse(outgoing: ServerResponse, response: Response):
141
237
  async function handleDevServerRequest(
142
238
  capsule: LoadedAgentCapsule,
143
239
  request: Request,
144
- channelDedupeKeys: Set<string>,
240
+ channelRuntime: DevChannelRuntimeState,
241
+ options: DevHttpServerOptions,
145
242
  ): Promise<Response> {
146
243
  try {
147
244
  const url = new URL(request.url);
148
245
  const route = normalizePath(url.pathname);
149
246
 
247
+ if (request.method === "GET" && route === "/health") {
248
+ return jsonResponse({ status: "ok", agent: capsule.config.name });
249
+ }
250
+
150
251
  if (request.method === "GET" && route === "/") {
252
+ const auth = await authorizeDevRoute(capsule, request, options, "chat");
253
+ if (auth) {
254
+ return auth;
255
+ }
256
+
151
257
  const env = await loadCapsuleEnv(capsule.root);
152
258
  return htmlResponse(renderChatApp(capsule, url.origin, env));
153
259
  }
154
260
 
155
261
  if (request.method === "GET" && route === "/_agentkit") {
262
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
263
+ if (auth) {
264
+ return auth;
265
+ }
266
+
156
267
  const env = await loadCapsuleEnv(capsule.root);
157
268
  return jsonResponse(buildInspectState(capsule, env));
158
269
  }
159
270
 
160
- if (request.method === "GET" && route === "/health") {
161
- return jsonResponse({ status: "ok", agent: capsule.config.name });
162
- }
163
-
164
271
  if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
165
272
  const body = await readJsonBody(request);
166
- const message = readUserMessage(body);
273
+ const auth = await authorizeDevRoute(capsule, request, options, "chat", body);
274
+ if (auth) {
275
+ return auth;
276
+ }
277
+
278
+ const { message, conversationId } = readChatRequest(body);
167
279
  const result = await runAgentMessage(capsule, {
168
280
  message,
281
+ conversationId,
169
282
  signal: request.signal,
170
283
  });
171
284
 
@@ -181,6 +294,11 @@ async function handleDevServerRequest(
181
294
  const toolMatch = route.match(/^\/tools\/([^/]+)$/);
182
295
 
183
296
  if (request.method === "POST" && toolMatch) {
297
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
298
+ if (auth) {
299
+ return auth;
300
+ }
301
+
184
302
  const body = await readJsonBody(request);
185
303
  const result = await runToolFromCwd(capsule.root, {
186
304
  name: decodeURIComponent(toolMatch[1]),
@@ -194,13 +312,18 @@ async function handleDevServerRequest(
194
312
  const fileMatch = route.match(/^\/files\/(.+)$/);
195
313
 
196
314
  if (fileMatch) {
315
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
316
+ if (auth) {
317
+ return auth;
318
+ }
319
+
197
320
  return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
198
321
  }
199
322
 
200
- const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
323
+ const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp|discord|slack)\/([^/]+)\/webhook$/);
201
324
 
202
325
  if (channelMatch) {
203
- return await handlePortableChannel(capsule, request, channelDedupeKeys, {
326
+ return await handlePortableChannel(capsule, request, channelRuntime, {
204
327
  name: decodeURIComponent(channelMatch[1]),
205
328
  type: channelMatch[2] as ChannelType,
206
329
  provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
@@ -208,25 +331,149 @@ async function handleDevServerRequest(
208
331
  }
209
332
 
210
333
  if (request.method === "GET" && route === "/v1/conversations") {
334
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
335
+ if (auth) {
336
+ return auth;
337
+ }
338
+
211
339
  return jsonResponse({ conversations: await listConversations(capsule) });
212
340
  }
213
341
 
214
342
  const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
215
343
 
216
344
  if (request.method === "GET" && conversationMatch) {
345
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
346
+ if (auth) {
347
+ return auth;
348
+ }
349
+
217
350
  return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
218
351
  }
219
352
 
353
+ const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
354
+
355
+ if (request.method === "GET" && conversationTraceMatch) {
356
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
357
+ if (auth) {
358
+ return auth;
359
+ }
360
+
361
+ return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
362
+ }
363
+
220
364
  return jsonError(new AgentKitError("not_found", `Route ${request.method} ${route} was not found.`), 404);
221
365
  } catch (error) {
222
366
  return jsonError(error, statusForError(error));
223
367
  }
224
368
  }
225
369
 
370
+ async function authorizeDevRoute(
371
+ capsule: LoadedAgentCapsule,
372
+ request: Request,
373
+ options: DevHttpServerOptions,
374
+ kind: "chat" | "admin",
375
+ body?: unknown,
376
+ ): Promise<Response | null> {
377
+ if (options.access === "local") {
378
+ return null;
379
+ }
380
+
381
+ const env = await loadCapsuleEnv(capsule.root);
382
+
383
+ if (kind === "admin") {
384
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
385
+ missingCode: "deploy_not_ready",
386
+ missingMessage: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set.",
387
+ unauthorizedMessage: "Missing or invalid AgentKit admin token.",
388
+ });
389
+ }
390
+
391
+ if (capsule.config.access.mode === "public") {
392
+ return null;
393
+ }
394
+
395
+ if (capsule.config.access.mode === "private") {
396
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
397
+ missingCode: "deploy_not_ready",
398
+ missingMessage: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set.",
399
+ unauthorizedMessage: "Missing or invalid deploy access token.",
400
+ });
401
+ }
402
+
403
+ return authorizeBearerSecret(request, body, env.AGENTKIT_ACCESS_TOKEN, {
404
+ missingCode: "deploy_not_ready",
405
+ missingMessage: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set.",
406
+ unauthorizedMessage: "Missing or invalid deploy access token.",
407
+ });
408
+ }
409
+
410
+ function authorizeBearerSecret(
411
+ request: Request,
412
+ body: unknown,
413
+ secret: string | undefined,
414
+ messages: { missingCode: string; missingMessage: string; unauthorizedMessage: string },
415
+ ): Response | null {
416
+ if (!secret) {
417
+ return jsonResponse(
418
+ {
419
+ error: {
420
+ code: messages.missingCode,
421
+ message: messages.missingMessage,
422
+ },
423
+ },
424
+ { status: 503 },
425
+ );
426
+ }
427
+
428
+ if (readRequestAccessToken(request, body) !== secret) {
429
+ return jsonResponse(
430
+ {
431
+ error: {
432
+ code: "unauthorized",
433
+ message: messages.unauthorizedMessage,
434
+ },
435
+ },
436
+ { status: 401 },
437
+ );
438
+ }
439
+
440
+ return null;
441
+ }
442
+
443
+ function readRequestAccessToken(request: Request, body: unknown): string {
444
+ const authorization = request.headers.get("authorization");
445
+
446
+ if (authorization?.startsWith("Bearer ")) {
447
+ return authorization.slice("Bearer ".length).trim();
448
+ }
449
+
450
+ const header =
451
+ request.headers.get("x-agentkit-access-token") ??
452
+ request.headers.get("x-agentkit-deploy-access-token") ??
453
+ "";
454
+ const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
455
+ if (token.trim()) {
456
+ return token.trim();
457
+ }
458
+
459
+ if (body && typeof body === "object") {
460
+ const record = body as { access_token?: unknown; accessToken?: unknown };
461
+ const bodyToken =
462
+ typeof record.access_token === "string"
463
+ ? record.access_token
464
+ : typeof record.accessToken === "string"
465
+ ? record.accessToken
466
+ : "";
467
+ return bodyToken.trim();
468
+ }
469
+
470
+ return "";
471
+ }
472
+
226
473
  async function handlePortableChannel(
227
474
  capsule: LoadedAgentCapsule,
228
475
  request: Request,
229
- dedupeKeys: Set<string>,
476
+ channelRuntime: DevChannelRuntimeState,
230
477
  route: { name: string; type: ChannelType; provider: ChannelProvider },
231
478
  ): Promise<Response> {
232
479
  const channel = (capsule.config.channels ?? []).find(
@@ -258,28 +505,59 @@ async function handlePortableChannel(
258
505
  throw new AgentKitError(verification.code, verification.message);
259
506
  }
260
507
 
261
- if (channel.type === "whatsapp" && channel.provider === "meta" && request.method === "GET") {
262
- return new Response(new URL(request.url).searchParams.get("hub.challenge") ?? "");
508
+ const webhookResponse = adapter.getWebhookResponse
509
+ ? await adapter.getWebhookResponse({ ...rawEvent, channel, secrets })
510
+ : null;
511
+
512
+ if (webhookResponse) {
513
+ return channelAdapterResponse(webhookResponse);
263
514
  }
264
515
 
265
516
  const events = await adapter.normalizeInbound(rawEvent);
266
517
  const deliveries = [];
267
518
 
268
519
  for (const event of events) {
269
- if (dedupeKeys.has(event.dedupeKey)) {
520
+ if (channelRuntime.dedupeKeys.has(event.dedupeKey)) {
270
521
  deliveries.push({ state: "duplicate", dedupeKey: event.dedupeKey });
271
522
  continue;
272
523
  }
273
524
 
274
- dedupeKeys.add(event.dedupeKey);
525
+ channelRuntime.dedupeKeys.add(event.dedupeKey);
526
+
527
+ const processedMessage = await processDevChannelMessage(capsule, adapter, channel, event.message, secrets);
275
528
 
276
- if (event.kind !== "message" || !event.message) {
529
+ if (event.kind !== "message" || !processedMessage) {
277
530
  deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
278
531
  continue;
279
532
  }
280
533
 
534
+ const conversationId = channelConversationId(channel.name, event.externalIdentity.key);
535
+
536
+ if (isChannelBufferEnabled(channel)) {
537
+ const buffer = resolveChannelBuffer(channel);
538
+ if (buffer.mode !== "debounce") {
539
+ throw new AgentKitError("runtime_error", "Unexpected channel buffer mode.");
540
+ }
541
+
542
+ bufferDevChannelMessage(capsule, channelRuntime, {
543
+ bufferKey: `${channel.name}:${event.externalIdentity.key}`,
544
+ conversationId,
545
+ content: processedMessage.content,
546
+ receivedAt: event.receivedAt,
547
+ buffer,
548
+ });
549
+ deliveries.push({
550
+ state: "buffered",
551
+ dedupeKey: event.dedupeKey,
552
+ conversationId,
553
+ flushAfterMs: buffer.quietWindowMs,
554
+ });
555
+ continue;
556
+ }
557
+
281
558
  const result = await runAgentMessage(capsule, {
282
- message: event.message.content,
559
+ message: processedMessage.content,
560
+ conversationId,
283
561
  signal: request.signal,
284
562
  });
285
563
  deliveries.push({
@@ -290,10 +568,186 @@ async function handlePortableChannel(
290
568
  });
291
569
  }
292
570
 
571
+ if (channel.type === "discord" && channel.provider === "discord") {
572
+ return jsonResponse(discordLocalInteractionResponse(deliveries));
573
+ }
574
+
293
575
  return jsonResponse({ deliveries });
294
576
  }
295
577
 
578
+ async function processDevChannelMessage(
579
+ capsule: LoadedAgentCapsule,
580
+ adapter: ChannelAdapter,
581
+ channel: AgentChannel,
582
+ message: NormalizedChannelMessage | undefined,
583
+ secrets: Record<string, string>,
584
+ ): Promise<NormalizedChannelMessage | null> {
585
+ if (!message) {
586
+ return null;
587
+ }
588
+
589
+ if (message.contentType === "text" && message.content) {
590
+ return message;
591
+ }
592
+
593
+ if (message.contentType !== "audio" || !message.audio || channel.audio?.mode !== "transcribe") {
594
+ return null;
595
+ }
596
+
597
+ if (!adapter.downloadMedia) {
598
+ return null;
599
+ }
600
+
601
+ const env = await loadCapsuleEnv(capsule.root);
602
+ const transcriptionConfig = resolveTranscriptionConfig(capsule.config.transcription);
603
+ const secretName = transcriptionConfig.secret;
604
+ const download = await adapter.downloadMedia({
605
+ type: channel.type,
606
+ provider: channel.provider,
607
+ channelId: channel.name,
608
+ message,
609
+ secrets: {
610
+ ...secrets,
611
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
612
+ ...(env.AGENTKIT_TELEGRAM_API_BASE_URL ? { AGENTKIT_TELEGRAM_API_BASE_URL: env.AGENTKIT_TELEGRAM_API_BASE_URL } : {}),
613
+ },
614
+ });
615
+
616
+ if (!download.ok) {
617
+ return null;
618
+ }
619
+
620
+ const transcription = await transcribeAudio({
621
+ config: transcriptionConfig,
622
+ secrets: {
623
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
624
+ },
625
+ audio: download.audio,
626
+ filename: download.filename,
627
+ ...(download.mimeType ? { mimeType: download.mimeType } : {}),
628
+ ...(download.durationSeconds !== undefined ? { durationSeconds: download.durationSeconds } : {}),
629
+ });
630
+
631
+ if (!transcription.ok) {
632
+ return null;
633
+ }
634
+
635
+ return {
636
+ role: "user",
637
+ contentType: "text",
638
+ content: `The user sent an audio message.\n\nTranscript:\n${transcription.text}`,
639
+ providerMessageId: message.providerMessageId,
640
+ };
641
+ }
642
+
643
+ function bufferDevChannelMessage(
644
+ capsule: LoadedAgentCapsule,
645
+ runtime: DevChannelRuntimeState,
646
+ input: {
647
+ bufferKey: string;
648
+ conversationId: string;
649
+ content: string;
650
+ receivedAt: string;
651
+ buffer: {
652
+ quietWindowMs: number;
653
+ maxWaitMs: number;
654
+ maxMessages: number;
655
+ maxChars: number;
656
+ };
657
+ },
658
+ ): void {
659
+ const now = Date.now();
660
+ const existing = runtime.buffers.get(input.bufferKey);
661
+ const pending: DevChannelBuffer = existing ?? {
662
+ conversationId: input.conversationId,
663
+ messages: [],
664
+ firstReceivedMs: now,
665
+ lastReceivedMs: now,
666
+ };
667
+
668
+ if (existing?.timer) {
669
+ clearTimeout(existing.timer);
670
+ }
671
+
672
+ pending.messages.push({
673
+ content: input.content,
674
+ receivedAt: input.receivedAt,
675
+ });
676
+ pending.lastReceivedMs = now;
677
+ runtime.buffers.set(input.bufferKey, pending);
678
+
679
+ if (shouldFlushDevChannelBuffer(pending, input.buffer)) {
680
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
681
+ return;
682
+ }
683
+
684
+ const elapsedMs = now - pending.firstReceivedMs;
685
+ const delayMs = Math.max(0, Math.min(input.buffer.quietWindowMs, input.buffer.maxWaitMs - elapsedMs));
686
+ pending.timer = setTimeout(() => {
687
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
688
+ }, delayMs);
689
+ unrefTimer(pending.timer);
690
+ }
691
+
692
+ function shouldFlushDevChannelBuffer(
693
+ pending: DevChannelBuffer,
694
+ buffer: {
695
+ maxWaitMs: number;
696
+ maxMessages: number;
697
+ maxChars: number;
698
+ },
699
+ ): boolean {
700
+ const totalChars = pending.messages.reduce((sum, message) => sum + message.content.length, 0);
701
+ return (
702
+ pending.messages.length >= buffer.maxMessages ||
703
+ totalChars >= buffer.maxChars ||
704
+ Date.now() - pending.firstReceivedMs >= buffer.maxWaitMs
705
+ );
706
+ }
707
+
708
+ async function flushDevChannelBuffer(
709
+ capsule: LoadedAgentCapsule,
710
+ runtime: DevChannelRuntimeState,
711
+ bufferKey: string,
712
+ ): Promise<void> {
713
+ const pending = runtime.buffers.get(bufferKey);
714
+
715
+ if (!pending) {
716
+ return;
717
+ }
718
+
719
+ runtime.buffers.delete(bufferKey);
720
+
721
+ if (pending.timer) {
722
+ clearTimeout(pending.timer);
723
+ }
724
+
725
+ const message = renderBufferedChannelMessage(pending.messages);
726
+
727
+ try {
728
+ await runAgentMessage(capsule, {
729
+ message,
730
+ conversationId: pending.conversationId,
731
+ runtime: {
732
+ invocation: "channel",
733
+ },
734
+ });
735
+ } catch {
736
+ // Local channel buffering is best-effort; hosted delivery logs own production failures.
737
+ }
738
+ }
739
+
740
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
741
+ if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
742
+ timer.unref();
743
+ }
744
+ }
745
+
296
746
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
747
+ if (capsule.config.storage.files?.driver !== "r2") {
748
+ throw new AgentKitError("files_not_configured", "File storage is not configured for this Agent Capsule.");
749
+ }
750
+
297
751
  const key = normalizeFileKey(rawKey);
298
752
  const root = resolve(capsule.root, ".agentkit/files");
299
753
  const path = resolve(root, key);
@@ -303,14 +757,8 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
303
757
  }
304
758
 
305
759
  if (request.method === "PUT") {
306
- const bytes = new Uint8Array(await request.arrayBuffer());
307
- const maxUploadBytes = capsule.config.storage.files?.driver === "r2"
308
- ? capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024
309
- : 10 * 1024 * 1024;
310
-
311
- if (bytes.byteLength > maxUploadBytes) {
312
- throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
313
- }
760
+ const maxUploadBytes = capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024;
761
+ const bytes = await readRequestBytesWithLimit(request, maxUploadBytes);
314
762
 
315
763
  await mkdir(dirname(path), { recursive: true });
316
764
  await writeFile(path, bytes);
@@ -333,6 +781,102 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
333
781
  throw new AgentKitError("method_not_allowed", "Unsupported file method.");
334
782
  }
335
783
 
784
+ export class BoundedDedupeCache {
785
+ private readonly entries = new Map<string, number>();
786
+
787
+ constructor(
788
+ private readonly maxKeys: number,
789
+ private readonly ttlMs: number,
790
+ ) {}
791
+
792
+ has(key: string): boolean {
793
+ this.pruneExpired();
794
+ return this.entries.has(key);
795
+ }
796
+
797
+ add(key: string): void {
798
+ this.pruneExpired();
799
+ this.entries.set(key, Date.now());
800
+ this.pruneOverflow();
801
+ }
802
+
803
+ get size(): number {
804
+ this.pruneExpired();
805
+ return this.entries.size;
806
+ }
807
+
808
+ private pruneExpired(): void {
809
+ const cutoff = Date.now() - this.ttlMs;
810
+
811
+ for (const [key, timestamp] of this.entries) {
812
+ if (timestamp >= cutoff) {
813
+ continue;
814
+ }
815
+
816
+ this.entries.delete(key);
817
+ }
818
+ }
819
+
820
+ private pruneOverflow(): void {
821
+ while (this.entries.size > this.maxKeys) {
822
+ const oldest = this.entries.keys().next().value;
823
+
824
+ if (typeof oldest !== "string") {
825
+ return;
826
+ }
827
+
828
+ this.entries.delete(oldest);
829
+ }
830
+ }
831
+ }
832
+
833
+ async function readRequestBytesWithLimit(request: Request, maxBytes: number): Promise<Uint8Array> {
834
+ const contentLength = request.headers.get("content-length");
835
+
836
+ if (contentLength) {
837
+ const declaredBytes = Number(contentLength);
838
+
839
+ if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
840
+ throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
841
+ }
842
+ }
843
+
844
+ if (!request.body) {
845
+ return new Uint8Array();
846
+ }
847
+
848
+ const reader = request.body.getReader();
849
+ const chunks: Uint8Array[] = [];
850
+ let total = 0;
851
+
852
+ while (true) {
853
+ const { done, value } = await reader.read();
854
+
855
+ if (done) {
856
+ break;
857
+ }
858
+
859
+ total += value.byteLength;
860
+
861
+ if (total > maxBytes) {
862
+ await reader.cancel().catch(() => undefined);
863
+ throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
864
+ }
865
+
866
+ chunks.push(value);
867
+ }
868
+
869
+ const bytes = new Uint8Array(total);
870
+ let offset = 0;
871
+
872
+ for (const chunk of chunks) {
873
+ bytes.set(chunk, offset);
874
+ offset += chunk.byteLength;
875
+ }
876
+
877
+ return bytes;
878
+ }
879
+
336
880
  async function listConversations(capsule: LoadedAgentCapsule) {
337
881
  const store = await openCapsuleStore(capsule);
338
882
 
@@ -363,6 +907,32 @@ async function getConversation(capsule: LoadedAgentCapsule, conversationId: stri
363
907
  }
364
908
  }
365
909
 
910
+ async function getConversationTrace(capsule: LoadedAgentCapsule, conversationId: string) {
911
+ if (!conversationId) {
912
+ throw new AgentKitError("validation_error", "Missing conversation id.");
913
+ }
914
+
915
+ const store = await openCapsuleStore(capsule);
916
+
917
+ try {
918
+ const conversation = store.getConversation(conversationId);
919
+
920
+ if (!conversation) {
921
+ throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
922
+ }
923
+
924
+ return {
925
+ ...conversation,
926
+ runs: store.listRunsForConversation(conversation.id).map((run) => ({
927
+ ...run,
928
+ toolCalls: store.listToolCallsForRun(run.id),
929
+ })),
930
+ };
931
+ } finally {
932
+ store.close();
933
+ }
934
+ }
935
+
366
936
  async function readJsonBody(request: Request): Promise<unknown> {
367
937
  const contentType = request.headers.get("content-type") ?? "";
368
938
 
@@ -377,20 +947,61 @@ async function readJsonBody(request: Request): Promise<unknown> {
377
947
  }
378
948
  }
379
949
 
380
- function readUserMessage(body: unknown): string {
381
- if (!isRecord(body) || !isRecord(body.message)) {
950
+ function readChatRequest(body: unknown): { message: string; conversationId?: string } {
951
+ if (!isRecord(body)) {
952
+ throw new AgentKitError(
953
+ "validation_error",
954
+ 'Expected body shape: {"message":{"role":"user","content":"hello"},"conversationId":"optional-id"}.',
955
+ );
956
+ }
957
+
958
+ const message = readUserMessage(body.message);
959
+ const conversationId = readOptionalConversationId(body.conversationId);
960
+
961
+ return { message, conversationId };
962
+ }
963
+
964
+ function readUserMessage(message: unknown): string {
965
+ if (typeof message === "string") {
966
+ const content = message.trim();
967
+
968
+ if (!content) {
969
+ throw new AgentKitError("validation_error", "message must be a non-empty string.");
970
+ }
971
+
972
+ return content;
973
+ }
974
+
975
+ if (!isRecord(message)) {
382
976
  throw new AgentKitError("validation_error", 'Expected body shape: {"message":{"role":"user","content":"hello"}}.');
383
977
  }
384
978
 
385
- if (body.message.role !== "user") {
979
+ if (message.role !== "user") {
386
980
  throw new AgentKitError("validation_error", 'message.role must be "user".');
387
981
  }
388
982
 
389
- if (typeof body.message.content !== "string" || body.message.content.trim().length === 0) {
983
+ if (typeof message.content !== "string" || message.content.trim().length === 0) {
390
984
  throw new AgentKitError("validation_error", "message.content must be a non-empty string.");
391
985
  }
392
986
 
393
- return body.message.content;
987
+ return message.content.trim();
988
+ }
989
+
990
+ function readOptionalConversationId(value: unknown): string | undefined {
991
+ if (value === undefined || value === null) {
992
+ return undefined;
993
+ }
994
+
995
+ if (typeof value !== "string" || value.trim().length === 0) {
996
+ throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
997
+ }
998
+
999
+ return value.trim();
1000
+ }
1001
+
1002
+ function channelConversationId(channelName: string, externalIdentityKey: string): string {
1003
+ const hash = createHash("sha256").update(`${channelName}:${externalIdentityKey}`).digest("hex").slice(0, 32);
1004
+ return `chn_${hash}`;
394
1005
  }
395
1006
 
396
1007
  function readToolInput(body: unknown): unknown {
@@ -433,6 +1044,7 @@ function statusForError(error: unknown): number {
433
1044
  error.code === "conversation_not_found" ||
434
1045
  error.code === "not_found" ||
435
1046
  error.code === "tool_not_found" ||
1047
+ error.code === "files_not_configured" ||
436
1048
  error.code === "file_not_found" ||
437
1049
  error.code === "channel_not_found"
438
1050
  ) {
@@ -525,9 +1137,69 @@ function adapterForChannel(type: ChannelType, provider: ChannelProvider): Channe
525
1137
  return metaWhatsappChannelAdapter;
526
1138
  }
527
1139
 
1140
+ if (type === "discord" && provider === "discord") {
1141
+ return discordChannelAdapter;
1142
+ }
1143
+
1144
+ if (type === "slack" && provider === "slack") {
1145
+ return slackChannelAdapter;
1146
+ }
1147
+
528
1148
  throw new AgentKitError("channel_not_found", `No channel adapter for ${type}/${provider}.`);
529
1149
  }
530
1150
 
1151
+ function channelAdapterResponse(response: {
1152
+ status?: number;
1153
+ headers?: Record<string, string>;
1154
+ body: string | Record<string, unknown>;
1155
+ }): Response {
1156
+ if (typeof response.body === "string") {
1157
+ return new Response(response.body, {
1158
+ status: response.status ?? 200,
1159
+ headers: response.headers,
1160
+ });
1161
+ }
1162
+
1163
+ return jsonResponse(response.body, {
1164
+ status: response.status ?? 200,
1165
+ headers: response.headers,
1166
+ });
1167
+ }
1168
+
1169
+ function discordLocalInteractionResponse(
1170
+ deliveries: Array<{
1171
+ state: string;
1172
+ message?: {
1173
+ content?: string;
1174
+ };
1175
+ }>,
1176
+ ): Record<string, unknown> {
1177
+ const sent = deliveries.find((delivery) => delivery.state === "sent" && delivery.message?.content);
1178
+
1179
+ if (sent?.message?.content) {
1180
+ return {
1181
+ type: 4,
1182
+ data: {
1183
+ content: sent.message.content,
1184
+ allowed_mentions: {
1185
+ parse: [],
1186
+ },
1187
+ },
1188
+ };
1189
+ }
1190
+
1191
+ return {
1192
+ type: 4,
1193
+ data: {
1194
+ content: "This Discord interaction was received but could not be processed.",
1195
+ flags: 64,
1196
+ allowed_mentions: {
1197
+ parse: [],
1198
+ },
1199
+ },
1200
+ };
1201
+ }
1202
+
531
1203
  async function resolveChannelSecrets(root: string, channel: AgentChannel): Promise<Record<string, string>> {
532
1204
  const env = await loadCapsuleEnv(root);
533
1205
  return Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
@@ -805,6 +1477,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
805
1477
  const textarea = document.querySelector("#message");
806
1478
  const messages = document.querySelector("#messages");
807
1479
  const button = form.querySelector("button");
1480
+ let conversationId = null;
808
1481
 
809
1482
  function addMessage(role, content, className = "") {
810
1483
  const wrapper = document.createElement("article");
@@ -817,6 +1490,13 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
817
1490
  messages.scrollTop = messages.scrollHeight;
818
1491
  }
819
1492
 
1493
+ textarea.addEventListener("keydown", (event) => {
1494
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
1495
+ event.preventDefault();
1496
+ if (button.disabled) return;
1497
+ form.requestSubmit();
1498
+ });
1499
+
820
1500
  form.addEventListener("submit", async (event) => {
821
1501
  event.preventDefault();
822
1502
  const content = textarea.value.trim();
@@ -830,7 +1510,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
830
1510
  const response = await fetch("/v1/chat", {
831
1511
  method: "POST",
832
1512
  headers: { "content-type": "application/json" },
833
- body: JSON.stringify({ message: { role: "user", content } }),
1513
+ body: JSON.stringify({ message: { role: "user", content }, conversationId }),
834
1514
  });
835
1515
  const data = await response.json();
836
1516
 
@@ -838,6 +1518,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
838
1518
  throw new Error(data.error?.message || "Chat request failed.");
839
1519
  }
840
1520
 
1521
+ conversationId = data.conversationId || conversationId;
841
1522
  addMessage(data.message.role, data.message.content);
842
1523
  } catch (error) {
843
1524
  addMessage("error", error instanceof Error ? error.message : String(error), "error");
@@ -881,6 +1562,19 @@ function listen(server: Server, hostname: string, port: number): Promise<number>
881
1562
  });
882
1563
  }
883
1564
 
1565
+ async function closeServerAfterListenError(server: Server): Promise<void> {
1566
+ if (!server.listening) {
1567
+ try {
1568
+ server.close();
1569
+ } catch {
1570
+ // The server never reached the listening state.
1571
+ }
1572
+ return;
1573
+ }
1574
+
1575
+ await closeServer(server);
1576
+ }
1577
+
884
1578
  function closeServer(server: Server): Promise<void> {
885
1579
  return new Promise((resolvePromise, reject) => {
886
1580
  server.close((error) => {
@@ -894,6 +1588,30 @@ function closeServer(server: Server): Promise<void> {
894
1588
  });
895
1589
  }
896
1590
 
1591
+ function isAddressInUseError(error: unknown): boolean {
1592
+ return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
1593
+ }
1594
+
1595
+ async function detectAgentKitOccupant(hostname: string, port: number): Promise<string | undefined> {
1596
+ const controller = new AbortController();
1597
+ const timeout = setTimeout(() => controller.abort(), 300);
1598
+
1599
+ try {
1600
+ const response = await fetch(`http://${hostname}:${port}/_agentkit`, { signal: controller.signal });
1601
+ const payload = await response.json().catch(() => null);
1602
+
1603
+ if (payload && typeof payload === "object" && "agent" in payload && typeof payload.agent === "string") {
1604
+ return payload.agent;
1605
+ }
1606
+ } catch {
1607
+ return undefined;
1608
+ } finally {
1609
+ clearTimeout(timeout);
1610
+ }
1611
+
1612
+ return undefined;
1613
+ }
1614
+
897
1615
  function findAvailablePort(): Promise<number> {
898
1616
  return new Promise((resolve, reject) => {
899
1617
  const server = createNetServer();