@andreprado/agentkit 0.1.0-alpha.14 → 0.1.0-alpha.16

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 (56) hide show
  1. package/README.md +2 -0
  2. package/docs/guides/add-channel.md +63 -0
  3. package/docs/guides/channel-security.md +32 -0
  4. package/docs/guides/connect-telegram.md +58 -0
  5. package/docs/guides/connect-whatsapp-zapster.md +65 -0
  6. package/docs/guides/run-evals.md +73 -25
  7. package/docs/llms-full.txt +24 -9
  8. package/docs/llms.txt +2 -0
  9. package/package.json +1 -1
  10. package/src/cli/cloud-client.ts +30 -10
  11. package/src/cli/commands/channels.ts +2 -0
  12. package/src/cli/deploy-readiness.ts +32 -11
  13. package/src/cli/index.ts +20 -6
  14. package/src/cloud/client.ts +4 -3
  15. package/src/cloud/contracts.ts +1 -1
  16. package/src/create-project.ts +1 -1
  17. package/src/index.ts +110 -1
  18. package/src/providers/pi.ts +14 -1
  19. package/src/providers/test.ts +36 -0
  20. package/src/runtime/channel-test-harness.ts +2 -0
  21. package/src/runtime/channels/telegram.ts +326 -10
  22. package/src/runtime/channels/whatsapp-zapster.ts +319 -0
  23. package/src/runtime/channels.ts +47 -1
  24. package/src/runtime/chat.ts +59 -42
  25. package/src/runtime/config.ts +96 -4
  26. package/src/runtime/core/manifest.ts +35 -3
  27. package/src/runtime/deploy-readiness.ts +3 -3
  28. package/src/runtime/dev-server.ts +243 -17
  29. package/src/runtime/env.ts +8 -3
  30. package/src/runtime/evals.ts +404 -69
  31. package/src/runtime/inspect.ts +46 -0
  32. package/src/runtime/prompt-context.ts +141 -0
  33. package/src/runtime/runtime-contract.ts +17 -7
  34. package/src/runtime/targets/cloudflare/build.ts +25 -3
  35. package/src/runtime/targets/container/server.ts +1 -1
  36. package/src/runtime/targets/vps/deploy.ts +25 -8
  37. package/src/runtime/tool-runner.ts +7 -0
  38. package/src/runtime/tools.ts +8 -2
  39. package/src/runtime/transcription.ts +483 -0
  40. package/src/templates/blank.ts +8 -3
  41. package/src/templates/dentista.ts +18 -10
  42. package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
  43. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  44. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
  45. package/src/templates/skills/agentkit-channels/SKILL.md +34 -1
  46. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +13 -0
  47. package/src/templates/skills/agentkit-channels/references/telegram.md +32 -0
  48. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +29 -0
  49. package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
  50. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  51. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  52. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  53. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  54. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  55. package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
  56. package/src/templates/support.ts +8 -3
@@ -7,7 +7,7 @@ import { Readable } from "node:stream";
7
7
 
8
8
  import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
9
9
  import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
10
- import type { ChannelAdapter, RawWebhookEvent } from "./channels";
10
+ import type { ChannelAdapter, NormalizedChannelMessage, RawWebhookEvent } from "./channels";
11
11
  import { telegramChannelAdapter } from "./channels/telegram";
12
12
  import { websiteChannelAdapter } from "./channels/website";
13
13
  import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
@@ -20,11 +20,13 @@ import { loadCapsuleEnv } from "./env";
20
20
  import { buildInspectState } from "./inspect";
21
21
  import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
22
22
  import { runToolFromCwd } from "./tool-runner";
23
+ import { resolveTranscriptionConfig, transcribeAudio } from "./transcription";
23
24
 
24
25
  export type AgentDevServerOptions = {
25
26
  port?: number;
26
27
  hostname?: string;
27
28
  basePort?: number;
29
+ access?: AgentDevServerAccessPolicy;
28
30
  };
29
31
 
30
32
  export type AgentDevServer = {
@@ -49,6 +51,12 @@ export type AgentDevServer = {
49
51
  const DEFAULT_PORT = 4123;
50
52
  const DEFAULT_HOSTNAME = "localhost";
51
53
 
54
+ export type AgentDevServerAccessPolicy = "local" | "enforce";
55
+
56
+ type DevHttpServerOptions = {
57
+ access: AgentDevServerAccessPolicy;
58
+ };
59
+
52
60
  type DevChannelRuntimeState = {
53
61
  dedupeKeys: Set<string>;
54
62
  buffers: Map<string, DevChannelBuffer>;
@@ -74,7 +82,9 @@ export async function startAgentDevServer(
74
82
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
75
83
  const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
76
84
  const autoPort = options.port === undefined;
77
- const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
85
+ const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort, {
86
+ access: options.access ?? "local",
87
+ });
78
88
 
79
89
  if (start.actualPort === undefined) {
80
90
  throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
@@ -106,9 +116,10 @@ async function listenForDevServer(
106
116
  hostname: string,
107
117
  requestedPort: number,
108
118
  autoPort: boolean,
119
+ serverOptions: DevHttpServerOptions,
109
120
  ): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
110
121
  if (requestedPort === 0) {
111
- const server = createDevHttpServer(capsule);
122
+ const server = createDevHttpServer(capsule, serverOptions);
112
123
  const actualPort = await listen(server, hostname, await findAvailablePort());
113
124
  return { server, actualPort, portConflict: null };
114
125
  }
@@ -117,7 +128,7 @@ async function listenForDevServer(
117
128
  const lastPort = autoPort ? requestedPort + 50 : requestedPort;
118
129
 
119
130
  for (let port = requestedPort; port <= lastPort; port += 1) {
120
- const server = createDevHttpServer(capsule);
131
+ const server = createDevHttpServer(capsule, serverOptions);
121
132
 
122
133
  try {
123
134
  const actualPort = await listen(server, hostname, port);
@@ -142,14 +153,14 @@ async function listenForDevServer(
142
153
  );
143
154
  }
144
155
 
145
- function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
156
+ function createDevHttpServer(capsule: LoadedAgentCapsule, options: DevHttpServerOptions): Server {
146
157
  const channelRuntime: DevChannelRuntimeState = {
147
158
  dedupeKeys: new Set<string>(),
148
159
  buffers: new Map<string, DevChannelBuffer>(),
149
160
  };
150
161
  const capsuleRoot = capsule.root;
151
162
  return createHttpServer((incoming, outgoing) => {
152
- void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
163
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime, options);
153
164
  });
154
165
  }
155
166
 
@@ -158,11 +169,12 @@ async function handleNodeRequest(
158
169
  incoming: IncomingMessage,
159
170
  outgoing: ServerResponse,
160
171
  channelRuntime: DevChannelRuntimeState,
172
+ options: DevHttpServerOptions,
161
173
  ): Promise<void> {
162
174
  try {
163
175
  const capsule = await loadAgentCapsule(capsuleRoot);
164
176
  const request = nodeRequestToFetchRequest(incoming);
165
- const response = await handleDevServerRequest(capsule, request, channelRuntime);
177
+ const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
166
178
  await writeFetchResponse(outgoing, response);
167
179
  } catch (error) {
168
180
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
@@ -222,27 +234,43 @@ async function handleDevServerRequest(
222
234
  capsule: LoadedAgentCapsule,
223
235
  request: Request,
224
236
  channelRuntime: DevChannelRuntimeState,
237
+ options: DevHttpServerOptions,
225
238
  ): Promise<Response> {
226
239
  try {
227
240
  const url = new URL(request.url);
228
241
  const route = normalizePath(url.pathname);
229
242
 
243
+ if (request.method === "GET" && route === "/health") {
244
+ return jsonResponse({ status: "ok", agent: capsule.config.name });
245
+ }
246
+
230
247
  if (request.method === "GET" && route === "/") {
248
+ const auth = await authorizeDevRoute(capsule, request, options, "chat");
249
+ if (auth) {
250
+ return auth;
251
+ }
252
+
231
253
  const env = await loadCapsuleEnv(capsule.root);
232
254
  return htmlResponse(renderChatApp(capsule, url.origin, env));
233
255
  }
234
256
 
235
257
  if (request.method === "GET" && route === "/_agentkit") {
258
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
259
+ if (auth) {
260
+ return auth;
261
+ }
262
+
236
263
  const env = await loadCapsuleEnv(capsule.root);
237
264
  return jsonResponse(buildInspectState(capsule, env));
238
265
  }
239
266
 
240
- if (request.method === "GET" && route === "/health") {
241
- return jsonResponse({ status: "ok", agent: capsule.config.name });
242
- }
243
-
244
267
  if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
245
268
  const body = await readJsonBody(request);
269
+ const auth = await authorizeDevRoute(capsule, request, options, "chat", body);
270
+ if (auth) {
271
+ return auth;
272
+ }
273
+
246
274
  const { message, conversationId } = readChatRequest(body);
247
275
  const result = await runAgentMessage(capsule, {
248
276
  message,
@@ -262,6 +290,11 @@ async function handleDevServerRequest(
262
290
  const toolMatch = route.match(/^\/tools\/([^/]+)$/);
263
291
 
264
292
  if (request.method === "POST" && toolMatch) {
293
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
294
+ if (auth) {
295
+ return auth;
296
+ }
297
+
265
298
  const body = await readJsonBody(request);
266
299
  const result = await runToolFromCwd(capsule.root, {
267
300
  name: decodeURIComponent(toolMatch[1]),
@@ -275,6 +308,11 @@ async function handleDevServerRequest(
275
308
  const fileMatch = route.match(/^\/files\/(.+)$/);
276
309
 
277
310
  if (fileMatch) {
311
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
312
+ if (auth) {
313
+ return auth;
314
+ }
315
+
278
316
  return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
279
317
  }
280
318
 
@@ -289,18 +327,33 @@ async function handleDevServerRequest(
289
327
  }
290
328
 
291
329
  if (request.method === "GET" && route === "/v1/conversations") {
330
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
331
+ if (auth) {
332
+ return auth;
333
+ }
334
+
292
335
  return jsonResponse({ conversations: await listConversations(capsule) });
293
336
  }
294
337
 
295
338
  const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
296
339
 
297
340
  if (request.method === "GET" && conversationMatch) {
341
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
342
+ if (auth) {
343
+ return auth;
344
+ }
345
+
298
346
  return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
299
347
  }
300
348
 
301
349
  const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
302
350
 
303
351
  if (request.method === "GET" && conversationTraceMatch) {
352
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
353
+ if (auth) {
354
+ return auth;
355
+ }
356
+
304
357
  return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
305
358
  }
306
359
 
@@ -310,6 +363,109 @@ async function handleDevServerRequest(
310
363
  }
311
364
  }
312
365
 
366
+ async function authorizeDevRoute(
367
+ capsule: LoadedAgentCapsule,
368
+ request: Request,
369
+ options: DevHttpServerOptions,
370
+ kind: "chat" | "admin",
371
+ body?: unknown,
372
+ ): Promise<Response | null> {
373
+ if (options.access === "local") {
374
+ return null;
375
+ }
376
+
377
+ const env = await loadCapsuleEnv(capsule.root);
378
+
379
+ if (kind === "admin") {
380
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
381
+ missingCode: "deploy_not_ready",
382
+ missingMessage: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set.",
383
+ unauthorizedMessage: "Missing or invalid AgentKit admin token.",
384
+ });
385
+ }
386
+
387
+ if (capsule.config.access.mode === "public") {
388
+ return null;
389
+ }
390
+
391
+ if (capsule.config.access.mode === "private") {
392
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
393
+ missingCode: "deploy_not_ready",
394
+ missingMessage: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set.",
395
+ unauthorizedMessage: "Missing or invalid deploy access token.",
396
+ });
397
+ }
398
+
399
+ return authorizeBearerSecret(request, body, env.AGENTKIT_ACCESS_TOKEN, {
400
+ missingCode: "deploy_not_ready",
401
+ missingMessage: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set.",
402
+ unauthorizedMessage: "Missing or invalid deploy access token.",
403
+ });
404
+ }
405
+
406
+ function authorizeBearerSecret(
407
+ request: Request,
408
+ body: unknown,
409
+ secret: string | undefined,
410
+ messages: { missingCode: string; missingMessage: string; unauthorizedMessage: string },
411
+ ): Response | null {
412
+ if (!secret) {
413
+ return jsonResponse(
414
+ {
415
+ error: {
416
+ code: messages.missingCode,
417
+ message: messages.missingMessage,
418
+ },
419
+ },
420
+ { status: 503 },
421
+ );
422
+ }
423
+
424
+ if (readRequestAccessToken(request, body) !== secret) {
425
+ return jsonResponse(
426
+ {
427
+ error: {
428
+ code: "unauthorized",
429
+ message: messages.unauthorizedMessage,
430
+ },
431
+ },
432
+ { status: 401 },
433
+ );
434
+ }
435
+
436
+ return null;
437
+ }
438
+
439
+ function readRequestAccessToken(request: Request, body: unknown): string {
440
+ const authorization = request.headers.get("authorization");
441
+
442
+ if (authorization?.startsWith("Bearer ")) {
443
+ return authorization.slice("Bearer ".length).trim();
444
+ }
445
+
446
+ const header =
447
+ request.headers.get("x-agentkit-access-token") ??
448
+ request.headers.get("x-agentkit-deploy-access-token") ??
449
+ "";
450
+ const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
451
+ if (token.trim()) {
452
+ return token.trim();
453
+ }
454
+
455
+ if (body && typeof body === "object") {
456
+ const record = body as { access_token?: unknown; accessToken?: unknown };
457
+ const bodyToken =
458
+ typeof record.access_token === "string"
459
+ ? record.access_token
460
+ : typeof record.accessToken === "string"
461
+ ? record.accessToken
462
+ : "";
463
+ return bodyToken.trim();
464
+ }
465
+
466
+ return "";
467
+ }
468
+
313
469
  async function handlePortableChannel(
314
470
  capsule: LoadedAgentCapsule,
315
471
  request: Request,
@@ -360,7 +516,9 @@ async function handlePortableChannel(
360
516
 
361
517
  channelRuntime.dedupeKeys.add(event.dedupeKey);
362
518
 
363
- if (event.kind !== "message" || !event.message) {
519
+ const processedMessage = await processDevChannelMessage(capsule, adapter, channel, event.message, secrets);
520
+
521
+ if (event.kind !== "message" || !processedMessage) {
364
522
  deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
365
523
  continue;
366
524
  }
@@ -376,7 +534,7 @@ async function handlePortableChannel(
376
534
  bufferDevChannelMessage(capsule, channelRuntime, {
377
535
  bufferKey: `${channel.name}:${event.externalIdentity.key}`,
378
536
  conversationId,
379
- content: event.message.content,
537
+ content: processedMessage.content,
380
538
  receivedAt: event.receivedAt,
381
539
  buffer,
382
540
  });
@@ -390,7 +548,7 @@ async function handlePortableChannel(
390
548
  }
391
549
 
392
550
  const result = await runAgentMessage(capsule, {
393
- message: event.message.content,
551
+ message: processedMessage.content,
394
552
  conversationId,
395
553
  signal: request.signal,
396
554
  });
@@ -405,6 +563,71 @@ async function handlePortableChannel(
405
563
  return jsonResponse({ deliveries });
406
564
  }
407
565
 
566
+ async function processDevChannelMessage(
567
+ capsule: LoadedAgentCapsule,
568
+ adapter: ChannelAdapter,
569
+ channel: AgentChannel,
570
+ message: NormalizedChannelMessage | undefined,
571
+ secrets: Record<string, string>,
572
+ ): Promise<NormalizedChannelMessage | null> {
573
+ if (!message) {
574
+ return null;
575
+ }
576
+
577
+ if (message.contentType === "text" && message.content) {
578
+ return message;
579
+ }
580
+
581
+ if (message.contentType !== "audio" || !message.audio || channel.audio?.mode !== "transcribe") {
582
+ return null;
583
+ }
584
+
585
+ if (!adapter.downloadMedia) {
586
+ return null;
587
+ }
588
+
589
+ const env = await loadCapsuleEnv(capsule.root);
590
+ const transcriptionConfig = resolveTranscriptionConfig(capsule.config.transcription);
591
+ const secretName = transcriptionConfig.secret;
592
+ const download = await adapter.downloadMedia({
593
+ type: channel.type,
594
+ provider: channel.provider,
595
+ channelId: channel.name,
596
+ message,
597
+ secrets: {
598
+ ...secrets,
599
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
600
+ ...(env.AGENTKIT_TELEGRAM_API_BASE_URL ? { AGENTKIT_TELEGRAM_API_BASE_URL: env.AGENTKIT_TELEGRAM_API_BASE_URL } : {}),
601
+ },
602
+ });
603
+
604
+ if (!download.ok) {
605
+ return null;
606
+ }
607
+
608
+ const transcription = await transcribeAudio({
609
+ config: transcriptionConfig,
610
+ secrets: {
611
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
612
+ },
613
+ audio: download.audio,
614
+ filename: download.filename,
615
+ ...(download.mimeType ? { mimeType: download.mimeType } : {}),
616
+ ...(download.durationSeconds !== undefined ? { durationSeconds: download.durationSeconds } : {}),
617
+ });
618
+
619
+ if (!transcription.ok) {
620
+ return null;
621
+ }
622
+
623
+ return {
624
+ role: "user",
625
+ contentType: "text",
626
+ content: `The user sent an audio message.\n\nTranscript:\n${transcription.text}`,
627
+ providerMessageId: message.providerMessageId,
628
+ };
629
+ }
630
+
408
631
  function bufferDevChannelMessage(
409
632
  capsule: LoadedAgentCapsule,
410
633
  runtime: DevChannelRuntimeState,
@@ -509,6 +732,10 @@ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
509
732
  }
510
733
 
511
734
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
735
+ if (capsule.config.storage.files?.driver !== "r2") {
736
+ throw new AgentKitError("files_not_configured", "File storage is not configured for this Agent Capsule.");
737
+ }
738
+
512
739
  const key = normalizeFileKey(rawKey);
513
740
  const root = resolve(capsule.root, ".agentkit/files");
514
741
  const path = resolve(root, key);
@@ -519,9 +746,7 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
519
746
 
520
747
  if (request.method === "PUT") {
521
748
  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;
749
+ const maxUploadBytes = capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024;
525
750
 
526
751
  if (bytes.byteLength > maxUploadBytes) {
527
752
  throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
@@ -715,6 +940,7 @@ function statusForError(error: unknown): number {
715
940
  error.code === "conversation_not_found" ||
716
941
  error.code === "not_found" ||
717
942
  error.code === "tool_not_found" ||
943
+ error.code === "files_not_configured" ||
718
944
  error.code === "file_not_found" ||
719
945
  error.code === "channel_not_found"
720
946
  ) {
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { chmod, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { AgentKitError } from "./errors";
@@ -83,7 +83,7 @@ export async function setCapsuleEnv(root: string, name: string, value: string):
83
83
  }
84
84
  }
85
85
 
86
- await writeFile(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
86
+ await writeSecureDotEnv(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
87
87
  }
88
88
 
89
89
  export async function unsetCapsuleEnv(root: string, name: string): Promise<boolean> {
@@ -103,10 +103,15 @@ export async function unsetCapsuleEnv(root: string, name: string): Promise<boole
103
103
  return false;
104
104
  }
105
105
 
106
- await writeFile(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
106
+ await writeSecureDotEnv(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
107
107
  return true;
108
108
  }
109
109
 
110
+ async function writeSecureDotEnv(path: string, source: string): Promise<void> {
111
+ await writeFile(path, source, { mode: 0o600 });
112
+ await chmod(path, 0o600);
113
+ }
114
+
110
115
  async function readDotEnvSource(path: string): Promise<string> {
111
116
  try {
112
117
  return await readFile(path, "utf8");