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

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