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

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