@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,684 @@
|
|
|
1
|
+
import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AgentConfig,
|
|
8
|
+
AgentProviderName,
|
|
9
|
+
AgentRuntime,
|
|
10
|
+
AccessMode,
|
|
11
|
+
AgentTool,
|
|
12
|
+
ToolVisibility,
|
|
13
|
+
AgentChannel,
|
|
14
|
+
ChannelProvider,
|
|
15
|
+
ChannelType,
|
|
16
|
+
TelegramAllowedUpdate,
|
|
17
|
+
AgentKnowledgeEmbeddingProvider,
|
|
18
|
+
ChannelBufferMode,
|
|
19
|
+
} from "../index";
|
|
20
|
+
import { AgentKitError } from "./errors";
|
|
21
|
+
|
|
22
|
+
const CONFIG_FILE = "agentkit.config.ts";
|
|
23
|
+
const AGENT_RUNTIMES = new Set<AgentRuntime>(["local", "edge"]);
|
|
24
|
+
const PROVIDERS = new Set<AgentProviderName>(["test", "openai", "anthropic", "openrouter", "custom"]);
|
|
25
|
+
const ACCESS_MODES = new Set<AccessMode>(["private", "token", "public"]);
|
|
26
|
+
const TOOL_VISIBILITIES = new Set<ToolVisibility>(["user", "internal"]);
|
|
27
|
+
const CHANNEL_TYPES = new Set<ChannelType>(["website", "telegram", "whatsapp"]);
|
|
28
|
+
const CHANNEL_PROVIDERS = new Set<ChannelProvider>(["agentkit", "telegram", "zapster", "meta"]);
|
|
29
|
+
const CHANNEL_BUFFER_MODES = new Set<ChannelBufferMode>(["off", "debounce"]);
|
|
30
|
+
const TELEGRAM_ALLOWED_UPDATES = new Set<TelegramAllowedUpdate>(["message"]);
|
|
31
|
+
const KNOWLEDGE_EMBEDDING_PROVIDERS = new Set<AgentKnowledgeEmbeddingProvider>(["none", "test", "openai"]);
|
|
32
|
+
const CHANNEL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
33
|
+
const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
34
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
|
|
36
|
+
export type LoadedAgentCapsule = {
|
|
37
|
+
root: string;
|
|
38
|
+
configPath: string;
|
|
39
|
+
config: AgentConfig;
|
|
40
|
+
instructionsPath: string;
|
|
41
|
+
instructions: string;
|
|
42
|
+
storagePath?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function findAgentCapsuleRoot(cwd: string): Promise<string> {
|
|
46
|
+
let current = resolve(cwd);
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
49
|
+
const configPath = join(current, CONFIG_FILE);
|
|
50
|
+
|
|
51
|
+
if (await fileExists(configPath)) {
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parent = dirname(current);
|
|
56
|
+
|
|
57
|
+
if (parent === current) {
|
|
58
|
+
throw new AgentKitError(
|
|
59
|
+
"missing_config",
|
|
60
|
+
`No ${CONFIG_FILE} found from ${resolve(cwd)} or any parent directory. Run this command from an Agent Capsule root.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
current = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function loadAgentCapsule(cwd = process.cwd()): Promise<LoadedAgentCapsule> {
|
|
69
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
70
|
+
const configPath = join(root, CONFIG_FILE);
|
|
71
|
+
const config = await loadConfigModule(configPath);
|
|
72
|
+
validateAgentConfig(config);
|
|
73
|
+
|
|
74
|
+
const instructionsPath = resolvePath(root, config.instructions);
|
|
75
|
+
|
|
76
|
+
let instructions: string;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
instructions = await readFile(instructionsPath, "utf8");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new AgentKitError(
|
|
82
|
+
"missing_prompt",
|
|
83
|
+
`Prompt file not found at ${relative(root, instructionsPath) || config.instructions}. Check the instructions path in ${CONFIG_FILE}.`,
|
|
84
|
+
{ cause: error },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
root,
|
|
90
|
+
configPath,
|
|
91
|
+
config,
|
|
92
|
+
instructionsPath,
|
|
93
|
+
instructions,
|
|
94
|
+
storagePath: resolveLocalStoragePath(root, config),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveLocalStoragePath(root: string, config: AgentConfig): string | undefined {
|
|
99
|
+
if (config.storage.path) {
|
|
100
|
+
return resolvePath(root, config.storage.path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (config.storage.driver === "agentkit") {
|
|
104
|
+
return resolvePath(root, ".agentkit/agentkit.db");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadConfigModule(configPath: string): Promise<unknown> {
|
|
111
|
+
const configUrl = pathToFileURL(configPath);
|
|
112
|
+
configUrl.searchParams.set("t", String(Date.now()));
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const module = (await import(configUrl.href)) as { default?: unknown };
|
|
116
|
+
return module.default;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isMissingAgentKitSelfImport(error)) {
|
|
119
|
+
return loadConfigModuleWithLocalSelfImport(configPath, error);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new AgentKitError(
|
|
123
|
+
"invalid_config",
|
|
124
|
+
`Could not load ${CONFIG_FILE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
{ cause: error },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadConfigModuleWithLocalSelfImport(configPath: string, originalError: unknown): Promise<unknown> {
|
|
131
|
+
const source = await readFile(configPath, "utf8");
|
|
132
|
+
const localAgentKitUrl = pathToFileURL(resolve(moduleDir, "../index.ts")).href;
|
|
133
|
+
const patchedSource = source.replace(
|
|
134
|
+
/from\s+["']@andreprado\/agentkit["']/g,
|
|
135
|
+
`from ${JSON.stringify(localAgentKitUrl)}`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (patchedSource === source) {
|
|
139
|
+
throw new AgentKitError(
|
|
140
|
+
"invalid_config",
|
|
141
|
+
`Could not load ${CONFIG_FILE}: ${originalError instanceof Error ? originalError.message : String(originalError)}`,
|
|
142
|
+
{ cause: originalError },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tempConfigPath = join(
|
|
147
|
+
dirname(configPath),
|
|
148
|
+
`.agentkit.config.loader-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.ts`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await writeFile(tempConfigPath, patchedSource);
|
|
153
|
+
const configUrl = pathToFileURL(tempConfigPath);
|
|
154
|
+
configUrl.searchParams.set("t", String(Date.now()));
|
|
155
|
+
const module = (await import(configUrl.href)) as { default?: unknown };
|
|
156
|
+
return module.default;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new AgentKitError(
|
|
159
|
+
"invalid_config",
|
|
160
|
+
`Could not load ${CONFIG_FILE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
161
|
+
{ cause: error },
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
await unlink(tempConfigPath).catch(() => undefined);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isMissingAgentKitSelfImport(error: unknown): boolean {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
return message.includes("@andreprado/agentkit");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateAgentConfig(value: unknown): asserts value is AgentConfig {
|
|
174
|
+
if (!isRecord(value)) {
|
|
175
|
+
throw invalidConfig("Config must export defineAgent({...}) as the default export.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
expectString(value.name, "name");
|
|
179
|
+
|
|
180
|
+
if (!AGENT_RUNTIMES.has(value.runtime as AgentRuntime)) {
|
|
181
|
+
throw invalidConfig('runtime must be "local" or "edge".');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!isRecord(value.provider)) {
|
|
185
|
+
throw invalidConfig("provider must be an object with name and model.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!PROVIDERS.has(value.provider.name as AgentProviderName)) {
|
|
189
|
+
throw invalidConfig(`provider.name must be one of: ${Array.from(PROVIDERS).join(", ")}.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
expectString(value.provider.model, "provider.model");
|
|
193
|
+
|
|
194
|
+
if (value.provider.name === "test" && value.provider.model !== "fake") {
|
|
195
|
+
throw invalidConfig('provider.model must be "fake" when provider.name is "test".');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
expectString(value.instructions, "instructions");
|
|
199
|
+
expectStringArray(value.secrets, "secrets");
|
|
200
|
+
|
|
201
|
+
if (value.tools !== undefined) {
|
|
202
|
+
if (!Array.isArray(value.tools)) {
|
|
203
|
+
throw invalidConfig("tools must be an array when provided.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const [index, tool] of value.tools.entries()) {
|
|
207
|
+
validateTool(tool, index);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
validateChannels(value.channels, value.runtime as AgentRuntime);
|
|
212
|
+
validateKnowledgeConfig(value.knowledge);
|
|
213
|
+
|
|
214
|
+
if (!isRecord(value.access) || !ACCESS_MODES.has(value.access.mode as AccessMode)) {
|
|
215
|
+
throw invalidConfig('access.mode must be "private", "token", or "public".');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!isRecord(value.storage)) {
|
|
219
|
+
throw invalidConfig("storage must be an object.");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (value.storage.driver !== "sqlite" && value.storage.driver !== "agentkit") {
|
|
223
|
+
throw invalidConfig('storage.driver must be "sqlite" or "agentkit".');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (value.storage.path !== undefined && typeof value.storage.path !== "string") {
|
|
227
|
+
throw invalidConfig("storage.path must be a string when provided.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
validateDatabaseConfig(value.storage.database);
|
|
231
|
+
validateFilesConfig(value.storage.files);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validateKnowledgeConfig(value: unknown): void {
|
|
235
|
+
if (value === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isRecord(value)) {
|
|
240
|
+
throw invalidConfig("knowledge must be an object when provided.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (value.sources !== undefined) {
|
|
244
|
+
if (!Array.isArray(value.sources)) {
|
|
245
|
+
throw invalidConfig("knowledge.sources must be an array when provided.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const [index, source] of value.sources.entries()) {
|
|
249
|
+
validateKnowledgeSource(source, index);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (value.embedding !== undefined) {
|
|
254
|
+
validateKnowledgeEmbedding(value.embedding);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (value.retrieval !== undefined) {
|
|
258
|
+
validateKnowledgeRetrieval(value.retrieval);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateKnowledgeSource(value: unknown, index: number): void {
|
|
263
|
+
if (typeof value === "string") {
|
|
264
|
+
expectString(value, `knowledge.sources[${index}]`);
|
|
265
|
+
validateKnowledgeSourceReference(value, `knowledge.sources[${index}]`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!isRecord(value)) {
|
|
270
|
+
throw invalidConfig(`knowledge.sources[${index}] must be a path string or an object with path or url.`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const hasPath = "path" in value;
|
|
274
|
+
const hasUrl = "url" in value;
|
|
275
|
+
|
|
276
|
+
if (hasPath === hasUrl) {
|
|
277
|
+
throw invalidConfig(`knowledge.sources[${index}] must set exactly one of path or url.`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (hasPath) {
|
|
281
|
+
expectString(value.path, `knowledge.sources[${index}].path`);
|
|
282
|
+
validateKnowledgeSourceReference(value.path, `knowledge.sources[${index}].path`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (hasUrl) {
|
|
286
|
+
expectString(value.url, `knowledge.sources[${index}].url`);
|
|
287
|
+
validateKnowledgeUrl(value.url, `knowledge.sources[${index}].url`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (value.title !== undefined) {
|
|
291
|
+
expectString(value.title, `knowledge.sources[${index}].title`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function validateKnowledgeSourceReference(value: string, label: string): void {
|
|
296
|
+
if (value.includes("\0")) {
|
|
297
|
+
throw invalidConfig(`${label} must not contain null bytes.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (/^https?:\/\//i.test(value)) {
|
|
301
|
+
validateKnowledgeUrl(value, label);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function validateKnowledgeUrl(value: string, label: string): void {
|
|
306
|
+
let url: URL;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
url = new URL(value);
|
|
310
|
+
} catch {
|
|
311
|
+
throw invalidConfig(`${label} must be a valid http or https URL.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
315
|
+
throw invalidConfig(`${label} must be a valid http or https URL.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateKnowledgeEmbedding(value: unknown): void {
|
|
320
|
+
if (!isRecord(value)) {
|
|
321
|
+
throw invalidConfig("knowledge.embedding must be an object when provided.");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (value.provider !== undefined && !KNOWLEDGE_EMBEDDING_PROVIDERS.has(value.provider as AgentKnowledgeEmbeddingProvider)) {
|
|
325
|
+
throw invalidConfig(
|
|
326
|
+
`knowledge.embedding.provider must be one of: ${Array.from(KNOWLEDGE_EMBEDDING_PROVIDERS).join(", ")}.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (value.model !== undefined) {
|
|
331
|
+
expectString(value.model, "knowledge.embedding.model");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (value.secret !== undefined) {
|
|
335
|
+
expectString(value.secret, "knowledge.embedding.secret");
|
|
336
|
+
validateSecretNames([value.secret], "knowledge.embedding.secret");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const dimensions = value.dimensions;
|
|
340
|
+
if (dimensions !== undefined) {
|
|
341
|
+
if (typeof dimensions !== "number" || !Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 65_536) {
|
|
342
|
+
throw invalidConfig("knowledge.embedding.dimensions must be a positive integer no larger than 65536.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateKnowledgeRetrieval(value: unknown): void {
|
|
348
|
+
if (!isRecord(value)) {
|
|
349
|
+
throw invalidConfig("knowledge.retrieval must be an object when provided.");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const topK = value.topK;
|
|
353
|
+
if (topK !== undefined && (typeof topK !== "number" || !Number.isInteger(topK) || topK <= 0 || topK > 50)) {
|
|
354
|
+
throw invalidConfig("knowledge.retrieval.topK must be a positive integer no larger than 50.");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (value.hybrid !== undefined && typeof value.hybrid !== "boolean") {
|
|
358
|
+
throw invalidConfig("knowledge.retrieval.hybrid must be a boolean when provided.");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function validateChannels(value: unknown, runtime: AgentRuntime): void {
|
|
363
|
+
if (value === undefined) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!Array.isArray(value)) {
|
|
368
|
+
throw invalidConfig("channels must be an array when provided.");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (value.length > 0 && runtime !== "edge") {
|
|
372
|
+
throw invalidConfig('channels require runtime: "edge".');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const names = new Set<string>();
|
|
376
|
+
|
|
377
|
+
for (const [index, channel] of value.entries()) {
|
|
378
|
+
validateChannel(channel, index);
|
|
379
|
+
|
|
380
|
+
if (names.has(channel.name)) {
|
|
381
|
+
throw invalidConfig(`channels[${index}].name must be unique within the Agent Capsule.`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
names.add(channel.name);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateChannel(value: unknown, index: number): asserts value is AgentChannel {
|
|
389
|
+
if (!isRecord(value)) {
|
|
390
|
+
throw invalidConfig(`channels[${index}] must be an object.`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
expectString(value.name, `channels[${index}].name`);
|
|
394
|
+
|
|
395
|
+
if (!CHANNEL_NAME_PATTERN.test(value.name)) {
|
|
396
|
+
throw invalidConfig(
|
|
397
|
+
`channels[${index}].name must be lowercase and contain only letters, numbers, and hyphens.`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!CHANNEL_TYPES.has(value.type as ChannelType)) {
|
|
402
|
+
throw invalidConfig(`channels[${index}].type must be one of: ${Array.from(CHANNEL_TYPES).join(", ")}.`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!CHANNEL_PROVIDERS.has(value.provider as ChannelProvider)) {
|
|
406
|
+
throw invalidConfig(`channels[${index}].provider must be one of: ${Array.from(CHANNEL_PROVIDERS).join(", ")}.`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
expectStringArray(value.secrets, `channels[${index}].secrets`);
|
|
410
|
+
validateSecretNames(value.secrets, `channels[${index}].secrets`);
|
|
411
|
+
validateChannelLimits(value.limits, index);
|
|
412
|
+
validateChannelBuffer(value.buffer, index);
|
|
413
|
+
|
|
414
|
+
if (value.type === "website") {
|
|
415
|
+
validateWebsiteChannel(value, index);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (value.type === "telegram") {
|
|
420
|
+
validateTelegramChannel(value, index);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
validateWhatsappChannel(value, index);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function validateWebsiteChannel(value: Record<string, unknown>, index: number): void {
|
|
428
|
+
if (value.provider !== "agentkit") {
|
|
429
|
+
throw invalidConfig(`channels[${index}].provider must be "agentkit" when type is "website".`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!isRecord(value.access) || value.access.mode !== "token") {
|
|
433
|
+
throw invalidConfig(`channels[${index}].access.mode must be "token" when type is "website".`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
expectRequiredSecrets(value.secrets, ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"], `channels[${index}]`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function validateTelegramChannel(value: Record<string, unknown>, index: number): void {
|
|
440
|
+
if (value.provider !== "telegram") {
|
|
441
|
+
throw invalidConfig(`channels[${index}].provider must be "telegram" when type is "telegram".`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
expectRequiredSecrets(value.secrets, ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"], `channels[${index}]`);
|
|
445
|
+
|
|
446
|
+
if (!Array.isArray(value.allowedUpdates)) {
|
|
447
|
+
throw invalidConfig(`channels[${index}].allowedUpdates must be an array.`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const update of value.allowedUpdates) {
|
|
451
|
+
if (!TELEGRAM_ALLOWED_UPDATES.has(update as TelegramAllowedUpdate)) {
|
|
452
|
+
throw invalidConfig(`channels[${index}].allowedUpdates currently supports only "message".`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function validateWhatsappChannel(value: Record<string, unknown>, index: number): void {
|
|
458
|
+
if (value.type !== "whatsapp") {
|
|
459
|
+
throw invalidConfig(`channels[${index}].type must be "website", "telegram", or "whatsapp".`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (value.provider === "zapster") {
|
|
463
|
+
expectRequiredSecrets(
|
|
464
|
+
value.secrets,
|
|
465
|
+
["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"],
|
|
466
|
+
`channels[${index}]`,
|
|
467
|
+
);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (value.provider === "meta") {
|
|
472
|
+
expectRequiredSecrets(
|
|
473
|
+
value.secrets,
|
|
474
|
+
["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"],
|
|
475
|
+
`channels[${index}]`,
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
throw invalidConfig(`channels[${index}].provider must be "zapster" or "meta" when type is "whatsapp".`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function validateChannelLimits(value: unknown, index: number): void {
|
|
484
|
+
if (value === undefined) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!isRecord(value)) {
|
|
489
|
+
throw invalidConfig(`channels[${index}].limits must be an object when provided.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const field of ["messagesPerDay", "usdPerDay"] as const) {
|
|
493
|
+
if (
|
|
494
|
+
value[field] !== undefined &&
|
|
495
|
+
(typeof value[field] !== "number" || !Number.isFinite(value[field]) || value[field] <= 0)
|
|
496
|
+
) {
|
|
497
|
+
throw invalidConfig(`channels[${index}].limits.${field} must be a positive number when provided.`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function validateChannelBuffer(value: unknown, index: number): void {
|
|
503
|
+
if (value === undefined) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!isRecord(value)) {
|
|
508
|
+
throw invalidConfig(`channels[${index}].buffer must be an object when provided.`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!CHANNEL_BUFFER_MODES.has(value.mode as ChannelBufferMode)) {
|
|
512
|
+
throw invalidConfig(`channels[${index}].buffer.mode must be "off" or "debounce".`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (value.mode === "off") {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const field of ["quietWindowMs", "maxWaitMs", "maxMessages", "maxChars"] as const) {
|
|
520
|
+
if (
|
|
521
|
+
value[field] === undefined ||
|
|
522
|
+
typeof value[field] !== "number" ||
|
|
523
|
+
!Number.isInteger(value[field]) ||
|
|
524
|
+
value[field] <= 0
|
|
525
|
+
) {
|
|
526
|
+
throw invalidConfig(`channels[${index}].buffer.${field} must be a positive integer.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if ((value.maxWaitMs as number) < (value.quietWindowMs as number)) {
|
|
531
|
+
throw invalidConfig(`channels[${index}].buffer.maxWaitMs must be greater than or equal to quietWindowMs.`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function validateSecretNames(secrets: string[], field: string): void {
|
|
536
|
+
for (const secret of secrets) {
|
|
537
|
+
if (!SECRET_NAME_PATTERN.test(secret)) {
|
|
538
|
+
throw invalidConfig(`${field} must contain environment variable names, not secret values.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function expectRequiredSecrets(value: unknown, requiredSecrets: string[], field: string): void {
|
|
544
|
+
const declaredSecrets = new Set(Array.isArray(value) ? value : []);
|
|
545
|
+
const missingSecrets = requiredSecrets.filter((secret) => !declaredSecrets.has(secret));
|
|
546
|
+
|
|
547
|
+
if (missingSecrets.length > 0) {
|
|
548
|
+
throw invalidConfig(`${field}.secrets is missing required secret names: ${missingSecrets.join(", ")}.`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function validateDatabaseConfig(value: unknown): void {
|
|
553
|
+
if (value === undefined) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!isRecord(value)) {
|
|
558
|
+
throw invalidConfig("storage.database must be an object when provided.");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (value.driver !== "none" && value.driver !== "turso") {
|
|
562
|
+
throw invalidConfig('storage.database.driver must be "none" or "turso".');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (value.schema !== undefined && typeof value.schema !== "string") {
|
|
566
|
+
throw invalidConfig("storage.database.schema must be a string when provided.");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (value.migrations !== undefined && typeof value.migrations !== "string") {
|
|
570
|
+
throw invalidConfig("storage.database.migrations must be a string when provided.");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function validateFilesConfig(value: unknown): void {
|
|
575
|
+
if (value === undefined) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!isRecord(value)) {
|
|
580
|
+
throw invalidConfig("storage.files must be an object when provided.");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (value.driver !== "none" && value.driver !== "r2") {
|
|
584
|
+
throw invalidConfig('storage.files.driver must be "none" or "r2".');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (value.bucket !== undefined && typeof value.bucket !== "string") {
|
|
588
|
+
throw invalidConfig("storage.files.bucket must be a string when provided.");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (value.prefix !== undefined && typeof value.prefix !== "string") {
|
|
592
|
+
throw invalidConfig("storage.files.prefix must be a string when provided.");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (
|
|
596
|
+
value.maxUploadBytes !== undefined &&
|
|
597
|
+
(typeof value.maxUploadBytes !== "number" ||
|
|
598
|
+
!Number.isInteger(value.maxUploadBytes) ||
|
|
599
|
+
value.maxUploadBytes <= 0)
|
|
600
|
+
) {
|
|
601
|
+
throw invalidConfig("storage.files.maxUploadBytes must be a positive integer when provided.");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function validateTool(value: unknown, index: number): asserts value is AgentTool {
|
|
606
|
+
if (!isRecord(value)) {
|
|
607
|
+
throw invalidConfig(`tools[${index}] must be an object.`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
expectString(value.name, `tools[${index}].name`);
|
|
611
|
+
expectString(value.description, `tools[${index}].description`);
|
|
612
|
+
|
|
613
|
+
if (value.visibility !== undefined && !TOOL_VISIBILITIES.has(value.visibility as ToolVisibility)) {
|
|
614
|
+
throw invalidConfig(`tools[${index}].visibility must be "user" or "internal" when provided.`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!isRecord(value.inputSchema)) {
|
|
618
|
+
throw invalidConfig(`tools[${index}].inputSchema must be an object.`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (value.outputSchema !== undefined && !isRecord(value.outputSchema)) {
|
|
622
|
+
throw invalidConfig(`tools[${index}].outputSchema must be an object when provided.`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (value.secrets !== undefined) {
|
|
626
|
+
expectStringArray(value.secrets, `tools[${index}].secrets`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (value.permissions !== undefined) {
|
|
630
|
+
expectStringArray(value.permissions, `tools[${index}].permissions`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (value.timeoutMs !== undefined) {
|
|
634
|
+
if (typeof value.timeoutMs !== "number" || !Number.isInteger(value.timeoutMs) || value.timeoutMs <= 0) {
|
|
635
|
+
throw invalidConfig(`tools[${index}].timeoutMs must be a positive integer when provided.`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (typeof value.execute !== "function") {
|
|
640
|
+
throw invalidConfig(`tools[${index}].execute must be a function.`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (value.render !== undefined && typeof value.render !== "function") {
|
|
644
|
+
throw invalidConfig(`tools[${index}].render must be a function when provided.`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function resolvePath(root: string, path: string): string {
|
|
649
|
+
return isAbsolute(path) ? path : resolve(root, path);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
653
|
+
try {
|
|
654
|
+
const result = await stat(path);
|
|
655
|
+
return result.isFile();
|
|
656
|
+
} catch {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function invalidConfig(message: string): AgentKitError {
|
|
662
|
+
return new AgentKitError("invalid_config", `Invalid ${CONFIG_FILE}: ${message}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function expectString(value: unknown, field: string): asserts value is string {
|
|
666
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
667
|
+
throw invalidConfig(`${field} must be a non-empty string.`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function expectStringArray(value: unknown, field: string): asserts value is string[] {
|
|
672
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
673
|
+
throw invalidConfig(`${field} must be an array of non-empty strings.`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
678
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export async function validateConfigExists(cwd = process.cwd()): Promise<void> {
|
|
682
|
+
const root = await findAgentCapsuleRoot(cwd);
|
|
683
|
+
await access(join(root, CONFIG_FILE), constants.R_OK);
|
|
684
|
+
}
|