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

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 (39) hide show
  1. package/README.md +8 -0
  2. package/docs/guides/add-channel.md +4 -2
  3. package/docs/guides/add-managed-composio.md +161 -0
  4. package/docs/guides/channel-security.md +36 -39
  5. package/docs/guides/connect-telegram.md +3 -1
  6. package/docs/guides/debug-channel.md +141 -0
  7. package/docs/guides/prepare-deploy.md +7 -10
  8. package/docs/llms-full.txt +19 -5
  9. package/docs/llms.txt +11 -2
  10. package/package.json +1 -3
  11. package/src/cli/cloud-client.ts +12 -0
  12. package/src/cli/commands/channels.ts +119 -7
  13. package/src/cli/commands/transcribe.ts +171 -0
  14. package/src/cli/deploy-chat-ui.ts +146 -3
  15. package/src/cli/deploy-readiness.ts +112 -3
  16. package/src/cli/help.ts +19 -4
  17. package/src/cli/index.ts +198 -3
  18. package/src/create-project.ts +4 -32
  19. package/src/index.ts +149 -0
  20. package/src/runtime/chat.ts +2 -1
  21. package/src/runtime/config.ts +133 -0
  22. package/src/runtime/core/manifest.ts +31 -2
  23. package/src/runtime/deploy-readiness.ts +31 -1
  24. package/src/runtime/inspect.ts +109 -4
  25. package/src/runtime/integrations/composio.ts +423 -0
  26. package/src/runtime/skills.ts +95 -0
  27. package/src/runtime/targets/cloudflare/build.ts +395 -10
  28. package/src/runtime/tool-runner.ts +2 -1
  29. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -0
  30. package/src/templates/skills/agentkit-channels/SKILL.md +2 -0
  31. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +3 -1
  32. package/src/templates/skills/agentkit-channels/references/telegram.md +3 -1
  33. package/src/templates/skills/agentkit-deploy/SKILL.md +1 -1
  34. package/src/templates/skills/agentkit-integrations/SKILL.md +75 -0
  35. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +3 -2
  36. package/docs/guides/agentkit-skills-architecture.md +0 -472
  37. package/docs/guides/channels-implementation-map.md +0 -243
  38. package/docs/guides/channels-production-handoff.md +0 -118
  39. package/docs/portable-deploy-release-checklist.md +0 -41
package/docs/llms.txt CHANGED
@@ -16,13 +16,12 @@ Task guides:
16
16
  - Build container artifact: `agentkit build --target container`
17
17
  - Generate VPS handoff: `agentkit deploy --target vps --host agent.example.com --dry-run`
18
18
  - Add hosted channels: `docs/guides/add-channel.md`
19
+ - Add AgentKit-managed Composio: `docs/guides/add-managed-composio.md`
19
20
  - Buffer rapid channel messages: `docs/guides/add-channel.md#buffer-bursty-messages`
20
21
  - Connect Telegram: `docs/guides/connect-telegram.md`
21
22
  - Connect WhatsApp through Zapster: `docs/guides/connect-whatsapp-zapster.md`
22
23
  - Follow channel webhook and delivery-log safety rules: `docs/guides/channel-security.md`
23
- - Prepare Channels for production: `docs/guides/channels-production-handoff.md`
24
24
  - Follow secret, access, and tool safety rules: `docs/guides/security-rules.md`
25
- - Plan repo-local skills for coding agents: `docs/guides/agentkit-skills-architecture.md`
26
25
 
27
26
  Current local commands:
28
27
 
@@ -43,12 +42,19 @@ agentkit knowledge search <query> [--top-k <number>]
43
42
  agentkit eval run
44
43
  agentkit conversations list
45
44
  agentkit conversations show <conversation-id>
45
+ agentkit conversations trace <conversation-id> [--deploy]
46
46
  agentkit channels list
47
47
  agentkit channels add telegram support-telegram
48
48
  agentkit channels setup support-telegram
49
49
  agentkit channels status support-telegram
50
50
  agentkit channels test support-telegram --message "hello"
51
+ agentkit channels test-audio support-telegram --fixture voice-note
52
+ agentkit transcribe smoke --provider groq
51
53
  agentkit channels deliveries list support-telegram
54
+ agentkit integrations status [--toolkit googlecalendar]
55
+ agentkit integrations connect composio --toolkit gmail
56
+ agentkit skills status
57
+ agentkit skills sync
52
58
  agentkit login --token agk_user_...
53
59
  agentkit deploy doctor
54
60
  agentkit deploy --smoke "hello"
@@ -69,6 +75,8 @@ Generated capsules include `AGENTKIT.md`, `AGENTS.md`, and a repo-local `skills/
69
75
 
70
76
  UI testing is part of the handoff. For local UI testing, run `agentkit dev`, open the printed `Chat:` URL, and tell the owner the exact URL. After hosted deploy, run `agentkit chat-ui --deploy`, open the printed `Chat:` URL, and tell the owner it is connected to the deploy.
71
77
 
78
+ Hosted Chat UI shows the current conversation id, tool calls, tool errors, and a new-conversation control. Use `agentkit conversations trace <conversation-id> --deploy` to pull the hosted trace from the last deploy.
79
+
72
80
  `test/fake` is deterministic and validates scaffold, direct tool calls, and fake-provider evals. It does not validate natural conversation quality. Before claiming real conversation behavior has been tested, ask the owner which provider to use: OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
73
81
 
74
82
  AgentKit injects the current ISO timestamp, local date, weekday, local date/time, and timezone dynamically into every chat run. Set `timeZone` in `agentkit.config.ts` for scheduling agents so "today", "tomorrow", and weekdays resolve in the business/user timezone; otherwise AgentKit falls back to `AGENTKIT_TIME_ZONE`, valid `TZ`, then the runtime default. Do not hardcode today's date in prompts.
@@ -80,6 +88,7 @@ GET /_agentkit
80
88
  POST /v1/chat
81
89
  GET /v1/conversations
82
90
  GET /v1/conversations/:id
91
+ GET /v1/conversations/:id/trace
83
92
  ```
84
93
 
85
94
  Local `.env` is for development only. Use `.env.schema` as the committed secret-name contract; local AgentKit commands load `.env` directly. Hosted alpha deploys require `cloudflare_deploy_alpha` through `agentkit login --token ...`; production uses managed secrets in the hosted contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andreprado/agentkit",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -18,8 +18,6 @@
18
18
  "!src/**/*.test.ts",
19
19
  "!src/runtime/fixtures",
20
20
  "docs",
21
- "!docs/guides/backend-contracts.md",
22
- "!docs/guides/debug-channel.md",
23
21
  "README.md"
24
22
  ],
25
23
  "publishConfig": {
@@ -39,6 +39,18 @@ export type CloudLimitsResponse = {
39
39
  };
40
40
  };
41
41
 
42
+ export type CloudManagedComposioResponse = {
43
+ managed_composio?: {
44
+ entitlement?: "active" | "missing" | string;
45
+ api_key?: "set" | "missing" | string;
46
+ toolkits?: Array<{
47
+ toolkit?: string;
48
+ auth_config_env?: string;
49
+ auth_config?: "set" | "missing" | string;
50
+ }>;
51
+ };
52
+ };
53
+
42
54
  export type CloudProjectResolveResponse = {
43
55
  project?: {
44
56
  id?: string;
@@ -162,8 +162,10 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
162
162
  let channel = created.channel;
163
163
  printChannelStatus(channel);
164
164
  printSecretRepairHint(channel);
165
+ console.log("Channel resource: created_or_updated");
165
166
 
166
167
  if (hasMissingRequiredSecrets(channel)) {
168
+ console.log("Secrets: missing");
167
169
  console.log("");
168
170
  console.log("Connect blocked: set the missing managed secrets, then rerun this same command.");
169
171
  console.log("Run: npm run agentkit -- secret sync --from-local");
@@ -171,31 +173,40 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
171
173
  return;
172
174
  }
173
175
 
176
+ console.log("Secrets: validated");
177
+
174
178
  if (channel.type === "telegram") {
175
179
  const setup = await applyTelegramWebhook(channel, args.flags.api);
176
180
  channel = setup.channel ?? channel;
181
+ console.log("Telegram setWebhook: OK");
177
182
  console.log("Telegram webhook confirmed.");
178
183
  if (setup.setup?.get_webhook_info) {
179
184
  console.log(`Telegram pending updates: ${setup.setup.get_webhook_info.pending_update_count}`);
180
185
  }
181
186
 
182
- const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
183
- console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
187
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
184
188
  printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
189
+ if (!smoke) {
190
+ process.exitCode = 1;
191
+ }
185
192
  return;
186
193
  }
187
194
 
188
195
  if (channel.type === "whatsapp" && channel.provider === "zapster") {
189
- const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
190
- console.log(`Webhook smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
196
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
191
197
  console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
198
+ if (!smoke) {
199
+ process.exitCode = 1;
200
+ }
192
201
  } else if (channel.type === "whatsapp") {
193
202
  console.log("Smoke: skipped until the provider webhook is configured.");
194
203
  console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
195
204
  } else {
196
- const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
197
- console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
205
+ const smoke = await runConnectSmoke(() => runChannelSmoke(channel, args.flags.api, args.flags.message));
198
206
  console.log(`Next human step: use ${channel.webhook_url} from your website client.`);
207
+ if (!smoke) {
208
+ process.exitCode = 1;
209
+ }
199
210
  }
200
211
  return;
201
212
  }
@@ -274,6 +285,25 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
274
285
  return;
275
286
  }
276
287
 
288
+ if (subcommand === "test-audio") {
289
+ if (!first) {
290
+ throw new Error("Usage: agentkit channels test-audio <name> [--fixture voice-note|audio-file|<path>]");
291
+ }
292
+
293
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
294
+ const apiUrl = parseCloudApiUrl(args.flags.api);
295
+ const body = await channelAudioTestBody(channel, args.flags.fixture);
296
+ const signed = await runServerSignedChannelTest(channel, apiUrl, body);
297
+ const response = signed.response;
298
+ const payload = signed.payload;
299
+
300
+ console.log(JSON.stringify(payload, null, 2));
301
+ if (!response.ok) {
302
+ process.exitCode = 1;
303
+ }
304
+ return;
305
+ }
306
+
277
307
  if (subcommand === "deliveries") {
278
308
  if (first === "list") {
279
309
  if (!second) {
@@ -367,7 +397,7 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
367
397
  }
368
398
 
369
399
  throw new Error(
370
- "Usage: agentkit channels list | add | connect | setup | status | doctor | test | deliveries list | deliveries show | buffers list | buffers show | buffers flush | buffers clear | buffers retry",
400
+ "Usage: agentkit channels list | add | connect | setup | status | doctor | test | test-audio | deliveries list | deliveries show | buffers list | buffers show | buffers flush | buffers clear | buffers retry",
371
401
  );
372
402
  }
373
403
 
@@ -419,6 +449,20 @@ async function runChannelSmoke(
419
449
  return { status: "unknown" };
420
450
  }
421
451
 
452
+ async function runConnectSmoke(
453
+ run: () => Promise<{ status: string; delivery_id?: string }>,
454
+ ): Promise<{ status: string; delivery_id?: string } | null> {
455
+ try {
456
+ const smoke = await run();
457
+ console.log(`Synthetic smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
458
+ return smoke;
459
+ } catch (error) {
460
+ console.log("Synthetic smoke: failed");
461
+ console.log(` ${formatCommandError(error)}`);
462
+ return null;
463
+ }
464
+ }
465
+
422
466
  async function runServerSignedChannelTest(
423
467
  channel: CloudChannel,
424
468
  apiUrl: string,
@@ -601,6 +645,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
601
645
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
602
646
  }
603
647
 
648
+ function formatCommandError(error: unknown): string {
649
+ return error instanceof Error ? error.message : String(error);
650
+ }
651
+
604
652
  async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
605
653
  return readLocalDeployStateIfExists(cwd);
606
654
  }
@@ -974,6 +1022,70 @@ async function channelTestBody(
974
1022
  });
975
1023
  }
976
1024
 
1025
+ async function channelAudioTestBody(
1026
+ channel: CloudChannel,
1027
+ fixtureFlag: string | boolean | undefined,
1028
+ ): Promise<string> {
1029
+ if (typeof fixtureFlag === "string" && isExplicitFixturePath(fixtureFlag)) {
1030
+ return readFile(resolve(process.cwd(), fixtureFlag), "utf8");
1031
+ }
1032
+
1033
+ const fixture = typeof fixtureFlag === "string" ? fixtureFlag : "voice-note";
1034
+
1035
+ if (channel.type === "telegram") {
1036
+ if (fixture !== "voice-note" && fixture !== "telegram-voice-note" && fixture !== "telegram-audio-message") {
1037
+ throw new Error('Telegram audio smoke supports --fixture voice-note or --fixture telegram-voice-note.');
1038
+ }
1039
+
1040
+ return JSON.stringify({
1041
+ update_id: Date.now(),
1042
+ message: {
1043
+ message_id: 1,
1044
+ date: Math.floor(Date.now() / 1000),
1045
+ chat: { id: 456, type: "private" },
1046
+ from: { id: 123, first_name: "AgentKit" },
1047
+ voice: {
1048
+ file_id: `agentkit_voice_${Date.now()}`,
1049
+ file_unique_id: `agentkit_voice_unique_${Date.now()}`,
1050
+ duration: 12,
1051
+ mime_type: "audio/ogg",
1052
+ file_size: 42,
1053
+ },
1054
+ },
1055
+ });
1056
+ }
1057
+
1058
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
1059
+ if (fixture !== "voice-note" && fixture !== "audio-file" && fixture !== "zapster-audio" && fixture !== "zapster-audio-message") {
1060
+ throw new Error('Zapster audio smoke supports --fixture voice-note, --fixture audio-file, or --fixture zapster-audio.');
1061
+ }
1062
+
1063
+ return JSON.stringify({
1064
+ event_id: `zap_evt_audio_${Date.now()}`,
1065
+ instance_id: "local",
1066
+ message: {
1067
+ id: `wamid.audio.${Date.now()}`,
1068
+ from: "5511999999999",
1069
+ type: "audio",
1070
+ audio: {
1071
+ id: `media_audio_${Date.now()}`,
1072
+ url: `https://api.zapsterapi.com/media/audio/media_audio_${Date.now()}.ogg`,
1073
+ mime_type: "audio/ogg",
1074
+ duration: 12,
1075
+ file_size: 42,
1076
+ ...(fixture === "voice-note" ? { ptt: "true" } : {}),
1077
+ },
1078
+ },
1079
+ });
1080
+ }
1081
+
1082
+ throw new Error(`Audio smoke is not supported for ${channel.type}/${channel.provider}.`);
1083
+ }
1084
+
1085
+ function isExplicitFixturePath(value: string): boolean {
1086
+ return value.includes("/") || value.startsWith(".") || value.endsWith(".json");
1087
+ }
1088
+
977
1089
  function channelTestHeaders(channel: CloudChannel): Headers {
978
1090
  const headers = new Headers({ "Content-Type": "application/json" });
979
1091
 
@@ -0,0 +1,171 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, extname, resolve } from "node:path";
3
+
4
+ import type { TranscriptionProviderName } from "../../index";
5
+ import { findAgentCapsuleRoot, loadAgentCapsule } from "../../runtime/config";
6
+ import { loadCapsuleEnv } from "../../runtime/env";
7
+ import {
8
+ defaultTranscriptionSecret,
9
+ resolveTranscriptionConfig,
10
+ transcribeAudio,
11
+ type ResolvedTranscriptionConfig,
12
+ } from "../../runtime/transcription";
13
+ import type { ParsedArgs } from "../args";
14
+
15
+ type SmokeFixture = {
16
+ audio: Uint8Array;
17
+ filename: string;
18
+ mimeType: string;
19
+ source: string;
20
+ };
21
+
22
+ const defaultModels: Record<TranscriptionProviderName, string> = {
23
+ test: "fake",
24
+ openai: "gpt-4o-mini-transcribe",
25
+ groq: "whisper-large-v3-turbo",
26
+ };
27
+
28
+ // Short macOS-generated spoken "hello" sample encoded as M4A. M4A is supported by OpenAI and Groq.
29
+ const builtinSmokeM4aBase64 =
30
+ "AAAAHGZ0eXBNNEEgAAAAAE00QSBtcDQyaXNvbQAAA3ptb292AAAAbG12aGQAAAAA5kTZseZE2bEAAFYiAAA4AAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACDHRyYWsAAABcdGtoZAAAAAfmRNmx5kTZsQAAAAEAAAAAAAA4AAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAahtZGlhAAAAIG1kaGQAAAAA5kTZseZE2bEAAFYiAAA4AAAAAAAAAAAiaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAAAAAAABXm1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABInN0YmwAAAB2c3RzZAAAAAAAAAABAAAAZm1wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAABWIgAAAAAAM2VzZHMAAAAAA4CAgCIAAAAEgICAFEAUABgAAAAU+AAAfQAFgICAAhOIBoCAgAECAAAAD3NidGQAAAAASTE2AAAAGHN0dHMAAAAAAAAAAQAAAA4AAAQAAAAAKHN0c2MAAAAAAAAAAgAAAAEAAAALAAAAAQAAAAIAAAADAAAAAQAAAExzdHN6AAAAAAAAAAAAAAAOAAAABAAAAbkAAADiAAAA0wAAAL8AAADLAAAAzAAAAJMAAACmAAAA5wAAALcAAACnAAAAtgAAADoAAAAYc3RjbwAAAAAAAAACAAAQAAAAGJ8AAAD6dWR0YQAAAPJtZXRhAAAAAAAAACJoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAADEaWxzdAAAALwtLS0tAAAAHG1lYW4AAAAAY29tLmFwcGxlLmlUdW5lcwAAABRuYW1lAAAAAGlUdW5TTVBCAAAAhGRhdGEAAAABAAAAACAwMDAwMDAwMCAwMDAwMDg0MCAwMDAwMDEwOCAwMDAwMDAwMDAwMDAyRUI4IDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwIDAwMDAwMDAwAAAMYmZyZWUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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";
31
+
32
+ export async function handleTranscribeCommand(args: ParsedArgs): Promise<void> {
33
+ const [subcommand] = args.positional;
34
+
35
+ if (subcommand !== "smoke") {
36
+ throw new Error("Usage: agentkit transcribe smoke [--provider test|openai|groq] [--model <model>] [--fixture <audio-file>]");
37
+ }
38
+
39
+ const root = await findAgentCapsuleRoot(process.cwd());
40
+ const capsule = await loadAgentCapsule(root);
41
+ const env = compactEnv(await loadCapsuleEnv(root, process.env));
42
+ const configured = resolveTranscriptionConfig(capsule.config.transcription);
43
+ const provider = parseProviderFlag(args.flags.provider, configured);
44
+ const model = typeof args.flags.model === "string" ? args.flags.model : configured.enabled && configured.provider === provider ? configured.model : defaultModels[provider];
45
+ const secret = typeof args.flags.secret === "string"
46
+ ? args.flags.secret
47
+ : configured.enabled && configured.provider === provider
48
+ ? configured.secret
49
+ : defaultTranscriptionSecret(provider);
50
+ const fixture = await loadSmokeFixture(provider, args.flags.fixture);
51
+
52
+ const config: ResolvedTranscriptionConfig = {
53
+ enabled: true,
54
+ provider,
55
+ model,
56
+ secret,
57
+ language: typeof args.flags.language === "string" ? args.flags.language : configured.enabled ? configured.language : null,
58
+ prompt: typeof args.flags.prompt === "string" ? args.flags.prompt : configured.enabled ? configured.prompt : null,
59
+ limits: configured.enabled ? configured.limits : {},
60
+ rawAudioTtlSeconds: configured.enabled ? configured.rawAudioTtlSeconds : 3600,
61
+ };
62
+ const result = await transcribeAudio({
63
+ config,
64
+ secrets: env,
65
+ audio: fixture.audio,
66
+ filename: fixture.filename,
67
+ mimeType: fixture.mimeType,
68
+ });
69
+
70
+ console.log("Transcription smoke");
71
+ console.log(`Provider: ${provider}`);
72
+ console.log(`Model: ${model}`);
73
+ console.log(`Fixture: ${fixture.source} (${fixture.mimeType})`);
74
+
75
+ if (result.ok) {
76
+ console.log("Status: ok");
77
+ console.log(`Transcript: ${JSON.stringify(result.text)}`);
78
+ return;
79
+ }
80
+
81
+ console.log("Status: failed");
82
+ console.log(`Code: ${result.code}`);
83
+ console.log(`Message: ${result.message}`);
84
+ process.exitCode = 1;
85
+ }
86
+
87
+ function parseProviderFlag(
88
+ value: string | boolean | undefined,
89
+ configured: ResolvedTranscriptionConfig,
90
+ ): TranscriptionProviderName {
91
+ if (value === undefined || value === false) {
92
+ return configured.enabled ? configured.provider : "groq";
93
+ }
94
+
95
+ if (value === "test" || value === "openai" || value === "groq") {
96
+ return value;
97
+ }
98
+
99
+ throw new Error("Usage: agentkit transcribe smoke [--provider test|openai|groq] [--model <model>] [--fixture <audio-file>]");
100
+ }
101
+
102
+ function compactEnv(env: Record<string, string | undefined>): Record<string, string> {
103
+ const compact: Record<string, string> = {};
104
+
105
+ for (const [name, value] of Object.entries(env)) {
106
+ if (value !== undefined) {
107
+ compact[name] = value;
108
+ }
109
+ }
110
+
111
+ return compact;
112
+ }
113
+
114
+ async function loadSmokeFixture(
115
+ provider: TranscriptionProviderName,
116
+ fixtureFlag: string | boolean | undefined,
117
+ ): Promise<SmokeFixture> {
118
+ if (typeof fixtureFlag === "string") {
119
+ const filename = resolve(process.cwd(), fixtureFlag);
120
+ return {
121
+ audio: new Uint8Array(await readFile(filename)),
122
+ filename: basename(filename),
123
+ mimeType: mimeTypeForFilename(filename),
124
+ source: fixtureFlag,
125
+ };
126
+ }
127
+
128
+ if (provider === "test") {
129
+ return {
130
+ audio: new TextEncoder().encode("hello"),
131
+ filename: "agentkit-smoke.ogg",
132
+ mimeType: "audio/ogg",
133
+ source: "built-in text fixture",
134
+ };
135
+ }
136
+
137
+ return {
138
+ audio: new Uint8Array(Buffer.from(builtinSmokeM4aBase64, "base64")),
139
+ filename: "agentkit-transcribe-smoke.m4a",
140
+ mimeType: "audio/m4a",
141
+ source: "built-in spoken hello",
142
+ };
143
+ }
144
+
145
+ function mimeTypeForFilename(filename: string): string {
146
+ const extension = extname(filename).toLowerCase();
147
+
148
+ if (extension === ".ogg" || extension === ".opus") {
149
+ return "audio/ogg";
150
+ }
151
+ if (extension === ".mp3" || extension === ".mpeg" || extension === ".mpga") {
152
+ return "audio/mpeg";
153
+ }
154
+ if (extension === ".m4a") {
155
+ return "audio/m4a";
156
+ }
157
+ if (extension === ".mp4") {
158
+ return "audio/mp4";
159
+ }
160
+ if (extension === ".wav") {
161
+ return "audio/wav";
162
+ }
163
+ if (extension === ".webm") {
164
+ return "audio/webm";
165
+ }
166
+ if (extension === ".flac") {
167
+ return "audio/flac";
168
+ }
169
+
170
+ return "application/octet-stream";
171
+ }
@@ -70,6 +70,27 @@ export async function startDeployChatUiServer(
70
70
  return;
71
71
  }
72
72
 
73
+ if (
74
+ request.method === "GET" &&
75
+ (requestUrl.pathname === "/v1/conversations" ||
76
+ /^\/v1\/conversations\/[^/]+(?:\/trace)?$/.test(requestUrl.pathname))
77
+ ) {
78
+ const token = await readDeployAccessTokenForUi(tokenPath);
79
+ const upstream = await fetch(new URL(requestUrl.pathname, deployUrl).href, {
80
+ method: "GET",
81
+ headers: {
82
+ Authorization: `Bearer ${token}`,
83
+ },
84
+ });
85
+ const text = await upstream.text();
86
+
87
+ response.writeHead(upstream.status, {
88
+ "content-type": upstream.headers.get("content-type") ?? "application/json",
89
+ });
90
+ response.end(text);
91
+ return;
92
+ }
93
+
73
94
  sendJson(response, 404, { error: { code: "not_found", message: "Route not found." } });
74
95
  } catch (error) {
75
96
  sendJson(response, 500, {
@@ -196,6 +217,8 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
196
217
  header {
197
218
  border-bottom: 1px solid var(--line);
198
219
  padding: 18px 22px;
220
+ display: grid;
221
+ gap: 10px;
199
222
  }
200
223
 
201
224
  h1 {
@@ -204,12 +227,27 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
204
227
  line-height: 1.2;
205
228
  }
206
229
 
230
+ .topline {
231
+ display: flex;
232
+ align-items: start;
233
+ justify-content: space-between;
234
+ gap: 12px;
235
+ }
236
+
207
237
  code {
208
238
  color: var(--muted);
209
239
  font-size: 12px;
210
240
  overflow-wrap: anywhere;
211
241
  }
212
242
 
243
+ .meta {
244
+ display: flex;
245
+ flex-wrap: wrap;
246
+ gap: 8px 14px;
247
+ color: var(--muted);
248
+ font-size: 12px;
249
+ }
250
+
213
251
  #messages {
214
252
  padding: 22px;
215
253
  overflow: auto;
@@ -255,6 +293,39 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
255
293
  background: #fff6f6;
256
294
  }
257
295
 
296
+ #trace {
297
+ border-top: 1px solid var(--line);
298
+ padding: 12px 22px;
299
+ display: grid;
300
+ gap: 8px;
301
+ background: #fbfbfa;
302
+ }
303
+
304
+ .tool-call {
305
+ border: 1px solid var(--line);
306
+ border-radius: 8px;
307
+ padding: 10px 12px;
308
+ display: grid;
309
+ gap: 6px;
310
+ font-size: 12px;
311
+ }
312
+
313
+ .tool-head {
314
+ display: flex;
315
+ flex-wrap: wrap;
316
+ gap: 8px;
317
+ align-items: center;
318
+ color: var(--muted);
319
+ font-weight: 700;
320
+ }
321
+
322
+ .tool-call pre {
323
+ margin: 0;
324
+ max-height: 180px;
325
+ overflow: auto;
326
+ white-space: pre-wrap;
327
+ }
328
+
258
329
  form {
259
330
  border-top: 1px solid var(--line);
260
331
  padding: 14px;
@@ -286,6 +357,13 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
286
357
  cursor: pointer;
287
358
  }
288
359
 
360
+ button.secondary {
361
+ min-height: 34px;
362
+ color: var(--ink);
363
+ background: #eef1ee;
364
+ border: 1px solid var(--line);
365
+ }
366
+
289
367
  button:disabled {
290
368
  cursor: wait;
291
369
  opacity: 0.65;
@@ -305,10 +383,20 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
305
383
  <body>
306
384
  <main>
307
385
  <header>
308
- <h1>AgentKit Hosted Chat</h1>
309
- <code>${deployUrl}</code>
386
+ <div class="topline">
387
+ <div>
388
+ <h1>AgentKit Hosted Chat</h1>
389
+ <code>${deployUrl}</code>
390
+ </div>
391
+ <button class="secondary" id="new-conversation" type="button">New conversation</button>
392
+ </div>
393
+ <div class="meta">
394
+ <span>Conversation: <code id="conversation-id">none</code></span>
395
+ <span>Tool calls: <code id="tool-count">0</code></span>
396
+ </div>
310
397
  </header>
311
398
  <section id="messages" aria-live="polite"></section>
399
+ <section id="trace" aria-live="polite"></section>
312
400
  <form id="form">
313
401
  <textarea id="message" name="message" placeholder="Type a message" required autofocus></textarea>
314
402
  <button id="send" type="submit">Send</button>
@@ -319,6 +407,10 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
319
407
  const textarea = document.querySelector("#message");
320
408
  const send = document.querySelector("#send");
321
409
  const messages = document.querySelector("#messages");
410
+ const trace = document.querySelector("#trace");
411
+ const conversationLabel = document.querySelector("#conversation-id");
412
+ const toolCount = document.querySelector("#tool-count");
413
+ const newConversation = document.querySelector("#new-conversation");
322
414
  let conversationId = null;
323
415
 
324
416
  function addMessage(role, content, className = "") {
@@ -335,6 +427,55 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
335
427
  messages.scrollTop = messages.scrollHeight;
336
428
  }
337
429
 
430
+ function setConversationId(value) {
431
+ conversationId = value || null;
432
+ conversationLabel.textContent = conversationId || "none";
433
+ }
434
+
435
+ function renderTrace(payload) {
436
+ const calls = [];
437
+ for (const run of payload.runs || []) {
438
+ for (const call of run.toolCalls || []) {
439
+ calls.push(call);
440
+ }
441
+ }
442
+
443
+ toolCount.textContent = String(calls.length);
444
+ trace.innerHTML = "";
445
+
446
+ for (const call of calls.slice(-8)) {
447
+ const item = document.createElement("article");
448
+ item.className = "tool-call";
449
+ const head = document.createElement("div");
450
+ head.className = "tool-head";
451
+ head.textContent = [call.name, call.status, call.errorMessage].filter(Boolean).join(" · ");
452
+ const input = document.createElement("pre");
453
+ input.textContent = JSON.stringify(call.input || {}, null, 2);
454
+ item.append(head, input);
455
+ trace.append(item);
456
+ }
457
+ }
458
+
459
+ async function refreshTrace() {
460
+ if (!conversationId) {
461
+ toolCount.textContent = "0";
462
+ trace.innerHTML = "";
463
+ return;
464
+ }
465
+
466
+ const response = await fetch("/v1/conversations/" + encodeURIComponent(conversationId) + "/trace");
467
+ const payload = await response.json();
468
+ if (response.ok) renderTrace(payload);
469
+ }
470
+
471
+ newConversation.addEventListener("click", () => {
472
+ setConversationId(null);
473
+ messages.innerHTML = "";
474
+ trace.innerHTML = "";
475
+ toolCount.textContent = "0";
476
+ textarea.focus();
477
+ });
478
+
338
479
  textarea.addEventListener("keydown", (event) => {
339
480
  if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
340
481
  event.preventDefault();
@@ -363,10 +504,12 @@ function renderDeployChatUi(input: { deployUrl: string }): string {
363
504
  throw new Error(payload.error?.message || "Request failed");
364
505
  }
365
506
 
366
- conversationId = payload.conversationId || conversationId;
507
+ setConversationId(payload.conversationId || conversationId);
367
508
  addMessage("assistant", payload.message?.content || "");
509
+ await refreshTrace();
368
510
  } catch (error) {
369
511
  addMessage("error", error instanceof Error ? error.message : String(error), "error");
512
+ await refreshTrace().catch(() => {});
370
513
  } finally {
371
514
  send.disabled = false;
372
515
  textarea.focus();