@andreprado/agentkit 0.1.0-alpha.10

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 (122) hide show
  1. package/README.md +69 -0
  2. package/bin/agentkit.mjs +23 -0
  3. package/docs/guides/add-channel.md +114 -0
  4. package/docs/guides/add-knowledge.md +134 -0
  5. package/docs/guides/add-tool.md +342 -0
  6. package/docs/guides/agentkit-skills-architecture.md +471 -0
  7. package/docs/guides/channel-security.md +81 -0
  8. package/docs/guides/channels-implementation-map.md +243 -0
  9. package/docs/guides/channels-production-handoff.md +102 -0
  10. package/docs/guides/connect-telegram.md +110 -0
  11. package/docs/guides/connect-whatsapp-zapster.md +119 -0
  12. package/docs/guides/create-agent.md +220 -0
  13. package/docs/guides/prepare-deploy.md +209 -0
  14. package/docs/guides/run-evals.md +179 -0
  15. package/docs/guides/security-rules.md +156 -0
  16. package/docs/guides/use-provider.md +140 -0
  17. package/docs/llms-full.txt +876 -0
  18. package/docs/llms.txt +83 -0
  19. package/docs/portable-deploy-release-checklist.md +41 -0
  20. package/package.json +47 -0
  21. package/src/cli/args.ts +36 -0
  22. package/src/cli/cloud-client.ts +265 -0
  23. package/src/cli/commands/channels.ts +810 -0
  24. package/src/cli/commands/knowledge.ts +136 -0
  25. package/src/cli/constants.ts +4 -0
  26. package/src/cli/deploy-chat-ui.ts +392 -0
  27. package/src/cli/deploy-readiness.ts +348 -0
  28. package/src/cli/flags.ts +162 -0
  29. package/src/cli/help.ts +184 -0
  30. package/src/cli/index.ts +1276 -0
  31. package/src/cli/process.ts +31 -0
  32. package/src/cloud/artifact.ts +139 -0
  33. package/src/cloud/client.ts +79 -0
  34. package/src/cloud/contracts.ts +63 -0
  35. package/src/cloud/index.ts +3 -0
  36. package/src/create-project.ts +177 -0
  37. package/src/index.ts +408 -0
  38. package/src/providers/index.ts +25 -0
  39. package/src/providers/pi.ts +286 -0
  40. package/src/providers/test.ts +133 -0
  41. package/src/providers/types.ts +34 -0
  42. package/src/runtime/build.ts +43 -0
  43. package/src/runtime/channel-buffer.ts +30 -0
  44. package/src/runtime/channel-test-harness.ts +112 -0
  45. package/src/runtime/channels/telegram.ts +360 -0
  46. package/src/runtime/channels/website.ts +132 -0
  47. package/src/runtime/channels/whatsapp-meta.ts +71 -0
  48. package/src/runtime/channels/whatsapp-zapster.ts +278 -0
  49. package/src/runtime/channels.ts +138 -0
  50. package/src/runtime/chat.ts +218 -0
  51. package/src/runtime/config.ts +684 -0
  52. package/src/runtime/conversations.ts +38 -0
  53. package/src/runtime/core/deploy-state.ts +54 -0
  54. package/src/runtime/core/manifest.ts +213 -0
  55. package/src/runtime/core/targets.ts +133 -0
  56. package/src/runtime/database.ts +256 -0
  57. package/src/runtime/db-commands.ts +167 -0
  58. package/src/runtime/deploy-readiness.ts +105 -0
  59. package/src/runtime/deploy.ts +1 -0
  60. package/src/runtime/dev-server.ts +1247 -0
  61. package/src/runtime/docs.ts +36 -0
  62. package/src/runtime/env.ts +152 -0
  63. package/src/runtime/errors.ts +13 -0
  64. package/src/runtime/evals.ts +509 -0
  65. package/src/runtime/inspect.ts +203 -0
  66. package/src/runtime/knowledge/chunk.ts +333 -0
  67. package/src/runtime/knowledge/config.ts +135 -0
  68. package/src/runtime/knowledge/embeddings.ts +133 -0
  69. package/src/runtime/knowledge/ingest.ts +521 -0
  70. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  71. package/src/runtime/knowledge/retrieve.ts +283 -0
  72. package/src/runtime/knowledge/schema.ts +56 -0
  73. package/src/runtime/knowledge/tool.ts +64 -0
  74. package/src/runtime/knowledge/vector.ts +258 -0
  75. package/src/runtime/runtime-contract.ts +93 -0
  76. package/src/runtime/spec.ts +152 -0
  77. package/src/runtime/sync.ts +144 -0
  78. package/src/runtime/targets/cloudflare/build.ts +2517 -0
  79. package/src/runtime/targets/container/build.ts +146 -0
  80. package/src/runtime/targets/container/server.ts +33 -0
  81. package/src/runtime/targets/vps/deploy.ts +206 -0
  82. package/src/runtime/tool-runner.ts +65 -0
  83. package/src/runtime/tools.ts +470 -0
  84. package/src/runtime/traces.ts +41 -0
  85. package/src/storage/sqlite.ts +1118 -0
  86. package/src/templates/blank.ts +394 -0
  87. package/src/templates/dentista.ts +1003 -0
  88. package/src/templates/index.ts +33 -0
  89. package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
  90. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
  91. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  92. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  93. package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
  94. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  95. package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
  96. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
  97. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +41 -0
  98. package/src/templates/skills/agentkit-channels/references/telegram.md +38 -0
  99. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +44 -0
  100. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  101. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  102. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  103. package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
  104. package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
  105. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
  106. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
  107. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
  108. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
  109. package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
  110. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  111. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  112. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  113. package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
  114. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  115. package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
  116. package/src/templates/skills/agentkit-security/SKILL.md +55 -0
  117. package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
  118. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  119. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  120. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  121. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
  122. package/src/templates/support.ts +401 -0
@@ -0,0 +1,1247 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
4
+ import { createServer as createNetServer } from "node:net";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { Readable } from "node:stream";
7
+
8
+ import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
9
+ import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
10
+ import type { ChannelAdapter, RawWebhookEvent } from "./channels";
11
+ import { telegramChannelAdapter } from "./channels/telegram";
12
+ import { websiteChannelAdapter } from "./channels/website";
13
+ import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
14
+ import { zapsterWhatsappChannelAdapter } from "./channels/whatsapp-zapster";
15
+ import { runAgentMessage } from "./chat";
16
+ import { isChannelBufferEnabled, renderBufferedChannelMessage, resolveChannelBuffer } from "./channel-buffer";
17
+ import { type LoadedAgentCapsule, loadAgentCapsule } from "./config";
18
+ import { AgentKitError, isAgentKitError } from "./errors";
19
+ import { loadCapsuleEnv } from "./env";
20
+ import { buildInspectState } from "./inspect";
21
+ import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
22
+ import { runToolFromCwd } from "./tool-runner";
23
+
24
+ export type AgentDevServerOptions = {
25
+ port?: number;
26
+ hostname?: string;
27
+ basePort?: number;
28
+ };
29
+
30
+ export type AgentDevServer = {
31
+ capsule: LoadedAgentCapsule;
32
+ hostname: string;
33
+ requestedPort: number | null;
34
+ port: number;
35
+ portConflict: {
36
+ port: number;
37
+ agent?: string;
38
+ } | null;
39
+ urls: {
40
+ chat: string;
41
+ api: string;
42
+ inspect: string;
43
+ };
44
+ storage: string | null;
45
+ server: Server;
46
+ stop(): Promise<void>;
47
+ };
48
+
49
+ const DEFAULT_PORT = 4123;
50
+ const DEFAULT_HOSTNAME = "localhost";
51
+
52
+ type DevChannelRuntimeState = {
53
+ dedupeKeys: Set<string>;
54
+ buffers: Map<string, DevChannelBuffer>;
55
+ };
56
+
57
+ type DevChannelBuffer = {
58
+ conversationId: string;
59
+ messages: Array<{
60
+ content: string;
61
+ receivedAt: string;
62
+ }>;
63
+ firstReceivedMs: number;
64
+ lastReceivedMs: number;
65
+ timer?: ReturnType<typeof setTimeout>;
66
+ };
67
+
68
+ export async function startAgentDevServer(
69
+ cwd = process.cwd(),
70
+ options: AgentDevServerOptions = {},
71
+ ): Promise<AgentDevServer> {
72
+ const capsule = await loadAgentCapsule(cwd);
73
+ await syncConfiguredKnowledgeSources(capsule);
74
+ const hostname = options.hostname ?? DEFAULT_HOSTNAME;
75
+ const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
76
+ const autoPort = options.port === undefined;
77
+ const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
78
+
79
+ if (start.actualPort === undefined) {
80
+ throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
81
+ }
82
+
83
+ const actualPort = start.actualPort;
84
+
85
+ return {
86
+ capsule,
87
+ hostname,
88
+ requestedPort: options.port === undefined || options.port === 0 ? null : options.port,
89
+ port: actualPort,
90
+ portConflict: start.portConflict,
91
+ urls: {
92
+ chat: `http://${hostname}:${actualPort}`,
93
+ api: `http://${hostname}:${actualPort}/v1/chat`,
94
+ inspect: `http://${hostname}:${actualPort}/_agentkit`,
95
+ },
96
+ storage: capsule.storagePath ? relativePath(capsule.root, capsule.storagePath) : null,
97
+ server: start.server,
98
+ async stop() {
99
+ await closeServer(start.server);
100
+ },
101
+ };
102
+ }
103
+
104
+ async function listenForDevServer(
105
+ capsule: LoadedAgentCapsule,
106
+ hostname: string,
107
+ requestedPort: number,
108
+ autoPort: boolean,
109
+ ): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
110
+ if (requestedPort === 0) {
111
+ const server = createDevHttpServer(capsule);
112
+ const actualPort = await listen(server, hostname, await findAvailablePort());
113
+ return { server, actualPort, portConflict: null };
114
+ }
115
+
116
+ let firstConflict: AgentDevServer["portConflict"] = null;
117
+ const lastPort = autoPort ? requestedPort + 50 : requestedPort;
118
+
119
+ for (let port = requestedPort; port <= lastPort; port += 1) {
120
+ const server = createDevHttpServer(capsule);
121
+
122
+ try {
123
+ const actualPort = await listen(server, hostname, port);
124
+ return { server, actualPort, portConflict: port === requestedPort ? null : firstConflict };
125
+ } catch (error) {
126
+ await closeServerAfterListenError(server);
127
+
128
+ if (!autoPort || !isAddressInUseError(error)) {
129
+ throw error;
130
+ }
131
+
132
+ firstConflict ??= {
133
+ port,
134
+ agent: await detectAgentKitOccupant(hostname, port),
135
+ };
136
+ }
137
+ }
138
+
139
+ throw new AgentKitError(
140
+ "runtime_error",
141
+ `Could not allocate a local AgentKit dev server port after ${requestedPort}-${lastPort}.`,
142
+ );
143
+ }
144
+
145
+ function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
146
+ const channelRuntime: DevChannelRuntimeState = {
147
+ dedupeKeys: new Set<string>(),
148
+ buffers: new Map<string, DevChannelBuffer>(),
149
+ };
150
+ const capsuleRoot = capsule.root;
151
+ return createHttpServer((incoming, outgoing) => {
152
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
153
+ });
154
+ }
155
+
156
+ async function handleNodeRequest(
157
+ capsuleRoot: string,
158
+ incoming: IncomingMessage,
159
+ outgoing: ServerResponse,
160
+ channelRuntime: DevChannelRuntimeState,
161
+ ): Promise<void> {
162
+ try {
163
+ const capsule = await loadAgentCapsule(capsuleRoot);
164
+ const request = nodeRequestToFetchRequest(incoming);
165
+ const response = await handleDevServerRequest(capsule, request, channelRuntime);
166
+ await writeFetchResponse(outgoing, response);
167
+ } catch (error) {
168
+ await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
169
+ }
170
+ }
171
+
172
+ function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
173
+ const headers = new Headers();
174
+
175
+ for (const [key, value] of Object.entries(incoming.headers)) {
176
+ if (Array.isArray(value)) {
177
+ for (const item of value) {
178
+ headers.append(key, item);
179
+ }
180
+ } else if (value !== undefined) {
181
+ headers.set(key, value);
182
+ }
183
+ }
184
+
185
+ const protocol = headers.get("x-forwarded-proto") ?? "http";
186
+ const host = headers.get("host") ?? `${DEFAULT_HOSTNAME}:${DEFAULT_PORT}`;
187
+ const url = new URL(incoming.url ?? "/", `${protocol}://${host}`).href;
188
+ const init: RequestInit & { duplex?: "half" } = {
189
+ method: incoming.method ?? "GET",
190
+ headers,
191
+ };
192
+
193
+ if (init.method !== "GET" && init.method !== "HEAD") {
194
+ init.body = Readable.toWeb(incoming) as unknown as RequestInit["body"];
195
+ init.duplex = "half";
196
+ }
197
+
198
+ return new Request(url, init);
199
+ }
200
+
201
+ async function writeFetchResponse(outgoing: ServerResponse, response: Response): Promise<void> {
202
+ outgoing.statusCode = response.status;
203
+ response.headers.forEach((value, key) => {
204
+ outgoing.setHeader(key, value);
205
+ });
206
+
207
+ if (!response.body) {
208
+ outgoing.end();
209
+ return;
210
+ }
211
+
212
+ await new Promise<void>((resolvePromise, reject) => {
213
+ const stream = Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream);
214
+ stream.once("error", reject);
215
+ outgoing.once("finish", resolvePromise);
216
+ outgoing.once("error", reject);
217
+ stream.pipe(outgoing);
218
+ });
219
+ }
220
+
221
+ async function handleDevServerRequest(
222
+ capsule: LoadedAgentCapsule,
223
+ request: Request,
224
+ channelRuntime: DevChannelRuntimeState,
225
+ ): Promise<Response> {
226
+ try {
227
+ const url = new URL(request.url);
228
+ const route = normalizePath(url.pathname);
229
+
230
+ if (request.method === "GET" && route === "/") {
231
+ const env = await loadCapsuleEnv(capsule.root);
232
+ return htmlResponse(renderChatApp(capsule, url.origin, env));
233
+ }
234
+
235
+ if (request.method === "GET" && route === "/_agentkit") {
236
+ const env = await loadCapsuleEnv(capsule.root);
237
+ return jsonResponse(buildInspectState(capsule, env));
238
+ }
239
+
240
+ if (request.method === "GET" && route === "/health") {
241
+ return jsonResponse({ status: "ok", agent: capsule.config.name });
242
+ }
243
+
244
+ if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
245
+ const body = await readJsonBody(request);
246
+ const { message, conversationId } = readChatRequest(body);
247
+ const result = await runAgentMessage(capsule, {
248
+ message,
249
+ conversationId,
250
+ signal: request.signal,
251
+ });
252
+
253
+ return jsonResponse({
254
+ message: result.message,
255
+ conversationId: result.conversationId,
256
+ runId: result.runId,
257
+ userMessageId: result.userMessageId,
258
+ assistantMessageId: result.assistantMessageId,
259
+ });
260
+ }
261
+
262
+ const toolMatch = route.match(/^\/tools\/([^/]+)$/);
263
+
264
+ if (request.method === "POST" && toolMatch) {
265
+ const body = await readJsonBody(request);
266
+ const result = await runToolFromCwd(capsule.root, {
267
+ name: decodeURIComponent(toolMatch[1]),
268
+ input: readToolInput(body),
269
+ signal: request.signal,
270
+ });
271
+
272
+ return jsonResponse(result);
273
+ }
274
+
275
+ const fileMatch = route.match(/^\/files\/(.+)$/);
276
+
277
+ if (fileMatch) {
278
+ return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
279
+ }
280
+
281
+ const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
282
+
283
+ if (channelMatch) {
284
+ return await handlePortableChannel(capsule, request, channelRuntime, {
285
+ name: decodeURIComponent(channelMatch[1]),
286
+ type: channelMatch[2] as ChannelType,
287
+ provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
288
+ });
289
+ }
290
+
291
+ if (request.method === "GET" && route === "/v1/conversations") {
292
+ return jsonResponse({ conversations: await listConversations(capsule) });
293
+ }
294
+
295
+ const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
296
+
297
+ if (request.method === "GET" && conversationMatch) {
298
+ return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
299
+ }
300
+
301
+ const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
302
+
303
+ if (request.method === "GET" && conversationTraceMatch) {
304
+ return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
305
+ }
306
+
307
+ return jsonError(new AgentKitError("not_found", `Route ${request.method} ${route} was not found.`), 404);
308
+ } catch (error) {
309
+ return jsonError(error, statusForError(error));
310
+ }
311
+ }
312
+
313
+ async function handlePortableChannel(
314
+ capsule: LoadedAgentCapsule,
315
+ request: Request,
316
+ channelRuntime: DevChannelRuntimeState,
317
+ route: { name: string; type: ChannelType; provider: ChannelProvider },
318
+ ): Promise<Response> {
319
+ const channel = (capsule.config.channels ?? []).find(
320
+ (candidate) => candidate.name === route.name && candidate.type === route.type && candidate.provider === route.provider,
321
+ );
322
+
323
+ if (!channel) {
324
+ throw new AgentKitError("channel_not_found", `Channel "${route.name}" was not found in agentkit.config.ts.`);
325
+ }
326
+
327
+ const adapter = adapterForChannel(route.type, route.provider);
328
+ const rawBody = request.method === "GET" ? "" : await request.text();
329
+ const rawEvent: RawWebhookEvent = {
330
+ type: route.type,
331
+ provider: route.provider,
332
+ channelId: channel.name,
333
+ method: request.method,
334
+ url: request.url,
335
+ headers: request.headers,
336
+ rawBody,
337
+ receivedAt: new Date().toISOString(),
338
+ };
339
+ const secrets = await resolveChannelSecrets(capsule.root, channel);
340
+ const verification = adapter.verifyWebhook
341
+ ? await adapter.verifyWebhook({ ...rawEvent, channel, secrets })
342
+ : { ok: true as const };
343
+
344
+ if (!verification.ok) {
345
+ throw new AgentKitError(verification.code, verification.message);
346
+ }
347
+
348
+ if (channel.type === "whatsapp" && channel.provider === "meta" && request.method === "GET") {
349
+ return new Response(new URL(request.url).searchParams.get("hub.challenge") ?? "");
350
+ }
351
+
352
+ const events = await adapter.normalizeInbound(rawEvent);
353
+ const deliveries = [];
354
+
355
+ for (const event of events) {
356
+ if (channelRuntime.dedupeKeys.has(event.dedupeKey)) {
357
+ deliveries.push({ state: "duplicate", dedupeKey: event.dedupeKey });
358
+ continue;
359
+ }
360
+
361
+ channelRuntime.dedupeKeys.add(event.dedupeKey);
362
+
363
+ if (event.kind !== "message" || !event.message) {
364
+ deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
365
+ continue;
366
+ }
367
+
368
+ const conversationId = channelConversationId(channel.name, event.externalIdentity.key);
369
+
370
+ if (isChannelBufferEnabled(channel)) {
371
+ const buffer = resolveChannelBuffer(channel);
372
+ if (buffer.mode !== "debounce") {
373
+ throw new AgentKitError("runtime_error", "Unexpected channel buffer mode.");
374
+ }
375
+
376
+ bufferDevChannelMessage(capsule, channelRuntime, {
377
+ bufferKey: `${channel.name}:${event.externalIdentity.key}`,
378
+ conversationId,
379
+ content: event.message.content,
380
+ receivedAt: event.receivedAt,
381
+ buffer,
382
+ });
383
+ deliveries.push({
384
+ state: "buffered",
385
+ dedupeKey: event.dedupeKey,
386
+ conversationId,
387
+ flushAfterMs: buffer.quietWindowMs,
388
+ });
389
+ continue;
390
+ }
391
+
392
+ const result = await runAgentMessage(capsule, {
393
+ message: event.message.content,
394
+ conversationId,
395
+ signal: request.signal,
396
+ });
397
+ deliveries.push({
398
+ state: "sent",
399
+ dedupeKey: event.dedupeKey,
400
+ conversationId: result.conversationId,
401
+ message: result.message,
402
+ });
403
+ }
404
+
405
+ return jsonResponse({ deliveries });
406
+ }
407
+
408
+ function bufferDevChannelMessage(
409
+ capsule: LoadedAgentCapsule,
410
+ runtime: DevChannelRuntimeState,
411
+ input: {
412
+ bufferKey: string;
413
+ conversationId: string;
414
+ content: string;
415
+ receivedAt: string;
416
+ buffer: {
417
+ quietWindowMs: number;
418
+ maxWaitMs: number;
419
+ maxMessages: number;
420
+ maxChars: number;
421
+ };
422
+ },
423
+ ): void {
424
+ const now = Date.now();
425
+ const existing = runtime.buffers.get(input.bufferKey);
426
+ const pending: DevChannelBuffer = existing ?? {
427
+ conversationId: input.conversationId,
428
+ messages: [],
429
+ firstReceivedMs: now,
430
+ lastReceivedMs: now,
431
+ };
432
+
433
+ if (existing?.timer) {
434
+ clearTimeout(existing.timer);
435
+ }
436
+
437
+ pending.messages.push({
438
+ content: input.content,
439
+ receivedAt: input.receivedAt,
440
+ });
441
+ pending.lastReceivedMs = now;
442
+ runtime.buffers.set(input.bufferKey, pending);
443
+
444
+ if (shouldFlushDevChannelBuffer(pending, input.buffer)) {
445
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
446
+ return;
447
+ }
448
+
449
+ const elapsedMs = now - pending.firstReceivedMs;
450
+ const delayMs = Math.max(0, Math.min(input.buffer.quietWindowMs, input.buffer.maxWaitMs - elapsedMs));
451
+ pending.timer = setTimeout(() => {
452
+ void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
453
+ }, delayMs);
454
+ unrefTimer(pending.timer);
455
+ }
456
+
457
+ function shouldFlushDevChannelBuffer(
458
+ pending: DevChannelBuffer,
459
+ buffer: {
460
+ maxWaitMs: number;
461
+ maxMessages: number;
462
+ maxChars: number;
463
+ },
464
+ ): boolean {
465
+ const totalChars = pending.messages.reduce((sum, message) => sum + message.content.length, 0);
466
+ return (
467
+ pending.messages.length >= buffer.maxMessages ||
468
+ totalChars >= buffer.maxChars ||
469
+ Date.now() - pending.firstReceivedMs >= buffer.maxWaitMs
470
+ );
471
+ }
472
+
473
+ async function flushDevChannelBuffer(
474
+ capsule: LoadedAgentCapsule,
475
+ runtime: DevChannelRuntimeState,
476
+ bufferKey: string,
477
+ ): Promise<void> {
478
+ const pending = runtime.buffers.get(bufferKey);
479
+
480
+ if (!pending) {
481
+ return;
482
+ }
483
+
484
+ runtime.buffers.delete(bufferKey);
485
+
486
+ if (pending.timer) {
487
+ clearTimeout(pending.timer);
488
+ }
489
+
490
+ const message = renderBufferedChannelMessage(pending.messages);
491
+
492
+ try {
493
+ await runAgentMessage(capsule, {
494
+ message,
495
+ conversationId: pending.conversationId,
496
+ runtime: {
497
+ invocation: "channel",
498
+ },
499
+ });
500
+ } catch {
501
+ // Local channel buffering is best-effort; hosted delivery logs own production failures.
502
+ }
503
+ }
504
+
505
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
506
+ if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
507
+ timer.unref();
508
+ }
509
+ }
510
+
511
+ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
512
+ const key = normalizeFileKey(rawKey);
513
+ const root = resolve(capsule.root, ".agentkit/files");
514
+ const path = resolve(root, key);
515
+
516
+ if (!path.startsWith(`${root}/`) && path !== root) {
517
+ throw new AgentKitError("file_key_invalid", "File key cannot escape .agentkit/files.");
518
+ }
519
+
520
+ if (request.method === "PUT") {
521
+ const bytes = new Uint8Array(await request.arrayBuffer());
522
+ const maxUploadBytes = capsule.config.storage.files?.driver === "r2"
523
+ ? capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024
524
+ : 10 * 1024 * 1024;
525
+
526
+ if (bytes.byteLength > maxUploadBytes) {
527
+ throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
528
+ }
529
+
530
+ await mkdir(dirname(path), { recursive: true });
531
+ await writeFile(path, bytes);
532
+ return jsonResponse({ key, size: bytes.byteLength });
533
+ }
534
+
535
+ if (request.method === "GET") {
536
+ try {
537
+ return new Response(await readFile(path));
538
+ } catch {
539
+ throw new AgentKitError("file_not_found", `File "${key}" was not found.`);
540
+ }
541
+ }
542
+
543
+ if (request.method === "DELETE") {
544
+ await rm(path, { force: true });
545
+ return jsonResponse({ key, deleted: true });
546
+ }
547
+
548
+ throw new AgentKitError("method_not_allowed", "Unsupported file method.");
549
+ }
550
+
551
+ async function listConversations(capsule: LoadedAgentCapsule) {
552
+ const store = await openCapsuleStore(capsule);
553
+
554
+ try {
555
+ return store.listConversations();
556
+ } finally {
557
+ store.close();
558
+ }
559
+ }
560
+
561
+ async function getConversation(capsule: LoadedAgentCapsule, conversationId: string): Promise<ConversationRecord> {
562
+ if (!conversationId) {
563
+ throw new AgentKitError("validation_error", "Missing conversation id.");
564
+ }
565
+
566
+ const store = await openCapsuleStore(capsule);
567
+
568
+ try {
569
+ const conversation = store.getConversation(conversationId);
570
+
571
+ if (!conversation) {
572
+ throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
573
+ }
574
+
575
+ return conversation;
576
+ } finally {
577
+ store.close();
578
+ }
579
+ }
580
+
581
+ async function getConversationTrace(capsule: LoadedAgentCapsule, conversationId: string) {
582
+ if (!conversationId) {
583
+ throw new AgentKitError("validation_error", "Missing conversation id.");
584
+ }
585
+
586
+ const store = await openCapsuleStore(capsule);
587
+
588
+ try {
589
+ const conversation = store.getConversation(conversationId);
590
+
591
+ if (!conversation) {
592
+ throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
593
+ }
594
+
595
+ return {
596
+ ...conversation,
597
+ runs: store.listRunsForConversation(conversation.id).map((run) => ({
598
+ ...run,
599
+ toolCalls: store.listToolCallsForRun(run.id),
600
+ })),
601
+ };
602
+ } finally {
603
+ store.close();
604
+ }
605
+ }
606
+
607
+ async function readJsonBody(request: Request): Promise<unknown> {
608
+ const contentType = request.headers.get("content-type") ?? "";
609
+
610
+ if (!contentType.toLowerCase().includes("application/json")) {
611
+ throw new AgentKitError("validation_error", 'POST /v1/chat requires "content-type: application/json".');
612
+ }
613
+
614
+ try {
615
+ return await request.json();
616
+ } catch (error) {
617
+ throw new AgentKitError("validation_error", "Request body must be valid JSON.", { cause: error });
618
+ }
619
+ }
620
+
621
+ function readChatRequest(body: unknown): { message: string; conversationId?: string } {
622
+ if (!isRecord(body)) {
623
+ throw new AgentKitError(
624
+ "validation_error",
625
+ 'Expected body shape: {"message":{"role":"user","content":"hello"},"conversationId":"optional-id"}.',
626
+ );
627
+ }
628
+
629
+ const message = readUserMessage(body.message);
630
+ const conversationId = readOptionalConversationId(body.conversationId);
631
+
632
+ return { message, conversationId };
633
+ }
634
+
635
+ function readUserMessage(message: unknown): string {
636
+ if (typeof message === "string") {
637
+ const content = message.trim();
638
+
639
+ if (!content) {
640
+ throw new AgentKitError("validation_error", "message must be a non-empty string.");
641
+ }
642
+
643
+ return content;
644
+ }
645
+
646
+ if (!isRecord(message)) {
647
+ throw new AgentKitError("validation_error", 'Expected body shape: {"message":{"role":"user","content":"hello"}}.');
648
+ }
649
+
650
+ if (message.role !== "user") {
651
+ throw new AgentKitError("validation_error", 'message.role must be "user".');
652
+ }
653
+
654
+ if (typeof message.content !== "string" || message.content.trim().length === 0) {
655
+ throw new AgentKitError("validation_error", "message.content must be a non-empty string.");
656
+ }
657
+
658
+ return message.content.trim();
659
+ }
660
+
661
+ function readOptionalConversationId(value: unknown): string | undefined {
662
+ if (value === undefined || value === null) {
663
+ return undefined;
664
+ }
665
+
666
+ if (typeof value !== "string" || value.trim().length === 0) {
667
+ throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
668
+ }
669
+
670
+ return value.trim();
671
+ }
672
+
673
+ function channelConversationId(channelName: string, externalIdentityKey: string): string {
674
+ const hash = createHash("sha256").update(`${channelName}:${externalIdentityKey}`).digest("hex").slice(0, 32);
675
+ return `chn_${hash}`;
676
+ }
677
+
678
+ function readToolInput(body: unknown): unknown {
679
+ if (!isRecord(body)) {
680
+ throw new AgentKitError("validation_error", 'Expected body shape: {"input":{}}.');
681
+ }
682
+
683
+ return body.input ?? {};
684
+ }
685
+
686
+ function statusForError(error: unknown): number {
687
+ if (!isAgentKitError(error)) {
688
+ return 500;
689
+ }
690
+
691
+ if (
692
+ error.code === "validation_error" ||
693
+ error.code === "secret_not_found" ||
694
+ error.code === "tool_secret_missing" ||
695
+ error.code === "database_query_invalid"
696
+ ) {
697
+ return 400;
698
+ }
699
+
700
+ if (error.code === "constraint_conflict") {
701
+ return 409;
702
+ }
703
+
704
+ if (
705
+ error.code === "file_key_invalid" ||
706
+ error.code === "file_too_large" ||
707
+ error.code === "channel_payload_invalid" ||
708
+ error.code === "channel_signature_invalid" ||
709
+ error.code === "channel_secret_missing"
710
+ ) {
711
+ return 400;
712
+ }
713
+
714
+ if (
715
+ error.code === "conversation_not_found" ||
716
+ error.code === "not_found" ||
717
+ error.code === "tool_not_found" ||
718
+ error.code === "file_not_found" ||
719
+ error.code === "channel_not_found"
720
+ ) {
721
+ return 404;
722
+ }
723
+
724
+ if (error.code === "method_not_allowed") {
725
+ return 405;
726
+ }
727
+
728
+ if (error.code === "database_unavailable" || error.code === "database_transaction_unsupported") {
729
+ return 503;
730
+ }
731
+
732
+ if (error.code === "provider_not_supported" || error.code === "provider_model_unsupported") {
733
+ return 501;
734
+ }
735
+
736
+ return 500;
737
+ }
738
+
739
+ function jsonError(error: unknown, status: number): Response {
740
+ const code = isAgentKitError(error) ? error.code : "runtime_error";
741
+ const message = error instanceof Error ? error.message : String(error);
742
+
743
+ return jsonResponse(
744
+ {
745
+ error: {
746
+ code,
747
+ message,
748
+ },
749
+ },
750
+ { status },
751
+ );
752
+ }
753
+
754
+ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
755
+ const headers = new Headers(init.headers);
756
+ headers.set("content-type", "application/json; charset=utf-8");
757
+
758
+ return new Response(JSON.stringify(body, null, 2), {
759
+ ...init,
760
+ headers,
761
+ });
762
+ }
763
+
764
+ function htmlResponse(body: string): Response {
765
+ return new Response(body, {
766
+ headers: {
767
+ "content-type": "text/html; charset=utf-8",
768
+ },
769
+ });
770
+ }
771
+
772
+ function normalizePath(pathname: string): string {
773
+ if (pathname.length > 1 && pathname.endsWith("/")) {
774
+ return pathname.slice(0, -1);
775
+ }
776
+
777
+ return pathname;
778
+ }
779
+
780
+ function normalizeFileKey(rawKey: string): string {
781
+ const key = rawKey
782
+ .split("/")
783
+ .filter((part) => part.length > 0)
784
+ .join("/");
785
+
786
+ if (!key || key.includes("..") || key.startsWith("/") || key.includes("\\")) {
787
+ throw new AgentKitError("file_key_invalid", "File key must be a relative object key.");
788
+ }
789
+
790
+ return key;
791
+ }
792
+
793
+ function adapterForChannel(type: ChannelType, provider: ChannelProvider): ChannelAdapter {
794
+ if (type === "website" && provider === "agentkit") {
795
+ return websiteChannelAdapter;
796
+ }
797
+
798
+ if (type === "telegram" && provider === "telegram") {
799
+ return telegramChannelAdapter;
800
+ }
801
+
802
+ if (type === "whatsapp" && provider === "zapster") {
803
+ return zapsterWhatsappChannelAdapter;
804
+ }
805
+
806
+ if (type === "whatsapp" && provider === "meta") {
807
+ return metaWhatsappChannelAdapter;
808
+ }
809
+
810
+ throw new AgentKitError("channel_not_found", `No channel adapter for ${type}/${provider}.`);
811
+ }
812
+
813
+ async function resolveChannelSecrets(root: string, channel: AgentChannel): Promise<Record<string, string>> {
814
+ const env = await loadCapsuleEnv(root);
815
+ return Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
816
+ }
817
+
818
+ function relativePath(root: string, path: string): string {
819
+ const localPath = relative(root, path);
820
+ return localPath.length > 0 ? localPath : ".";
821
+ }
822
+
823
+ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record<string, string | undefined>): string {
824
+ const state = buildInspectState(capsule, env);
825
+ const storage = state.storage ?? "managed";
826
+
827
+ return `<!doctype html>
828
+ <html lang="en">
829
+ <head>
830
+ <meta charset="utf-8" />
831
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
832
+ <title>${escapeHtml(state.agent)} - AgentKit</title>
833
+ <style>
834
+ :root {
835
+ color-scheme: light;
836
+ --ink: #1c1f22;
837
+ --muted: #626a73;
838
+ --line: #d7dde3;
839
+ --surface: #f7f8f9;
840
+ --panel: #ffffff;
841
+ --accent: #116b5f;
842
+ --accent-ink: #ffffff;
843
+ --danger: #9f2626;
844
+ }
845
+
846
+ * {
847
+ box-sizing: border-box;
848
+ }
849
+
850
+ body {
851
+ margin: 0;
852
+ min-height: 100svh;
853
+ color: var(--ink);
854
+ background: var(--surface);
855
+ font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
856
+ }
857
+
858
+ main {
859
+ display: grid;
860
+ grid-template-columns: minmax(220px, 320px) minmax(0, 1fr);
861
+ min-height: 100svh;
862
+ }
863
+
864
+ aside {
865
+ border-right: 1px solid var(--line);
866
+ background: #eef2f4;
867
+ padding: 28px;
868
+ }
869
+
870
+ h1,
871
+ h2,
872
+ p {
873
+ margin: 0;
874
+ }
875
+
876
+ h1 {
877
+ font-size: 24px;
878
+ line-height: 1.15;
879
+ font-weight: 680;
880
+ }
881
+
882
+ .eyebrow {
883
+ color: var(--muted);
884
+ font-size: 12px;
885
+ font-weight: 700;
886
+ letter-spacing: 0;
887
+ text-transform: uppercase;
888
+ margin-bottom: 10px;
889
+ }
890
+
891
+ .meta {
892
+ display: grid;
893
+ gap: 14px;
894
+ margin-top: 30px;
895
+ }
896
+
897
+ .meta div {
898
+ display: grid;
899
+ gap: 4px;
900
+ }
901
+
902
+ .meta span {
903
+ color: var(--muted);
904
+ font-size: 12px;
905
+ font-weight: 650;
906
+ }
907
+
908
+ .meta code {
909
+ overflow-wrap: anywhere;
910
+ color: var(--ink);
911
+ font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
912
+ }
913
+
914
+ .workspace {
915
+ display: grid;
916
+ grid-template-rows: auto minmax(0, 1fr) auto;
917
+ min-width: 0;
918
+ min-height: 100svh;
919
+ }
920
+
921
+ header {
922
+ border-bottom: 1px solid var(--line);
923
+ background: var(--panel);
924
+ padding: 18px 24px;
925
+ }
926
+
927
+ header h2 {
928
+ font-size: 16px;
929
+ line-height: 1.3;
930
+ font-weight: 680;
931
+ }
932
+
933
+ header p {
934
+ color: var(--muted);
935
+ margin-top: 2px;
936
+ }
937
+
938
+ #messages {
939
+ display: flex;
940
+ flex-direction: column;
941
+ gap: 14px;
942
+ overflow: auto;
943
+ padding: 24px;
944
+ }
945
+
946
+ .message {
947
+ max-width: 780px;
948
+ border-left: 3px solid var(--line);
949
+ padding: 2px 0 2px 14px;
950
+ }
951
+
952
+ .message[data-role="assistant"] {
953
+ border-left-color: var(--accent);
954
+ }
955
+
956
+ .role {
957
+ color: var(--muted);
958
+ font-size: 12px;
959
+ font-weight: 700;
960
+ text-transform: uppercase;
961
+ }
962
+
963
+ .content {
964
+ margin-top: 4px;
965
+ white-space: pre-wrap;
966
+ }
967
+
968
+ form {
969
+ display: grid;
970
+ grid-template-columns: minmax(0, 1fr) auto;
971
+ gap: 12px;
972
+ border-top: 1px solid var(--line);
973
+ background: var(--panel);
974
+ padding: 16px 24px;
975
+ }
976
+
977
+ textarea {
978
+ min-height: 48px;
979
+ max-height: 160px;
980
+ resize: vertical;
981
+ border: 1px solid var(--line);
982
+ border-radius: 6px;
983
+ padding: 12px 13px;
984
+ color: var(--ink);
985
+ background: #fff;
986
+ font: inherit;
987
+ }
988
+
989
+ textarea:focus {
990
+ border-color: var(--accent);
991
+ outline: 3px solid color-mix(in srgb, var(--accent) 18%, transparent);
992
+ }
993
+
994
+ button {
995
+ align-self: end;
996
+ min-height: 48px;
997
+ border: 0;
998
+ border-radius: 6px;
999
+ padding: 0 18px;
1000
+ color: var(--accent-ink);
1001
+ background: var(--accent);
1002
+ font: inherit;
1003
+ font-weight: 700;
1004
+ cursor: pointer;
1005
+ }
1006
+
1007
+ button:disabled {
1008
+ cursor: progress;
1009
+ opacity: .64;
1010
+ }
1011
+
1012
+ .error {
1013
+ color: var(--danger);
1014
+ }
1015
+
1016
+ @media (max-width: 760px) {
1017
+ main {
1018
+ grid-template-columns: 1fr;
1019
+ }
1020
+
1021
+ aside {
1022
+ border-right: 0;
1023
+ border-bottom: 1px solid var(--line);
1024
+ padding: 20px;
1025
+ }
1026
+
1027
+ .meta {
1028
+ grid-template-columns: 1fr 1fr;
1029
+ margin-top: 18px;
1030
+ }
1031
+
1032
+ .workspace {
1033
+ min-height: 70svh;
1034
+ }
1035
+
1036
+ form {
1037
+ grid-template-columns: 1fr;
1038
+ padding: 14px;
1039
+ }
1040
+
1041
+ #messages {
1042
+ padding: 18px 14px;
1043
+ }
1044
+ }
1045
+ </style>
1046
+ </head>
1047
+ <body>
1048
+ <main>
1049
+ <aside>
1050
+ <p class="eyebrow">AgentKit local</p>
1051
+ <h1>${escapeHtml(state.agent)}</h1>
1052
+ <section class="meta" aria-label="Capsule state">
1053
+ <div>
1054
+ <span>Provider</span>
1055
+ <code>${escapeHtml(state.provider.name)} / ${escapeHtml(state.provider.model)}</code>
1056
+ </div>
1057
+ <div>
1058
+ <span>Runtime</span>
1059
+ <code>${escapeHtml(state.runtime)}</code>
1060
+ </div>
1061
+ <div>
1062
+ <span>Storage</span>
1063
+ <code>${escapeHtml(storage)}</code>
1064
+ </div>
1065
+ <div>
1066
+ <span>Inspect</span>
1067
+ <code>${escapeHtml(new URL("/_agentkit", baseUrl).href)}</code>
1068
+ </div>
1069
+ </section>
1070
+ </aside>
1071
+ <section class="workspace" aria-label="Chat workspace">
1072
+ <header>
1073
+ <h2>Chat</h2>
1074
+ <p>Messages are stored in the capsule SQLite database.</p>
1075
+ </header>
1076
+ <div id="messages" aria-live="polite"></div>
1077
+ <form id="chat-form">
1078
+ <textarea id="message" name="message" placeholder="Send a message to ${escapeHtml(
1079
+ state.agent,
1080
+ )}" required></textarea>
1081
+ <button type="submit">Send</button>
1082
+ </form>
1083
+ </section>
1084
+ </main>
1085
+ <script>
1086
+ const form = document.querySelector("#chat-form");
1087
+ const textarea = document.querySelector("#message");
1088
+ const messages = document.querySelector("#messages");
1089
+ const button = form.querySelector("button");
1090
+ let conversationId = null;
1091
+
1092
+ function addMessage(role, content, className = "") {
1093
+ const wrapper = document.createElement("article");
1094
+ wrapper.className = "message " + className;
1095
+ wrapper.dataset.role = role;
1096
+ wrapper.innerHTML = '<div class="role"></div><div class="content"></div>';
1097
+ wrapper.querySelector(".role").textContent = role;
1098
+ wrapper.querySelector(".content").textContent = content;
1099
+ messages.append(wrapper);
1100
+ messages.scrollTop = messages.scrollHeight;
1101
+ }
1102
+
1103
+ textarea.addEventListener("keydown", (event) => {
1104
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
1105
+ event.preventDefault();
1106
+ if (button.disabled) return;
1107
+ form.requestSubmit();
1108
+ });
1109
+
1110
+ form.addEventListener("submit", async (event) => {
1111
+ event.preventDefault();
1112
+ const content = textarea.value.trim();
1113
+ if (!content) return;
1114
+
1115
+ addMessage("user", content);
1116
+ textarea.value = "";
1117
+ button.disabled = true;
1118
+
1119
+ try {
1120
+ const response = await fetch("/v1/chat", {
1121
+ method: "POST",
1122
+ headers: { "content-type": "application/json" },
1123
+ body: JSON.stringify({ message: { role: "user", content }, conversationId }),
1124
+ });
1125
+ const data = await response.json();
1126
+
1127
+ if (!response.ok) {
1128
+ throw new Error(data.error?.message || "Chat request failed.");
1129
+ }
1130
+
1131
+ conversationId = data.conversationId || conversationId;
1132
+ addMessage(data.message.role, data.message.content);
1133
+ } catch (error) {
1134
+ addMessage("error", error instanceof Error ? error.message : String(error), "error");
1135
+ } finally {
1136
+ button.disabled = false;
1137
+ textarea.focus();
1138
+ }
1139
+ });
1140
+ </script>
1141
+ </body>
1142
+ </html>`;
1143
+ }
1144
+
1145
+ function escapeHtml(value: string): string {
1146
+ return value
1147
+ .replaceAll("&", "&amp;")
1148
+ .replaceAll("<", "&lt;")
1149
+ .replaceAll(">", "&gt;")
1150
+ .replaceAll('"', "&quot;")
1151
+ .replaceAll("'", "&#39;");
1152
+ }
1153
+
1154
+ function isRecord(value: unknown): value is Record<string, unknown> {
1155
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1156
+ }
1157
+
1158
+ function listen(server: Server, hostname: string, port: number): Promise<number> {
1159
+ return new Promise((resolvePromise, reject) => {
1160
+ server.once("error", reject);
1161
+ server.listen(port, hostname, () => {
1162
+ server.off("error", reject);
1163
+ const address = server.address();
1164
+
1165
+ if (address && typeof address === "object") {
1166
+ resolvePromise(address.port);
1167
+ return;
1168
+ }
1169
+
1170
+ reject(new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port."));
1171
+ });
1172
+ });
1173
+ }
1174
+
1175
+ async function closeServerAfterListenError(server: Server): Promise<void> {
1176
+ if (!server.listening) {
1177
+ try {
1178
+ server.close();
1179
+ } catch {
1180
+ // The server never reached the listening state.
1181
+ }
1182
+ return;
1183
+ }
1184
+
1185
+ await closeServer(server);
1186
+ }
1187
+
1188
+ function closeServer(server: Server): Promise<void> {
1189
+ return new Promise((resolvePromise, reject) => {
1190
+ server.close((error) => {
1191
+ if (error) {
1192
+ reject(error);
1193
+ return;
1194
+ }
1195
+
1196
+ resolvePromise();
1197
+ });
1198
+ });
1199
+ }
1200
+
1201
+ function isAddressInUseError(error: unknown): boolean {
1202
+ return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
1203
+ }
1204
+
1205
+ async function detectAgentKitOccupant(hostname: string, port: number): Promise<string | undefined> {
1206
+ const controller = new AbortController();
1207
+ const timeout = setTimeout(() => controller.abort(), 300);
1208
+
1209
+ try {
1210
+ const response = await fetch(`http://${hostname}:${port}/_agentkit`, { signal: controller.signal });
1211
+ const payload = await response.json().catch(() => null);
1212
+
1213
+ if (payload && typeof payload === "object" && "agent" in payload && typeof payload.agent === "string") {
1214
+ return payload.agent;
1215
+ }
1216
+ } catch {
1217
+ return undefined;
1218
+ } finally {
1219
+ clearTimeout(timeout);
1220
+ }
1221
+
1222
+ return undefined;
1223
+ }
1224
+
1225
+ function findAvailablePort(): Promise<number> {
1226
+ return new Promise((resolve, reject) => {
1227
+ const server = createNetServer();
1228
+
1229
+ server.once("error", reject);
1230
+ server.listen(0, "127.0.0.1", () => {
1231
+ const address = server.address();
1232
+ server.close((error) => {
1233
+ if (error) {
1234
+ reject(error);
1235
+ return;
1236
+ }
1237
+
1238
+ if (address && typeof address === "object") {
1239
+ resolve(address.port);
1240
+ return;
1241
+ }
1242
+
1243
+ reject(new AgentKitError("runtime_error", "Could not allocate an available local port."));
1244
+ });
1245
+ });
1246
+ });
1247
+ }