@andreprado/agentkit 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/bin/agentkit.mjs +23 -0
- package/docs/guides/add-channel.md +114 -0
- package/docs/guides/add-knowledge.md +134 -0
- package/docs/guides/add-tool.md +342 -0
- package/docs/guides/agentkit-skills-architecture.md +471 -0
- package/docs/guides/channel-security.md +81 -0
- package/docs/guides/channels-implementation-map.md +243 -0
- package/docs/guides/channels-production-handoff.md +102 -0
- package/docs/guides/connect-telegram.md +110 -0
- package/docs/guides/connect-whatsapp-zapster.md +119 -0
- package/docs/guides/create-agent.md +220 -0
- package/docs/guides/prepare-deploy.md +209 -0
- package/docs/guides/run-evals.md +179 -0
- package/docs/guides/security-rules.md +156 -0
- package/docs/guides/use-provider.md +140 -0
- package/docs/llms-full.txt +876 -0
- package/docs/llms.txt +83 -0
- package/docs/portable-deploy-release-checklist.md +41 -0
- package/package.json +47 -0
- package/src/cli/args.ts +36 -0
- package/src/cli/cloud-client.ts +265 -0
- package/src/cli/commands/channels.ts +810 -0
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/constants.ts +4 -0
- package/src/cli/deploy-chat-ui.ts +392 -0
- package/src/cli/deploy-readiness.ts +348 -0
- package/src/cli/flags.ts +162 -0
- package/src/cli/help.ts +184 -0
- package/src/cli/index.ts +1276 -0
- package/src/cli/process.ts +31 -0
- package/src/cloud/artifact.ts +139 -0
- package/src/cloud/client.ts +79 -0
- package/src/cloud/contracts.ts +63 -0
- package/src/cloud/index.ts +3 -0
- package/src/create-project.ts +177 -0
- package/src/index.ts +408 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/pi.ts +286 -0
- package/src/providers/test.ts +133 -0
- package/src/providers/types.ts +34 -0
- package/src/runtime/build.ts +43 -0
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channel-test-harness.ts +112 -0
- package/src/runtime/channels/telegram.ts +360 -0
- package/src/runtime/channels/website.ts +132 -0
- package/src/runtime/channels/whatsapp-meta.ts +71 -0
- package/src/runtime/channels/whatsapp-zapster.ts +278 -0
- package/src/runtime/channels.ts +138 -0
- package/src/runtime/chat.ts +218 -0
- package/src/runtime/config.ts +684 -0
- package/src/runtime/conversations.ts +38 -0
- package/src/runtime/core/deploy-state.ts +54 -0
- package/src/runtime/core/manifest.ts +213 -0
- package/src/runtime/core/targets.ts +133 -0
- package/src/runtime/database.ts +256 -0
- package/src/runtime/db-commands.ts +167 -0
- package/src/runtime/deploy-readiness.ts +105 -0
- package/src/runtime/deploy.ts +1 -0
- package/src/runtime/dev-server.ts +1247 -0
- package/src/runtime/docs.ts +36 -0
- package/src/runtime/env.ts +152 -0
- package/src/runtime/errors.ts +13 -0
- package/src/runtime/evals.ts +509 -0
- package/src/runtime/inspect.ts +203 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +283 -0
- package/src/runtime/knowledge/schema.ts +56 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/runtime-contract.ts +93 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +2517 -0
- package/src/runtime/targets/container/build.ts +146 -0
- package/src/runtime/targets/container/server.ts +33 -0
- package/src/runtime/targets/vps/deploy.ts +206 -0
- package/src/runtime/tool-runner.ts +65 -0
- package/src/runtime/tools.ts +470 -0
- package/src/runtime/traces.ts +41 -0
- package/src/storage/sqlite.ts +1118 -0
- package/src/templates/blank.ts +394 -0
- package/src/templates/dentista.ts +1003 -0
- package/src/templates/index.ts +33 -0
- package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +41 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +38 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +44 -0
- package/src/templates/skills/agentkit-database/SKILL.md +45 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
- package/src/templates/skills/agentkit-security/SKILL.md +55 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
- package/src/templates/support.ts +401 -0
|
@@ -0,0 +1,2517 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { build as esbuild, type Plugin } from "esbuild";
|
|
7
|
+
|
|
8
|
+
import { loadAgentCapsule, type LoadedAgentCapsule } from "../../config";
|
|
9
|
+
import { buildSharedAgentManifest, type ManifestSchema, type SharedAgentManifest } from "../../core/manifest";
|
|
10
|
+
import { AgentKitError } from "../../errors";
|
|
11
|
+
import { resolveKnowledgeConfig } from "../../knowledge/config";
|
|
12
|
+
import { KNOWLEDGE_SCHEMA_SQL } from "../../knowledge/schema";
|
|
13
|
+
|
|
14
|
+
export type BuildTarget = "cloudflare";
|
|
15
|
+
|
|
16
|
+
export type AgentBuildResult = {
|
|
17
|
+
target: BuildTarget;
|
|
18
|
+
root: string;
|
|
19
|
+
buildDir: string;
|
|
20
|
+
manifestPath: string;
|
|
21
|
+
workerPath: string;
|
|
22
|
+
wranglerPath: string;
|
|
23
|
+
durableObjectSchemaPath: string;
|
|
24
|
+
tursoSchemaPath: string | null;
|
|
25
|
+
bundleSha256: string;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AgentManifest = {
|
|
30
|
+
schema_version: SharedAgentManifest["schema_version"];
|
|
31
|
+
name: SharedAgentManifest["name"];
|
|
32
|
+
runtime: "edge";
|
|
33
|
+
target: "cloudflare";
|
|
34
|
+
provider: SharedAgentManifest["provider"];
|
|
35
|
+
prompt: SharedAgentManifest["prompt"];
|
|
36
|
+
secrets: SharedAgentManifest["secrets"];
|
|
37
|
+
tools: SharedAgentManifest["tools"];
|
|
38
|
+
channels: SharedAgentManifest["channels"];
|
|
39
|
+
knowledge: SharedAgentManifest["knowledge"];
|
|
40
|
+
access: SharedAgentManifest["access"];
|
|
41
|
+
storage: {
|
|
42
|
+
driver: "agentkit";
|
|
43
|
+
database: SharedAgentManifest["storage"]["database"];
|
|
44
|
+
files:
|
|
45
|
+
| {
|
|
46
|
+
driver: "none";
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
driver: "r2";
|
|
50
|
+
binding: "AGENTKIT_FILES";
|
|
51
|
+
bucket: string;
|
|
52
|
+
prefix: string;
|
|
53
|
+
maxUploadBytes: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type AgentManifestWithBundle = AgentManifest & {
|
|
59
|
+
bundle: {
|
|
60
|
+
content_type: "application/javascript";
|
|
61
|
+
sha256: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const CLOUDFLARE_BUILD_DIR = ".agentkit/build/cloudflare";
|
|
66
|
+
const AGENTKIT_BUILD_DIR = ".agentkit/build";
|
|
67
|
+
const CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-27";
|
|
68
|
+
const CLOUDFLARE_COMPATIBILITY_FLAGS = ["nodejs_compat"] as const;
|
|
69
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
70
|
+
|
|
71
|
+
export async function buildCloudflareAgentCapsule(
|
|
72
|
+
cwd = process.cwd(),
|
|
73
|
+
): Promise<AgentBuildResult> {
|
|
74
|
+
const capsule = await loadAgentCapsule(cwd);
|
|
75
|
+
validateCloudflareBuild(capsule);
|
|
76
|
+
await assertEdgeCompatibleImports(capsule);
|
|
77
|
+
|
|
78
|
+
const tursoSchema = await loadTursoSchema(capsule);
|
|
79
|
+
const manifest = buildManifest(capsule, tursoSchema);
|
|
80
|
+
const warnings = buildWarnings(capsule);
|
|
81
|
+
const agentkitBuildDir = join(capsule.root, AGENTKIT_BUILD_DIR);
|
|
82
|
+
const cloudflareBuildDir = join(capsule.root, CLOUDFLARE_BUILD_DIR);
|
|
83
|
+
const manifestPath = join(agentkitBuildDir, "agent.manifest.json");
|
|
84
|
+
const workerEntryPath = join(cloudflareBuildDir, "worker.entry.ts");
|
|
85
|
+
const workerPath = join(cloudflareBuildDir, "worker.js");
|
|
86
|
+
const wranglerPath = join(cloudflareBuildDir, "wrangler.jsonc");
|
|
87
|
+
const durableObjectSchemaPath = join(cloudflareBuildDir, "durable-object-schema.sql");
|
|
88
|
+
const tursoSchemaPath = tursoSchema ? join(cloudflareBuildDir, "turso-schema.sql") : null;
|
|
89
|
+
const bundleConfig = await writeEdgeBundleConfig(capsule);
|
|
90
|
+
|
|
91
|
+
await mkdir(agentkitBuildDir, { recursive: true });
|
|
92
|
+
await mkdir(cloudflareBuildDir, { recursive: true });
|
|
93
|
+
try {
|
|
94
|
+
const workerEntrySource = renderCloudflareWorker({
|
|
95
|
+
manifest,
|
|
96
|
+
instructions: capsule.instructions,
|
|
97
|
+
configPath: bundleConfig.configPath,
|
|
98
|
+
});
|
|
99
|
+
await writeFile(workerEntryPath, workerEntrySource);
|
|
100
|
+
await bundleCloudflareWorker(workerEntryPath, workerPath, bundleConfig.shimPath);
|
|
101
|
+
} finally {
|
|
102
|
+
await Promise.all(bundleConfig.cleanupPaths.map((path) => unlink(path).catch(() => undefined)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const workerSource = await readFile(workerPath, "utf8");
|
|
106
|
+
const bundleSha256 = sha256(workerSource);
|
|
107
|
+
const manifestWithBundle: AgentManifestWithBundle = {
|
|
108
|
+
...manifest,
|
|
109
|
+
bundle: {
|
|
110
|
+
content_type: "application/javascript",
|
|
111
|
+
sha256: bundleSha256,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await writeFile(manifestPath, `${JSON.stringify(manifestWithBundle, null, 2)}\n`);
|
|
116
|
+
await writeFile(wranglerPath, renderWranglerConfig(capsule.config.name, manifest.storage.files));
|
|
117
|
+
await writeFile(durableObjectSchemaPath, renderDurableObjectSchema());
|
|
118
|
+
if (tursoSchemaPath && tursoSchema) {
|
|
119
|
+
await writeFile(tursoSchemaPath, tursoSchema.source);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
target: "cloudflare",
|
|
124
|
+
root: capsule.root,
|
|
125
|
+
buildDir: cloudflareBuildDir,
|
|
126
|
+
manifestPath,
|
|
127
|
+
workerPath,
|
|
128
|
+
wranglerPath,
|
|
129
|
+
durableObjectSchemaPath,
|
|
130
|
+
tursoSchemaPath,
|
|
131
|
+
bundleSha256,
|
|
132
|
+
warnings,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateCloudflareBuild(capsule: LoadedAgentCapsule): void {
|
|
137
|
+
if (capsule.config.runtime !== "edge") {
|
|
138
|
+
throw new AgentKitError(
|
|
139
|
+
"build_not_edge",
|
|
140
|
+
'Cloudflare deploy requires runtime: "edge" in agentkit.config.ts.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (capsule.config.storage.driver !== "agentkit") {
|
|
145
|
+
throw new AgentKitError(
|
|
146
|
+
"build_storage_driver",
|
|
147
|
+
'Cloudflare deploy requires storage.driver: "agentkit" so hosted state is managed by AgentKit Cloud.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const knowledge = resolveKnowledgeConfig(capsule.config);
|
|
152
|
+
|
|
153
|
+
if (knowledge.enabled && capsule.config.storage.database?.driver !== "turso") {
|
|
154
|
+
throw new AgentKitError(
|
|
155
|
+
"build_knowledge_database",
|
|
156
|
+
'Cloudflare Knowledge deploy requires storage.database.driver: "turso" so chunks and retrieval state are available to the hosted runtime.',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildWarnings(capsule: LoadedAgentCapsule): string[] {
|
|
162
|
+
const warnings: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (capsule.config.access.mode === "private") {
|
|
165
|
+
warnings.push(
|
|
166
|
+
"Private access requires setting AGENTKIT_PRIVATE_TOKEN as a Cloudflare secret before /chat accepts requests.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (capsule.config.storage.database?.driver === "turso") {
|
|
171
|
+
warnings.push(
|
|
172
|
+
"AgentKit Cloud will provision Turso and inject TURSO_DATABASE_URL/TURSO_AUTH_TOKEN; only --local-wrangler deploys must provide them manually.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (capsule.config.storage.files?.driver === "r2") {
|
|
177
|
+
warnings.push("R2 file storage requires the AGENTKIT_FILES binding in wrangler.jsonc to point at an existing bucket.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return warnings;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildManifest(
|
|
184
|
+
capsule: LoadedAgentCapsule,
|
|
185
|
+
tursoSchema: ManifestSchema | null,
|
|
186
|
+
): AgentManifest {
|
|
187
|
+
const shared = buildSharedAgentManifest(capsule, { databaseSchema: tursoSchema });
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
schema_version: shared.schema_version,
|
|
191
|
+
name: shared.name,
|
|
192
|
+
runtime: "edge",
|
|
193
|
+
target: "cloudflare",
|
|
194
|
+
provider: shared.provider,
|
|
195
|
+
prompt: shared.prompt,
|
|
196
|
+
secrets: shared.secrets,
|
|
197
|
+
tools: shared.tools,
|
|
198
|
+
channels: shared.channels,
|
|
199
|
+
knowledge: shared.knowledge,
|
|
200
|
+
access: shared.access,
|
|
201
|
+
storage: {
|
|
202
|
+
driver: "agentkit",
|
|
203
|
+
database: shared.storage.database,
|
|
204
|
+
files: buildCloudflareFilesManifest(shared.storage.files),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildCloudflareFilesManifest(files: SharedAgentManifest["storage"]["files"]): AgentManifest["storage"]["files"] {
|
|
210
|
+
if (files.driver === "none") {
|
|
211
|
+
return { driver: "none" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
driver: "r2",
|
|
216
|
+
binding: "AGENTKIT_FILES",
|
|
217
|
+
bucket: files.bucket,
|
|
218
|
+
prefix: files.prefix,
|
|
219
|
+
maxUploadBytes: files.maxUploadBytes,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function loadTursoSchema(capsule: LoadedAgentCapsule): Promise<ManifestSchema | null> {
|
|
224
|
+
const database = capsule.config.storage.database;
|
|
225
|
+
const knowledge = resolveKnowledgeConfig(capsule.config);
|
|
226
|
+
const sources: string[] = [];
|
|
227
|
+
|
|
228
|
+
if (!database || database.driver !== "turso") {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (database.schema) {
|
|
233
|
+
sources.push(await readFile(resolve(capsule.root, database.schema), "utf8"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (knowledge.enabled) {
|
|
237
|
+
sources.push(KNOWLEDGE_SCHEMA_SQL);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (sources.length === 0) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const source = sources.map((part) => part.trim()).filter(Boolean).join("\n\n") + "\n";
|
|
245
|
+
const digest = sha256(source);
|
|
246
|
+
return { source, sha256: digest };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const EDGE_INCOMPATIBLE_IMPORTS = new Map<string, string>([
|
|
250
|
+
["bun:sqlite", "Use ctx.db inside tools instead of importing SQLite or Bun database APIs."],
|
|
251
|
+
["node:fs", "Cloudflare Workers cannot read local files. Move static data into config, prompts, files, or a tool input."],
|
|
252
|
+
["fs", "Cloudflare Workers cannot read local files. Move static data into config, prompts, files, or a tool input."],
|
|
253
|
+
["node:path", "Cloudflare Workers do not provide Node path APIs in tool bundles. Avoid filesystem path logic in edge tools."],
|
|
254
|
+
["path", "Cloudflare Workers do not provide Node path APIs in tool bundles. Avoid filesystem path logic in edge tools."],
|
|
255
|
+
["node:net", "Cloudflare Workers cannot open raw TCP sockets from tools. Use fetch-compatible APIs."],
|
|
256
|
+
["net", "Cloudflare Workers cannot open raw TCP sockets from tools. Use fetch-compatible APIs."],
|
|
257
|
+
["node:child_process", "Cloudflare Workers cannot spawn local processes. Move this work to a local/container target."],
|
|
258
|
+
["child_process", "Cloudflare Workers cannot spawn local processes. Move this work to a local/container target."],
|
|
259
|
+
["better-sqlite3", "Use ctx.db inside tools instead of importing native database clients."],
|
|
260
|
+
["sqlite3", "Use ctx.db inside tools instead of importing native database clients."],
|
|
261
|
+
["libsql", "Use ctx.db inside tools so AgentKit can route local SQLite and hosted Turso consistently."],
|
|
262
|
+
["@libsql/client", "Use ctx.db inside tools so AgentKit can route local SQLite and hosted Turso consistently."],
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
async function assertEdgeCompatibleImports(capsule: LoadedAgentCapsule): Promise<void> {
|
|
266
|
+
const files = await listSourceFiles(capsule.root);
|
|
267
|
+
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const source = await readFile(file, "utf8");
|
|
270
|
+
|
|
271
|
+
for (const specifier of findImportSpecifiers(source)) {
|
|
272
|
+
const guidance = edgeImportGuidance(specifier);
|
|
273
|
+
|
|
274
|
+
if (!guidance) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw new AgentKitError(
|
|
279
|
+
"edge_incompatible_import",
|
|
280
|
+
[
|
|
281
|
+
`Cloudflare build cannot include import "${specifier}" from ${relative(capsule.root, file)}.`,
|
|
282
|
+
`Target runtime: cloudflare.`,
|
|
283
|
+
guidance,
|
|
284
|
+
].join(" "),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function listSourceFiles(root: string): Promise<string[]> {
|
|
291
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
292
|
+
const files: string[] = [];
|
|
293
|
+
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
if (entry.name === "node_modules" || entry.name === ".agentkit" || entry.name === ".git") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const path = join(root, entry.name);
|
|
300
|
+
|
|
301
|
+
if (entry.isDirectory()) {
|
|
302
|
+
files.push(...(await listSourceFiles(path)));
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (entry.isFile() && [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(extname(entry.name))) {
|
|
307
|
+
files.push(path);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return files.sort();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findImportSpecifiers(source: string): string[] {
|
|
315
|
+
const specifiers: string[] = [];
|
|
316
|
+
const patterns = [
|
|
317
|
+
/\bimport\s+(?:type\s+)?(?:[^"'()]*?\s+from\s+)?["']([^"']+)["']/g,
|
|
318
|
+
/\bexport\s+[^"'()]*?\s+from\s+["']([^"']+)["']/g,
|
|
319
|
+
/\brequire\(\s*["']([^"']+)["']\s*\)/g,
|
|
320
|
+
/\bimport\(\s*["']([^"']+)["']\s*\)/g,
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const pattern of patterns) {
|
|
324
|
+
for (const match of source.matchAll(pattern)) {
|
|
325
|
+
if (match[1]) {
|
|
326
|
+
specifiers.push(match[1]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return specifiers;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function edgeImportGuidance(specifier: string): string | null {
|
|
335
|
+
if (EDGE_INCOMPATIBLE_IMPORTS.has(specifier)) {
|
|
336
|
+
return EDGE_INCOMPATIBLE_IMPORTS.get(specifier) ?? null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const [blocked, guidance] of EDGE_INCOMPATIBLE_IMPORTS) {
|
|
340
|
+
if (specifier === blocked || specifier.startsWith(`${blocked}/`)) {
|
|
341
|
+
return guidance;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function writeEdgeBundleConfig(
|
|
349
|
+
capsule: LoadedAgentCapsule,
|
|
350
|
+
): Promise<{ configPath: string; shimPath: string; cleanupPaths: string[] }> {
|
|
351
|
+
const source = await readFile(capsule.configPath, "utf8");
|
|
352
|
+
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
353
|
+
const shimPath = join(dirname(capsule.configPath), `.agentkit.edge-shim-${suffix}.ts`);
|
|
354
|
+
const bundleConfigPath = join(dirname(capsule.configPath), `.agentkit.config.edge-bundle-${suffix}.ts`);
|
|
355
|
+
const localAgentKitPath = resolve(moduleDir, "../../../index.ts");
|
|
356
|
+
const shimImport = JSON.stringify(`./${relative(dirname(capsule.configPath), shimPath)}`);
|
|
357
|
+
const patchedSource = source
|
|
358
|
+
.replace(
|
|
359
|
+
/from\s+(["'])file:\/\/([^"']+)\1/g,
|
|
360
|
+
(_match, quote: string, path: string) => `from ${quote}${decodeURIComponent(path)}${quote}`,
|
|
361
|
+
)
|
|
362
|
+
.replace(/from\s+["']@andreprado\/agentkit["']/g, `from ${shimImport}`)
|
|
363
|
+
.replace(
|
|
364
|
+
new RegExp(`from\\s+["']${escapeRegExp(localAgentKitPath)}["']`, "g"),
|
|
365
|
+
`from ${shimImport}`,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
await writeFile(shimPath, renderEdgeShim());
|
|
369
|
+
await writeFile(bundleConfigPath, patchedSource);
|
|
370
|
+
return { configPath: bundleConfigPath, shimPath, cleanupPaths: [bundleConfigPath, shimPath] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function renderEdgeShim(): string {
|
|
374
|
+
return `export function defineAgent(config) {
|
|
375
|
+
return config;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function defineTool(tool) {
|
|
379
|
+
return tool;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function websiteChannel(input) {
|
|
383
|
+
return {
|
|
384
|
+
type: "website",
|
|
385
|
+
provider: "agentkit",
|
|
386
|
+
name: input.name,
|
|
387
|
+
access: input.access ?? { mode: "token" },
|
|
388
|
+
secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"], "website channel"),
|
|
389
|
+
...(input.limits ? { limits: input.limits } : {}),
|
|
390
|
+
...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "website channel") } : {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function telegramChannel(input) {
|
|
395
|
+
return {
|
|
396
|
+
type: "telegram",
|
|
397
|
+
provider: "telegram",
|
|
398
|
+
name: input.name,
|
|
399
|
+
secrets: normalizeChannelSecrets(input.secrets ?? ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"], "telegram channel"),
|
|
400
|
+
allowedUpdates: input.allowedUpdates ?? ["message"],
|
|
401
|
+
...(input.limits ? { limits: input.limits } : {}),
|
|
402
|
+
...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "telegram channel") } : {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function whatsappChannel(input) {
|
|
407
|
+
return {
|
|
408
|
+
type: "whatsapp",
|
|
409
|
+
provider: input.provider,
|
|
410
|
+
name: input.name,
|
|
411
|
+
secrets: normalizeChannelSecrets(input.secrets ?? defaultWhatsappChannelSecrets(input.provider), \`\${input.provider} whatsapp channel\`),
|
|
412
|
+
...(input.limits ? { limits: input.limits } : {}),
|
|
413
|
+
...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, \`\${input.provider} whatsapp channel\`) } : {}),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function defaultWhatsappChannelSecrets(provider) {
|
|
418
|
+
if (provider === "zapster") {
|
|
419
|
+
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function normalizeChannelSecrets(secrets, context) {
|
|
426
|
+
const uniqueSecrets = new Set();
|
|
427
|
+
|
|
428
|
+
for (const secret of secrets) {
|
|
429
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(secret)) {
|
|
430
|
+
throw new Error(\`\${context} secrets must be environment variable names, not secret values.\`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
uniqueSecrets.add(secret);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return Array.from(uniqueSecrets).sort();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function normalizeChannelBuffer(input, context) {
|
|
440
|
+
if (input.mode === "off") {
|
|
441
|
+
return { mode: "off" };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (input.mode !== undefined && input.mode !== "debounce") {
|
|
445
|
+
throw new Error(\`\${context} buffer.mode must be "off" or "debounce".\`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const buffer = {
|
|
449
|
+
mode: "debounce",
|
|
450
|
+
quietWindowMs: input.quietWindowMs ?? 2500,
|
|
451
|
+
maxWaitMs: input.maxWaitMs ?? 12000,
|
|
452
|
+
maxMessages: input.maxMessages ?? 20,
|
|
453
|
+
maxChars: input.maxChars ?? 8000,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
for (const field of ["quietWindowMs", "maxWaitMs", "maxMessages", "maxChars"]) {
|
|
457
|
+
if (!Number.isInteger(buffer[field]) || buffer[field] <= 0) {
|
|
458
|
+
throw new Error(\`\${context} buffer.\${field} must be a positive integer.\`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (buffer.maxWaitMs < buffer.quietWindowMs) {
|
|
463
|
+
throw new Error(\`\${context} buffer.maxWaitMs must be greater than or equal to buffer.quietWindowMs.\`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return buffer;
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function escapeRegExp(value: string): string {
|
|
472
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function bundleCloudflareWorker(entryPath: string, workerPath: string, edgeShimPath: string): Promise<void> {
|
|
476
|
+
const localAgentKitPath = resolve(moduleDir, "../../../index.ts");
|
|
477
|
+
const localAgentKitFileUrl = pathToFileURL(localAgentKitPath).href;
|
|
478
|
+
const localAgentKitImportPattern = new RegExp(
|
|
479
|
+
`^(?:${escapeRegExp(localAgentKitPath)}|${escapeRegExp(localAgentKitFileUrl)})$`,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const result = await esbuild({
|
|
484
|
+
entryPoints: [entryPath],
|
|
485
|
+
bundle: true,
|
|
486
|
+
platform: "browser",
|
|
487
|
+
format: "esm",
|
|
488
|
+
target: "es2022",
|
|
489
|
+
external: ["cloudflare:workers"],
|
|
490
|
+
minify: true,
|
|
491
|
+
write: false,
|
|
492
|
+
logLevel: "silent",
|
|
493
|
+
plugins: [
|
|
494
|
+
{
|
|
495
|
+
name: "agentkit-edge-shim",
|
|
496
|
+
setup(build) {
|
|
497
|
+
build.onResolve({ filter: /^@andreprado\/agentkit$/ }, () => ({
|
|
498
|
+
path: edgeShimPath,
|
|
499
|
+
}));
|
|
500
|
+
build.onResolve({ filter: localAgentKitImportPattern }, () => ({
|
|
501
|
+
path: edgeShimPath,
|
|
502
|
+
}));
|
|
503
|
+
},
|
|
504
|
+
} satisfies Plugin,
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const output = result.outputFiles?.[0];
|
|
509
|
+
|
|
510
|
+
if (!output) {
|
|
511
|
+
throw new Error("No bundle was emitted.");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const processShim = "var process = globalThis.process ?? (globalThis.process = { env: {}, versions: {} });\n";
|
|
515
|
+
await writeFile(workerPath, processShim + output.text);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw new AgentKitError(
|
|
518
|
+
"edge_bundle_failed",
|
|
519
|
+
`Could not bundle Agent Capsule for Cloudflare Workers. ${formatBuildError(error)}`,
|
|
520
|
+
{ cause: error },
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function formatBuildError(error: unknown): string {
|
|
526
|
+
if (error && typeof error === "object" && "errors" in error && Array.isArray(error.errors)) {
|
|
527
|
+
const messages = error.errors.map((log) => String(log?.text ?? log?.message ?? "")).filter(Boolean);
|
|
528
|
+
if (messages.length > 0) {
|
|
529
|
+
return messages.join("\n");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return error instanceof Error ? error.message : String(error);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderCloudflareWorker(input: { manifest: AgentManifest; instructions: string; configPath: string }): string {
|
|
537
|
+
const manifestJson = JSON.stringify(input.manifest, null, 2);
|
|
538
|
+
const instructionsJson = JSON.stringify(input.instructions);
|
|
539
|
+
const configImportPath = JSON.stringify(input.configPath);
|
|
540
|
+
const providerImportPath = JSON.stringify(resolve(moduleDir, "../../../providers/index.ts"));
|
|
541
|
+
|
|
542
|
+
return `import { DurableObject } from "cloudflare:workers";
|
|
543
|
+
import agentConfig from ${configImportPath};
|
|
544
|
+
import { getProviderAdapter } from ${providerImportPath};
|
|
545
|
+
|
|
546
|
+
const manifest = ${manifestJson};
|
|
547
|
+
const instructions = ${instructionsJson};
|
|
548
|
+
const hostedKnowledgeTool = manifest.knowledge.enabled ? createHostedKnowledgeSearchTool() : null;
|
|
549
|
+
const hostedTools = hostedKnowledgeTool ? [hostedKnowledgeTool, ...(agentConfig.tools ?? [])] : (agentConfig.tools ?? []);
|
|
550
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 30000;
|
|
551
|
+
|
|
552
|
+
export class AgentKitConversationStore extends DurableObject {
|
|
553
|
+
constructor(ctx, env) {
|
|
554
|
+
super(ctx, env);
|
|
555
|
+
this.sql = ctx.storage.sql;
|
|
556
|
+
this.sql.exec(\`
|
|
557
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
558
|
+
id TEXT PRIMARY KEY,
|
|
559
|
+
agent_name TEXT NOT NULL,
|
|
560
|
+
title TEXT,
|
|
561
|
+
created_at TEXT NOT NULL,
|
|
562
|
+
updated_at TEXT NOT NULL
|
|
563
|
+
);
|
|
564
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
565
|
+
id TEXT PRIMARY KEY,
|
|
566
|
+
conversation_id TEXT NOT NULL,
|
|
567
|
+
role TEXT NOT NULL,
|
|
568
|
+
content TEXT NOT NULL,
|
|
569
|
+
created_at TEXT NOT NULL
|
|
570
|
+
);
|
|
571
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
572
|
+
id TEXT PRIMARY KEY,
|
|
573
|
+
conversation_id TEXT NOT NULL,
|
|
574
|
+
provider TEXT NOT NULL,
|
|
575
|
+
model TEXT NOT NULL,
|
|
576
|
+
status TEXT NOT NULL,
|
|
577
|
+
error_code TEXT,
|
|
578
|
+
error_message TEXT,
|
|
579
|
+
created_at TEXT NOT NULL,
|
|
580
|
+
completed_at TEXT
|
|
581
|
+
);
|
|
582
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
583
|
+
id TEXT PRIMARY KEY,
|
|
584
|
+
run_id TEXT NOT NULL,
|
|
585
|
+
conversation_id TEXT NOT NULL,
|
|
586
|
+
tool_name TEXT NOT NULL,
|
|
587
|
+
visibility TEXT NOT NULL,
|
|
588
|
+
status TEXT NOT NULL,
|
|
589
|
+
input_json TEXT NOT NULL,
|
|
590
|
+
output_json TEXT,
|
|
591
|
+
error_message TEXT,
|
|
592
|
+
created_at TEXT NOT NULL,
|
|
593
|
+
completed_at TEXT
|
|
594
|
+
);
|
|
595
|
+
\`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async fetch(request) {
|
|
599
|
+
const url = new URL(request.url);
|
|
600
|
+
|
|
601
|
+
if (request.method === "POST" && url.pathname === "/messages") {
|
|
602
|
+
return this.persistMessages(await request.json());
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (request.method === "GET" && url.pathname === "/messages") {
|
|
606
|
+
return this.getMessages(url.searchParams.get("conversationId"));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return Response.json({ error: { code: "not_found", message: "Unknown Durable Object route." } }, { status: 404 });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
getMessages(conversationId) {
|
|
613
|
+
if (!conversationId) {
|
|
614
|
+
return Response.json({ messages: [] });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const rows = this.sql.exec(
|
|
618
|
+
"SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY rowid ASC",
|
|
619
|
+
String(conversationId),
|
|
620
|
+
).toArray();
|
|
621
|
+
|
|
622
|
+
return Response.json({
|
|
623
|
+
messages: rows
|
|
624
|
+
.filter((row) => row.role === "user" || row.role === "assistant")
|
|
625
|
+
.map((row) => ({
|
|
626
|
+
role: String(row.role),
|
|
627
|
+
content: String(row.content),
|
|
628
|
+
})),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
persistMessages(input) {
|
|
633
|
+
const now = new Date().toISOString();
|
|
634
|
+
const conversationId = String(input.conversationId || crypto.randomUUID());
|
|
635
|
+
const runId = crypto.randomUUID();
|
|
636
|
+
const userMessageId = crypto.randomUUID();
|
|
637
|
+
const assistantMessageId = crypto.randomUUID();
|
|
638
|
+
const title = titleFromMessage(String(input.userMessage || ""));
|
|
639
|
+
|
|
640
|
+
this.sql.exec(
|
|
641
|
+
"INSERT OR IGNORE INTO conversations (id, agent_name, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
642
|
+
conversationId,
|
|
643
|
+
manifest.name,
|
|
644
|
+
title,
|
|
645
|
+
now,
|
|
646
|
+
now,
|
|
647
|
+
);
|
|
648
|
+
this.sql.exec("UPDATE conversations SET updated_at = ? WHERE id = ?", now, conversationId);
|
|
649
|
+
this.sql.exec(
|
|
650
|
+
"INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
651
|
+
userMessageId,
|
|
652
|
+
conversationId,
|
|
653
|
+
"user",
|
|
654
|
+
String(input.userMessage || ""),
|
|
655
|
+
now,
|
|
656
|
+
);
|
|
657
|
+
this.sql.exec(
|
|
658
|
+
"INSERT INTO runs (id, conversation_id, provider, model, status, error_code, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
659
|
+
runId,
|
|
660
|
+
conversationId,
|
|
661
|
+
manifest.provider.name,
|
|
662
|
+
manifest.provider.model,
|
|
663
|
+
String(input.status || "completed"),
|
|
664
|
+
input.errorCode ? String(input.errorCode) : null,
|
|
665
|
+
input.errorMessage ? String(input.errorMessage) : null,
|
|
666
|
+
now,
|
|
667
|
+
now,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
if (input.assistantMessage !== undefined) {
|
|
671
|
+
this.sql.exec(
|
|
672
|
+
"INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
673
|
+
assistantMessageId,
|
|
674
|
+
conversationId,
|
|
675
|
+
"assistant",
|
|
676
|
+
String(input.assistantMessage || ""),
|
|
677
|
+
now,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const toolCall of input.toolCalls || []) {
|
|
682
|
+
this.sql.exec(
|
|
683
|
+
"INSERT INTO tool_calls (id, run_id, conversation_id, tool_name, visibility, status, input_json, output_json, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
684
|
+
String(toolCall.id || crypto.randomUUID()),
|
|
685
|
+
runId,
|
|
686
|
+
conversationId,
|
|
687
|
+
String(toolCall.name || ""),
|
|
688
|
+
String(toolCall.visibility || "user"),
|
|
689
|
+
String(toolCall.status || "completed"),
|
|
690
|
+
JSON.stringify(toolCall.input ?? {}),
|
|
691
|
+
toolCall.output === undefined ? null : JSON.stringify(toolCall.output),
|
|
692
|
+
toolCall.errorMessage ? String(toolCall.errorMessage) : null,
|
|
693
|
+
now,
|
|
694
|
+
now,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return Response.json({
|
|
699
|
+
conversationId,
|
|
700
|
+
runId,
|
|
701
|
+
userMessageId,
|
|
702
|
+
assistantMessageId: input.assistantMessage === undefined ? null : assistantMessageId,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export default {
|
|
708
|
+
async fetch(request, env) {
|
|
709
|
+
const url = new URL(request.url);
|
|
710
|
+
|
|
711
|
+
if (request.method === "GET" && url.pathname === "/_agentkit") {
|
|
712
|
+
const auth = authorizeAdmin(request, env);
|
|
713
|
+
|
|
714
|
+
if (auth) {
|
|
715
|
+
return auth;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return Response.json({
|
|
719
|
+
agent: manifest.name,
|
|
720
|
+
runtime: manifest.runtime,
|
|
721
|
+
target: manifest.target,
|
|
722
|
+
provider: manifest.provider,
|
|
723
|
+
prompt: manifest.prompt,
|
|
724
|
+
tools: manifest.tools.map((tool) => tool.name),
|
|
725
|
+
channels: manifest.channels.map((channel) => ({
|
|
726
|
+
name: channel.name,
|
|
727
|
+
type: channel.type,
|
|
728
|
+
provider: channel.provider,
|
|
729
|
+
secrets: channel.secrets,
|
|
730
|
+
})),
|
|
731
|
+
knowledge: manifest.knowledge,
|
|
732
|
+
access: manifest.access,
|
|
733
|
+
storage: manifest.storage,
|
|
734
|
+
database: inspectHostedDatabase(),
|
|
735
|
+
secrets: Object.fromEntries(manifest.secrets.map((name) => [name, env[name] ? "set" : "missing"])),
|
|
736
|
+
managedSecrets: hostedManagedSecrets(),
|
|
737
|
+
userSecrets: manifest.secrets.filter((name) => !hostedManagedSecrets().includes(name)),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
742
|
+
return Response.json({
|
|
743
|
+
agent: manifest.name,
|
|
744
|
+
status: "ready",
|
|
745
|
+
inspect: "/_agentkit",
|
|
746
|
+
chat: "/chat",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
751
|
+
return Response.json({ status: "ok", agent: manifest.name });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (isHostedChannelWebhookPath(url.pathname)) {
|
|
755
|
+
return forwardHostedChannelWebhook(request, env, url);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (request.method === "POST" && (url.pathname === "/chat" || url.pathname === "/v1/chat")) {
|
|
759
|
+
return handleChat(request, env);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (request.method === "POST" && url.pathname.startsWith("/tools/")) {
|
|
763
|
+
return handleTool(request, env, decodeURIComponent(url.pathname.slice("/tools/".length)));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (request.method === "GET" && url.pathname === "/_agentkit/turso/health") {
|
|
767
|
+
return handleTursoHealth(request, env);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (request.method === "POST" && url.pathname === "/_agentkit/turso/query") {
|
|
771
|
+
return handleTursoQuery(request, env);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (url.pathname.startsWith("/files/")) {
|
|
775
|
+
return handleFile(request, env, url.pathname.slice("/files/".length));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return Response.json({ error: { code: "not_found", message: "Unknown AgentKit route." } }, { status: 404 });
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
function isHostedChannelWebhookPath(pathname) {
|
|
783
|
+
return (
|
|
784
|
+
/^\\/channels\\/[^/]+\\/(?:website|telegram)\\/webhook$/.test(pathname) ||
|
|
785
|
+
/^\\/channels\\/[^/]+\\/whatsapp\\/(?:zapster|meta)\\/webhook$/.test(pathname)
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function forwardHostedChannelWebhook(request, env, url) {
|
|
790
|
+
if (!env.AGENTKIT_CLOUD_API_URL) {
|
|
791
|
+
return Response.json(
|
|
792
|
+
{
|
|
793
|
+
error: {
|
|
794
|
+
code: "channel_webhook_proxy_not_configured",
|
|
795
|
+
message: "Hosted channel webhooks require AGENTKIT_CLOUD_API_URL on the deployed Worker.",
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
{ status: 501 },
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const target = new URL(\`\${url.pathname}\${url.search}\`, env.AGENTKIT_CLOUD_API_URL);
|
|
803
|
+
const headers = new Headers(request.headers);
|
|
804
|
+
headers.delete("host");
|
|
805
|
+
headers.delete("content-length");
|
|
806
|
+
|
|
807
|
+
if (env.AGENTKIT_DEPLOY_ID) {
|
|
808
|
+
headers.set("X-AgentKit-Deploy-ID", env.AGENTKIT_DEPLOY_ID);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (env.AGENTKIT_RUNTIME_GATE_TOKEN) {
|
|
812
|
+
headers.set("X-AgentKit-Runtime-Gate", env.AGENTKIT_RUNTIME_GATE_TOKEN);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return fetch(target.href, {
|
|
816
|
+
method: request.method,
|
|
817
|
+
headers,
|
|
818
|
+
body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
|
|
819
|
+
redirect: "manual",
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function handleTursoHealth(request, env) {
|
|
824
|
+
const auth = authorizeAdmin(request, env);
|
|
825
|
+
|
|
826
|
+
if (auth) {
|
|
827
|
+
return auth;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const configError = requireTurso(env);
|
|
831
|
+
|
|
832
|
+
if (configError) {
|
|
833
|
+
return configError;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const response = await fetch(toTursoBaseUrl(env.TURSO_DATABASE_URL) + "/health", {
|
|
837
|
+
headers: {
|
|
838
|
+
Authorization: \`Bearer \${env.TURSO_AUTH_TOKEN}\`,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
return Response.json({
|
|
843
|
+
driver: "turso",
|
|
844
|
+
ok: response.ok,
|
|
845
|
+
status: response.status,
|
|
846
|
+
}, { status: response.ok ? 200 : 502 });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function handleTursoQuery(request, env) {
|
|
850
|
+
const auth = authorizeAdmin(request, env);
|
|
851
|
+
|
|
852
|
+
if (auth) {
|
|
853
|
+
return auth;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const configError = requireTurso(env);
|
|
857
|
+
|
|
858
|
+
if (configError) {
|
|
859
|
+
return configError;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let body;
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
body = await request.json();
|
|
866
|
+
} catch {
|
|
867
|
+
return Response.json(
|
|
868
|
+
{ error: { code: "validation_error", message: "Expected JSON body with sql and optional args fields." } },
|
|
869
|
+
{ status: 400 },
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const sql = String(body.sql || "").trim();
|
|
874
|
+
|
|
875
|
+
if (!sql) {
|
|
876
|
+
return Response.json({ error: { code: "validation_error", message: "sql must be non-empty." } }, { status: 400 });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const args = Array.isArray(body.args) ? body.args : [];
|
|
880
|
+
try {
|
|
881
|
+
const result = await executeTursoSql(env, sql, args);
|
|
882
|
+
return Response.json(result);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
return hostedErrorResponse(error);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function handleFile(request, env, rawKey) {
|
|
889
|
+
const auth = authorizeAdmin(request, env);
|
|
890
|
+
|
|
891
|
+
if (auth) {
|
|
892
|
+
return auth;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (manifest.storage.files.driver !== "r2") {
|
|
896
|
+
return Response.json({ error: { code: "files_not_configured", message: "R2 files are not configured." } }, { status: 404 });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (!env.AGENTKIT_FILES) {
|
|
900
|
+
return Response.json(
|
|
901
|
+
{ error: { code: "deploy_not_ready", message: "AGENTKIT_FILES R2 binding is not configured." } },
|
|
902
|
+
{ status: 503 },
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const key = objectKey(rawKey);
|
|
907
|
+
|
|
908
|
+
if (!key) {
|
|
909
|
+
return Response.json({ error: { code: "validation_error", message: "file key must be non-empty." } }, { status: 400 });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (request.method === "PUT" || request.method === "POST") {
|
|
913
|
+
const contentLengthHeader = request.headers.get("content-length");
|
|
914
|
+
|
|
915
|
+
if (contentLengthHeader === null) {
|
|
916
|
+
return Response.json(
|
|
917
|
+
{ error: { code: "length_required", message: "File upload requires content-length." } },
|
|
918
|
+
{ status: 411 },
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const contentLength = Number(contentLengthHeader);
|
|
923
|
+
|
|
924
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
925
|
+
return Response.json(
|
|
926
|
+
{ error: { code: "validation_error", message: "content-length must be a valid non-negative number." } },
|
|
927
|
+
{ status: 400 },
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (contentLength > manifest.storage.files.maxUploadBytes) {
|
|
932
|
+
return Response.json(
|
|
933
|
+
{ error: { code: "payload_too_large", message: "File exceeds configured maxUploadBytes." } },
|
|
934
|
+
{ status: 413 },
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const body = await request.arrayBuffer();
|
|
939
|
+
|
|
940
|
+
if (body.byteLength > manifest.storage.files.maxUploadBytes) {
|
|
941
|
+
return Response.json(
|
|
942
|
+
{ error: { code: "payload_too_large", message: "File exceeds configured maxUploadBytes." } },
|
|
943
|
+
{ status: 413 },
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
await env.AGENTKIT_FILES.put(key, body, {
|
|
948
|
+
httpMetadata: {
|
|
949
|
+
contentType: request.headers.get("content-type") || "application/octet-stream",
|
|
950
|
+
},
|
|
951
|
+
customMetadata: {
|
|
952
|
+
agent: manifest.name,
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
return Response.json({ key, bucket: manifest.storage.files.bucket });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (request.method === "GET") {
|
|
960
|
+
const object = await env.AGENTKIT_FILES.get(key);
|
|
961
|
+
|
|
962
|
+
if (!object) {
|
|
963
|
+
return Response.json({ error: { code: "not_found", message: "File not found." } }, { status: 404 });
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const headers = new Headers();
|
|
967
|
+
object.writeHttpMetadata(headers);
|
|
968
|
+
headers.set("etag", object.httpEtag);
|
|
969
|
+
return new Response(object.body, { headers });
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (request.method === "DELETE") {
|
|
973
|
+
await env.AGENTKIT_FILES.delete(key);
|
|
974
|
+
return Response.json({ key, deleted: true });
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return Response.json({ error: { code: "method_not_allowed", message: "Unsupported file method." } }, { status: 405 });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function handleTool(request, env, toolName) {
|
|
981
|
+
const auth = authorizeAdmin(request, env);
|
|
982
|
+
|
|
983
|
+
if (auth) {
|
|
984
|
+
return auth;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let body;
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
body = await request.json();
|
|
991
|
+
} catch {
|
|
992
|
+
return Response.json(
|
|
993
|
+
{ error: { code: "validation_error", message: "Expected JSON body with optional input field." } },
|
|
994
|
+
{ status: 400 },
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
const result = await executeHostedTool({
|
|
1000
|
+
name: toolName,
|
|
1001
|
+
input: isRecord(body) && "input" in body ? body.input : body ?? {},
|
|
1002
|
+
env,
|
|
1003
|
+
signal: request.signal,
|
|
1004
|
+
invocation: "tool",
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
return Response.json({
|
|
1008
|
+
tool: {
|
|
1009
|
+
id: result.id,
|
|
1010
|
+
name: result.name,
|
|
1011
|
+
visibility: result.visibility,
|
|
1012
|
+
},
|
|
1013
|
+
input: result.input,
|
|
1014
|
+
output: result.output,
|
|
1015
|
+
});
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
return hostedErrorResponse(error);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function handleChat(request, env) {
|
|
1022
|
+
let body;
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
body = await request.json();
|
|
1026
|
+
} catch {
|
|
1027
|
+
return Response.json(
|
|
1028
|
+
{ error: { code: "validation_error", message: "Expected JSON body with a message field." } },
|
|
1029
|
+
{ status: 400 },
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const auth = await authorizeChat(request, env, body);
|
|
1034
|
+
|
|
1035
|
+
if (auth) {
|
|
1036
|
+
return auth;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const message = readChatMessage(body);
|
|
1040
|
+
|
|
1041
|
+
if (!message) {
|
|
1042
|
+
return Response.json(
|
|
1043
|
+
{ error: { code: "validation_error", message: "message must be a non-empty string." } },
|
|
1044
|
+
{ status: 400 },
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
let conversationId;
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
conversationId = normalizeConversationId(body.conversationId) ?? crypto.randomUUID();
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
return hostedErrorResponse(error);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const previousMessages = await loadConversationMessages(env, conversationId).catch(() => []);
|
|
1057
|
+
let content;
|
|
1058
|
+
const toolCalls = [];
|
|
1059
|
+
|
|
1060
|
+
const toolRequest = parseToolRequest(message);
|
|
1061
|
+
|
|
1062
|
+
if (toolRequest) {
|
|
1063
|
+
try {
|
|
1064
|
+
const toolResult = await executeHostedTool({
|
|
1065
|
+
name: toolRequest.name,
|
|
1066
|
+
input: toolRequest.input,
|
|
1067
|
+
env,
|
|
1068
|
+
signal: request.signal,
|
|
1069
|
+
invocation: "chat",
|
|
1070
|
+
});
|
|
1071
|
+
toolCalls.push(toolResult);
|
|
1072
|
+
content =
|
|
1073
|
+
toolResult.visibility === "internal"
|
|
1074
|
+
? \`Tool \${toolResult.name}: completed\`
|
|
1075
|
+
: \`Tool \${toolResult.name}: \${JSON.stringify(toolResult.output)}\`;
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
await persistFailure(env, conversationId, message, error);
|
|
1078
|
+
return hostedErrorResponse(error);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (content === undefined) {
|
|
1083
|
+
try {
|
|
1084
|
+
content = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
await persistFailure(env, conversationId, message, error);
|
|
1087
|
+
return hostedErrorResponse(error);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const persisted = await persist(env, {
|
|
1092
|
+
conversationId,
|
|
1093
|
+
userMessage: message,
|
|
1094
|
+
assistantMessage: content,
|
|
1095
|
+
toolCalls,
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
return Response.json({
|
|
1099
|
+
message: {
|
|
1100
|
+
role: "assistant",
|
|
1101
|
+
content,
|
|
1102
|
+
},
|
|
1103
|
+
...persisted,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function readChatMessage(body) {
|
|
1108
|
+
if (typeof body.message === "string") {
|
|
1109
|
+
return body.message.trim();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (body.message && typeof body.message === "object" && body.message.role === "user" && typeof body.message.content === "string") {
|
|
1113
|
+
return body.message.content.trim();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return "";
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function isRecord(value) {
|
|
1120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function callHostedProvider(env, messages, signal) {
|
|
1124
|
+
const adapter = getProviderAdapter(manifest.provider);
|
|
1125
|
+
const result = await adapter.run({
|
|
1126
|
+
agent: {
|
|
1127
|
+
name: manifest.name,
|
|
1128
|
+
runtime: manifest.runtime,
|
|
1129
|
+
provider: manifest.provider,
|
|
1130
|
+
},
|
|
1131
|
+
instructions: withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
|
|
1132
|
+
messages,
|
|
1133
|
+
tools: hostedTools,
|
|
1134
|
+
toolRuntime: createHostedProviderToolRuntime(env, signal),
|
|
1135
|
+
env,
|
|
1136
|
+
signal,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
return result.message.content;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function appendHostedKnowledgePromptPolicy(baseInstructions) {
|
|
1143
|
+
if (!manifest.knowledge.enabled) {
|
|
1144
|
+
return baseInstructions;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return \`\${baseInstructions.trimEnd()}
|
|
1148
|
+
|
|
1149
|
+
Knowledge rules:
|
|
1150
|
+
- Use agentkit_search_knowledge before answering business-specific factual questions about the user's documents, policies, prices, availability, services, procedures, or source-specific claims.
|
|
1151
|
+
- Do not invent prices, policies, legal/clinical claims, availability, or source-specific facts when Knowledge has not returned support for the answer.
|
|
1152
|
+
- 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.
|
|
1153
|
+
\`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function withToolVisibilityInstructions(baseInstructions, tools) {
|
|
1157
|
+
const internalTools = tools
|
|
1158
|
+
.filter((tool) => tool.visibility === "internal")
|
|
1159
|
+
.map((tool) => tool.name)
|
|
1160
|
+
.sort();
|
|
1161
|
+
|
|
1162
|
+
if (internalTools.length === 0) {
|
|
1163
|
+
return baseInstructions;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return \`\${baseInstructions.trimEnd()}
|
|
1167
|
+
|
|
1168
|
+
Tool visibility rules:
|
|
1169
|
+
- Tool outputs marked internal are operational state for the agent, not user-facing content.
|
|
1170
|
+
- 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.
|
|
1171
|
+
- Internal tools: \${internalTools.join(", ")}.
|
|
1172
|
+
\`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function createHostedKnowledgeSearchTool() {
|
|
1176
|
+
return {
|
|
1177
|
+
name: manifest.knowledge.internalTool || "agentkit_search_knowledge",
|
|
1178
|
+
description: "Search the Agent Capsule's configured Knowledge sources for business-specific facts.",
|
|
1179
|
+
visibility: "internal",
|
|
1180
|
+
inputSchema: {
|
|
1181
|
+
type: "object",
|
|
1182
|
+
properties: {
|
|
1183
|
+
query: { type: "string" },
|
|
1184
|
+
topK: { type: "integer" },
|
|
1185
|
+
},
|
|
1186
|
+
required: ["query"],
|
|
1187
|
+
additionalProperties: false,
|
|
1188
|
+
},
|
|
1189
|
+
outputSchema: {
|
|
1190
|
+
type: "object",
|
|
1191
|
+
properties: {
|
|
1192
|
+
results: {
|
|
1193
|
+
type: "array",
|
|
1194
|
+
items: {
|
|
1195
|
+
type: "object",
|
|
1196
|
+
properties: {
|
|
1197
|
+
content: { type: "string" },
|
|
1198
|
+
score: { type: "number" },
|
|
1199
|
+
source: { type: "object" },
|
|
1200
|
+
},
|
|
1201
|
+
required: ["content", "score", "source"],
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
required: ["results"],
|
|
1206
|
+
additionalProperties: false,
|
|
1207
|
+
},
|
|
1208
|
+
secrets: manifest.knowledge.embedding.secret ? [manifest.knowledge.embedding.secret] : [],
|
|
1209
|
+
async execute(input, ctx) {
|
|
1210
|
+
const query = typeof input?.query === "string" ? input.query : "";
|
|
1211
|
+
const topK = normalizeKnowledgeTopK(input?.topK ?? manifest.knowledge.retrieval.topK);
|
|
1212
|
+
const results = await searchHostedKnowledge(ctx.database, query, topK, ctx.secrets ?? {});
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
results: results.map((result) => ({
|
|
1216
|
+
content: result.content,
|
|
1217
|
+
score: result.score,
|
|
1218
|
+
source: result.source,
|
|
1219
|
+
})),
|
|
1220
|
+
};
|
|
1221
|
+
},
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function searchHostedKnowledge(database, query, topK, secrets) {
|
|
1226
|
+
const normalizedQuery = String(query || "").trim();
|
|
1227
|
+
|
|
1228
|
+
if (!normalizedQuery) {
|
|
1229
|
+
throw agentKitError("knowledge_query_invalid", "Knowledge search query must be a non-empty string.");
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const hasEmbeddingProvider = manifest.knowledge.embedding.provider !== "none";
|
|
1233
|
+
const lexical =
|
|
1234
|
+
manifest.knowledge.retrieval.hybrid || !hasEmbeddingProvider
|
|
1235
|
+
? await searchHostedKnowledgeLexical(database, normalizedQuery, Math.max(topK * 4, topK))
|
|
1236
|
+
: [];
|
|
1237
|
+
const semantic = hasEmbeddingProvider
|
|
1238
|
+
? await searchHostedKnowledgeSemantic(database, normalizedQuery, topK * 4, secrets)
|
|
1239
|
+
: [];
|
|
1240
|
+
|
|
1241
|
+
return combineHostedKnowledgeResults(lexical, semantic, topK);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function searchHostedKnowledgeLexical(database, query, limit) {
|
|
1245
|
+
const tokens = hostedKnowledgeQueryTokens(query);
|
|
1246
|
+
|
|
1247
|
+
if (tokens.length === 0) {
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const result = await database.query(
|
|
1253
|
+
\`
|
|
1254
|
+
SELECT
|
|
1255
|
+
c.id AS chunk_id,
|
|
1256
|
+
c.source_id AS source_id,
|
|
1257
|
+
c.content AS content,
|
|
1258
|
+
c.title AS title,
|
|
1259
|
+
c.section AS section,
|
|
1260
|
+
c.locator AS locator,
|
|
1261
|
+
s.source_key AS source_key,
|
|
1262
|
+
rank
|
|
1263
|
+
FROM agentkit_knowledge_fts
|
|
1264
|
+
JOIN agentkit_knowledge_chunks c ON c.id = agentkit_knowledge_fts.chunk_id
|
|
1265
|
+
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
1266
|
+
WHERE agentkit_knowledge_fts MATCH ?
|
|
1267
|
+
ORDER BY rank ASC
|
|
1268
|
+
LIMIT ?
|
|
1269
|
+
\`,
|
|
1270
|
+
[tokens.map((token) => \`"\${token.replace(/"/g, '""')}"\`).join(" OR "), limit],
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
|
|
1274
|
+
} catch {
|
|
1275
|
+
return searchHostedKnowledgeLexicalFallback(database, query, limit);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async function searchHostedKnowledgeLexicalFallback(database, query, limit) {
|
|
1280
|
+
const like = \`%\${query.toLowerCase()}%\`;
|
|
1281
|
+
const result = await database.query(
|
|
1282
|
+
\`
|
|
1283
|
+
SELECT
|
|
1284
|
+
c.id AS chunk_id,
|
|
1285
|
+
c.source_id AS source_id,
|
|
1286
|
+
c.content AS content,
|
|
1287
|
+
c.title AS title,
|
|
1288
|
+
c.section AS section,
|
|
1289
|
+
c.locator AS locator,
|
|
1290
|
+
s.source_key AS source_key
|
|
1291
|
+
FROM agentkit_knowledge_chunks c
|
|
1292
|
+
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
1293
|
+
WHERE lower(c.content) LIKE ?
|
|
1294
|
+
OR lower(COALESCE(c.title, '')) LIKE ?
|
|
1295
|
+
OR lower(COALESCE(c.section, '')) LIKE ?
|
|
1296
|
+
ORDER BY c.created_at DESC, c.ordinal ASC
|
|
1297
|
+
LIMIT ?
|
|
1298
|
+
\`,
|
|
1299
|
+
[like, like, like, limit],
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function searchHostedKnowledgeSemantic(database, query, limit, secrets) {
|
|
1306
|
+
const queryEmbedding = await createHostedKnowledgeEmbedding(query, secrets);
|
|
1307
|
+
|
|
1308
|
+
if (!queryEmbedding) {
|
|
1309
|
+
return [];
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
return await searchHostedKnowledgeNativeVector(database, queryEmbedding, limit);
|
|
1314
|
+
} catch {
|
|
1315
|
+
return searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
async function searchHostedKnowledgeNativeVector(database, queryEmbedding, limit) {
|
|
1320
|
+
const vector = JSON.stringify(queryEmbedding);
|
|
1321
|
+
const result = await database.query(
|
|
1322
|
+
\`
|
|
1323
|
+
SELECT
|
|
1324
|
+
c.id AS chunk_id,
|
|
1325
|
+
c.source_id AS source_id,
|
|
1326
|
+
c.content AS content,
|
|
1327
|
+
c.title AS title,
|
|
1328
|
+
c.section AS section,
|
|
1329
|
+
c.locator AS locator,
|
|
1330
|
+
s.source_key AS source_key,
|
|
1331
|
+
vector_distance_cos(v.embedding, vector32(?)) AS vector_distance
|
|
1332
|
+
FROM vector_top_k('agentkit_knowledge_vectors_embedding_idx', vector32(?), ?) nearest
|
|
1333
|
+
JOIN agentkit_knowledge_vectors v ON v.id = nearest.id
|
|
1334
|
+
JOIN agentkit_knowledge_chunks c ON c.id = v.chunk_id
|
|
1335
|
+
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
1336
|
+
ORDER BY vector_distance ASC
|
|
1337
|
+
\`,
|
|
1338
|
+
[vector, vector, limit],
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
return result.rows
|
|
1342
|
+
.map((row) => {
|
|
1343
|
+
const distance = Number(row.vector_distance);
|
|
1344
|
+
const semantic = Number.isFinite(distance) ? 1 - distance : 0;
|
|
1345
|
+
return hostedKnowledgeResultFromRow(row, 0, semantic);
|
|
1346
|
+
})
|
|
1347
|
+
.filter((result) => result.match.semantic !== null && result.match.semantic > 0)
|
|
1348
|
+
.slice(0, limit);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
async function searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit) {
|
|
1352
|
+
const result = await database.query(
|
|
1353
|
+
\`
|
|
1354
|
+
SELECT
|
|
1355
|
+
c.id AS chunk_id,
|
|
1356
|
+
c.source_id AS source_id,
|
|
1357
|
+
c.content AS content,
|
|
1358
|
+
c.title AS title,
|
|
1359
|
+
c.section AS section,
|
|
1360
|
+
c.locator AS locator,
|
|
1361
|
+
c.embedding_json AS embedding_json,
|
|
1362
|
+
s.source_key AS source_key
|
|
1363
|
+
FROM agentkit_knowledge_chunks c
|
|
1364
|
+
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
1365
|
+
WHERE c.embedding_json IS NOT NULL
|
|
1366
|
+
ORDER BY c.created_at DESC, c.ordinal ASC
|
|
1367
|
+
\`,
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
return result.rows
|
|
1371
|
+
.map((row) => {
|
|
1372
|
+
const embedding = parseHostedKnowledgeEmbedding(row.embedding_json);
|
|
1373
|
+
const semantic = embedding ? hostedCosineSimilarity(queryEmbedding, embedding) : 0;
|
|
1374
|
+
return hostedKnowledgeResultFromRow(row, 0, semantic);
|
|
1375
|
+
})
|
|
1376
|
+
.filter((result) => result.match.semantic !== null && result.match.semantic > 0)
|
|
1377
|
+
.sort((left, right) => (right.match.semantic ?? 0) - (left.match.semantic ?? 0))
|
|
1378
|
+
.slice(0, limit);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async function createHostedKnowledgeEmbedding(input, secrets) {
|
|
1382
|
+
const provider = manifest.knowledge.embedding.provider;
|
|
1383
|
+
|
|
1384
|
+
if (provider === "none") {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (provider === "test") {
|
|
1389
|
+
return hostedFakeEmbedding(input, manifest.knowledge.embedding.dimensions || 32);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (provider !== "openai") {
|
|
1393
|
+
throw agentKitError("knowledge_embedding_provider_unsupported", "Unsupported knowledge embedding provider.");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const secretName = manifest.knowledge.embedding.secret || "OPENAI_API_KEY";
|
|
1397
|
+
const apiKey = secrets[secretName];
|
|
1398
|
+
|
|
1399
|
+
if (!apiKey) {
|
|
1400
|
+
throw agentKitError("knowledge_embedding_secret_missing", \`Knowledge embeddings require missing secret "\${secretName}".\`);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
1404
|
+
method: "POST",
|
|
1405
|
+
headers: {
|
|
1406
|
+
"content-type": "application/json",
|
|
1407
|
+
authorization: \`Bearer \${apiKey}\`,
|
|
1408
|
+
},
|
|
1409
|
+
body: JSON.stringify({
|
|
1410
|
+
model: manifest.knowledge.embedding.model || "text-embedding-3-small",
|
|
1411
|
+
input,
|
|
1412
|
+
encoding_format: "float",
|
|
1413
|
+
...(manifest.knowledge.embedding.dimensions ? { dimensions: manifest.knowledge.embedding.dimensions } : {}),
|
|
1414
|
+
}),
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
if (!response.ok) {
|
|
1418
|
+
const text = await response.text().catch(() => "");
|
|
1419
|
+
throw agentKitError(
|
|
1420
|
+
"knowledge_embedding_failed",
|
|
1421
|
+
\`OpenAI embedding request failed with HTTP \${response.status}.\${text ? " " + text.slice(0, 240) : ""}\`,
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const payload = await response.json();
|
|
1426
|
+
const embedding = payload?.data?.[0]?.embedding;
|
|
1427
|
+
|
|
1428
|
+
if (!Array.isArray(embedding) || embedding.some((value) => typeof value !== "number")) {
|
|
1429
|
+
throw agentKitError("knowledge_embedding_failed", "OpenAI embedding response did not contain a numeric vector.");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return embedding;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function combineHostedKnowledgeResults(lexical, semantic, topK) {
|
|
1436
|
+
const byChunk = new Map();
|
|
1437
|
+
|
|
1438
|
+
for (const result of lexical) {
|
|
1439
|
+
byChunk.set(result.chunkId, result);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
for (const result of semantic) {
|
|
1443
|
+
const existing = byChunk.get(result.chunkId);
|
|
1444
|
+
|
|
1445
|
+
if (!existing) {
|
|
1446
|
+
byChunk.set(result.chunkId, {
|
|
1447
|
+
...result,
|
|
1448
|
+
score: hostedCombinedScore(0, result.match.semantic),
|
|
1449
|
+
});
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
byChunk.set(result.chunkId, {
|
|
1454
|
+
...existing,
|
|
1455
|
+
score: hostedCombinedScore(existing.match.lexical, result.match.semantic),
|
|
1456
|
+
match: {
|
|
1457
|
+
lexical: existing.match.lexical,
|
|
1458
|
+
semantic: result.match.semantic,
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return Array.from(byChunk.values())
|
|
1464
|
+
.map((result) => ({
|
|
1465
|
+
...result,
|
|
1466
|
+
score: result.score || hostedCombinedScore(result.match.lexical, result.match.semantic),
|
|
1467
|
+
}))
|
|
1468
|
+
.sort((left, right) => right.score - left.score || left.source.path.localeCompare(right.source.path))
|
|
1469
|
+
.slice(0, topK);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function hostedKnowledgeResultFromRow(row, lexical, semantic) {
|
|
1473
|
+
return {
|
|
1474
|
+
chunkId: String(row.chunk_id),
|
|
1475
|
+
content: String(row.content),
|
|
1476
|
+
score: hostedCombinedScore(lexical, semantic),
|
|
1477
|
+
source: {
|
|
1478
|
+
id: String(row.source_id),
|
|
1479
|
+
path: String(row.source_key),
|
|
1480
|
+
title: typeof row.title === "string" ? row.title : null,
|
|
1481
|
+
section: typeof row.section === "string" ? row.section : null,
|
|
1482
|
+
locator: typeof row.locator === "string" ? row.locator : null,
|
|
1483
|
+
},
|
|
1484
|
+
match: {
|
|
1485
|
+
lexical,
|
|
1486
|
+
semantic,
|
|
1487
|
+
},
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function normalizeKnowledgeTopK(value) {
|
|
1492
|
+
const topK = Number(value);
|
|
1493
|
+
|
|
1494
|
+
if (!Number.isInteger(topK) || topK <= 0 || topK > 50) {
|
|
1495
|
+
throw agentKitError("knowledge_query_invalid", "Knowledge search topK must be a positive integer no larger than 50.");
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return topK;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function hostedKnowledgeQueryTokens(query) {
|
|
1502
|
+
return Array.from(new Set(String(query).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [])).slice(0, 12);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function parseHostedKnowledgeEmbedding(value) {
|
|
1506
|
+
if (typeof value !== "string") {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
const parsed = JSON.parse(value);
|
|
1512
|
+
return Array.isArray(parsed) && parsed.every((item) => typeof item === "number") ? parsed : null;
|
|
1513
|
+
} catch {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function hostedCombinedScore(lexical, semantic) {
|
|
1519
|
+
const semanticScore = semantic === null ? 0 : Math.max(0, (semantic + 1) / 2);
|
|
1520
|
+
return Number((lexical * 0.6 + semanticScore * 0.4).toFixed(6));
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function hostedLexicalScore(index) {
|
|
1524
|
+
return Number((1 / (index + 1)).toFixed(6));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function hostedCosineSimilarity(left, right) {
|
|
1528
|
+
if (left.length !== right.length || left.length === 0) {
|
|
1529
|
+
return 0;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
let dot = 0;
|
|
1533
|
+
let leftNorm = 0;
|
|
1534
|
+
let rightNorm = 0;
|
|
1535
|
+
|
|
1536
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
1537
|
+
dot += left[index] * right[index];
|
|
1538
|
+
leftNorm += left[index] * left[index];
|
|
1539
|
+
rightNorm += right[index] * right[index];
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
1543
|
+
return 0;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
async function hostedFakeEmbedding(input, dimensions) {
|
|
1550
|
+
const values = Array.from({ length: dimensions }, () => 0);
|
|
1551
|
+
const tokens = String(input).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [];
|
|
1552
|
+
const encoder = new TextEncoder();
|
|
1553
|
+
|
|
1554
|
+
for (const token of tokens) {
|
|
1555
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token)));
|
|
1556
|
+
const slot = hash[0] % dimensions;
|
|
1557
|
+
const sign = hash[1] % 2 === 0 ? 1 : -1;
|
|
1558
|
+
values[slot] += sign * (1 + (hash[2] % 7) / 10);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const norm = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
|
|
1562
|
+
return norm === 0 ? values : values.map((value) => Number((value / norm).toFixed(8)));
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async function persistFailure(env, conversationId, userMessage, error) {
|
|
1566
|
+
await persist(env, {
|
|
1567
|
+
conversationId,
|
|
1568
|
+
userMessage,
|
|
1569
|
+
status: "failed",
|
|
1570
|
+
errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error",
|
|
1571
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
1572
|
+
toolCalls: [],
|
|
1573
|
+
}).catch(() => undefined);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function parseToolRequest(content) {
|
|
1577
|
+
let parsed;
|
|
1578
|
+
|
|
1579
|
+
try {
|
|
1580
|
+
parsed = JSON.parse(content);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (!isRecord(parsed) || typeof parsed.tool !== "string" || parsed.tool.length === 0) {
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
name: parsed.tool,
|
|
1591
|
+
input: "input" in parsed ? parsed.input : {},
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function inspectHostedDatabase() {
|
|
1596
|
+
const schema = manifest.storage.database.driver === "turso" ? manifest.storage.database.schema : null;
|
|
1597
|
+
|
|
1598
|
+
return {
|
|
1599
|
+
local: {
|
|
1600
|
+
driver: "sqlite",
|
|
1601
|
+
path: ".agentkit/agentkit.db",
|
|
1602
|
+
schema,
|
|
1603
|
+
},
|
|
1604
|
+
hosted: {
|
|
1605
|
+
driver: manifest.storage.database.driver,
|
|
1606
|
+
provisioning: manifest.storage.database.driver === "turso" ? "agentkit-managed" : "none",
|
|
1607
|
+
schema,
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function hostedManagedSecrets() {
|
|
1613
|
+
if (manifest.storage.database.driver !== "turso") {
|
|
1614
|
+
return [];
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return ["TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL"];
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function createHostedProviderToolRuntime(env, signal) {
|
|
1621
|
+
return {
|
|
1622
|
+
definitions: hostedTools,
|
|
1623
|
+
execute(request) {
|
|
1624
|
+
return executeHostedTool({
|
|
1625
|
+
name: request.name,
|
|
1626
|
+
input: request.input,
|
|
1627
|
+
env,
|
|
1628
|
+
signal,
|
|
1629
|
+
invocation: "chat",
|
|
1630
|
+
});
|
|
1631
|
+
},
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async function executeHostedTool(request) {
|
|
1636
|
+
const tool = hostedTools.find((candidate) => candidate.name === request.name);
|
|
1637
|
+
const id = crypto.randomUUID();
|
|
1638
|
+
|
|
1639
|
+
if (!tool) {
|
|
1640
|
+
throw agentKitError("tool_not_found", \`Tool "\${request.name}" is not declared in agentkit.config.ts.\`);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const input = normalizeOptionalNulls(request.input ?? {}, tool.inputSchema);
|
|
1644
|
+
validateSchema(input, tool.inputSchema, \`tool "\${tool.name}" input\`);
|
|
1645
|
+
|
|
1646
|
+
const secrets = resolveToolSecrets(tool, request.env);
|
|
1647
|
+
const database = createHostedDatabaseRunner(request.env);
|
|
1648
|
+
const output = await runToolWithTimeout(
|
|
1649
|
+
tool,
|
|
1650
|
+
input,
|
|
1651
|
+
{
|
|
1652
|
+
secrets,
|
|
1653
|
+
database,
|
|
1654
|
+
signal: request.signal,
|
|
1655
|
+
runtime: {
|
|
1656
|
+
environment: "hosted",
|
|
1657
|
+
invocation: request.invocation ?? "tool",
|
|
1658
|
+
target: "cloudflare",
|
|
1659
|
+
database: manifest.storage.database.driver === "turso" ? "turso" : "none",
|
|
1660
|
+
},
|
|
1661
|
+
},
|
|
1662
|
+
tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS,
|
|
1663
|
+
);
|
|
1664
|
+
|
|
1665
|
+
if (tool.outputSchema) {
|
|
1666
|
+
validateSchema(output, tool.outputSchema, \`tool "\${tool.name}" output\`);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
return {
|
|
1670
|
+
id,
|
|
1671
|
+
name: tool.name,
|
|
1672
|
+
visibility: tool.visibility ?? "user",
|
|
1673
|
+
status: "completed",
|
|
1674
|
+
input,
|
|
1675
|
+
output,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function resolveToolSecrets(tool, env) {
|
|
1680
|
+
const secrets = {};
|
|
1681
|
+
|
|
1682
|
+
for (const secretName of tool.secrets ?? []) {
|
|
1683
|
+
if (!env[secretName]) {
|
|
1684
|
+
throw agentKitError("tool_secret_missing", \`Tool "\${tool.name}" requires missing secret "\${secretName}".\`);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
secrets[secretName] = env[secretName];
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return secrets;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
async function runToolWithTimeout(tool, input, context, timeoutMs) {
|
|
1694
|
+
const controller = new AbortController();
|
|
1695
|
+
const parentSignal = context.signal;
|
|
1696
|
+
|
|
1697
|
+
if (parentSignal?.aborted) {
|
|
1698
|
+
throw agentKitError("runtime_aborted", \`Tool "\${tool.name}" was aborted before it started.\`);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
|
1702
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
1703
|
+
|
|
1704
|
+
let timeout;
|
|
1705
|
+
|
|
1706
|
+
try {
|
|
1707
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1708
|
+
timeout = setTimeout(() => {
|
|
1709
|
+
controller.abort();
|
|
1710
|
+
reject(agentKitError("tool_timeout", \`Tool "\${tool.name}" timed out after \${timeoutMs}ms.\`));
|
|
1711
|
+
}, timeoutMs);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
const execution = Promise.resolve().then(() =>
|
|
1715
|
+
tool.execute(input, {
|
|
1716
|
+
secrets: context.secrets,
|
|
1717
|
+
signal: controller.signal,
|
|
1718
|
+
runtime: context.runtime,
|
|
1719
|
+
database: context.database,
|
|
1720
|
+
db: context.database,
|
|
1721
|
+
storage: {
|
|
1722
|
+
sql: context.database,
|
|
1723
|
+
},
|
|
1724
|
+
}),
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
return await Promise.race([execution, timeoutPromise]);
|
|
1728
|
+
} finally {
|
|
1729
|
+
if (timeout) {
|
|
1730
|
+
clearTimeout(timeout);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function createHostedDatabaseRunner(env) {
|
|
1738
|
+
const run = async (sql, args = []) => {
|
|
1739
|
+
validateDatabaseQuery(sql, args);
|
|
1740
|
+
|
|
1741
|
+
const configError = requireTurso(env);
|
|
1742
|
+
|
|
1743
|
+
if (configError) {
|
|
1744
|
+
const payload = await configError.json().catch(() => null);
|
|
1745
|
+
const error = payload?.error;
|
|
1746
|
+
throw agentKitError(error?.code || "database_not_configured", error?.message || "Hosted database is not configured.");
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
return executeTursoSql(env, sql, args);
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
run.execute = run;
|
|
1753
|
+
run.query = run;
|
|
1754
|
+
run.batch = async (statements) => {
|
|
1755
|
+
if (!Array.isArray(statements)) {
|
|
1756
|
+
throw agentKitError("database_query_invalid", "ctx.db.batch requires an array of statements.");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
for (const [index, statement] of statements.entries()) {
|
|
1760
|
+
if (!isRecord(statement)) {
|
|
1761
|
+
throw agentKitError("database_query_invalid", \`ctx.db.batch statement \${index} must be an object.\`);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
validateDatabaseQuery(statement.sql, statement.args ?? []);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const configError = requireTurso(env);
|
|
1768
|
+
|
|
1769
|
+
if (configError) {
|
|
1770
|
+
const payload = await configError.json().catch(() => null);
|
|
1771
|
+
const error = payload?.error;
|
|
1772
|
+
throw agentKitError(error?.code || "database_not_configured", error?.message || "Hosted database is not configured.");
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return executeTursoBatch(env, statements);
|
|
1776
|
+
};
|
|
1777
|
+
run.transaction = async () => {
|
|
1778
|
+
throw agentKitError(
|
|
1779
|
+
"database_transaction_unsupported",
|
|
1780
|
+
"Hosted Cloudflare tools do not expose long-lived interactive transactions in v1. Use ctx.db.batch([...]) for atomic Turso pipeline operations.",
|
|
1781
|
+
);
|
|
1782
|
+
};
|
|
1783
|
+
return run;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function validateDatabaseQuery(sql, args) {
|
|
1787
|
+
if (typeof sql !== "string" || sql.trim().length === 0) {
|
|
1788
|
+
throw agentKitError("database_query_invalid", "Database SQL must be a non-empty string.");
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (!Array.isArray(args)) {
|
|
1792
|
+
throw agentKitError("database_query_invalid", "Database SQL args must be an array.");
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
for (const arg of args) {
|
|
1796
|
+
if (arg !== null && typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
|
|
1797
|
+
throw agentKitError("database_query_invalid", "Database SQL args may only contain strings, numbers, booleans, or null.");
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function validateSchema(value, schema, label) {
|
|
1803
|
+
const error = validateValue(value, schema, "$");
|
|
1804
|
+
|
|
1805
|
+
if (error) {
|
|
1806
|
+
throw agentKitError("tool_validation_error", \`\${label} failed schema validation: \${error}\`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function validateValue(value, schema, path) {
|
|
1811
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((item) => Object.is(item, value))) {
|
|
1812
|
+
return \`\${path} must be one of \${JSON.stringify(schema.enum)}.\`;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1816
|
+
const errors = types.map((type) => validateValueForType(value, schema, type, path)).filter((error) => error !== null);
|
|
1817
|
+
|
|
1818
|
+
if (errors.length < types.length) {
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
if (types.length > 1) {
|
|
1823
|
+
return \`\${path} must match one of these schema types: \${types.join(", ")}.\`;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
return errors[0] ?? null;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function validateValueForType(value, schema, type, path) {
|
|
1830
|
+
switch (type) {
|
|
1831
|
+
case "object":
|
|
1832
|
+
return validateObject(value, schema, path);
|
|
1833
|
+
case "array":
|
|
1834
|
+
return validateArray(value, schema, path);
|
|
1835
|
+
case "string":
|
|
1836
|
+
return typeof value === "string" ? null : \`\${path} must be a string.\`;
|
|
1837
|
+
case "number":
|
|
1838
|
+
return typeof value === "number" && Number.isFinite(value) ? null : \`\${path} must be a number.\`;
|
|
1839
|
+
case "integer":
|
|
1840
|
+
return Number.isInteger(value) ? null : \`\${path} must be an integer.\`;
|
|
1841
|
+
case "boolean":
|
|
1842
|
+
return typeof value === "boolean" ? null : \`\${path} must be a boolean.\`;
|
|
1843
|
+
case "null":
|
|
1844
|
+
return value === null ? null : \`\${path} must be null.\`;
|
|
1845
|
+
default:
|
|
1846
|
+
return \`\${path} has unsupported schema type "\${type}".\`;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function validateObject(value, schema, path) {
|
|
1851
|
+
if (!isRecord(value)) {
|
|
1852
|
+
return \`\${path} must be an object.\`;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
for (const property of schema.required ?? []) {
|
|
1856
|
+
if (!(property in value)) {
|
|
1857
|
+
return \`\${path}.\${property} is required.\`;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
for (const [property, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
1862
|
+
if (property in value) {
|
|
1863
|
+
const error = validateValue(value[property], propertySchema, \`\${path}.\${property}\`);
|
|
1864
|
+
|
|
1865
|
+
if (error) {
|
|
1866
|
+
return error;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (schema.additionalProperties === false) {
|
|
1872
|
+
const allowedProperties = new Set(Object.keys(schema.properties ?? {}));
|
|
1873
|
+
|
|
1874
|
+
for (const property of Object.keys(value)) {
|
|
1875
|
+
if (!allowedProperties.has(property)) {
|
|
1876
|
+
return \`\${path}.\${property} is not allowed.\`;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function validateArray(value, schema, path) {
|
|
1885
|
+
if (!Array.isArray(value)) {
|
|
1886
|
+
return \`\${path} must be an array.\`;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (!schema.items) {
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
for (const [index, item] of value.entries()) {
|
|
1894
|
+
const error = validateValue(item, schema.items, \`\${path}[\${index}]\`);
|
|
1895
|
+
|
|
1896
|
+
if (error) {
|
|
1897
|
+
return error;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function normalizeOptionalNulls(value, schema) {
|
|
1905
|
+
const types = schemaTypes(schema);
|
|
1906
|
+
|
|
1907
|
+
if (types.includes("object") && isRecord(value)) {
|
|
1908
|
+
const required = new Set(schema.required ?? []);
|
|
1909
|
+
const properties = schema.properties ?? {};
|
|
1910
|
+
const normalized = { ...value };
|
|
1911
|
+
|
|
1912
|
+
for (const [property, propertySchema] of Object.entries(properties)) {
|
|
1913
|
+
if (!(property in normalized)) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (normalized[property] === null && !required.has(property) && !allowsNull(propertySchema)) {
|
|
1918
|
+
delete normalized[property];
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
normalized[property] = normalizeOptionalNulls(normalized[property], propertySchema);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
return normalized;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (types.includes("array") && Array.isArray(value) && schema.items) {
|
|
1929
|
+
return value.map((item) => normalizeOptionalNulls(item, schema.items));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return value;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function schemaTypes(schema) {
|
|
1936
|
+
return Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function allowsNull(schema) {
|
|
1940
|
+
return schemaTypes(schema).includes("null") || (Array.isArray(schema.enum) && schema.enum.some((item) => item === null));
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
async function authorizeChat(request, env, body) {
|
|
1944
|
+
const runtimeGate = await authorizeRuntimeGate(request, env, body);
|
|
1945
|
+
|
|
1946
|
+
if (runtimeGate) {
|
|
1947
|
+
if (runtimeGate === "authorized") {
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
return runtimeGate;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
if (manifest.access.mode === "public") {
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (manifest.access.mode === "private") {
|
|
1959
|
+
if (!env.AGENTKIT_PRIVATE_TOKEN) {
|
|
1960
|
+
return Response.json(
|
|
1961
|
+
{
|
|
1962
|
+
error: {
|
|
1963
|
+
code: "deploy_not_ready",
|
|
1964
|
+
message: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set as a Cloudflare secret.",
|
|
1965
|
+
},
|
|
1966
|
+
},
|
|
1967
|
+
{ status: 503 },
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
const expected = \`Bearer \${env.AGENTKIT_PRIVATE_TOKEN}\`;
|
|
1972
|
+
if (request.headers.get("authorization") !== expected) {
|
|
1973
|
+
return Response.json(
|
|
1974
|
+
{ error: { code: "unauthorized", message: "Missing or invalid deploy access token." } },
|
|
1975
|
+
{ status: 401 },
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
if (manifest.access.mode === "token") {
|
|
1981
|
+
if (!env.AGENTKIT_ACCESS_TOKEN) {
|
|
1982
|
+
return Response.json(
|
|
1983
|
+
{
|
|
1984
|
+
error: {
|
|
1985
|
+
code: "deploy_not_ready",
|
|
1986
|
+
message: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set as a Cloudflare secret.",
|
|
1987
|
+
},
|
|
1988
|
+
},
|
|
1989
|
+
{ status: 503 },
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const expected = \`Bearer \${env.AGENTKIT_ACCESS_TOKEN}\`;
|
|
1994
|
+
if (request.headers.get("authorization") !== expected) {
|
|
1995
|
+
return Response.json(
|
|
1996
|
+
{ error: { code: "unauthorized", message: "Missing or invalid deploy access token." } },
|
|
1997
|
+
{ status: 401 },
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
async function authorizeRuntimeGate(request, env, body) {
|
|
2006
|
+
if (!env.AGENTKIT_CLOUD_API_URL || !env.AGENTKIT_DEPLOY_ID || !env.AGENTKIT_RUNTIME_GATE_TOKEN) {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
let response;
|
|
2011
|
+
const token = deployAccessToken(request, body);
|
|
2012
|
+
|
|
2013
|
+
try {
|
|
2014
|
+
response = await fetch(
|
|
2015
|
+
new URL(\`/v1/runtime/deploys/\${encodeURIComponent(env.AGENTKIT_DEPLOY_ID)}/authorize\`, env.AGENTKIT_CLOUD_API_URL).href,
|
|
2016
|
+
{
|
|
2017
|
+
method: "POST",
|
|
2018
|
+
headers: {
|
|
2019
|
+
"Authorization": \`Bearer \${env.AGENTKIT_RUNTIME_GATE_TOKEN}.\${token}\`,
|
|
2020
|
+
"Content-Type": "application/json",
|
|
2021
|
+
"X-AgentKit-Deploy-Access-Token": token,
|
|
2022
|
+
},
|
|
2023
|
+
body: JSON.stringify({
|
|
2024
|
+
token,
|
|
2025
|
+
}),
|
|
2026
|
+
},
|
|
2027
|
+
);
|
|
2028
|
+
} catch {
|
|
2029
|
+
return Response.json(
|
|
2030
|
+
{ error: { code: "runtime_gate_unavailable", message: "AgentKit runtime gate is temporarily unavailable." } },
|
|
2031
|
+
{ status: 503 },
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
if (response.ok) {
|
|
2036
|
+
return "authorized";
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
const payload = await response.json().catch(() => ({
|
|
2040
|
+
error: {
|
|
2041
|
+
code: "runtime_gate_failed",
|
|
2042
|
+
message: "AgentKit runtime gate rejected the request.",
|
|
2043
|
+
},
|
|
2044
|
+
}));
|
|
2045
|
+
return Response.json(payload, { status: response.status });
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function deployAccessToken(request, body) {
|
|
2049
|
+
const authorization = request.headers.get("authorization");
|
|
2050
|
+
|
|
2051
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
2052
|
+
return authorization.slice("Bearer ".length);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
const tokenHeader =
|
|
2056
|
+
request.headers.get("x-agentkit-access-token") ??
|
|
2057
|
+
request.headers.get("x-agentkit-deploy-access-token") ??
|
|
2058
|
+
"";
|
|
2059
|
+
const token = tokenHeader.startsWith("Bearer ") ? tokenHeader.slice("Bearer ".length) : tokenHeader;
|
|
2060
|
+
if (token.trim()) {
|
|
2061
|
+
return token.trim();
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (body && typeof body === "object") {
|
|
2065
|
+
const bodyToken = typeof body.access_token === "string" ? body.access_token : typeof body.accessToken === "string" ? body.accessToken : "";
|
|
2066
|
+
return bodyToken.trim();
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
return "";
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function authorizeAdmin(request, env) {
|
|
2073
|
+
if (!env.AGENTKIT_PRIVATE_TOKEN) {
|
|
2074
|
+
return Response.json(
|
|
2075
|
+
{
|
|
2076
|
+
error: {
|
|
2077
|
+
code: "deploy_not_ready",
|
|
2078
|
+
message: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set as a Cloudflare secret.",
|
|
2079
|
+
},
|
|
2080
|
+
},
|
|
2081
|
+
{ status: 503 },
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const expected = \`Bearer \${env.AGENTKIT_PRIVATE_TOKEN}\`;
|
|
2086
|
+
if (request.headers.get("authorization") !== expected) {
|
|
2087
|
+
return Response.json(
|
|
2088
|
+
{ error: { code: "unauthorized", message: "Missing or invalid AgentKit admin token." } },
|
|
2089
|
+
{ status: 401 },
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
async function persist(env, input) {
|
|
2097
|
+
if (!env.AGENTKIT_CONVERSATIONS) {
|
|
2098
|
+
return {};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const id = env.AGENTKIT_CONVERSATIONS.idFromName(input.conversationId);
|
|
2102
|
+
const stub = env.AGENTKIT_CONVERSATIONS.get(id);
|
|
2103
|
+
const response = await stub.fetch("https://agentkit.internal/messages", {
|
|
2104
|
+
method: "POST",
|
|
2105
|
+
body: JSON.stringify(input),
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
if (!response.ok) {
|
|
2109
|
+
throw new Error(\`Could not persist conversation: \${response.status}\`);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
return response.json();
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function loadConversationMessages(env, conversationId) {
|
|
2116
|
+
if (!env.AGENTKIT_CONVERSATIONS || !conversationId) {
|
|
2117
|
+
return [];
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const id = env.AGENTKIT_CONVERSATIONS.idFromName(conversationId);
|
|
2121
|
+
const stub = env.AGENTKIT_CONVERSATIONS.get(id);
|
|
2122
|
+
const response = await stub.fetch(
|
|
2123
|
+
\`https://agentkit.internal/messages?conversationId=\${encodeURIComponent(conversationId)}\`,
|
|
2124
|
+
);
|
|
2125
|
+
|
|
2126
|
+
if (!response.ok) {
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const payload = await response.json();
|
|
2131
|
+
return Array.isArray(payload.messages)
|
|
2132
|
+
? payload.messages.filter((message) =>
|
|
2133
|
+
message &&
|
|
2134
|
+
typeof message === "object" &&
|
|
2135
|
+
(message.role === "user" || message.role === "assistant") &&
|
|
2136
|
+
typeof message.content === "string"
|
|
2137
|
+
)
|
|
2138
|
+
: [];
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function normalizeConversationId(value) {
|
|
2142
|
+
if (value === undefined || value === null) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (typeof value !== "string") {
|
|
2147
|
+
throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const conversationId = value.trim();
|
|
2151
|
+
if (!conversationId) {
|
|
2152
|
+
throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
return conversationId;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function agentKitError(code, message) {
|
|
2159
|
+
const error = new Error(message);
|
|
2160
|
+
error.code = code;
|
|
2161
|
+
return error;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function hostedErrorResponse(error) {
|
|
2165
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error";
|
|
2166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2167
|
+
const status =
|
|
2168
|
+
code === "tool_not_found" || code === "database_not_configured" || code === "files_not_configured"
|
|
2169
|
+
? 404
|
|
2170
|
+
: code === "constraint_conflict"
|
|
2171
|
+
? 409
|
|
2172
|
+
: code === "tool_validation_error" || code === "validation_error" || code === "database_query_invalid"
|
|
2173
|
+
? 400
|
|
2174
|
+
: code === "database_unavailable" || code === "database_transaction_unsupported"
|
|
2175
|
+
? 503
|
|
2176
|
+
: code === "database_schema_error"
|
|
2177
|
+
? 500
|
|
2178
|
+
: code === "provider_not_supported"
|
|
2179
|
+
? 501
|
|
2180
|
+
: code === "tool_secret_missing" || code === "secret_not_found" || code === "deploy_not_ready"
|
|
2181
|
+
? 503
|
|
2182
|
+
: 500;
|
|
2183
|
+
|
|
2184
|
+
return Response.json({ error: { code, message } }, { status });
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
async function executeTursoSql(env, sql, args) {
|
|
2188
|
+
const results = await executeTursoPipeline(env, [
|
|
2189
|
+
{
|
|
2190
|
+
type: "execute",
|
|
2191
|
+
stmt: {
|
|
2192
|
+
sql,
|
|
2193
|
+
args: args.map(toTursoArg),
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
]);
|
|
2197
|
+
|
|
2198
|
+
return results[0] ?? emptyDatabaseResult();
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
async function executeTursoBatch(env, statements) {
|
|
2202
|
+
const results = await executeTursoPipeline(
|
|
2203
|
+
env,
|
|
2204
|
+
statements.map((statement) => ({
|
|
2205
|
+
type: "execute",
|
|
2206
|
+
stmt: {
|
|
2207
|
+
sql: statement.sql,
|
|
2208
|
+
args: (statement.args ?? []).map(toTursoArg),
|
|
2209
|
+
},
|
|
2210
|
+
})),
|
|
2211
|
+
);
|
|
2212
|
+
|
|
2213
|
+
return results;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
async function executeTursoPipeline(env, executeRequests) {
|
|
2217
|
+
let response;
|
|
2218
|
+
|
|
2219
|
+
try {
|
|
2220
|
+
response = await fetch(toTursoBaseUrl(env.TURSO_DATABASE_URL) + "/v2/pipeline", {
|
|
2221
|
+
method: "POST",
|
|
2222
|
+
headers: {
|
|
2223
|
+
Authorization: \`Bearer \${env.TURSO_AUTH_TOKEN}\`,
|
|
2224
|
+
"Content-Type": "application/json",
|
|
2225
|
+
},
|
|
2226
|
+
body: JSON.stringify({
|
|
2227
|
+
requests: [...executeRequests, { type: "close" }],
|
|
2228
|
+
}),
|
|
2229
|
+
});
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
throw agentKitError("database_unavailable", \`Turso SQL over HTTP request failed before a response: \${error instanceof Error ? error.message : String(error)}\`);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
const payload = await response.json().catch(() => null);
|
|
2235
|
+
|
|
2236
|
+
if (!response.ok) {
|
|
2237
|
+
throw mapTursoHttpError(response.status, payload);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const results = [];
|
|
2241
|
+
|
|
2242
|
+
for (const [index, result] of (payload?.results ?? []).entries()) {
|
|
2243
|
+
if (index >= executeRequests.length) {
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
const error = result?.response?.error ?? result?.error;
|
|
2248
|
+
|
|
2249
|
+
if (error) {
|
|
2250
|
+
throw mapTursoSqlError(error);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
results.push(normalizeTursoExecuteResult(result));
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
return results;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function normalizeTursoExecuteResult(payload) {
|
|
2260
|
+
const result = payload?.response?.result ?? payload?.results?.[0]?.response?.result;
|
|
2261
|
+
const columns = Array.isArray(result?.cols) ? result.cols.map((column) => String(column.name || "")) : [];
|
|
2262
|
+
const rows = Array.isArray(result?.rows)
|
|
2263
|
+
? result.rows.map((row) =>
|
|
2264
|
+
Object.fromEntries(row.map((value, index) => [columns[index] || \`column_\${index}\`, fromTursoValue(value)])),
|
|
2265
|
+
)
|
|
2266
|
+
: [];
|
|
2267
|
+
|
|
2268
|
+
return {
|
|
2269
|
+
rows,
|
|
2270
|
+
rowsAffected: Number(result?.affected_row_count ?? 0),
|
|
2271
|
+
lastInsertRowid: result?.last_insert_rowid ?? null,
|
|
2272
|
+
columns,
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function emptyDatabaseResult() {
|
|
2277
|
+
return {
|
|
2278
|
+
rows: [],
|
|
2279
|
+
rowsAffected: 0,
|
|
2280
|
+
lastInsertRowid: null,
|
|
2281
|
+
columns: [],
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function mapTursoHttpError(status, payload) {
|
|
2286
|
+
if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
|
|
2287
|
+
return agentKitError("database_unavailable", \`Turso SQL over HTTP is unavailable (HTTP \${status}).\`);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
return mapTursoSqlError(payload?.error ?? payload);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function mapTursoSqlError(error) {
|
|
2294
|
+
const message = readTursoErrorMessage(error);
|
|
2295
|
+
|
|
2296
|
+
if (/constraint failed|unique constraint|primary key|foreign key|check constraint|SQLITE_CONSTRAINT/i.test(message)) {
|
|
2297
|
+
return agentKitError("constraint_conflict", "Database constraint conflict.");
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
if (/no such table|no such column|has no column named|schema|SQLITE_SCHEMA/i.test(message)) {
|
|
2301
|
+
return agentKitError("database_schema_error", \`Database schema error: \${message}\`);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
if (/syntax error|incomplete input|SQLITE_ERROR|invalid sql/i.test(message)) {
|
|
2305
|
+
return agentKitError("database_query_invalid", \`Database query is invalid: \${message}\`);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
return agentKitError("database_unavailable", \`Turso SQL over HTTP failed: \${message}\`);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function readTursoErrorMessage(error) {
|
|
2312
|
+
if (!error) {
|
|
2313
|
+
return "unknown Turso error";
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (typeof error === "string") {
|
|
2317
|
+
return error;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (typeof error.message === "string") {
|
|
2321
|
+
return error.message;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
if (typeof error.code === "string") {
|
|
2325
|
+
return error.code;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
return JSON.stringify(error);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function fromTursoValue(value) {
|
|
2332
|
+
if (!value || value.type === "null") {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
if (value.type === "integer") {
|
|
2337
|
+
const numberValue = Number(value.value);
|
|
2338
|
+
return Number.isSafeInteger(numberValue) ? numberValue : String(value.value);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
if (value.type === "float") {
|
|
2342
|
+
return Number(value.value);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
if (value.type === "text" || value.type === "blob") {
|
|
2346
|
+
return String(value.value ?? "");
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
return value.value ?? null;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function requireTurso(env) {
|
|
2353
|
+
if (manifest.storage.database.driver !== "turso") {
|
|
2354
|
+
return Response.json({ error: { code: "database_not_configured", message: "Turso is not configured." } }, { status: 404 });
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
if (!env.TURSO_DATABASE_URL || !env.TURSO_AUTH_TOKEN) {
|
|
2358
|
+
return Response.json(
|
|
2359
|
+
{
|
|
2360
|
+
error: {
|
|
2361
|
+
code: "deploy_not_ready",
|
|
2362
|
+
message: "TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set as Cloudflare secrets.",
|
|
2363
|
+
},
|
|
2364
|
+
},
|
|
2365
|
+
{ status: 503 },
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function toTursoBaseUrl(value) {
|
|
2373
|
+
return String(value).replace(/^libsql:\\/\\//, "https://").replace(/\\/$/, "");
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
function toTursoArg(value) {
|
|
2377
|
+
if (value === null || value === undefined) {
|
|
2378
|
+
return { type: "null" };
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (typeof value === "boolean") {
|
|
2382
|
+
return { type: "integer", value: value ? "1" : "0" };
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
2386
|
+
return { type: "integer", value: String(value) };
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
if (typeof value === "number") {
|
|
2390
|
+
return { type: "float", value: String(value) };
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (typeof value === "string") {
|
|
2394
|
+
return { type: "text", value };
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
throw agentKitError("database_query_invalid", "Database SQL args may only contain strings, numbers, booleans, or null.");
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function objectKey(rawKey) {
|
|
2401
|
+
const decoded = decodeURIComponent(String(rawKey || "")).replace(/^\\/+/, "");
|
|
2402
|
+
|
|
2403
|
+
if (!decoded || decoded.includes("..")) {
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
const prefix = manifest.storage.files.driver === "r2" ? manifest.storage.files.prefix : "";
|
|
2408
|
+
return [prefix, decoded].filter(Boolean).join("/").replace(/\\/+/g, "/");
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
function titleFromMessage(content) {
|
|
2412
|
+
const compact = content.split(/\\r?\\n/, 1)[0].replace(/\\s+/g, " ").trim();
|
|
2413
|
+
return compact.length <= 80 ? compact : \`\${compact.slice(0, 77)}...\`;
|
|
2414
|
+
}
|
|
2415
|
+
`;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function renderWranglerConfig(agentName: string, files: AgentManifest["storage"]["files"]): string {
|
|
2419
|
+
const config: Record<string, unknown> = {
|
|
2420
|
+
$schema: "node_modules/wrangler/config-schema.json",
|
|
2421
|
+
name: `agentkit-${slugify(agentName)}`,
|
|
2422
|
+
main: "./worker.js",
|
|
2423
|
+
compatibility_date: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
2424
|
+
compatibility_flags: [...CLOUDFLARE_COMPATIBILITY_FLAGS],
|
|
2425
|
+
observability: {
|
|
2426
|
+
enabled: true,
|
|
2427
|
+
},
|
|
2428
|
+
durable_objects: {
|
|
2429
|
+
bindings: [
|
|
2430
|
+
{
|
|
2431
|
+
name: "AGENTKIT_CONVERSATIONS",
|
|
2432
|
+
class_name: "AgentKitConversationStore",
|
|
2433
|
+
},
|
|
2434
|
+
],
|
|
2435
|
+
},
|
|
2436
|
+
migrations: [
|
|
2437
|
+
{
|
|
2438
|
+
tag: "v1",
|
|
2439
|
+
new_sqlite_classes: ["AgentKitConversationStore"],
|
|
2440
|
+
},
|
|
2441
|
+
],
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
if (files.driver === "r2") {
|
|
2445
|
+
config.r2_buckets = [
|
|
2446
|
+
{
|
|
2447
|
+
binding: files.binding,
|
|
2448
|
+
bucket_name: files.bucket,
|
|
2449
|
+
},
|
|
2450
|
+
];
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
return `${JSON.stringify(
|
|
2454
|
+
config,
|
|
2455
|
+
null,
|
|
2456
|
+
2,
|
|
2457
|
+
)}\n`;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function renderDurableObjectSchema(): string {
|
|
2461
|
+
return `CREATE TABLE IF NOT EXISTS conversations (
|
|
2462
|
+
id TEXT PRIMARY KEY,
|
|
2463
|
+
agent_name TEXT NOT NULL,
|
|
2464
|
+
title TEXT,
|
|
2465
|
+
created_at TEXT NOT NULL,
|
|
2466
|
+
updated_at TEXT NOT NULL
|
|
2467
|
+
);
|
|
2468
|
+
|
|
2469
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
2470
|
+
id TEXT PRIMARY KEY,
|
|
2471
|
+
conversation_id TEXT NOT NULL,
|
|
2472
|
+
role TEXT NOT NULL,
|
|
2473
|
+
content TEXT NOT NULL,
|
|
2474
|
+
created_at TEXT NOT NULL
|
|
2475
|
+
);
|
|
2476
|
+
|
|
2477
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
2478
|
+
id TEXT PRIMARY KEY,
|
|
2479
|
+
conversation_id TEXT NOT NULL,
|
|
2480
|
+
provider TEXT NOT NULL,
|
|
2481
|
+
model TEXT NOT NULL,
|
|
2482
|
+
status TEXT NOT NULL,
|
|
2483
|
+
error_code TEXT,
|
|
2484
|
+
error_message TEXT,
|
|
2485
|
+
created_at TEXT NOT NULL,
|
|
2486
|
+
completed_at TEXT
|
|
2487
|
+
);
|
|
2488
|
+
|
|
2489
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
2490
|
+
id TEXT PRIMARY KEY,
|
|
2491
|
+
run_id TEXT NOT NULL,
|
|
2492
|
+
conversation_id TEXT NOT NULL,
|
|
2493
|
+
tool_name TEXT NOT NULL,
|
|
2494
|
+
visibility TEXT NOT NULL,
|
|
2495
|
+
status TEXT NOT NULL,
|
|
2496
|
+
input_json TEXT NOT NULL,
|
|
2497
|
+
output_json TEXT,
|
|
2498
|
+
error_message TEXT,
|
|
2499
|
+
created_at TEXT NOT NULL,
|
|
2500
|
+
completed_at TEXT
|
|
2501
|
+
);
|
|
2502
|
+
`;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function slugify(value: string): string {
|
|
2506
|
+
const slug = value
|
|
2507
|
+
.toLowerCase()
|
|
2508
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
2509
|
+
.replace(/^-+|-+$/g, "")
|
|
2510
|
+
.slice(0, 48);
|
|
2511
|
+
|
|
2512
|
+
return slug || "agent";
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function sha256(source: string): string {
|
|
2516
|
+
return createHash("sha256").update(source).digest("hex");
|
|
2517
|
+
}
|