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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +67 -6
  2. package/docs/guides/add-channel.md +118 -7
  3. package/docs/guides/add-knowledge.md +144 -0
  4. package/docs/guides/add-managed-composio.md +163 -0
  5. package/docs/guides/add-tool.md +1 -1
  6. package/docs/guides/channel-security.md +97 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +78 -1
  10. package/docs/guides/connect-whatsapp-zapster.md +112 -8
  11. package/docs/guides/create-agent.md +45 -4
  12. package/docs/guides/debug-channel.md +147 -0
  13. package/docs/guides/improve-from-production.md +151 -0
  14. package/docs/guides/prepare-deploy.md +47 -17
  15. package/docs/guides/replay-production-traces.md +72 -0
  16. package/docs/guides/run-evals.md +147 -20
  17. package/docs/guides/security-rules.md +7 -6
  18. package/docs/guides/send-feedback.md +135 -0
  19. package/docs/guides/use-provider.md +27 -3
  20. package/docs/llms-full.txt +303 -55
  21. package/docs/llms.txt +57 -7
  22. package/package.json +2 -5
  23. package/src/cli/args.ts +57 -0
  24. package/src/cli/cloud-client.ts +377 -0
  25. package/src/cli/commands/channels.ts +1315 -0
  26. package/src/cli/commands/feedback.ts +438 -0
  27. package/src/cli/commands/knowledge.ts +136 -0
  28. package/src/cli/commands/transcribe.ts +171 -0
  29. package/src/cli/constants.ts +4 -0
  30. package/src/cli/deploy-chat-ui.ts +535 -0
  31. package/src/cli/deploy-readiness.ts +481 -0
  32. package/src/cli/flags.ts +162 -0
  33. package/src/cli/help.ts +236 -0
  34. package/src/cli/index.ts +1167 -1005
  35. package/src/cli/process.ts +31 -0
  36. package/src/cloud/artifact.ts +139 -0
  37. package/src/cloud/client.ts +80 -0
  38. package/src/cloud/contracts.ts +63 -0
  39. package/src/cloud/index.ts +3 -0
  40. package/src/create-project.ts +21 -6
  41. package/src/index.ts +479 -7
  42. package/src/providers/pi.ts +70 -16
  43. package/src/providers/test.ts +88 -1
  44. package/src/providers/types.ts +7 -0
  45. package/src/runtime/channel-buffer.ts +30 -0
  46. package/src/runtime/channel-test-harness.ts +8 -1
  47. package/src/runtime/channels/discord.ts +896 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +466 -23
  50. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  51. package/src/runtime/channels/whatsapp-zapster.ts +677 -40
  52. package/src/runtime/channels.ts +86 -3
  53. package/src/runtime/chat.ts +130 -38
  54. package/src/runtime/config.ts +483 -18
  55. package/src/runtime/core/manifest.ts +103 -5
  56. package/src/runtime/core/targets.ts +5 -5
  57. package/src/runtime/database.ts +93 -2
  58. package/src/runtime/db-commands.ts +9 -0
  59. package/src/runtime/deploy-readiness.ts +46 -4
  60. package/src/runtime/deploy.ts +1 -1
  61. package/src/runtime/dev-server.ts +759 -41
  62. package/src/runtime/env.ts +8 -3
  63. package/src/runtime/evals.ts +589 -43
  64. package/src/runtime/improve.ts +868 -0
  65. package/src/runtime/inspect.ts +194 -4
  66. package/src/runtime/integrations/composio.ts +423 -0
  67. package/src/runtime/knowledge/chunk.ts +333 -0
  68. package/src/runtime/knowledge/config.ts +135 -0
  69. package/src/runtime/knowledge/embeddings.ts +133 -0
  70. package/src/runtime/knowledge/ingest.ts +521 -0
  71. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  72. package/src/runtime/knowledge/retrieve.ts +303 -0
  73. package/src/runtime/knowledge/schema.ts +100 -0
  74. package/src/runtime/knowledge/tool.ts +64 -0
  75. package/src/runtime/knowledge/vector.ts +258 -0
  76. package/src/runtime/prompt-context.ts +141 -0
  77. package/src/runtime/runtime-contract.ts +86 -8
  78. package/src/runtime/skills.ts +95 -0
  79. package/src/runtime/spec.ts +152 -0
  80. package/src/runtime/sync.ts +144 -0
  81. package/src/runtime/targets/cloudflare/build.ts +1430 -185
  82. package/src/runtime/targets/container/server.ts +1 -1
  83. package/src/runtime/targets/vps/deploy.ts +26 -9
  84. package/src/runtime/tool-runner.ts +9 -1
  85. package/src/runtime/tools.ts +128 -2
  86. package/src/runtime/traces.ts +41 -0
  87. package/src/runtime/transcription.ts +483 -0
  88. package/src/storage/sqlite.ts +149 -3
  89. package/src/templates/blank.ts +76 -17
  90. package/src/templates/dentista.ts +1011 -0
  91. package/src/templates/index.ts +2 -0
  92. package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
  93. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
  94. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  95. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  96. package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
  97. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  98. package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
  99. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
  100. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
  101. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  102. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  103. package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
  104. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
  105. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  106. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  107. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  108. package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
  109. package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
  110. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
  111. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
  112. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
  113. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
  114. package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
  115. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  116. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  117. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  118. package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
  119. package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
  120. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  121. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  122. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  123. package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
  124. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  125. package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
  126. package/src/templates/skills/agentkit-security/SKILL.md +56 -0
  127. package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
  128. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  129. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  130. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  131. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
  132. package/src/templates/support.ts +77 -18
  133. package/docs/guides/channels-production-handoff.md +0 -99
  134. package/docs/portable-deploy-release-checklist.md +0 -41
  135. package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
@@ -8,6 +8,8 @@ import { build as esbuild, type Plugin } from "esbuild";
8
8
  import { loadAgentCapsule, type LoadedAgentCapsule } from "../../config";
9
9
  import { buildSharedAgentManifest, type ManifestSchema, type SharedAgentManifest } from "../../core/manifest";
10
10
  import { AgentKitError } from "../../errors";
11
+ import { resolveKnowledgeConfig } from "../../knowledge/config";
12
+ import { KNOWLEDGE_SCHEMA_SQL } from "../../knowledge/schema";
11
13
 
12
14
  export type BuildTarget = "cloudflare";
13
15
 
@@ -30,10 +32,14 @@ export type AgentManifest = {
30
32
  runtime: "edge";
31
33
  target: "cloudflare";
32
34
  provider: SharedAgentManifest["provider"];
35
+ timeZone: SharedAgentManifest["timeZone"];
33
36
  prompt: SharedAgentManifest["prompt"];
34
37
  secrets: SharedAgentManifest["secrets"];
35
38
  tools: SharedAgentManifest["tools"];
39
+ integrations: SharedAgentManifest["integrations"];
36
40
  channels: SharedAgentManifest["channels"];
41
+ transcription: SharedAgentManifest["transcription"];
42
+ knowledge: SharedAgentManifest["knowledge"];
37
43
  access: SharedAgentManifest["access"];
38
44
  storage: {
39
45
  driver: "agentkit";
@@ -145,6 +151,14 @@ function validateCloudflareBuild(capsule: LoadedAgentCapsule): void {
145
151
  );
146
152
  }
147
153
 
154
+ const knowledge = resolveKnowledgeConfig(capsule.config);
155
+
156
+ if (knowledge.enabled && capsule.config.storage.database?.driver !== "turso") {
157
+ throw new AgentKitError(
158
+ "build_knowledge_database",
159
+ 'Cloudflare Knowledge deploy requires storage.database.driver: "turso" so chunks and retrieval state are available to the hosted runtime.',
160
+ );
161
+ }
148
162
  }
149
163
 
150
164
  function buildWarnings(capsule: LoadedAgentCapsule): string[] {
@@ -181,10 +195,14 @@ function buildManifest(
181
195
  runtime: "edge",
182
196
  target: "cloudflare",
183
197
  provider: shared.provider,
198
+ timeZone: shared.timeZone,
184
199
  prompt: shared.prompt,
185
200
  secrets: shared.secrets,
186
201
  tools: shared.tools,
202
+ integrations: shared.integrations,
187
203
  channels: shared.channels,
204
+ transcription: shared.transcription,
205
+ knowledge: shared.knowledge,
188
206
  access: shared.access,
189
207
  storage: {
190
208
  driver: "agentkit",
@@ -210,12 +228,26 @@ function buildCloudflareFilesManifest(files: SharedAgentManifest["storage"]["fil
210
228
 
211
229
  async function loadTursoSchema(capsule: LoadedAgentCapsule): Promise<ManifestSchema | null> {
212
230
  const database = capsule.config.storage.database;
231
+ const knowledge = resolveKnowledgeConfig(capsule.config);
232
+ const sources: string[] = [];
233
+
234
+ if (!database || database.driver !== "turso") {
235
+ return null;
236
+ }
237
+
238
+ if (database.schema) {
239
+ sources.push(await readFile(resolve(capsule.root, database.schema), "utf8"));
240
+ }
241
+
242
+ if (knowledge.enabled) {
243
+ sources.push(KNOWLEDGE_SCHEMA_SQL);
244
+ }
213
245
 
214
- if (!database || database.driver !== "turso" || !database.schema) {
246
+ if (sources.length === 0) {
215
247
  return null;
216
248
  }
217
249
 
218
- const source = await readFile(resolve(capsule.root, database.schema), "utf8");
250
+ const source = sources.map((part) => part.trim()).filter(Boolean).join("\n\n") + "\n";
219
251
  const digest = sha256(source);
220
252
  return { source, sha256: digest };
221
253
  }
@@ -288,8 +320,8 @@ async function listSourceFiles(root: string): Promise<string[]> {
288
320
  function findImportSpecifiers(source: string): string[] {
289
321
  const specifiers: string[] = [];
290
322
  const patterns = [
291
- /\bimport\s+(?:type\s+)?(?:[^"'()]*?\s+from\s+)?["']([^"']+)["']/g,
292
- /\bexport\s+[^"'()]*?\s+from\s+["']([^"']+)["']/g,
323
+ /\bimport\s+(?!type\b)(?:[^"'()]*?\s+from\s+)?["']([^"']+)["']/g,
324
+ /\bexport\s+(?!type\b)[^"'()]*?\s+from\s+["']([^"']+)["']/g,
293
325
  /\brequire\(\s*["']([^"']+)["']\s*\)/g,
294
326
  /\bimport\(\s*["']([^"']+)["']\s*\)/g,
295
327
  ];
@@ -353,6 +385,26 @@ export function defineTool(tool) {
353
385
  return tool;
354
386
  }
355
387
 
388
+ export function composioManaged(input) {
389
+ const name = normalizeIntegrationName(input.name ?? "composio", "composio integration name");
390
+ const toolkits = normalizeToolkitList(input.toolkits, "composioManaged.toolkits");
391
+ const tools = input.tools ? normalizeComposioToolMap(input.tools, toolkits) : undefined;
392
+ const tags = input.tags ? normalizeIntegrationTags(input.tags) : undefined;
393
+ const confirmExternalWrites =
394
+ input.confirmExternalWrites === undefined ? undefined : Boolean(input.confirmExternalWrites);
395
+
396
+ return {
397
+ type: "composio",
398
+ provider: "composio",
399
+ mode: "managed",
400
+ name,
401
+ toolkits,
402
+ ...(tools ? { tools } : {}),
403
+ ...(tags ? { tags } : {}),
404
+ ...(confirmExternalWrites === undefined ? {} : { confirmExternalWrites }),
405
+ };
406
+ }
407
+
356
408
  export function websiteChannel(input) {
357
409
  return {
358
410
  type: "website",
@@ -361,6 +413,7 @@ export function websiteChannel(input) {
361
413
  access: input.access ?? { mode: "token" },
362
414
  secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"], "website channel"),
363
415
  ...(input.limits ? { limits: input.limits } : {}),
416
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "website channel") } : {}),
364
417
  };
365
418
  }
366
419
 
@@ -372,6 +425,7 @@ export function telegramChannel(input) {
372
425
  secrets: normalizeChannelSecrets(input.secrets ?? ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"], "telegram channel"),
373
426
  allowedUpdates: input.allowedUpdates ?? ["message"],
374
427
  ...(input.limits ? { limits: input.limits } : {}),
428
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "telegram channel") } : {}),
375
429
  };
376
430
  }
377
431
 
@@ -382,12 +436,35 @@ export function whatsappChannel(input) {
382
436
  name: input.name,
383
437
  secrets: normalizeChannelSecrets(input.secrets ?? defaultWhatsappChannelSecrets(input.provider), \`\${input.provider} whatsapp channel\`),
384
438
  ...(input.limits ? { limits: input.limits } : {}),
439
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, \`\${input.provider} whatsapp channel\`) } : {}),
440
+ };
441
+ }
442
+
443
+ export function discordChannel(input) {
444
+ return {
445
+ type: "discord",
446
+ provider: "discord",
447
+ name: input.name,
448
+ secrets: normalizeChannelSecrets(input.secrets ?? ["DISCORD_PUBLIC_KEY"], "discord channel"),
449
+ ...(input.limits ? { limits: input.limits } : {}),
450
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "discord channel") } : {}),
451
+ };
452
+ }
453
+
454
+ export function slackChannel(input) {
455
+ return {
456
+ type: "slack",
457
+ provider: "slack",
458
+ name: input.name,
459
+ secrets: normalizeChannelSecrets(input.secrets ?? ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], "slack channel"),
460
+ ...(input.limits ? { limits: input.limits } : {}),
461
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "slack channel") } : {}),
385
462
  };
386
463
  }
387
464
 
388
465
  function defaultWhatsappChannelSecrets(provider) {
389
466
  if (provider === "zapster") {
390
- return ["ZAPSTER_API_KEY", "ZAPSTER_WEBHOOK_SECRET"];
467
+ return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
391
468
  }
392
469
 
393
470
  return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
@@ -406,6 +483,135 @@ function normalizeChannelSecrets(secrets, context) {
406
483
 
407
484
  return Array.from(uniqueSecrets).sort();
408
485
  }
486
+
487
+ function normalizeIntegrationName(value, context) {
488
+ const name = value.trim();
489
+
490
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
491
+ throw new Error(\`\${context} must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.\`);
492
+ }
493
+
494
+ return name;
495
+ }
496
+
497
+ function normalizeToolkitList(values, context) {
498
+ if (!Array.isArray(values) || values.length === 0) {
499
+ throw new Error(\`\${context} must include at least one toolkit slug.\`);
500
+ }
501
+
502
+ const toolkits = new Set();
503
+
504
+ for (const value of values) {
505
+ if (typeof value !== "string") {
506
+ throw new Error(\`\${context} entries must be toolkit slug strings.\`);
507
+ }
508
+
509
+ const toolkit = value.trim().toLowerCase();
510
+
511
+ if (!/^[a-z][a-z0-9_-]*$/.test(toolkit)) {
512
+ throw new Error(\`\${context} entries must start with a letter and contain only letters, numbers, underscores, and hyphens.\`);
513
+ }
514
+
515
+ toolkits.add(toolkit);
516
+ }
517
+
518
+ return Array.from(toolkits).sort();
519
+ }
520
+
521
+ function normalizeComposioToolMap(values, toolkits) {
522
+ const toolkitSet = new Set(toolkits);
523
+ const normalized = {};
524
+
525
+ for (const [rawToolkit, rawTools] of Object.entries(values)) {
526
+ const toolkit = rawToolkit.trim().toLowerCase();
527
+
528
+ if (!toolkitSet.has(toolkit)) {
529
+ throw new Error(\`composioManaged.tools.\${rawToolkit} must reference a toolkit listed in composioManaged.toolkits.\`);
530
+ }
531
+
532
+ if (!Array.isArray(rawTools) || rawTools.length === 0) {
533
+ throw new Error(\`composioManaged.tools.\${rawToolkit} must include at least one Composio action slug.\`);
534
+ }
535
+
536
+ const tools = new Set();
537
+
538
+ for (const rawTool of rawTools) {
539
+ if (typeof rawTool !== "string") {
540
+ throw new Error(\`composioManaged.tools.\${rawToolkit} entries must be Composio action slug strings.\`);
541
+ }
542
+
543
+ const tool = rawTool.trim().toUpperCase();
544
+
545
+ if (!/^[A-Z][A-Z0-9_]*$/.test(tool)) {
546
+ throw new Error(\`composioManaged.tools.\${rawToolkit} entries must be uppercase Composio action slugs.\`);
547
+ }
548
+
549
+ tools.add(tool);
550
+ }
551
+
552
+ normalized[toolkit] = Array.from(tools).sort();
553
+ }
554
+
555
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
556
+ }
557
+
558
+ function normalizeIntegrationTags(tags) {
559
+ const normalized = {};
560
+
561
+ if (tags.enable !== undefined) {
562
+ normalized.enable = normalizeTagList(tags.enable, "composioManaged.tags.enable");
563
+ }
564
+
565
+ if (tags.disable !== undefined) {
566
+ normalized.disable = normalizeTagList(tags.disable, "composioManaged.tags.disable");
567
+ }
568
+
569
+ return normalized;
570
+ }
571
+
572
+ function normalizeTagList(values, context) {
573
+ if (!Array.isArray(values)) {
574
+ throw new Error(\`\${context} must be an array of tag strings.\`);
575
+ }
576
+
577
+ return Array.from(new Set(values.map((value) => {
578
+ if (typeof value !== "string" || value.trim().length === 0) {
579
+ throw new Error(\`\${context} entries must be non-empty strings.\`);
580
+ }
581
+
582
+ return value.trim();
583
+ }))).sort();
584
+ }
585
+
586
+ function normalizeChannelBuffer(input, context) {
587
+ if (input.mode === "off") {
588
+ return { mode: "off" };
589
+ }
590
+
591
+ if (input.mode !== undefined && input.mode !== "debounce") {
592
+ throw new Error(\`\${context} buffer.mode must be "off" or "debounce".\`);
593
+ }
594
+
595
+ const buffer = {
596
+ mode: "debounce",
597
+ quietWindowMs: input.quietWindowMs ?? 2500,
598
+ maxWaitMs: input.maxWaitMs ?? 12000,
599
+ maxMessages: input.maxMessages ?? 20,
600
+ maxChars: input.maxChars ?? 8000,
601
+ };
602
+
603
+ for (const field of ["quietWindowMs", "maxWaitMs", "maxMessages", "maxChars"]) {
604
+ if (!Number.isInteger(buffer[field]) || buffer[field] <= 0) {
605
+ throw new Error(\`\${context} buffer.\${field} must be a positive integer.\`);
606
+ }
607
+ }
608
+
609
+ if (buffer.maxWaitMs < buffer.quietWindowMs) {
610
+ throw new Error(\`\${context} buffer.maxWaitMs must be greater than or equal to buffer.quietWindowMs.\`);
611
+ }
612
+
613
+ return buffer;
614
+ }
409
615
  `;
410
616
  }
411
617
 
@@ -479,14 +685,18 @@ function renderCloudflareWorker(input: { manifest: AgentManifest; instructions:
479
685
  const instructionsJson = JSON.stringify(input.instructions);
480
686
  const configImportPath = JSON.stringify(input.configPath);
481
687
  const providerImportPath = JSON.stringify(resolve(moduleDir, "../../../providers/index.ts"));
688
+ const composioIntegrationImportPath = JSON.stringify(resolve(moduleDir, "../../integrations/composio.ts"));
689
+ const promptContextImportPath = JSON.stringify(resolve(moduleDir, "../../prompt-context.ts"));
482
690
 
483
691
  return `import { DurableObject } from "cloudflare:workers";
484
692
  import agentConfig from ${configImportPath};
485
693
  import { getProviderAdapter } from ${providerImportPath};
694
+ import { MANAGED_COMPOSIO_USER_ID_BINDING, createManagedComposioTools } from ${composioIntegrationImportPath};
695
+ import { appendRuntimePromptContext, buildRuntimeClockContext } from ${promptContextImportPath};
486
696
 
487
697
  const manifest = ${manifestJson};
488
698
  const instructions = ${instructionsJson};
489
- const hostedTools = agentConfig.tools ?? [];
699
+ const hostedKnowledgeTool = manifest.knowledge.enabled ? createHostedKnowledgeSearchTool() : null;
490
700
  const DEFAULT_TOOL_TIMEOUT_MS = 30000;
491
701
 
492
702
  export class AgentKitConversationStore extends DurableObject {
@@ -516,6 +726,10 @@ export class AgentKitConversationStore extends DurableObject {
516
726
  status TEXT NOT NULL,
517
727
  error_code TEXT,
518
728
  error_message TEXT,
729
+ input_tokens INTEGER,
730
+ output_tokens INTEGER,
731
+ total_tokens INTEGER,
732
+ cost_json TEXT,
519
733
  created_at TEXT NOT NULL,
520
734
  completed_at TEXT
521
735
  );
@@ -528,11 +742,29 @@ export class AgentKitConversationStore extends DurableObject {
528
742
  status TEXT NOT NULL,
529
743
  input_json TEXT NOT NULL,
530
744
  output_json TEXT,
745
+ rendered_json TEXT,
531
746
  error_message TEXT,
532
747
  created_at TEXT NOT NULL,
533
748
  completed_at TEXT
534
749
  );
535
750
  \`);
751
+ try {
752
+ this.sql.exec("ALTER TABLE tool_calls ADD COLUMN rendered_json TEXT");
753
+ } catch {
754
+ // Existing Durable Object tables either already have this column or do not need migration.
755
+ }
756
+ for (const statement of [
757
+ "ALTER TABLE runs ADD COLUMN input_tokens INTEGER",
758
+ "ALTER TABLE runs ADD COLUMN output_tokens INTEGER",
759
+ "ALTER TABLE runs ADD COLUMN total_tokens INTEGER",
760
+ "ALTER TABLE runs ADD COLUMN cost_json TEXT",
761
+ ]) {
762
+ try {
763
+ this.sql.exec(statement);
764
+ } catch {
765
+ // Existing Durable Object tables either already have this column or do not need migration.
766
+ }
767
+ }
536
768
  }
537
769
 
538
770
  async fetch(request) {
@@ -542,9 +774,173 @@ export class AgentKitConversationStore extends DurableObject {
542
774
  return this.persistMessages(await request.json());
543
775
  }
544
776
 
777
+ if (request.method === "GET" && url.pathname === "/messages") {
778
+ return this.getMessages(url.searchParams.get("conversationId"));
779
+ }
780
+
781
+ if (request.method === "GET" && url.pathname === "/conversations") {
782
+ return this.listConversations();
783
+ }
784
+
785
+ const conversationTraceMatch = url.pathname.match(/^\\/conversations\\/([^/]+)\\/trace$/);
786
+
787
+ if (request.method === "GET" && conversationTraceMatch) {
788
+ return this.getConversationTrace(decodeURIComponent(conversationTraceMatch[1] || ""));
789
+ }
790
+
791
+ const conversationMatch = url.pathname.match(/^\\/conversations\\/([^/]+)$/);
792
+
793
+ if (request.method === "GET" && conversationMatch) {
794
+ return this.getConversation(decodeURIComponent(conversationMatch[1] || ""));
795
+ }
796
+
545
797
  return Response.json({ error: { code: "not_found", message: "Unknown Durable Object route." } }, { status: 404 });
546
798
  }
547
799
 
800
+ getMessages(conversationId) {
801
+ if (!conversationId) {
802
+ return Response.json({ messages: [] });
803
+ }
804
+
805
+ const rows = this.sql.exec(
806
+ "SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY rowid ASC",
807
+ String(conversationId),
808
+ ).toArray();
809
+
810
+ return Response.json({
811
+ messages: rows
812
+ .filter((row) => row.role === "user" || row.role === "assistant")
813
+ .map((row) => ({
814
+ role: String(row.role),
815
+ content: String(row.content),
816
+ })),
817
+ });
818
+ }
819
+
820
+ listConversations() {
821
+ const rows = this.sql.exec(\`
822
+ SELECT conversations.id, conversations.agent_name, conversations.title, conversations.created_at, conversations.updated_at, COUNT(messages.id) AS message_count
823
+ FROM conversations
824
+ LEFT JOIN messages ON messages.conversation_id = conversations.id
825
+ GROUP BY conversations.id, conversations.agent_name, conversations.title, conversations.created_at, conversations.updated_at
826
+ ORDER BY conversations.updated_at DESC
827
+ \`).toArray();
828
+
829
+ return Response.json({
830
+ conversations: rows.map((row) => ({
831
+ id: String(row.id),
832
+ agentName: String(row.agent_name),
833
+ title: row.title === null || row.title === undefined ? null : String(row.title),
834
+ createdAt: String(row.created_at),
835
+ updatedAt: String(row.updated_at),
836
+ messageCount: Number(row.message_count || 0),
837
+ })),
838
+ });
839
+ }
840
+
841
+ getConversation(conversationId) {
842
+ const conversation = this.readConversation(conversationId);
843
+
844
+ if (!conversation) {
845
+ return Response.json(
846
+ { error: { code: "conversation_not_found", message: \`Conversation "\${conversationId}" was not found.\` } },
847
+ { status: 404 },
848
+ );
849
+ }
850
+
851
+ return Response.json(conversation);
852
+ }
853
+
854
+ getConversationTrace(conversationId) {
855
+ const conversation = this.readConversation(conversationId);
856
+
857
+ if (!conversation) {
858
+ return Response.json(
859
+ { error: { code: "conversation_not_found", message: \`Conversation "\${conversationId}" was not found.\` } },
860
+ { status: 404 },
861
+ );
862
+ }
863
+
864
+ const runs = this.sql.exec(
865
+ "SELECT id, conversation_id, provider, model, status, error_code, error_message, input_tokens, output_tokens, total_tokens, cost_json, created_at, completed_at FROM runs WHERE conversation_id = ? ORDER BY rowid ASC",
866
+ conversationId,
867
+ ).toArray();
868
+
869
+ return Response.json({
870
+ ...conversation,
871
+ runs: runs.map((run) => ({
872
+ id: String(run.id),
873
+ conversationId: String(run.conversation_id),
874
+ provider: String(run.provider),
875
+ model: String(run.model),
876
+ status: String(run.status),
877
+ errorCode: run.error_code === null || run.error_code === undefined ? null : String(run.error_code),
878
+ errorMessage: run.error_message === null || run.error_message === undefined ? null : String(run.error_message),
879
+ usage: readStoredUsage(run),
880
+ startedAt: String(run.created_at),
881
+ completedAt: run.completed_at === null || run.completed_at === undefined ? null : String(run.completed_at),
882
+ toolCalls: this.readToolCalls(String(run.id)),
883
+ })),
884
+ });
885
+ }
886
+
887
+ readConversation(conversationId) {
888
+ if (!conversationId) {
889
+ return null;
890
+ }
891
+
892
+ const row = this.sql.exec(
893
+ "SELECT id, agent_name, title, created_at, updated_at FROM conversations WHERE id = ?",
894
+ conversationId,
895
+ ).one();
896
+
897
+ if (!row) {
898
+ return null;
899
+ }
900
+
901
+ const messages = this.sql.exec(
902
+ "SELECT id, conversation_id, role, content, created_at FROM messages WHERE conversation_id = ? ORDER BY rowid ASC",
903
+ conversationId,
904
+ ).toArray();
905
+
906
+ return {
907
+ id: String(row.id),
908
+ agentName: String(row.agent_name),
909
+ title: row.title === null || row.title === undefined ? null : String(row.title),
910
+ createdAt: String(row.created_at),
911
+ updatedAt: String(row.updated_at),
912
+ messages: messages.map((message) => ({
913
+ id: String(message.id),
914
+ conversationId: String(message.conversation_id),
915
+ role: String(message.role),
916
+ content: String(message.content),
917
+ createdAt: String(message.created_at),
918
+ })),
919
+ };
920
+ }
921
+
922
+ readToolCalls(runId) {
923
+ const rows = this.sql.exec(
924
+ "SELECT id, run_id, conversation_id, tool_name, visibility, status, input_json, output_json, rendered_json, error_message, created_at, completed_at FROM tool_calls WHERE run_id = ? ORDER BY rowid ASC",
925
+ runId,
926
+ ).toArray();
927
+
928
+ return rows.map((row) => ({
929
+ id: String(row.id),
930
+ runId: String(row.run_id),
931
+ conversationId: String(row.conversation_id),
932
+ name: String(row.tool_name),
933
+ visibility: String(row.visibility),
934
+ status: String(row.status),
935
+ input: parseStoredJson(row.input_json, {}),
936
+ output: row.output_json === null || row.output_json === undefined ? undefined : parseStoredJson(row.output_json, null),
937
+ rendered: row.rendered_json === null || row.rendered_json === undefined ? undefined : parseStoredJson(row.rendered_json, null),
938
+ errorMessage: row.error_message === null || row.error_message === undefined ? null : String(row.error_message),
939
+ createdAt: String(row.created_at),
940
+ completedAt: row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),
941
+ }));
942
+ }
943
+
548
944
  persistMessages(input) {
549
945
  const now = new Date().toISOString();
550
946
  const conversationId = String(input.conversationId || crypto.randomUUID());
@@ -552,6 +948,7 @@ export class AgentKitConversationStore extends DurableObject {
552
948
  const userMessageId = crypto.randomUUID();
553
949
  const assistantMessageId = crypto.randomUUID();
554
950
  const title = titleFromMessage(String(input.userMessage || ""));
951
+ const usage = normalizeRuntimeUsage(input.usage);
555
952
 
556
953
  this.sql.exec(
557
954
  "INSERT OR IGNORE INTO conversations (id, agent_name, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
@@ -571,7 +968,7 @@ export class AgentKitConversationStore extends DurableObject {
571
968
  now,
572
969
  );
573
970
  this.sql.exec(
574
- "INSERT INTO runs (id, conversation_id, provider, model, status, error_code, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
971
+ "INSERT INTO runs (id, conversation_id, provider, model, status, error_code, error_message, input_tokens, output_tokens, total_tokens, cost_json, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
575
972
  runId,
576
973
  conversationId,
577
974
  manifest.provider.name,
@@ -579,6 +976,10 @@ export class AgentKitConversationStore extends DurableObject {
579
976
  String(input.status || "completed"),
580
977
  input.errorCode ? String(input.errorCode) : null,
581
978
  input.errorMessage ? String(input.errorMessage) : null,
979
+ usage.inputTokens ?? null,
980
+ usage.outputTokens ?? null,
981
+ usage.totalTokens ?? null,
982
+ usage.cost && Object.keys(usage.cost).length > 0 ? JSON.stringify(usage.cost) : null,
582
983
  now,
583
984
  now,
584
985
  );
@@ -596,7 +997,7 @@ export class AgentKitConversationStore extends DurableObject {
596
997
 
597
998
  for (const toolCall of input.toolCalls || []) {
598
999
  this.sql.exec(
599
- "INSERT INTO tool_calls (id, run_id, conversation_id, tool_name, visibility, status, input_json, output_json, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1000
+ "INSERT INTO tool_calls (id, run_id, conversation_id, tool_name, visibility, status, input_json, output_json, rendered_json, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
600
1001
  String(toolCall.id || crypto.randomUUID()),
601
1002
  runId,
602
1003
  conversationId,
@@ -605,6 +1006,7 @@ export class AgentKitConversationStore extends DurableObject {
605
1006
  String(toolCall.status || "completed"),
606
1007
  JSON.stringify(toolCall.input ?? {}),
607
1008
  toolCall.output === undefined ? null : JSON.stringify(toolCall.output),
1009
+ toolCall.rendered === undefined ? null : JSON.stringify(toolCall.rendered),
608
1010
  toolCall.errorMessage ? String(toolCall.errorMessage) : null,
609
1011
  now,
610
1012
  now,
@@ -621,7 +1023,7 @@ export class AgentKitConversationStore extends DurableObject {
621
1023
  }
622
1024
 
623
1025
  export default {
624
- async fetch(request, env) {
1026
+ async fetch(request, env, ctx) {
625
1027
  const url = new URL(request.url);
626
1028
 
627
1029
  if (request.method === "GET" && url.pathname === "/_agentkit") {
@@ -631,19 +1033,23 @@ export default {
631
1033
  return auth;
632
1034
  }
633
1035
 
1036
+ const hostedTools = createHostedTools(env);
1037
+
634
1038
  return Response.json({
635
1039
  agent: manifest.name,
636
1040
  runtime: manifest.runtime,
637
1041
  target: manifest.target,
638
1042
  provider: manifest.provider,
639
1043
  prompt: manifest.prompt,
640
- tools: manifest.tools.map((tool) => tool.name),
1044
+ tools: hostedTools.map((tool) => tool.name),
1045
+ integrations: hostedIntegrations(env),
641
1046
  channels: manifest.channels.map((channel) => ({
642
1047
  name: channel.name,
643
1048
  type: channel.type,
644
1049
  provider: channel.provider,
645
1050
  secrets: channel.secrets,
646
1051
  })),
1052
+ knowledge: manifest.knowledge,
647
1053
  access: manifest.access,
648
1054
  storage: manifest.storage,
649
1055
  database: inspectHostedDatabase(),
@@ -654,6 +1060,15 @@ export default {
654
1060
  }
655
1061
 
656
1062
  if (request.method === "GET" && url.pathname === "/") {
1063
+ const auth = await authorizeDeployAccess(request, env, {}, {
1064
+ notConfiguredCode: "cloud_access_not_configured",
1065
+ notConfiguredMessage: "Hosted AgentKit Cloud routes require a deploy access token gate.",
1066
+ });
1067
+
1068
+ if (auth) {
1069
+ return auth;
1070
+ }
1071
+
657
1072
  return Response.json({
658
1073
  agent: manifest.name,
659
1074
  status: "ready",
@@ -662,20 +1077,45 @@ export default {
662
1077
  });
663
1078
  }
664
1079
 
1080
+ if (request.method === "GET" && url.pathname === "/health") {
1081
+ const auth = await authorizeDeployAccess(request, env, {}, {
1082
+ notConfiguredCode: "cloud_access_not_configured",
1083
+ notConfiguredMessage: "Hosted AgentKit Cloud routes require a deploy access token gate.",
1084
+ });
1085
+
1086
+ if (auth) {
1087
+ return auth;
1088
+ }
1089
+
1090
+ return Response.json({ ok: true });
1091
+ }
1092
+
1093
+ if (isHostedChannelWebhookPath(url.pathname)) {
1094
+ return forwardHostedChannelWebhook(request, env, url);
1095
+ }
1096
+
665
1097
  if (request.method === "POST" && (url.pathname === "/chat" || url.pathname === "/v1/chat")) {
666
- return handleChat(request, env);
1098
+ return handleChat(request, env, ctx);
667
1099
  }
668
1100
 
669
- if (request.method === "POST" && url.pathname.startsWith("/tools/")) {
670
- return handleTool(request, env, decodeURIComponent(url.pathname.slice("/tools/".length)));
1101
+ if (request.method === "GET" && (url.pathname === "/conversations" || url.pathname === "/v1/conversations")) {
1102
+ return handleConversationRead(request, env, "list");
1103
+ }
1104
+
1105
+ const conversationTraceMatch = url.pathname.match(/^\\/(?:v1\\/)?conversations\\/([^/]+)\\/trace$/);
1106
+
1107
+ if (request.method === "GET" && conversationTraceMatch) {
1108
+ return handleConversationRead(request, env, "trace", decodeURIComponent(conversationTraceMatch[1] || ""));
671
1109
  }
672
1110
 
673
- if (request.method === "GET" && url.pathname === "/_agentkit/turso/health") {
674
- return handleTursoHealth(request, env);
1111
+ const conversationMatch = url.pathname.match(/^\\/(?:v1\\/)?conversations\\/([^/]+)$/);
1112
+
1113
+ if (request.method === "GET" && conversationMatch) {
1114
+ return handleConversationRead(request, env, "show", decodeURIComponent(conversationMatch[1] || ""));
675
1115
  }
676
1116
 
677
- if (request.method === "POST" && url.pathname === "/_agentkit/turso/query") {
678
- return handleTursoQuery(request, env);
1117
+ if (request.method === "POST" && url.pathname.startsWith("/tools/")) {
1118
+ return handleTool(request, env, decodeURIComponent(url.pathname.slice("/tools/".length)));
679
1119
  }
680
1120
 
681
1121
  if (url.pathname.startsWith("/files/")) {
@@ -686,69 +1126,46 @@ export default {
686
1126
  },
687
1127
  };
688
1128
 
689
- async function handleTursoHealth(request, env) {
690
- const auth = authorizeAdmin(request, env);
691
-
692
- if (auth) {
693
- return auth;
694
- }
695
-
696
- const configError = requireTurso(env);
697
-
698
- if (configError) {
699
- return configError;
700
- }
701
-
702
- const response = await fetch(toTursoBaseUrl(env.TURSO_DATABASE_URL) + "/health", {
703
- headers: {
704
- Authorization: \`Bearer \${env.TURSO_AUTH_TOKEN}\`,
705
- },
706
- });
707
-
708
- return Response.json({
709
- driver: "turso",
710
- ok: response.ok,
711
- status: response.status,
712
- }, { status: response.ok ? 200 : 502 });
1129
+ function isHostedChannelWebhookPath(pathname) {
1130
+ return (
1131
+ /^\\/channels\\/[^/]+\\/(?:website\\/agentkit|telegram\\/telegram|whatsapp\\/(?:zapster|meta)|discord\\/discord|slack\\/slack)\\/webhook$/.test(pathname) ||
1132
+ /^\\/channels\\/[^/]+\\/(?:website|telegram|discord|slack)\\/webhook$/.test(pathname) ||
1133
+ /^\\/channels\\/[^/]+\\/whatsapp\\/(?:zapster|meta)\\/webhook$/.test(pathname)
1134
+ );
713
1135
  }
714
1136
 
715
- async function handleTursoQuery(request, env) {
716
- const auth = authorizeAdmin(request, env);
717
-
718
- if (auth) {
719
- return auth;
720
- }
721
-
722
- const configError = requireTurso(env);
723
-
724
- if (configError) {
725
- return configError;
726
- }
727
-
728
- let body;
729
-
730
- try {
731
- body = await request.json();
732
- } catch {
1137
+ async function forwardHostedChannelWebhook(request, env, url) {
1138
+ if (!env.AGENTKIT_CLOUD_API_URL) {
733
1139
  return Response.json(
734
- { error: { code: "validation_error", message: "Expected JSON body with sql and optional args fields." } },
735
- { status: 400 },
1140
+ {
1141
+ error: {
1142
+ code: "channel_webhook_proxy_not_configured",
1143
+ message: "Hosted channel webhooks require AGENTKIT_CLOUD_API_URL on the deployed Worker.",
1144
+ },
1145
+ },
1146
+ { status: 501 },
736
1147
  );
737
1148
  }
738
1149
 
739
- const sql = String(body.sql || "").trim();
1150
+ const target = new URL(\`\${url.pathname}\${url.search}\`, env.AGENTKIT_CLOUD_API_URL);
1151
+ const headers = new Headers(request.headers);
1152
+ headers.delete("host");
1153
+ headers.delete("content-length");
740
1154
 
741
- if (!sql) {
742
- return Response.json({ error: { code: "validation_error", message: "sql must be non-empty." } }, { status: 400 });
1155
+ if (env.AGENTKIT_DEPLOY_ID) {
1156
+ headers.set("X-AgentKit-Deploy-ID", env.AGENTKIT_DEPLOY_ID);
743
1157
  }
744
1158
 
745
- const args = Array.isArray(body.args) ? body.args : [];
746
- try {
747
- const result = await executeTursoSql(env, sql, args);
748
- return Response.json(result);
749
- } catch (error) {
750
- return hostedErrorResponse(error);
1159
+ if (env.AGENTKIT_RUNTIME_GATE_TOKEN) {
1160
+ headers.set("X-AgentKit-Runtime-Gate", env.AGENTKIT_RUNTIME_GATE_TOKEN);
751
1161
  }
1162
+
1163
+ return fetch(target.href, {
1164
+ method: request.method,
1165
+ headers,
1166
+ body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
1167
+ redirect: "manual",
1168
+ });
752
1169
  }
753
1170
 
754
1171
  async function handleFile(request, env, rawKey) {
@@ -878,13 +1295,14 @@ async function handleTool(request, env, toolName) {
878
1295
  },
879
1296
  input: result.input,
880
1297
  output: result.output,
1298
+ ...(result.rendered ? { rendered: result.rendered } : {}),
881
1299
  });
882
1300
  } catch (error) {
883
1301
  return hostedErrorResponse(error);
884
1302
  }
885
1303
  }
886
1304
 
887
- async function handleChat(request, env) {
1305
+ async function handleChat(request, env, ctx) {
888
1306
  let body;
889
1307
 
890
1308
  try {
@@ -911,47 +1329,58 @@ async function handleChat(request, env) {
911
1329
  );
912
1330
  }
913
1331
 
914
- const conversationId = String(body.conversationId || crypto.randomUUID());
915
- let content;
916
- const toolCalls = [];
917
-
918
- const toolRequest = parseToolRequest(message);
1332
+ let conversationId;
919
1333
 
920
- if (toolRequest) {
921
- try {
922
- const toolResult = await executeHostedTool({
923
- name: toolRequest.name,
924
- input: toolRequest.input,
925
- env,
926
- signal: request.signal,
927
- invocation: "chat",
928
- });
929
- toolCalls.push(toolResult);
930
- content =
931
- toolResult.visibility === "internal"
932
- ? \`Tool \${toolResult.name}: completed\`
933
- : \`Tool \${toolResult.name}: \${JSON.stringify(toolResult.output)}\`;
934
- } catch (error) {
935
- await persistFailure(env, conversationId, message, error);
936
- return hostedErrorResponse(error);
937
- }
1334
+ try {
1335
+ conversationId = normalizeConversationId(body.conversationId) ?? crypto.randomUUID();
1336
+ } catch (error) {
1337
+ return hostedErrorResponse(error);
938
1338
  }
939
1339
 
940
- if (content === undefined) {
941
- try {
942
- content = await callHostedProvider(env, message, request.signal);
943
- } catch (error) {
944
- await persistFailure(env, conversationId, message, error);
945
- return hostedErrorResponse(error);
946
- }
1340
+ const previousMessages = await loadConversationMessages(env, conversationId).catch(() => []);
1341
+ let content;
1342
+ let providerResult;
1343
+ const toolCalls = [];
1344
+
1345
+ try {
1346
+ providerResult = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal, toolCalls);
1347
+ content = providerResult.content;
1348
+ } catch (error) {
1349
+ const persisted = await persistFailure(env, conversationId, message, error, toolCalls);
1350
+ queueRuntimeUsage(ctx, sendRuntimeUsage(env, {
1351
+ kind: "chat_run",
1352
+ status: "failed",
1353
+ conversationId,
1354
+ runId: persisted?.runId,
1355
+ provider: manifest.provider.name,
1356
+ model: manifest.provider.model,
1357
+ metadata: {
1358
+ error_code: error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error",
1359
+ tool_calls: toolCalls.length,
1360
+ },
1361
+ }));
1362
+ return hostedErrorResponse(error);
947
1363
  }
948
1364
 
949
1365
  const persisted = await persist(env, {
950
1366
  conversationId,
951
1367
  userMessage: message,
952
1368
  assistantMessage: content,
1369
+ usage: providerResult.usage,
953
1370
  toolCalls,
954
1371
  });
1372
+ queueRuntimeUsage(ctx, sendRuntimeUsage(env, {
1373
+ kind: "chat_run",
1374
+ status: "succeeded",
1375
+ conversationId: persisted.conversationId ?? conversationId,
1376
+ runId: persisted.runId,
1377
+ provider: manifest.provider.name,
1378
+ model: manifest.provider.model,
1379
+ usage: providerResult.usage,
1380
+ metadata: {
1381
+ tool_calls: toolCalls.length,
1382
+ },
1383
+ }));
955
1384
 
956
1385
  return Response.json({
957
1386
  message: {
@@ -962,6 +1391,39 @@ async function handleChat(request, env) {
962
1391
  });
963
1392
  }
964
1393
 
1394
+ async function handleConversationRead(request, env, action, conversationId) {
1395
+ const auth = await authorizeConversationRead(request, env);
1396
+
1397
+ if (auth) {
1398
+ return auth;
1399
+ }
1400
+
1401
+ if (!env.AGENTKIT_CONVERSATIONS) {
1402
+ return Response.json({ error: { code: "conversations_not_configured", message: "Hosted conversation storage is not configured." } }, { status: 503 });
1403
+ }
1404
+
1405
+ if (action === "list") {
1406
+ const stub = hostedConversationStore(env);
1407
+ return stub.fetch("https://agentkit.internal/conversations");
1408
+ }
1409
+
1410
+ if (!conversationId) {
1411
+ return Response.json({ error: { code: "validation_error", message: "Missing conversation id." } }, { status: 400 });
1412
+ }
1413
+
1414
+ const stub = hostedConversationStore(env);
1415
+ const suffix = action === "trace" ? "/trace" : "";
1416
+ const path = \`/conversations/\${encodeURIComponent(conversationId)}\${suffix}\`;
1417
+ const response = await stub.fetch(\`https://agentkit.internal\${path}\`);
1418
+
1419
+ if (response.status !== 404) {
1420
+ return response;
1421
+ }
1422
+
1423
+ const legacyStub = legacyConversationStore(env, conversationId);
1424
+ return legacyStub.fetch(\`https://agentkit.internal\${path}\`);
1425
+ }
1426
+
965
1427
  function readChatMessage(body) {
966
1428
  if (typeof body.message === "string") {
967
1429
  return body.message.trim();
@@ -978,26 +1440,53 @@ function isRecord(value) {
978
1440
  return typeof value === "object" && value !== null && !Array.isArray(value);
979
1441
  }
980
1442
 
981
- async function callHostedProvider(env, message, signal) {
1443
+ async function callHostedProvider(env, messages, signal, toolCalls = []) {
982
1444
  const adapter = getProviderAdapter(manifest.provider);
1445
+ const hostedTools = createHostedTools(env);
1446
+ const clock = buildRuntimeClockContext({
1447
+ timeZone: manifest.timeZone,
1448
+ env,
1449
+ });
983
1450
  const result = await adapter.run({
984
1451
  agent: {
985
1452
  name: manifest.name,
986
1453
  runtime: manifest.runtime,
987
1454
  provider: manifest.provider,
988
1455
  },
989
- instructions: withToolVisibilityInstructions(instructions, hostedTools),
990
- messages: [{ role: "user", content: message }],
1456
+ instructions: appendRuntimePromptContext(
1457
+ withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1458
+ {
1459
+ clock,
1460
+ },
1461
+ ),
1462
+ messages,
991
1463
  tools: hostedTools,
992
- toolRuntime: createHostedProviderToolRuntime(env, signal),
1464
+ toolRuntime: createHostedProviderToolRuntime(env, signal, clock, toolCalls),
993
1465
  env,
994
1466
  signal,
995
1467
  });
996
1468
 
997
- return result.message.content;
1469
+ return {
1470
+ content: result.message.content,
1471
+ usage: result.usage,
1472
+ };
998
1473
  }
999
1474
 
1000
- function withToolVisibilityInstructions(baseInstructions, tools) {
1475
+ function appendHostedKnowledgePromptPolicy(baseInstructions) {
1476
+ if (!manifest.knowledge.enabled) {
1477
+ return baseInstructions;
1478
+ }
1479
+
1480
+ return \`\${baseInstructions.trimEnd()}
1481
+
1482
+ Knowledge rules:
1483
+ - Use agentkit_search_knowledge before answering business-specific factual questions about the user's documents, policies, prices, availability, services, procedures, or source-specific claims.
1484
+ - Do not invent prices, policies, legal/clinical claims, availability, or source-specific facts when Knowledge has not returned support for the answer.
1485
+ - Use retrieved Knowledge as context for the final answer, but do not expose raw retrieval JSON, scores, chunk IDs, or internal tool output objects to the user.
1486
+ \`;
1487
+ }
1488
+
1489
+ function withToolVisibilityInstructions(baseInstructions, tools) {
1001
1490
  const internalTools = tools
1002
1491
  .filter((tool) => tool.visibility === "internal")
1003
1492
  .map((tool) => tool.name)
@@ -1016,36 +1505,407 @@ Tool visibility rules:
1016
1505
  \`;
1017
1506
  }
1018
1507
 
1019
- async function persistFailure(env, conversationId, userMessage, error) {
1020
- await persist(env, {
1021
- conversationId,
1022
- userMessage,
1023
- status: "failed",
1024
- errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error",
1025
- errorMessage: error instanceof Error ? error.message : String(error),
1026
- toolCalls: [],
1027
- }).catch(() => undefined);
1508
+ function createHostedKnowledgeSearchTool() {
1509
+ return {
1510
+ name: manifest.knowledge.internalTool || "agentkit_search_knowledge",
1511
+ description: "Search the Agent Capsule's configured Knowledge sources for business-specific facts.",
1512
+ visibility: "internal",
1513
+ inputSchema: {
1514
+ type: "object",
1515
+ properties: {
1516
+ query: { type: "string" },
1517
+ topK: { type: "integer" },
1518
+ },
1519
+ required: ["query"],
1520
+ additionalProperties: false,
1521
+ },
1522
+ outputSchema: {
1523
+ type: "object",
1524
+ properties: {
1525
+ results: {
1526
+ type: "array",
1527
+ items: {
1528
+ type: "object",
1529
+ properties: {
1530
+ content: { type: "string" },
1531
+ score: { type: "number" },
1532
+ source: { type: "object" },
1533
+ },
1534
+ required: ["content", "score", "source"],
1535
+ },
1536
+ },
1537
+ },
1538
+ required: ["results"],
1539
+ additionalProperties: false,
1540
+ },
1541
+ secrets: manifest.knowledge.embedding.secret ? [manifest.knowledge.embedding.secret] : [],
1542
+ async execute(input, ctx) {
1543
+ const query = typeof input?.query === "string" ? input.query : "";
1544
+ const topK = normalizeKnowledgeTopK(input?.topK ?? manifest.knowledge.retrieval.topK);
1545
+ const results = await searchHostedKnowledge(ctx.database, query, topK, ctx.secrets ?? {});
1546
+
1547
+ return {
1548
+ results: results.map((result) => ({
1549
+ content: result.content,
1550
+ score: result.score,
1551
+ source: result.source,
1552
+ })),
1553
+ };
1554
+ },
1555
+ };
1028
1556
  }
1029
1557
 
1030
- function parseToolRequest(content) {
1031
- let parsed;
1558
+ async function searchHostedKnowledge(database, query, topK, secrets) {
1559
+ const normalizedQuery = String(query || "").trim();
1560
+
1561
+ if (!normalizedQuery) {
1562
+ throw agentKitError("knowledge_query_invalid", "Knowledge search query must be a non-empty string.");
1563
+ }
1564
+
1565
+ const hasEmbeddingProvider = manifest.knowledge.embedding.provider !== "none";
1566
+ const lexical =
1567
+ manifest.knowledge.retrieval.hybrid || !hasEmbeddingProvider
1568
+ ? await searchHostedKnowledgeLexical(database, normalizedQuery, Math.max(topK * 4, topK))
1569
+ : [];
1570
+ const semantic = hasEmbeddingProvider
1571
+ ? await searchHostedKnowledgeSemantic(database, normalizedQuery, topK * 4, secrets)
1572
+ : [];
1573
+
1574
+ return combineHostedKnowledgeResults(lexical, semantic, topK);
1575
+ }
1576
+
1577
+ async function searchHostedKnowledgeLexical(database, query, limit) {
1578
+ const tokens = hostedKnowledgeQueryTokens(query);
1579
+
1580
+ if (tokens.length === 0) {
1581
+ return [];
1582
+ }
1032
1583
 
1033
1584
  try {
1034
- parsed = JSON.parse(content);
1585
+ const result = await database.query(
1586
+ \`
1587
+ SELECT
1588
+ c.id AS chunk_id,
1589
+ c.source_id AS source_id,
1590
+ c.content AS content,
1591
+ c.title AS title,
1592
+ c.section AS section,
1593
+ c.locator AS locator,
1594
+ s.source_key AS source_key,
1595
+ rank
1596
+ FROM agentkit_knowledge_fts
1597
+ JOIN agentkit_knowledge_chunks c ON c.id = agentkit_knowledge_fts.chunk_id
1598
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1599
+ WHERE agentkit_knowledge_fts MATCH ?
1600
+ ORDER BY rank ASC
1601
+ LIMIT ?
1602
+ \`,
1603
+ [tokens.map((token) => \`"\${token.replace(/"/g, '""')}"\`).join(" OR "), limit],
1604
+ );
1605
+
1606
+ return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
1035
1607
  } catch {
1036
- return null;
1608
+ return searchHostedKnowledgeLexicalFallback(database, query, limit);
1609
+ }
1610
+ }
1611
+
1612
+ async function searchHostedKnowledgeLexicalFallback(database, query, limit) {
1613
+ const like = \`%\${query.toLowerCase()}%\`;
1614
+ const result = await database.query(
1615
+ \`
1616
+ SELECT
1617
+ c.id AS chunk_id,
1618
+ c.source_id AS source_id,
1619
+ c.content AS content,
1620
+ c.title AS title,
1621
+ c.section AS section,
1622
+ c.locator AS locator,
1623
+ s.source_key AS source_key
1624
+ FROM agentkit_knowledge_chunks c
1625
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1626
+ WHERE lower(c.content) LIKE ?
1627
+ OR lower(COALESCE(c.title, '')) LIKE ?
1628
+ OR lower(COALESCE(c.section, '')) LIKE ?
1629
+ ORDER BY c.created_at DESC, c.ordinal ASC
1630
+ LIMIT ?
1631
+ \`,
1632
+ [like, like, like, limit],
1633
+ );
1634
+
1635
+ return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
1636
+ }
1637
+
1638
+ async function searchHostedKnowledgeSemantic(database, query, limit, secrets) {
1639
+ const queryEmbedding = await createHostedKnowledgeEmbedding(query, secrets);
1640
+
1641
+ if (!queryEmbedding) {
1642
+ return [];
1037
1643
  }
1038
1644
 
1039
- if (!isRecord(parsed) || typeof parsed.tool !== "string" || parsed.tool.length === 0) {
1645
+ try {
1646
+ return await searchHostedKnowledgeNativeVector(database, queryEmbedding, limit);
1647
+ } catch {
1648
+ return searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit);
1649
+ }
1650
+ }
1651
+
1652
+ async function searchHostedKnowledgeNativeVector(database, queryEmbedding, limit) {
1653
+ const vector = JSON.stringify(queryEmbedding);
1654
+ const result = await database.query(
1655
+ \`
1656
+ SELECT
1657
+ c.id AS chunk_id,
1658
+ c.source_id AS source_id,
1659
+ c.content AS content,
1660
+ c.title AS title,
1661
+ c.section AS section,
1662
+ c.locator AS locator,
1663
+ s.source_key AS source_key,
1664
+ vector_distance_cos(v.embedding, vector32(?)) AS vector_distance
1665
+ FROM vector_top_k('agentkit_knowledge_vectors_embedding_idx', vector32(?), ?) nearest
1666
+ JOIN agentkit_knowledge_vectors v ON v.id = nearest.id
1667
+ JOIN agentkit_knowledge_chunks c ON c.id = v.chunk_id
1668
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1669
+ ORDER BY vector_distance ASC
1670
+ \`,
1671
+ [vector, vector, limit],
1672
+ );
1673
+
1674
+ return result.rows
1675
+ .map((row) => {
1676
+ const distance = Number(row.vector_distance);
1677
+ const semantic = Number.isFinite(distance) ? 1 - distance : 0;
1678
+ return hostedKnowledgeResultFromRow(row, 0, semantic);
1679
+ })
1680
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
1681
+ .slice(0, limit);
1682
+ }
1683
+
1684
+ async function searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit) {
1685
+ const result = await database.query(
1686
+ \`
1687
+ SELECT
1688
+ c.id AS chunk_id,
1689
+ c.source_id AS source_id,
1690
+ c.content AS content,
1691
+ c.title AS title,
1692
+ c.section AS section,
1693
+ c.locator AS locator,
1694
+ c.embedding_json AS embedding_json,
1695
+ s.source_key AS source_key
1696
+ FROM agentkit_knowledge_chunks c
1697
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1698
+ WHERE c.embedding_json IS NOT NULL
1699
+ ORDER BY c.created_at DESC, c.ordinal ASC
1700
+ \`,
1701
+ );
1702
+
1703
+ return result.rows
1704
+ .map((row) => {
1705
+ const embedding = parseHostedKnowledgeEmbedding(row.embedding_json);
1706
+ const semantic = embedding ? hostedCosineSimilarity(queryEmbedding, embedding) : 0;
1707
+ return hostedKnowledgeResultFromRow(row, 0, semantic);
1708
+ })
1709
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
1710
+ .sort((left, right) => (right.match.semantic ?? 0) - (left.match.semantic ?? 0))
1711
+ .slice(0, limit);
1712
+ }
1713
+
1714
+ async function createHostedKnowledgeEmbedding(input, secrets) {
1715
+ const provider = manifest.knowledge.embedding.provider;
1716
+
1717
+ if (provider === "none") {
1040
1718
  return null;
1041
1719
  }
1042
1720
 
1721
+ if (provider === "test") {
1722
+ return hostedFakeEmbedding(input, manifest.knowledge.embedding.dimensions || 32);
1723
+ }
1724
+
1725
+ if (provider !== "openai") {
1726
+ throw agentKitError("knowledge_embedding_provider_unsupported", "Unsupported knowledge embedding provider.");
1727
+ }
1728
+
1729
+ const secretName = manifest.knowledge.embedding.secret || "OPENAI_API_KEY";
1730
+ const apiKey = secrets[secretName];
1731
+
1732
+ if (!apiKey) {
1733
+ throw agentKitError("knowledge_embedding_secret_missing", \`Knowledge embeddings require missing secret "\${secretName}".\`);
1734
+ }
1735
+
1736
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
1737
+ method: "POST",
1738
+ headers: {
1739
+ "content-type": "application/json",
1740
+ authorization: \`Bearer \${apiKey}\`,
1741
+ },
1742
+ body: JSON.stringify({
1743
+ model: manifest.knowledge.embedding.model || "text-embedding-3-small",
1744
+ input,
1745
+ encoding_format: "float",
1746
+ ...(manifest.knowledge.embedding.dimensions ? { dimensions: manifest.knowledge.embedding.dimensions } : {}),
1747
+ }),
1748
+ });
1749
+
1750
+ if (!response.ok) {
1751
+ const text = await response.text().catch(() => "");
1752
+ throw agentKitError(
1753
+ "knowledge_embedding_failed",
1754
+ \`OpenAI embedding request failed with HTTP \${response.status}.\${text ? " " + text.slice(0, 240) : ""}\`,
1755
+ );
1756
+ }
1757
+
1758
+ const payload = await response.json();
1759
+ const embedding = payload?.data?.[0]?.embedding;
1760
+
1761
+ if (!Array.isArray(embedding) || embedding.some((value) => typeof value !== "number")) {
1762
+ throw agentKitError("knowledge_embedding_failed", "OpenAI embedding response did not contain a numeric vector.");
1763
+ }
1764
+
1765
+ return embedding;
1766
+ }
1767
+
1768
+ function combineHostedKnowledgeResults(lexical, semantic, topK) {
1769
+ const byChunk = new Map();
1770
+
1771
+ for (const result of lexical) {
1772
+ byChunk.set(result.chunkId, result);
1773
+ }
1774
+
1775
+ for (const result of semantic) {
1776
+ const existing = byChunk.get(result.chunkId);
1777
+
1778
+ if (!existing) {
1779
+ byChunk.set(result.chunkId, {
1780
+ ...result,
1781
+ score: hostedCombinedScore(0, result.match.semantic),
1782
+ });
1783
+ continue;
1784
+ }
1785
+
1786
+ byChunk.set(result.chunkId, {
1787
+ ...existing,
1788
+ score: hostedCombinedScore(existing.match.lexical, result.match.semantic),
1789
+ match: {
1790
+ lexical: existing.match.lexical,
1791
+ semantic: result.match.semantic,
1792
+ },
1793
+ });
1794
+ }
1795
+
1796
+ return Array.from(byChunk.values())
1797
+ .map((result) => ({
1798
+ ...result,
1799
+ score: result.score || hostedCombinedScore(result.match.lexical, result.match.semantic),
1800
+ }))
1801
+ .sort((left, right) => right.score - left.score || left.source.path.localeCompare(right.source.path))
1802
+ .slice(0, topK);
1803
+ }
1804
+
1805
+ function hostedKnowledgeResultFromRow(row, lexical, semantic) {
1043
1806
  return {
1044
- name: parsed.tool,
1045
- input: "input" in parsed ? parsed.input : {},
1807
+ chunkId: String(row.chunk_id),
1808
+ content: String(row.content),
1809
+ score: hostedCombinedScore(lexical, semantic),
1810
+ source: {
1811
+ id: String(row.source_id),
1812
+ path: String(row.source_key),
1813
+ title: typeof row.title === "string" ? row.title : null,
1814
+ section: typeof row.section === "string" ? row.section : null,
1815
+ locator: typeof row.locator === "string" ? row.locator : null,
1816
+ },
1817
+ match: {
1818
+ lexical,
1819
+ semantic,
1820
+ },
1046
1821
  };
1047
1822
  }
1048
1823
 
1824
+ function normalizeKnowledgeTopK(value) {
1825
+ const topK = Number(value);
1826
+
1827
+ if (!Number.isInteger(topK) || topK <= 0 || topK > 50) {
1828
+ throw agentKitError("knowledge_query_invalid", "Knowledge search topK must be a positive integer no larger than 50.");
1829
+ }
1830
+
1831
+ return topK;
1832
+ }
1833
+
1834
+ function hostedKnowledgeQueryTokens(query) {
1835
+ return Array.from(new Set(String(query).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [])).slice(0, 12);
1836
+ }
1837
+
1838
+ function parseHostedKnowledgeEmbedding(value) {
1839
+ if (typeof value !== "string") {
1840
+ return null;
1841
+ }
1842
+
1843
+ try {
1844
+ const parsed = JSON.parse(value);
1845
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === "number") ? parsed : null;
1846
+ } catch {
1847
+ return null;
1848
+ }
1849
+ }
1850
+
1851
+ function hostedCombinedScore(lexical, semantic) {
1852
+ const semanticScore = semantic === null ? 0 : Math.max(0, (semantic + 1) / 2);
1853
+ return Number((lexical * 0.6 + semanticScore * 0.4).toFixed(6));
1854
+ }
1855
+
1856
+ function hostedLexicalScore(index) {
1857
+ return Number((1 / (index + 1)).toFixed(6));
1858
+ }
1859
+
1860
+ function hostedCosineSimilarity(left, right) {
1861
+ if (left.length !== right.length || left.length === 0) {
1862
+ return 0;
1863
+ }
1864
+
1865
+ let dot = 0;
1866
+ let leftNorm = 0;
1867
+ let rightNorm = 0;
1868
+
1869
+ for (let index = 0; index < left.length; index += 1) {
1870
+ dot += left[index] * right[index];
1871
+ leftNorm += left[index] * left[index];
1872
+ rightNorm += right[index] * right[index];
1873
+ }
1874
+
1875
+ if (leftNorm === 0 || rightNorm === 0) {
1876
+ return 0;
1877
+ }
1878
+
1879
+ return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
1880
+ }
1881
+
1882
+ async function hostedFakeEmbedding(input, dimensions) {
1883
+ const values = Array.from({ length: dimensions }, () => 0);
1884
+ const tokens = String(input).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [];
1885
+ const encoder = new TextEncoder();
1886
+
1887
+ for (const token of tokens) {
1888
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token)));
1889
+ const slot = hash[0] % dimensions;
1890
+ const sign = hash[1] % 2 === 0 ? 1 : -1;
1891
+ values[slot] += sign * (1 + (hash[2] % 7) / 10);
1892
+ }
1893
+
1894
+ const norm = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
1895
+ return norm === 0 ? values : values.map((value) => Number((value / norm).toFixed(8)));
1896
+ }
1897
+
1898
+ async function persistFailure(env, conversationId, userMessage, error, toolCalls = []) {
1899
+ return persist(env, {
1900
+ conversationId,
1901
+ userMessage,
1902
+ status: "failed",
1903
+ errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error",
1904
+ errorMessage: error instanceof Error ? error.message : String(error),
1905
+ toolCalls,
1906
+ }).catch(() => undefined);
1907
+ }
1908
+
1049
1909
  function inspectHostedDatabase() {
1050
1910
  const schema = manifest.storage.database.driver === "turso" ? manifest.storage.database.schema : null;
1051
1911
 
@@ -1064,29 +1924,69 @@ function inspectHostedDatabase() {
1064
1924
  }
1065
1925
 
1066
1926
  function hostedManagedSecrets() {
1067
- if (manifest.storage.database.driver !== "turso") {
1068
- return [];
1927
+ const secrets = [];
1928
+
1929
+ if (manifest.storage.database.driver === "turso") {
1930
+ secrets.push("TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL");
1931
+ }
1932
+
1933
+ if (manifest.integrations.some((integration) => integration.provider === "composio" && integration.mode === "managed")) {
1934
+ secrets.push("COMPOSIO_API_KEY");
1935
+ }
1936
+
1937
+ return secrets.sort();
1938
+ }
1939
+
1940
+ function createHostedTools(env) {
1941
+ return [
1942
+ ...(hostedKnowledgeTool ? [hostedKnowledgeTool] : []),
1943
+ ...createManagedComposioTools(agentConfig, { userId: env[MANAGED_COMPOSIO_USER_ID_BINDING] }),
1944
+ ...(agentConfig.tools ?? []),
1945
+ ];
1946
+ }
1947
+
1948
+ function hostedIntegrations(env) {
1949
+ const userId = env[MANAGED_COMPOSIO_USER_ID_BINDING];
1950
+
1951
+ if (!userId) {
1952
+ return manifest.integrations;
1069
1953
  }
1070
1954
 
1071
- return ["TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL"];
1955
+ return manifest.integrations.map((integration) =>
1956
+ integration.provider === "composio" && integration.mode === "managed"
1957
+ ? { ...integration, userId }
1958
+ : integration,
1959
+ );
1072
1960
  }
1073
1961
 
1074
- function createHostedProviderToolRuntime(env, signal) {
1962
+ function createHostedProviderToolRuntime(env, signal, clock, toolCalls) {
1963
+ const hostedTools = createHostedTools(env);
1964
+
1075
1965
  return {
1076
1966
  definitions: hostedTools,
1077
- execute(request) {
1078
- return executeHostedTool({
1079
- name: request.name,
1080
- input: request.input,
1081
- env,
1082
- signal,
1083
- invocation: "chat",
1084
- });
1967
+ async execute(request) {
1968
+ try {
1969
+ const result = await executeHostedTool({
1970
+ tools: hostedTools,
1971
+ name: request.name,
1972
+ input: request.input,
1973
+ env,
1974
+ signal,
1975
+ invocation: "chat",
1976
+ clock,
1977
+ });
1978
+ toolCalls?.push(result);
1979
+ return result;
1980
+ } catch (error) {
1981
+ toolCalls?.push(failedHostedToolCall(hostedTools, request, error));
1982
+ throw error;
1983
+ }
1085
1984
  },
1086
1985
  };
1087
1986
  }
1088
1987
 
1089
1988
  async function executeHostedTool(request) {
1989
+ const hostedTools = request.tools ?? createHostedTools(request.env);
1090
1990
  const tool = hostedTools.find((candidate) => candidate.name === request.name);
1091
1991
  const id = crypto.randomUUID();
1092
1992
 
@@ -1099,6 +1999,16 @@ async function executeHostedTool(request) {
1099
1999
 
1100
2000
  const secrets = resolveToolSecrets(tool, request.env);
1101
2001
  const database = createHostedDatabaseRunner(request.env);
2002
+ const clock = request.clock ?? buildRuntimeClockContext({
2003
+ timeZone: manifest.timeZone,
2004
+ env: request.env,
2005
+ });
2006
+ const runtime = {
2007
+ environment: "hosted",
2008
+ invocation: request.invocation ?? "tool",
2009
+ target: "cloudflare",
2010
+ database: manifest.storage.database.driver === "turso" ? "turso" : "none",
2011
+ };
1102
2012
  const output = await runToolWithTimeout(
1103
2013
  tool,
1104
2014
  input,
@@ -1106,12 +2016,8 @@ async function executeHostedTool(request) {
1106
2016
  secrets,
1107
2017
  database,
1108
2018
  signal: request.signal,
1109
- runtime: {
1110
- environment: "hosted",
1111
- invocation: request.invocation ?? "tool",
1112
- target: "cloudflare",
1113
- database: manifest.storage.database.driver === "turso" ? "turso" : "none",
1114
- },
2019
+ clock,
2020
+ runtime,
1115
2021
  },
1116
2022
  tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS,
1117
2023
  );
@@ -1120,6 +2026,8 @@ async function executeHostedTool(request) {
1120
2026
  validateSchema(output, tool.outputSchema, \`tool "\${tool.name}" output\`);
1121
2027
  }
1122
2028
 
2029
+ const rendered = renderHostedToolOutput(tool, input, output, runtime, clock);
2030
+
1123
2031
  return {
1124
2032
  id,
1125
2033
  name: tool.name,
@@ -1127,9 +2035,135 @@ async function executeHostedTool(request) {
1127
2035
  status: "completed",
1128
2036
  input,
1129
2037
  output,
2038
+ ...(rendered ? { rendered } : {}),
2039
+ };
2040
+ }
2041
+
2042
+ function failedHostedToolCall(tools, request, error) {
2043
+ const tool = tools.find((candidate) => candidate.name === request.name);
2044
+
2045
+ return {
2046
+ id: crypto.randomUUID(),
2047
+ name: String(request.name || ""),
2048
+ visibility: tool?.visibility ?? "user",
2049
+ status: "failed",
2050
+ input: request.input ?? {},
2051
+ errorMessage: error instanceof Error ? error.message : String(error),
2052
+ };
2053
+ }
2054
+
2055
+ function renderHostedToolOutput(tool, input, output, runtime, clock) {
2056
+ if (!tool.render) {
2057
+ return undefined;
2058
+ }
2059
+
2060
+ const rendered = tool.render(output, {
2061
+ input,
2062
+ runtime,
2063
+ clock,
2064
+ });
2065
+
2066
+ if (typeof rendered === "string") {
2067
+ return { text: rendered };
2068
+ }
2069
+
2070
+ if (!isRecord(rendered) || typeof rendered.text !== "string" || rendered.text.trim().length === 0) {
2071
+ throw agentKitError("tool_validation_error", \`tool "\${tool.name}" render must return a non-empty text string.\`);
2072
+ }
2073
+
2074
+ if (rendered.blocks !== undefined) {
2075
+ if (!Array.isArray(rendered.blocks)) {
2076
+ throw agentKitError("tool_validation_error", \`tool "\${tool.name}" render.blocks must be an array when provided.\`);
2077
+ }
2078
+
2079
+ for (const [index, block] of rendered.blocks.entries()) {
2080
+ validateRenderBlock(block, \`tool "\${tool.name}" render.blocks[\${index}]\`);
2081
+ }
2082
+ }
2083
+
2084
+ return {
2085
+ text: rendered.text,
2086
+ ...(rendered.blocks ? { blocks: rendered.blocks } : {}),
1130
2087
  };
1131
2088
  }
1132
2089
 
2090
+ function validateRenderBlock(block, label) {
2091
+ if (!isRecord(block)) {
2092
+ throw agentKitError("tool_validation_error", \`\${label} must be an object.\`);
2093
+ }
2094
+
2095
+ if (block.type === "text") {
2096
+ if (typeof block.text !== "string" || block.text.trim().length === 0) {
2097
+ throw agentKitError("tool_validation_error", \`\${label}.text must be a non-empty string.\`);
2098
+ }
2099
+
2100
+ return;
2101
+ }
2102
+
2103
+ if (block.type === "card") {
2104
+ if (typeof block.title !== "string" || block.title.trim().length === 0) {
2105
+ throw agentKitError("tool_validation_error", \`\${label}.title must be a non-empty string.\`);
2106
+ }
2107
+
2108
+ validateOptionalString(block.subtitle, \`\${label}.subtitle\`);
2109
+ validateRenderFields(block.fields, \`\${label}.fields\`);
2110
+ validateRenderActions(block.actions, \`\${label}.actions\`);
2111
+ return;
2112
+ }
2113
+
2114
+ throw agentKitError("tool_validation_error", \`\${label}.type must be "text" or "card".\`);
2115
+ }
2116
+
2117
+ function validateRenderFields(value, label) {
2118
+ if (value === undefined) {
2119
+ return;
2120
+ }
2121
+
2122
+ if (!Array.isArray(value)) {
2123
+ throw agentKitError("tool_validation_error", \`\${label} must be an array when provided.\`);
2124
+ }
2125
+
2126
+ for (const [index, field] of value.entries()) {
2127
+ if (!isRecord(field)) {
2128
+ throw agentKitError("tool_validation_error", \`\${label}[\${index}] must be an object.\`);
2129
+ }
2130
+
2131
+ validateRequiredString(field.label, \`\${label}[\${index}].label\`);
2132
+ validateRequiredString(field.value, \`\${label}[\${index}].value\`);
2133
+ }
2134
+ }
2135
+
2136
+ function validateRenderActions(value, label) {
2137
+ if (value === undefined) {
2138
+ return;
2139
+ }
2140
+
2141
+ if (!Array.isArray(value)) {
2142
+ throw agentKitError("tool_validation_error", \`\${label} must be an array when provided.\`);
2143
+ }
2144
+
2145
+ for (const [index, action] of value.entries()) {
2146
+ if (!isRecord(action)) {
2147
+ throw agentKitError("tool_validation_error", \`\${label}[\${index}] must be an object.\`);
2148
+ }
2149
+
2150
+ validateRequiredString(action.label, \`\${label}[\${index}].label\`);
2151
+ validateOptionalString(action.href, \`\${label}[\${index}].href\`);
2152
+ }
2153
+ }
2154
+
2155
+ function validateRequiredString(value, label) {
2156
+ if (typeof value !== "string" || value.trim().length === 0) {
2157
+ throw agentKitError("tool_validation_error", \`\${label} must be a non-empty string.\`);
2158
+ }
2159
+ }
2160
+
2161
+ function validateOptionalString(value, label) {
2162
+ if (value !== undefined && typeof value !== "string") {
2163
+ throw agentKitError("tool_validation_error", \`\${label} must be a string when provided.\`);
2164
+ }
2165
+ }
2166
+
1133
2167
  function resolveToolSecrets(tool, env) {
1134
2168
  const secrets = {};
1135
2169
 
@@ -1170,6 +2204,7 @@ async function runToolWithTimeout(tool, input, context, timeoutMs) {
1170
2204
  secrets: context.secrets,
1171
2205
  signal: controller.signal,
1172
2206
  runtime: context.runtime,
2207
+ clock: context.clock,
1173
2208
  database: context.database,
1174
2209
  db: context.database,
1175
2210
  storage: {
@@ -1395,6 +2430,20 @@ function allowsNull(schema) {
1395
2430
  }
1396
2431
 
1397
2432
  async function authorizeChat(request, env, body) {
2433
+ return authorizeDeployAccess(request, env, body, {
2434
+ notConfiguredCode: "chat_access_not_configured",
2435
+ notConfiguredMessage: "Hosted chat requires a deploy access token gate.",
2436
+ });
2437
+ }
2438
+
2439
+ async function authorizeConversationRead(request, env) {
2440
+ return authorizeDeployAccess(request, env, {}, {
2441
+ notConfiguredCode: "conversation_access_not_configured",
2442
+ notConfiguredMessage: "Hosted conversation reads require a deploy access token gate.",
2443
+ });
2444
+ }
2445
+
2446
+ async function authorizeDeployAccess(request, env, body, options) {
1398
2447
  const runtimeGate = await authorizeRuntimeGate(request, env, body);
1399
2448
 
1400
2449
  if (runtimeGate) {
@@ -1405,52 +2454,28 @@ async function authorizeChat(request, env, body) {
1405
2454
  return runtimeGate;
1406
2455
  }
1407
2456
 
1408
- if (manifest.access.mode === "public") {
1409
- return null;
1410
- }
2457
+ const token = deployAccessToken(request, body);
2458
+ const acceptedTokens = [env.AGENTKIT_PRIVATE_TOKEN, env.AGENTKIT_ACCESS_TOKEN].filter(
2459
+ (value) => typeof value === "string" && value.length > 0,
2460
+ );
1411
2461
 
1412
- if (manifest.access.mode === "private") {
1413
- if (!env.AGENTKIT_PRIVATE_TOKEN) {
1414
- return Response.json(
1415
- {
1416
- error: {
1417
- code: "deploy_not_ready",
1418
- message: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set as a Cloudflare secret.",
1419
- },
2462
+ if (acceptedTokens.length === 0) {
2463
+ return Response.json(
2464
+ {
2465
+ error: {
2466
+ code: options.notConfiguredCode,
2467
+ message: options.notConfiguredMessage,
1420
2468
  },
1421
- { status: 503 },
1422
- );
1423
- }
1424
-
1425
- const expected = \`Bearer \${env.AGENTKIT_PRIVATE_TOKEN}\`;
1426
- if (request.headers.get("authorization") !== expected) {
1427
- return Response.json(
1428
- { error: { code: "unauthorized", message: "Missing or invalid deploy access token." } },
1429
- { status: 401 },
1430
- );
1431
- }
2469
+ },
2470
+ { status: 503 },
2471
+ );
1432
2472
  }
1433
2473
 
1434
- if (manifest.access.mode === "token") {
1435
- if (!env.AGENTKIT_ACCESS_TOKEN) {
1436
- return Response.json(
1437
- {
1438
- error: {
1439
- code: "deploy_not_ready",
1440
- message: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set as a Cloudflare secret.",
1441
- },
1442
- },
1443
- { status: 503 },
1444
- );
1445
- }
1446
-
1447
- const expected = \`Bearer \${env.AGENTKIT_ACCESS_TOKEN}\`;
1448
- if (request.headers.get("authorization") !== expected) {
1449
- return Response.json(
1450
- { error: { code: "unauthorized", message: "Missing or invalid deploy access token." } },
1451
- { status: 401 },
1452
- );
1453
- }
2474
+ if (!token || !acceptedTokens.includes(token)) {
2475
+ return Response.json(
2476
+ { error: { code: "unauthorized", message: "Missing or invalid deploy access token." } },
2477
+ { status: 401 },
2478
+ );
1454
2479
  }
1455
2480
 
1456
2481
  return null;
@@ -1552,8 +2577,7 @@ async function persist(env, input) {
1552
2577
  return {};
1553
2578
  }
1554
2579
 
1555
- const id = env.AGENTKIT_CONVERSATIONS.idFromName(input.conversationId);
1556
- const stub = env.AGENTKIT_CONVERSATIONS.get(id);
2580
+ const stub = hostedConversationStore(env);
1557
2581
  const response = await stub.fetch("https://agentkit.internal/messages", {
1558
2582
  method: "POST",
1559
2583
  body: JSON.stringify(input),
@@ -1566,6 +2590,148 @@ async function persist(env, input) {
1566
2590
  return response.json();
1567
2591
  }
1568
2592
 
2593
+ function queueRuntimeUsage(ctx, promise) {
2594
+ const safePromise = promise.catch(() => undefined);
2595
+
2596
+ if (ctx && typeof ctx.waitUntil === "function") {
2597
+ ctx.waitUntil(safePromise);
2598
+ return;
2599
+ }
2600
+
2601
+ void safePromise;
2602
+ }
2603
+
2604
+ async function sendRuntimeUsage(env, input) {
2605
+ if (!env.AGENTKIT_CLOUD_API_URL || !env.AGENTKIT_DEPLOY_ID || !env.AGENTKIT_RUNTIME_GATE_TOKEN) {
2606
+ return;
2607
+ }
2608
+
2609
+ const usage = normalizeRuntimeUsage(input.usage);
2610
+ const metadata = isRecord(input.metadata) ? input.metadata : {};
2611
+ const body = {
2612
+ kind: input.kind || "chat_run",
2613
+ status: input.status || "succeeded",
2614
+ ...(input.runId ? { run_id: String(input.runId) } : {}),
2615
+ ...(input.conversationId ? { conversation_id: String(input.conversationId) } : {}),
2616
+ ...(input.provider ? { provider: String(input.provider) } : {}),
2617
+ ...(input.model ? { model: String(input.model) } : {}),
2618
+ ...(usage.inputTokens !== undefined ? { input_tokens: usage.inputTokens } : {}),
2619
+ ...(usage.outputTokens !== undefined ? { output_tokens: usage.outputTokens } : {}),
2620
+ ...(usage.totalTokens !== undefined ? { total_tokens: usage.totalTokens } : {}),
2621
+ ...(usage.cost && Object.keys(usage.cost).length > 0 ? { cost: usage.cost } : {}),
2622
+ ...(usage.cost?.total !== undefined ? { cost_usd: usage.cost.total } : {}),
2623
+ metadata: sanitizeRuntimeUsageMetadata(metadata),
2624
+ };
2625
+
2626
+ const url = new URL(
2627
+ "/v1/runtime/deploys/" + encodeURIComponent(String(env.AGENTKIT_DEPLOY_ID)) + "/usage",
2628
+ env.AGENTKIT_CLOUD_API_URL,
2629
+ );
2630
+ await fetch(url.href, {
2631
+ method: "POST",
2632
+ headers: {
2633
+ Authorization: "Bearer " + env.AGENTKIT_RUNTIME_GATE_TOKEN,
2634
+ "Content-Type": "application/json",
2635
+ },
2636
+ body: JSON.stringify(body),
2637
+ });
2638
+ }
2639
+
2640
+ function sanitizeRuntimeUsageMetadata(value) {
2641
+ const metadata = {};
2642
+
2643
+ for (const [key, raw] of Object.entries(value)) {
2644
+ if (!/^[A-Za-z0-9_.:-]{1,80}$/.test(key)) {
2645
+ continue;
2646
+ }
2647
+
2648
+ if (typeof raw === "string") {
2649
+ metadata[key] = raw.length > 160 ? raw.slice(0, 160) : raw;
2650
+ } else if (typeof raw === "number" && Number.isFinite(raw)) {
2651
+ metadata[key] = raw;
2652
+ } else if (typeof raw === "boolean") {
2653
+ metadata[key] = raw;
2654
+ }
2655
+ }
2656
+
2657
+ return metadata;
2658
+ }
2659
+
2660
+ async function loadConversationMessages(env, conversationId) {
2661
+ if (!env.AGENTKIT_CONVERSATIONS || !conversationId) {
2662
+ return [];
2663
+ }
2664
+
2665
+ const stub = hostedConversationStore(env);
2666
+ const response = await stub.fetch(
2667
+ \`https://agentkit.internal/messages?conversationId=\${encodeURIComponent(conversationId)}\`,
2668
+ );
2669
+
2670
+ if (!response.ok) {
2671
+ return [];
2672
+ }
2673
+
2674
+ const payload = await response.json();
2675
+ const messages = Array.isArray(payload.messages)
2676
+ ? payload.messages.filter((message) =>
2677
+ message &&
2678
+ typeof message === "object" &&
2679
+ (message.role === "user" || message.role === "assistant") &&
2680
+ typeof message.content === "string"
2681
+ )
2682
+ : [];
2683
+
2684
+ if (messages.length > 0) {
2685
+ return messages;
2686
+ }
2687
+
2688
+ const legacyStub = legacyConversationStore(env, conversationId);
2689
+ const legacyResponse = await legacyStub.fetch(
2690
+ \`https://agentkit.internal/messages?conversationId=\${encodeURIComponent(conversationId)}\`,
2691
+ );
2692
+
2693
+ if (!legacyResponse.ok) {
2694
+ return [];
2695
+ }
2696
+
2697
+ const legacyPayload = await legacyResponse.json();
2698
+ return Array.isArray(legacyPayload.messages)
2699
+ ? legacyPayload.messages.filter((message) =>
2700
+ message &&
2701
+ typeof message === "object" &&
2702
+ (message.role === "user" || message.role === "assistant") &&
2703
+ typeof message.content === "string"
2704
+ )
2705
+ : [];
2706
+ }
2707
+
2708
+ function hostedConversationStore(env) {
2709
+ const id = env.AGENTKIT_CONVERSATIONS.idFromName("agentkit-conversations");
2710
+ return env.AGENTKIT_CONVERSATIONS.get(id);
2711
+ }
2712
+
2713
+ function legacyConversationStore(env, conversationId) {
2714
+ const id = env.AGENTKIT_CONVERSATIONS.idFromName(conversationId);
2715
+ return env.AGENTKIT_CONVERSATIONS.get(id);
2716
+ }
2717
+
2718
+ function normalizeConversationId(value) {
2719
+ if (value === undefined || value === null) {
2720
+ return null;
2721
+ }
2722
+
2723
+ if (typeof value !== "string") {
2724
+ throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
2725
+ }
2726
+
2727
+ const conversationId = value.trim();
2728
+ if (!conversationId) {
2729
+ throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
2730
+ }
2731
+
2732
+ return conversationId;
2733
+ }
2734
+
1569
2735
  function agentKitError(code, message) {
1570
2736
  const error = new Error(message);
1571
2737
  error.code = code;
@@ -1823,6 +2989,80 @@ function titleFromMessage(content) {
1823
2989
  const compact = content.split(/\\r?\\n/, 1)[0].replace(/\\s+/g, " ").trim();
1824
2990
  return compact.length <= 80 ? compact : \`\${compact.slice(0, 77)}...\`;
1825
2991
  }
2992
+
2993
+ function readStoredUsage(row) {
2994
+ const usage = {};
2995
+ const inputTokens = normalizeRuntimeInteger(row.input_tokens);
2996
+ const outputTokens = normalizeRuntimeInteger(row.output_tokens);
2997
+ const totalTokens = normalizeRuntimeInteger(row.total_tokens);
2998
+ const cost = normalizeRuntimeCost(parseStoredJson(row.cost_json, {}));
2999
+
3000
+ if (inputTokens !== undefined) usage.inputTokens = inputTokens;
3001
+ if (outputTokens !== undefined) usage.outputTokens = outputTokens;
3002
+ if (totalTokens !== undefined) usage.totalTokens = totalTokens;
3003
+ if (Object.keys(cost).length > 0) usage.cost = cost;
3004
+
3005
+ return usage;
3006
+ }
3007
+
3008
+ function normalizeRuntimeUsage(value) {
3009
+ if (!isRecord(value)) {
3010
+ return {};
3011
+ }
3012
+
3013
+ const usage = {};
3014
+ const inputTokens = normalizeRuntimeInteger(value.inputTokens ?? value.input_tokens);
3015
+ const outputTokens = normalizeRuntimeInteger(value.outputTokens ?? value.output_tokens);
3016
+ const totalTokens = normalizeRuntimeInteger(value.totalTokens ?? value.total_tokens);
3017
+ const cost = normalizeRuntimeCost(value.cost);
3018
+
3019
+ if (inputTokens !== undefined) usage.inputTokens = inputTokens;
3020
+ if (outputTokens !== undefined) usage.outputTokens = outputTokens;
3021
+ if (totalTokens !== undefined) usage.totalTokens = totalTokens;
3022
+ if (Object.keys(cost).length > 0) usage.cost = cost;
3023
+
3024
+ return usage;
3025
+ }
3026
+
3027
+ function normalizeRuntimeCost(value) {
3028
+ if (!isRecord(value)) {
3029
+ return {};
3030
+ }
3031
+
3032
+ const cost = {};
3033
+
3034
+ for (const field of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
3035
+ const normalized = normalizeRuntimeNumber(value[field]);
3036
+
3037
+ if (normalized !== undefined) {
3038
+ cost[field] = normalized;
3039
+ }
3040
+ }
3041
+
3042
+ return cost;
3043
+ }
3044
+
3045
+ function normalizeRuntimeInteger(value) {
3046
+ const normalized = normalizeRuntimeNumber(value);
3047
+ return normalized !== undefined && Number.isInteger(normalized) ? normalized : undefined;
3048
+ }
3049
+
3050
+ function normalizeRuntimeNumber(value) {
3051
+ const parsed = typeof value === "string" && value.trim() ? Number(value) : value;
3052
+ return typeof parsed === "number" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
3053
+ }
3054
+
3055
+ function parseStoredJson(value, fallback) {
3056
+ if (typeof value !== "string") {
3057
+ return fallback;
3058
+ }
3059
+
3060
+ try {
3061
+ return JSON.parse(value);
3062
+ } catch {
3063
+ return fallback;
3064
+ }
3065
+ }
1826
3066
  `;
1827
3067
  }
1828
3068
 
@@ -1893,6 +3133,10 @@ CREATE TABLE IF NOT EXISTS runs (
1893
3133
  status TEXT NOT NULL,
1894
3134
  error_code TEXT,
1895
3135
  error_message TEXT,
3136
+ input_tokens INTEGER,
3137
+ output_tokens INTEGER,
3138
+ total_tokens INTEGER,
3139
+ cost_json TEXT,
1896
3140
  created_at TEXT NOT NULL,
1897
3141
  completed_at TEXT
1898
3142
  );
@@ -1906,6 +3150,7 @@ CREATE TABLE IF NOT EXISTS tool_calls (
1906
3150
  status TEXT NOT NULL,
1907
3151
  input_json TEXT NOT NULL,
1908
3152
  output_json TEXT,
3153
+ rendered_json TEXT,
1909
3154
  error_message TEXT,
1910
3155
  created_at TEXT NOT NULL,
1911
3156
  completed_at TEXT