agenticweb-cli 0.1.1 → 0.2.0
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 +23 -7
- package/dist/agenticweb.mjs +254 -44
- package/dist/runtime/edge-worker.js +133 -6
- package/dist/runtime/server.mjs +133 -6
- package/dist/runtime/worker.js +133 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,10 +40,25 @@ npx agenticweb-cli serve --edge
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
- **init** starts a project and asks you to prove you own the site (a DNS record or a small file).
|
|
43
|
-
- **build** reads your site and pulls out what matters: pages, products, menus, hours, FAQs and policies.
|
|
43
|
+
- **build** reads your site and pulls out what matters: pages, products, menus, hours, FAQs and policies.
|
|
44
44
|
- **review** opens a page where you choose what agents can see and which actions they can use. Everything starts off.
|
|
45
45
|
- **serve --edge** shows your site with the agent version added, at `http://127.0.0.1:8787`.
|
|
46
46
|
|
|
47
|
+
## Do I need an AI key?
|
|
48
|
+
|
|
49
|
+
**No.** AgenticWeb works fully without one. The benchmark above was run with no AI key at all.
|
|
50
|
+
|
|
51
|
+
**What an AI provider is for:** it's the AI that AgenticWeb can call on for a few optional extras. The core work never depends on it: building the agent version, the safety checks, bookings and checkout all run on AgenticWeb's own rules.
|
|
52
|
+
|
|
53
|
+
| | Without a key | With a key |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Reading your site | Built-in rules, good for well-built sites | Smarter on messy pages (and PDFs, with Claude) |
|
|
56
|
+
| Answering agents' questions | The matching facts, with sources | A short written answer, with sources |
|
|
57
|
+
| Testing your site as an agent | Quick checks with set answers | A real AI agent tries each task |
|
|
58
|
+
| Questions your site can't answer yet | The question, for you to answer | A draft answer from your site's own facts |
|
|
59
|
+
|
|
60
|
+
**Pick any provider:** Claude (the default), OpenAI, DeepSeek, Gemini, or a free model running on your own computer (Ollama or LM Studio). It's one line in the config, and the [guide](docs/GUIDE.md#choosing-an-ai-provider) shows how. Your key stays on your computer or server, and requests go straight to the provider you chose, never to us.
|
|
61
|
+
|
|
47
62
|
## Put it on your site
|
|
48
63
|
|
|
49
64
|
**Recommended: edge mode.** AgenticWeb runs in front of your site on your own domain. Nothing gets installed on your site, so it works with WordPress, Shopify themes, Webflow, custom code or anything else.
|
|
@@ -62,15 +77,15 @@ This prepares a Cloudflare Worker for your domain. Not on Cloudflare? Use `--tar
|
|
|
62
77
|
| Markdown pages | Any page, as clean text, when an agent asks for it |
|
|
63
78
|
| Product and business data | Prices, stock, hours and FAQs in the standard schema.org format |
|
|
64
79
|
| `/.agent/ask` | Ask a question in plain words and get an answer with sources |
|
|
65
|
-
| MCP server | Your site as tools for Claude and other AI apps |
|
|
80
|
+
| MCP server | Your site as tools for ChatGPT, Claude, Gemini, Cursor and other AI apps |
|
|
66
81
|
| WebMCP | The same tools for AI agents built into browsers |
|
|
67
82
|
| UCP and ACP checkout | The checkout protocols shopping agents use, for any store, not only Shopify |
|
|
68
83
|
|
|
69
84
|
## Check that it works
|
|
70
85
|
|
|
71
|
-
- **Test it like an agent.** `agenticweb test`
|
|
86
|
+
- **Test it like an agent.** `npx agenticweb-cli test` tries your real tasks, like "book a table for 4 on Friday", on both your site and the agent version (with an AI key, a real AI agent does them). You get an **Agent Readiness score**, a badge, and a warning when a task that used to work breaks.
|
|
72
87
|
- **Prove it's the same story.** Every build checks each fact (prices, hours, contact details) against your live pages and signs the result with your key. Anyone can check it with `npx agenticweb-cli check <agent-branch-url>`.
|
|
73
|
-
- **Fill the gaps.** `agenticweb suggest`
|
|
88
|
+
- **Fill the gaps.** `npx agenticweb-cli suggest` lists questions agents asked that your site couldn't answer, with a draft answer for each if you have an AI key. You approve them and add them to your site, and agents pick them up from there.
|
|
74
89
|
|
|
75
90
|
## Works with
|
|
76
91
|
|
|
@@ -88,10 +103,11 @@ Shopify, WooCommerce, Stripe, Cal.com, any API with an OpenAPI description, any
|
|
|
88
103
|
|
|
89
104
|
More in [SECURITY.md](SECURITY.md).
|
|
90
105
|
|
|
91
|
-
## Use it from
|
|
106
|
+
## Use it from your AI assistant
|
|
92
107
|
|
|
93
|
-
- **Claude Code
|
|
94
|
-
- **Claude Desktop
|
|
108
|
+
- **Any coding assistant** (Claude Code, Codex, Cursor, Gemini CLI and others) can run the `npx agenticweb-cli` commands for you.
|
|
109
|
+
- **Any MCP app** (ChatGPT, Claude Desktop, Cursor, VS Code and others): add an MCP server with the command `npx` and the arguments `["agenticweb-cli@latest", "mcp"]`.
|
|
110
|
+
- **Claude Code plugin:** run `/plugin marketplace add adibfarah/agenticweb`, then `/agentify https://mysite.com` for a guided walkthrough.
|
|
95
111
|
|
|
96
112
|
## Learn more
|
|
97
113
|
|
package/dist/agenticweb.mjs
CHANGED
|
@@ -20788,6 +20788,122 @@ var init_util2 = __esm({
|
|
|
20788
20788
|
}
|
|
20789
20789
|
});
|
|
20790
20790
|
|
|
20791
|
+
// packages/core/src/llm-providers.ts
|
|
20792
|
+
function resolveLlm(s, env) {
|
|
20793
|
+
if (!s.enabled) return void 0;
|
|
20794
|
+
const info = PROVIDERS[s.provider];
|
|
20795
|
+
const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
|
|
20796
|
+
const apiKey = env[keyEnv] || void 0;
|
|
20797
|
+
if (s.provider === "anthropic") {
|
|
20798
|
+
if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
|
|
20799
|
+
} else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
|
|
20800
|
+
return void 0;
|
|
20801
|
+
}
|
|
20802
|
+
const baseUrl = s.baseUrl ?? info.baseUrl;
|
|
20803
|
+
if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
|
|
20804
|
+
return {
|
|
20805
|
+
provider: s.provider,
|
|
20806
|
+
label: info.label,
|
|
20807
|
+
apiKey,
|
|
20808
|
+
baseUrl,
|
|
20809
|
+
jsonMode: info.jsonMode,
|
|
20810
|
+
maxTokensParam: info.maxTokensParam,
|
|
20811
|
+
model: (configured) => {
|
|
20812
|
+
if (s.model) return s.model;
|
|
20813
|
+
if (s.provider === "anthropic") return configured || info.defaultModel;
|
|
20814
|
+
if (configured && !configured.startsWith("claude-")) return configured;
|
|
20815
|
+
if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
|
|
20816
|
+
return info.defaultModel;
|
|
20817
|
+
}
|
|
20818
|
+
};
|
|
20819
|
+
}
|
|
20820
|
+
async function chatCompletion(r, body, f = fetch) {
|
|
20821
|
+
if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
|
|
20822
|
+
const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
20823
|
+
method: "POST",
|
|
20824
|
+
headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
|
|
20825
|
+
body: JSON.stringify(body)
|
|
20826
|
+
});
|
|
20827
|
+
if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
20828
|
+
return await res.json();
|
|
20829
|
+
}
|
|
20830
|
+
function jsonSchemaOf(schema) {
|
|
20831
|
+
const clean = (v) => {
|
|
20832
|
+
if (Array.isArray(v)) return v.map(clean);
|
|
20833
|
+
if (!v || typeof v !== "object") return v;
|
|
20834
|
+
const out = {};
|
|
20835
|
+
for (const [k, val] of Object.entries(v)) {
|
|
20836
|
+
if (k === "$schema") continue;
|
|
20837
|
+
if (k === "type" && Array.isArray(val)) {
|
|
20838
|
+
const types = val.filter((t) => t !== "null");
|
|
20839
|
+
out.type = types[0] ?? "string";
|
|
20840
|
+
if (types.length < val.length) out.nullable = true;
|
|
20841
|
+
continue;
|
|
20842
|
+
}
|
|
20843
|
+
out[k] = clean(val);
|
|
20844
|
+
}
|
|
20845
|
+
return out;
|
|
20846
|
+
};
|
|
20847
|
+
return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
|
|
20848
|
+
}
|
|
20849
|
+
async function extractJson(r, args, f = fetch) {
|
|
20850
|
+
const schemaJson = jsonSchemaOf(args.schema);
|
|
20851
|
+
const system = r.jsonMode === "schema" ? args.system : `${args.system}
|
|
20852
|
+
|
|
20853
|
+
Reply with one JSON object only, matching this JSON Schema:
|
|
20854
|
+
${JSON.stringify(schemaJson)}`;
|
|
20855
|
+
const messages = [
|
|
20856
|
+
{ role: "system", content: system },
|
|
20857
|
+
{ role: "user", content: args.content }
|
|
20858
|
+
];
|
|
20859
|
+
const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
|
|
20860
|
+
let problem = "";
|
|
20861
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
20862
|
+
const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
|
|
20863
|
+
const choice = res.choices[0];
|
|
20864
|
+
if (!choice) throw new Error(`${r.label} returned no answer.`);
|
|
20865
|
+
if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
|
|
20866
|
+
const text2 = choice.message.content ?? "";
|
|
20867
|
+
let value;
|
|
20868
|
+
try {
|
|
20869
|
+
value = parseJson(text2);
|
|
20870
|
+
} catch {
|
|
20871
|
+
problem = "The reply wasn't valid JSON.";
|
|
20872
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
|
|
20873
|
+
continue;
|
|
20874
|
+
}
|
|
20875
|
+
const parsed = args.schema.safeParse(value);
|
|
20876
|
+
if (parsed.success) return parsed.data;
|
|
20877
|
+
problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
20878
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
|
|
20879
|
+
}
|
|
20880
|
+
throw new Error(`${r.label} didn't return valid data: ${problem}`);
|
|
20881
|
+
}
|
|
20882
|
+
var PROVIDER_IDS, PROVIDERS, maxTokens, parseJson;
|
|
20883
|
+
var init_llm_providers = __esm({
|
|
20884
|
+
"packages/core/src/llm-providers.ts"() {
|
|
20885
|
+
"use strict";
|
|
20886
|
+
init_zod();
|
|
20887
|
+
PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
|
|
20888
|
+
PROVIDERS = {
|
|
20889
|
+
anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
|
|
20890
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
|
|
20891
|
+
deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
|
|
20892
|
+
gemini: {
|
|
20893
|
+
label: "Gemini",
|
|
20894
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
20895
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
20896
|
+
defaultModel: "gemini-3-flash-preview",
|
|
20897
|
+
jsonMode: "schema",
|
|
20898
|
+
maxTokensParam: "max_tokens"
|
|
20899
|
+
},
|
|
20900
|
+
"openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
|
|
20901
|
+
};
|
|
20902
|
+
maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
|
|
20903
|
+
parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
|
|
20904
|
+
}
|
|
20905
|
+
});
|
|
20906
|
+
|
|
20791
20907
|
// packages/core/src/config.ts
|
|
20792
20908
|
var ActionOverride, crawlDefaults, llmDefaults, edgeDefaults, testsDefaults, commerceDefaults, ProjectConfig;
|
|
20793
20909
|
var init_config = __esm({
|
|
@@ -20795,6 +20911,7 @@ var init_config = __esm({
|
|
|
20795
20911
|
"use strict";
|
|
20796
20912
|
init_zod();
|
|
20797
20913
|
init_schema();
|
|
20914
|
+
init_llm_providers();
|
|
20798
20915
|
ActionOverride = external_exports.object({
|
|
20799
20916
|
name: external_exports.string(),
|
|
20800
20917
|
enabled: external_exports.boolean().optional(),
|
|
@@ -20810,6 +20927,7 @@ var init_config = __esm({
|
|
|
20810
20927
|
crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
|
|
20811
20928
|
llmDefaults = {
|
|
20812
20929
|
enabled: true,
|
|
20930
|
+
provider: "anthropic",
|
|
20813
20931
|
classifyModel: "claude-opus-5-5",
|
|
20814
20932
|
extractModel: "claude-opus-5-5",
|
|
20815
20933
|
actionsModel: "claude-opus-5-5",
|
|
@@ -20851,6 +20969,14 @@ var init_config = __esm({
|
|
|
20851
20969
|
actions: external_exports.array(ActionOverride).default([]),
|
|
20852
20970
|
llm: external_exports.object({
|
|
20853
20971
|
enabled: external_exports.boolean().default(true),
|
|
20972
|
+
/** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
|
|
20973
|
+
provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
|
|
20974
|
+
/** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
|
|
20975
|
+
baseUrl: external_exports.string().url().optional(),
|
|
20976
|
+
/** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
|
|
20977
|
+
apiKeyEnv: external_exports.string().optional(),
|
|
20978
|
+
/** One model for every step. Without it, non-Claude providers use their own default model. */
|
|
20979
|
+
model: external_exports.string().optional(),
|
|
20854
20980
|
classifyModel: external_exports.string().default(llmDefaults.classifyModel),
|
|
20855
20981
|
extractModel: external_exports.string().default(llmDefaults.extractModel),
|
|
20856
20982
|
actionsModel: external_exports.string().default(llmDefaults.actionsModel),
|
|
@@ -20951,6 +21077,7 @@ var init_src = __esm({
|
|
|
20951
21077
|
init_fields();
|
|
20952
21078
|
init_util2();
|
|
20953
21079
|
init_config();
|
|
21080
|
+
init_llm_providers();
|
|
20954
21081
|
}
|
|
20955
21082
|
});
|
|
20956
21083
|
|
|
@@ -90044,9 +90171,9 @@ var init_client = __esm({
|
|
|
90044
90171
|
}
|
|
90045
90172
|
return url2.toString();
|
|
90046
90173
|
}
|
|
90047
|
-
_calculateNonstreamingTimeout(
|
|
90174
|
+
_calculateNonstreamingTimeout(maxTokens2) {
|
|
90048
90175
|
const defaultTimeout = 10 * 60;
|
|
90049
|
-
const expectedTimeout = 60 * 60 *
|
|
90176
|
+
const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
|
|
90050
90177
|
if (expectedTimeout > defaultTimeout) {
|
|
90051
90178
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
|
|
90052
90179
|
}
|
|
@@ -90396,11 +90523,11 @@ var init_client = __esm({
|
|
|
90396
90523
|
const jitter2 = 1 - Math.random() * 0.25;
|
|
90397
90524
|
return sleepSeconds * jitter2 * 1e3;
|
|
90398
90525
|
}
|
|
90399
|
-
calculateNonstreamingTimeout(
|
|
90526
|
+
calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
|
|
90400
90527
|
const maxTime = 60 * 60 * 1e3;
|
|
90401
90528
|
const defaultTime = 60 * 10 * 1e3;
|
|
90402
|
-
const expectedTime = maxTime *
|
|
90403
|
-
if (expectedTime > defaultTime || maxNonstreamingTokens != null &&
|
|
90529
|
+
const expectedTime = maxTime * maxTokens2 / 128e3;
|
|
90530
|
+
if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
|
|
90404
90531
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
|
|
90405
90532
|
}
|
|
90406
90533
|
return defaultTime;
|
|
@@ -91266,15 +91393,26 @@ var init_zod2 = __esm({
|
|
|
91266
91393
|
});
|
|
91267
91394
|
|
|
91268
91395
|
// packages/understand/src/llm.ts
|
|
91269
|
-
function
|
|
91270
|
-
|
|
91396
|
+
function llmFromSettings(s, env = process.env, f) {
|
|
91397
|
+
const r = resolveLlm(s, env);
|
|
91398
|
+
if (!r) return void 0;
|
|
91399
|
+
if (r.provider === "anthropic") {
|
|
91400
|
+
const claude = new ClaudeLlm(r.apiKey ? new Anthropic({ apiKey: r.apiKey }) : void 0);
|
|
91401
|
+
const llm = {
|
|
91402
|
+
extract: (a) => claude.extract({ ...a, model: r.model(a.model) }),
|
|
91403
|
+
readDocument: (a) => claude.readDocument({ ...a, model: r.model(a.model) })
|
|
91404
|
+
};
|
|
91405
|
+
return { llm, label: r.label, resolved: r };
|
|
91406
|
+
}
|
|
91407
|
+
return { llm: new ChatCompletionsLlm(r, f), label: r.label, resolved: r };
|
|
91271
91408
|
}
|
|
91272
|
-
var RefusalError, ClaudeLlm;
|
|
91409
|
+
var RefusalError, ClaudeLlm, ChatCompletionsLlm, apiKeyEnvFor;
|
|
91273
91410
|
var init_llm = __esm({
|
|
91274
91411
|
"packages/understand/src/llm.ts"() {
|
|
91275
91412
|
"use strict";
|
|
91276
91413
|
init_sdk();
|
|
91277
91414
|
init_zod2();
|
|
91415
|
+
init_src();
|
|
91278
91416
|
RefusalError = class extends Error {
|
|
91279
91417
|
};
|
|
91280
91418
|
ClaudeLlm = class {
|
|
@@ -91322,6 +91460,18 @@ var init_llm = __esm({
|
|
|
91322
91460
|
return message.parsed_output;
|
|
91323
91461
|
}
|
|
91324
91462
|
};
|
|
91463
|
+
ChatCompletionsLlm = class {
|
|
91464
|
+
r;
|
|
91465
|
+
f;
|
|
91466
|
+
constructor(r, f) {
|
|
91467
|
+
this.r = r;
|
|
91468
|
+
this.f = f;
|
|
91469
|
+
}
|
|
91470
|
+
extract(args) {
|
|
91471
|
+
return extractJson(this.r, args, this.f);
|
|
91472
|
+
}
|
|
91473
|
+
};
|
|
91474
|
+
apiKeyEnvFor = (s) => s.apiKeyEnv ?? PROVIDERS[s.provider].apiKeyEnv;
|
|
91325
91475
|
}
|
|
91326
91476
|
});
|
|
91327
91477
|
|
|
@@ -116494,9 +116644,19 @@ async function createClaudeAsker(opts = {}) {
|
|
|
116494
116644
|
}
|
|
116495
116645
|
};
|
|
116496
116646
|
}
|
|
116647
|
+
async function askerFor(s, env, f) {
|
|
116648
|
+
const r = resolveLlm(s, env);
|
|
116649
|
+
if (!r) return void 0;
|
|
116650
|
+
if (r.provider === "anthropic") {
|
|
116651
|
+
const claude = await createClaudeAsker({ apiKey: r.apiKey });
|
|
116652
|
+
return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
|
|
116653
|
+
}
|
|
116654
|
+
return { label: r.label, extract: (a) => extractJson(r, a, f) };
|
|
116655
|
+
}
|
|
116497
116656
|
var init_claude = __esm({
|
|
116498
116657
|
"packages/runtime/src/claude.ts"() {
|
|
116499
116658
|
"use strict";
|
|
116659
|
+
init_src();
|
|
116500
116660
|
}
|
|
116501
116661
|
});
|
|
116502
116662
|
|
|
@@ -116683,7 +116843,7 @@ async function cmdCi(project, opts, out = { log: (m) => console.log(m) }) {
|
|
|
116683
116843
|
const cron = hours === 24 ? "17 3 * * *" : `17 */${hours} * * *`;
|
|
116684
116844
|
const file3 = path6.join(project.dir, ".github", "workflows", "agenticweb-sync.yml");
|
|
116685
116845
|
await mkdir4(path6.dirname(file3), { recursive: true });
|
|
116686
|
-
await writeFile3(file3, WORKFLOW(cron, opts.target ?? config2.deploy.target));
|
|
116846
|
+
await writeFile3(file3, WORKFLOW(cron, opts.target ?? config2.deploy.target, config2.llm.apiKeyEnv ?? PROVIDERS[config2.llm.provider].apiKeyEnv));
|
|
116687
116847
|
out.log(`Wrote ${path6.relative(project.dir, file3)}. It rebuilds every ${hours} hour(s).`);
|
|
116688
116848
|
out.log("Add the repository secrets it uses (Settings \u2192 Secrets and variables \u2192 Actions), then commit the file.");
|
|
116689
116849
|
}
|
|
@@ -116691,10 +116851,11 @@ var WORKFLOW;
|
|
|
116691
116851
|
var init_sync = __esm({
|
|
116692
116852
|
"packages/cli/src/sync.ts"() {
|
|
116693
116853
|
"use strict";
|
|
116854
|
+
init_src();
|
|
116694
116855
|
init_src7();
|
|
116695
116856
|
init_project();
|
|
116696
116857
|
init_commands();
|
|
116697
|
-
WORKFLOW = (cron, target) => `# Keeps your agent branch in sync with your site. Generated by AgenticWeb.
|
|
116858
|
+
WORKFLOW = (cron, target, aiKeyEnv) => `# Keeps your agent branch in sync with your site. Generated by AgenticWeb.
|
|
116698
116859
|
name: Sync agent branch
|
|
116699
116860
|
on:
|
|
116700
116861
|
schedule:
|
|
@@ -116712,14 +116873,14 @@ jobs:
|
|
|
116712
116873
|
- name: Rebuild the agent branch
|
|
116713
116874
|
run: npx agenticweb-cli@latest build
|
|
116714
116875
|
env:
|
|
116715
|
-
|
|
116876
|
+
${aiKeyEnv}: \${{ secrets.${aiKeyEnv} }}
|
|
116716
116877
|
# The contents of .agenticweb/signing-key.json, so every build is signed with the same key.
|
|
116717
116878
|
AGENTICWEB_SIGNING_KEY: \${{ secrets.AGENTICWEB_SIGNING_KEY }}
|
|
116718
116879
|
- name: Agent Readiness tests (a task that used to pass and now fails stops the deploy)
|
|
116719
116880
|
if: hashFiles('agenticweb.tests.json') != ''
|
|
116720
116881
|
run: npx agenticweb-cli@latest test --strict
|
|
116721
116882
|
env:
|
|
116722
|
-
|
|
116883
|
+
${aiKeyEnv}: \${{ secrets.${aiKeyEnv} }}
|
|
116723
116884
|
- name: Keep the readiness report
|
|
116724
116885
|
if: always() && hashFiles('agent-readiness/report.md') != ''
|
|
116725
116886
|
uses: actions/upload-artifact@v4
|
|
@@ -119109,6 +119270,7 @@ __export(readiness_exports, {
|
|
|
119109
119270
|
TasksFile: () => TasksFile,
|
|
119110
119271
|
badgeSvg: () => badgeSvg,
|
|
119111
119272
|
basicChecks: () => basicChecks,
|
|
119273
|
+
chatCompletionsLoop: () => chatCompletionsLoop,
|
|
119112
119274
|
claudeLoop: () => claudeLoop,
|
|
119113
119275
|
cmdTest: () => cmdTest,
|
|
119114
119276
|
crawlForAnswer: () => crawlForAnswer,
|
|
@@ -119266,7 +119428,7 @@ async function crawlForAnswer(site, expect, opts = {}) {
|
|
|
119266
119428
|
}
|
|
119267
119429
|
async function builtInAgentArm(task, branch) {
|
|
119268
119430
|
if (task.action) {
|
|
119269
|
-
if (!task.input) return { ran: false, success: false, requests: 0, tokens: 0, note: "needs
|
|
119431
|
+
if (!task.input) return { ran: false, success: false, requests: 0, tokens: 0, note: "needs an AI key (see llm.provider) or an `input` to run without it" };
|
|
119270
119432
|
const res2 = await branch.fetcher(
|
|
119271
119433
|
new Request(`${branch.baseUrl}/api/${task.action}`, {
|
|
119272
119434
|
method: "POST",
|
|
@@ -119343,14 +119505,44 @@ async function claudeLoop(args) {
|
|
|
119343
119505
|
}
|
|
119344
119506
|
return done("", `stopped after ${args.maxTurns} turns`);
|
|
119345
119507
|
}
|
|
119346
|
-
async function
|
|
119508
|
+
async function chatCompletionsLoop(args) {
|
|
119509
|
+
const messages = [
|
|
119510
|
+
{ role: "system", content: args.system },
|
|
119511
|
+
{ role: "user", content: args.task }
|
|
119512
|
+
];
|
|
119513
|
+
const tools = args.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema } }));
|
|
119514
|
+
let tokens = 0;
|
|
119515
|
+
const calls = [];
|
|
119516
|
+
const results = [];
|
|
119517
|
+
const done = (answer, note) => ({ answer, requests: calls.length, tokens, calls, results, note });
|
|
119518
|
+
for (let turn = 0; turn < args.maxTurns; turn++) {
|
|
119519
|
+
const res = await chatCompletion(args.r, { model: args.model, messages, tools, ...maxTokens(args.r, 8e3) }, args.fetch);
|
|
119520
|
+
tokens += (res.usage?.prompt_tokens ?? 0) + (res.usage?.completion_tokens ?? 0);
|
|
119521
|
+
const choice = res.choices[0];
|
|
119522
|
+
if (!choice) return done("", `${args.r.label} returned no answer`);
|
|
119523
|
+
const msg = choice.message;
|
|
119524
|
+
if (msg.refusal || choice.finish_reason === "content_filter") return done(msg.content ?? "", `${args.r.label} declined the task`);
|
|
119525
|
+
messages.push({ role: "assistant", content: msg.content ?? null, ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {} });
|
|
119526
|
+
if (!msg.tool_calls?.length) return done((msg.content ?? "").trim(), choice.finish_reason === "length" ? "the answer was cut off" : void 0);
|
|
119527
|
+
for (const call of msg.tool_calls) {
|
|
119528
|
+
calls.push(call.function.name);
|
|
119529
|
+
let out;
|
|
119530
|
+
try {
|
|
119531
|
+
out = await args.run(call.function.name, call.function.arguments ? JSON.parse(call.function.arguments) : {});
|
|
119532
|
+
} catch (err) {
|
|
119533
|
+
out = { text: `Error: ${err.message}`, isError: true };
|
|
119534
|
+
}
|
|
119535
|
+
results.push({ name: call.function.name, status: out.status, isError: out.isError });
|
|
119536
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: out.text.slice(0, 4e4) || "(empty)" });
|
|
119537
|
+
}
|
|
119538
|
+
}
|
|
119539
|
+
return done("", `stopped after ${args.maxTurns} turns`);
|
|
119540
|
+
}
|
|
119541
|
+
async function aiAgentArm(task, ctx) {
|
|
119347
119542
|
const mcp = await connectMcp(ctx.branch);
|
|
119348
119543
|
try {
|
|
119349
119544
|
const tools = (await mcp.listTools()).map((t) => ({ name: t.name, description: t.description ?? "", input_schema: t.inputSchema }));
|
|
119350
|
-
const r = await
|
|
119351
|
-
client: ctx.client,
|
|
119352
|
-
model: ctx.model,
|
|
119353
|
-
effort: ctx.effort,
|
|
119545
|
+
const r = await ctx.loop({
|
|
119354
119546
|
maxTurns: ctx.maxTurns,
|
|
119355
119547
|
system: SYSTEM(ctx.site, "agent"),
|
|
119356
119548
|
task: task.task,
|
|
@@ -119372,13 +119564,10 @@ async function claudeAgentArm(task, ctx) {
|
|
|
119372
119564
|
await mcp.close();
|
|
119373
119565
|
}
|
|
119374
119566
|
}
|
|
119375
|
-
async function
|
|
119567
|
+
async function aiHumanArm(task, ctx) {
|
|
119376
119568
|
const f = ctx.fetch ?? fetch;
|
|
119377
119569
|
const origin = new URL(ctx.site);
|
|
119378
|
-
const r = await
|
|
119379
|
-
client: ctx.client,
|
|
119380
|
-
model: ctx.model,
|
|
119381
|
-
effort: ctx.effort,
|
|
119570
|
+
const r = await ctx.loop({
|
|
119382
119571
|
maxTurns: ctx.maxTurns,
|
|
119383
119572
|
system: SYSTEM(ctx.site, "human"),
|
|
119384
119573
|
task: task.task,
|
|
@@ -119428,7 +119617,7 @@ function reportMarkdown(r) {
|
|
|
119428
119617
|
const lines = [
|
|
119429
119618
|
`# Agent Readiness: ${r.score}/100`,
|
|
119430
119619
|
"",
|
|
119431
|
-
`${r.site}, tested ${r.ranAt.slice(0, 16).replace("T", " ")} UTC against ${r.target}. ${r.mode === "
|
|
119620
|
+
`${r.site}, tested ${r.ranAt.slice(0, 16).replace("T", " ")} UTC against ${r.target}. ${r.mode === "ai" ? `Tasks were done by ${r.provider ?? "an AI agent"} (${r.model}) using tools.` : "Built-in mode: no AI. Lookups use /ask; actions run with the input in the tasks file."}`,
|
|
119432
119621
|
"",
|
|
119433
119622
|
`- **Tasks:** ${t.passed} of ${t.total} done on the agent branch${t.humanRan ? `, ${t.humanPassed} of ${t.humanRan} on the human site` : ""}.`,
|
|
119434
119623
|
`- **Basics:** ${r.checks.filter((c) => c.ok).length} of ${r.checks.length} checks pass.`,
|
|
@@ -119493,24 +119682,38 @@ ${parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n
|
|
|
119493
119682
|
let tasks = parsed.data.tasks;
|
|
119494
119683
|
if (opts.only) tasks = tasks.filter((t) => t.id === opts.only);
|
|
119495
119684
|
if (!tasks.length) throw new UserError(opts.only ? `No task "${opts.only}" in ${TASKS_FILE}.` : `${TASKS_FILE} has no tasks.`);
|
|
119496
|
-
const
|
|
119497
|
-
|
|
119498
|
-
|
|
119499
|
-
|
|
119500
|
-
|
|
119501
|
-
|
|
119502
|
-
|
|
119685
|
+
const ai = opts.noLlm ? void 0 : opts.client ? { provider: "anthropic", label: "Claude" } : resolveLlm(config2.llm, process.env);
|
|
119686
|
+
const useAi = Boolean(ai);
|
|
119687
|
+
let loop;
|
|
119688
|
+
let model = config2.tests.model;
|
|
119689
|
+
if (ai && (ai.provider === "anthropic" || opts.client)) {
|
|
119690
|
+
let client = opts.client;
|
|
119691
|
+
if (!client) {
|
|
119692
|
+
const { default: Anthropic2 } = await Promise.resolve().then(() => (init_sdk(), sdk_exports));
|
|
119693
|
+
const r = ai;
|
|
119694
|
+
client = r.apiKey ? new Anthropic2({ apiKey: r.apiKey }) : new Anthropic2();
|
|
119695
|
+
model = r.model(config2.tests.model);
|
|
119696
|
+
}
|
|
119697
|
+
const c = client;
|
|
119698
|
+
const m = model;
|
|
119699
|
+
loop = (a) => claudeLoop({ ...a, client: c, model: m, effort: config2.tests.effort });
|
|
119700
|
+
} else if (ai) {
|
|
119701
|
+
const r = ai;
|
|
119702
|
+
model = r.model(config2.tests.model);
|
|
119703
|
+
const m = model;
|
|
119704
|
+
loop = (a) => chatCompletionsLoop({ ...a, r, model: m, fetch: opts.fetchLlm });
|
|
119705
|
+
}
|
|
119706
|
+
const llm = useAi && !opts.client ? await askerFor(config2.llm, process.env, opts.fetchLlm) : void 0;
|
|
119503
119707
|
const branch = opts.url ? remoteAgentBranch(opts.url) : await localAgentBranch(graph, config2, { llm });
|
|
119504
|
-
const
|
|
119505
|
-
const ctx = { client, model, effort: config2.tests.effort, maxTurns: config2.tests.maxTurns, site: config2.site };
|
|
119708
|
+
const ctx = { loop, maxTurns: config2.tests.maxTurns, site: config2.site };
|
|
119506
119709
|
const enabled = graph.actions.filter((a) => a.enabled);
|
|
119507
|
-
out.log(`Testing ${tasks.length} task(s) on ${opts.url ?? "a local copy of your agent branch"}${
|
|
119710
|
+
out.log(`Testing ${tasks.length} task(s) on ${opts.url ?? "a local copy of your agent branch"}${useAi ? ` with ${ai.label} (${model})` : ` in built-in mode (set ${apiKeyEnvFor(config2.llm)} to test with an AI agent)`}\u2026`);
|
|
119508
119711
|
const outcomes = [];
|
|
119509
119712
|
for (const t of tasks) {
|
|
119510
|
-
const agent =
|
|
119713
|
+
const agent = useAi ? await aiAgentArm(t, { ...ctx, branch, remote: Boolean(opts.url), actions: enabled }) : await builtInAgentArm(t, branch);
|
|
119511
119714
|
let human;
|
|
119512
119715
|
if (t.human && !t.action && t.expect.length) {
|
|
119513
|
-
human =
|
|
119716
|
+
human = useAi ? await aiHumanArm(t, { ...ctx, fetch: opts.fetchHuman }) : await crawlForAnswer(config2.site, t.expect, { fetch: opts.fetchHuman });
|
|
119514
119717
|
} else if (t.action) human = { ran: false, success: false, requests: 0, tokens: 0, note: "not run: it would submit forms on your live site" };
|
|
119515
119718
|
outcomes.push({ id: t.id, task: t.task, agent, human });
|
|
119516
119719
|
out.log(` ${agent.ran ? agent.success ? "\u2713" : "\u2717" : "-"} ${t.id}${agent.note ? `: ${agent.note}` : ""}`);
|
|
@@ -119526,8 +119729,8 @@ ${parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n
|
|
|
119526
119729
|
const report = {
|
|
119527
119730
|
site: config2.site,
|
|
119528
119731
|
target: opts.url ?? "a local copy of the agent branch",
|
|
119529
|
-
mode:
|
|
119530
|
-
...
|
|
119732
|
+
mode: useAi ? "ai" : "built-in",
|
|
119733
|
+
...useAi ? { provider: ai.label, model } : {},
|
|
119531
119734
|
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
119532
119735
|
score: readinessScore(outcomes, checks),
|
|
119533
119736
|
tasks: outcomes,
|
|
@@ -119568,6 +119771,7 @@ var init_readiness = __esm({
|
|
|
119568
119771
|
"packages/cli/src/readiness.ts"() {
|
|
119569
119772
|
"use strict";
|
|
119570
119773
|
init_zod();
|
|
119774
|
+
init_src();
|
|
119571
119775
|
init_src7();
|
|
119572
119776
|
init_src5();
|
|
119573
119777
|
init_src3();
|
|
@@ -119746,7 +119950,7 @@ ${p.markdown}`).join("\n"));
|
|
|
119746
119950
|
const gaps = (await findGaps(project, opts.days)).filter((x) => !known.has(normQ(x.question)) && !siteText.includes(normQ(x.question))).slice(0, 30);
|
|
119747
119951
|
let llm;
|
|
119748
119952
|
if (opts.llm) llm = opts.llm;
|
|
119749
|
-
else if (opts.llm !== false
|
|
119953
|
+
else if (opts.llm !== false) llm = llmFromSettings(config2.llm)?.llm;
|
|
119750
119954
|
const added = gaps.length ? await draftSuggestions(g, gaps, { llm, model: config2.llm.askModel }) : [];
|
|
119751
119955
|
const counts = new Map((await findGaps(project, opts.days)).map((x) => [normQ(x.question), x.asked]));
|
|
119752
119956
|
const updated = existing.map((s) => ({ ...s, asked: Math.max(s.asked, [s.question, ...s.covers].reduce((n, q) => n + (counts.get(normQ(q)) ?? 0), 0)) }));
|
|
@@ -119852,7 +120056,7 @@ async function cmdServe(project, opts, out = { log: (m) => console.log(m) }) {
|
|
|
119852
120056
|
const edge = opts.edge ? { ...config2.edge, origin: opts.edge === true ? config2.edge.origin ?? config2.site : opts.edge, publicUrl } : void 0;
|
|
119853
120057
|
const baseUrl = edge ? `${publicUrl}${config2.edge.prefix}` : config2.deploy.baseUrl ?? publicUrl;
|
|
119854
120058
|
let llm;
|
|
119855
|
-
if (!opts.noLlm
|
|
120059
|
+
if (!opts.noLlm) llm = await askerFor(config2.llm, process.env);
|
|
119856
120060
|
const hookSecret = opts.hookSecret ?? process.env.AGENTICWEB_HOOK_SECRET;
|
|
119857
120061
|
const onTraffic = serveHooks.traffic?.(project);
|
|
119858
120062
|
const engine = await serveHooks.engine?.(project, graph, config2, baseUrl);
|
|
@@ -119894,7 +120098,7 @@ async function cmdServe(project, opts, out = { log: (m) => console.log(m) }) {
|
|
|
119894
120098
|
out.log(` Ask: ${agentUrl}/ask?q=your+question`);
|
|
119895
120099
|
out.log(` MCP: ${agentUrl}/mcp`);
|
|
119896
120100
|
out.log(` Actions: ${engine ? engine.actions().map((a) => a.name).join(", ") || "none enabled" : "none enabled"}`);
|
|
119897
|
-
out.log(` /ask uses: ${llm ?
|
|
120101
|
+
out.log(` /ask uses: ${llm ? llm.label : `built-in search (set ${apiKeyEnvFor(config2.llm)} for written answers)`}`);
|
|
119898
120102
|
if (opts.syncEveryMinutes) out.log(` Sync: every ${opts.syncEveryMinutes} minutes`);
|
|
119899
120103
|
if (hookSecret) out.log(` Webhook: POST ${agentUrl}/hooks/refresh (header x-agenticweb-secret)`);
|
|
119900
120104
|
out.log("Press Ctrl+C to stop.");
|
|
@@ -120207,8 +120411,13 @@ async function cmdBuild(project, opts = {}, out = stdout) {
|
|
|
120207
120411
|
let llm;
|
|
120208
120412
|
if (opts.llm !== false && config2.llm.enabled) {
|
|
120209
120413
|
if (opts.llm) llm = opts.llm;
|
|
120210
|
-
else
|
|
120211
|
-
|
|
120414
|
+
else {
|
|
120415
|
+
const ai = llmFromSettings(config2.llm);
|
|
120416
|
+
if (ai) {
|
|
120417
|
+
llm = ai.llm;
|
|
120418
|
+
out.log(` Reading the site with ${ai.label}.`);
|
|
120419
|
+
} else out.log(` No AI key found: using built-in rules. Set ${apiKeyEnvFor(config2.llm)} for richer extraction.`);
|
|
120420
|
+
}
|
|
120212
120421
|
}
|
|
120213
120422
|
const extra = opts.connectors ? await opts.connectors(config2) : await Promise.resolve().then(() => (init_actions_hook(), actions_hook_exports)).then((m) => m.defaultConnectors(config2, (msg) => out.log(` ${msg}`)));
|
|
120214
120423
|
let graph = await understand(c, {
|
|
@@ -120507,7 +120716,7 @@ function requiredEnv(graph, config2, target = "node") {
|
|
|
120507
120716
|
for (const v of Object.values(c.headers ?? {})) if (v.startsWith("env:")) env.set(v.slice(4), `Header value for ${a.name}`);
|
|
120508
120717
|
}
|
|
120509
120718
|
if (config2.notifications.email === "resend") env.set(config2.notifications.apiKeyEnv, "Resend API key (confirmation emails)");
|
|
120510
|
-
env.set(
|
|
120719
|
+
if (config2.llm.enabled) env.set(config2.llm.apiKeyEnv ?? PROVIDERS[config2.llm.provider].apiKeyEnv, `Optional: ${PROVIDERS[config2.llm.provider].label} key for written answers on /ask`);
|
|
120511
120720
|
env.set(config2.runtime.apiKeysEnv, 'Optional: agent API keys as "key:agent-name,key2:other"');
|
|
120512
120721
|
return [...env];
|
|
120513
120722
|
}
|
|
@@ -120659,6 +120868,7 @@ var runtimeTargets;
|
|
|
120659
120868
|
var init_deploy_runtime = __esm({
|
|
120660
120869
|
"packages/cli/src/deploy-runtime.ts"() {
|
|
120661
120870
|
"use strict";
|
|
120871
|
+
init_src();
|
|
120662
120872
|
init_project();
|
|
120663
120873
|
runtimeTargets = ["node", "docker", "cloudflare-worker", "edge"];
|
|
120664
120874
|
}
|
|
@@ -57878,9 +57878,9 @@ var init_client = __esm({
|
|
|
57878
57878
|
}
|
|
57879
57879
|
return url2.toString();
|
|
57880
57880
|
}
|
|
57881
|
-
_calculateNonstreamingTimeout(
|
|
57881
|
+
_calculateNonstreamingTimeout(maxTokens2) {
|
|
57882
57882
|
const defaultTimeout = 10 * 60;
|
|
57883
|
-
const expectedTimeout = 60 * 60 *
|
|
57883
|
+
const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
|
|
57884
57884
|
if (expectedTimeout > defaultTimeout) {
|
|
57885
57885
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
|
|
57886
57886
|
}
|
|
@@ -58230,11 +58230,11 @@ var init_client = __esm({
|
|
|
58230
58230
|
const jitter2 = 1 - Math.random() * 0.25;
|
|
58231
58231
|
return sleepSeconds * jitter2 * 1e3;
|
|
58232
58232
|
}
|
|
58233
|
-
calculateNonstreamingTimeout(
|
|
58233
|
+
calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
|
|
58234
58234
|
const maxTime = 60 * 60 * 1e3;
|
|
58235
58235
|
const defaultTime = 60 * 10 * 1e3;
|
|
58236
|
-
const expectedTime = maxTime *
|
|
58237
|
-
if (expectedTime > defaultTime || maxNonstreamingTokens != null &&
|
|
58236
|
+
const expectedTime = maxTime * maxTokens2 / 128e3;
|
|
58237
|
+
if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
|
|
58238
58238
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
|
|
58239
58239
|
}
|
|
58240
58240
|
return defaultTime;
|
|
@@ -59392,6 +59392,115 @@ function entityFileName(type) {
|
|
|
59392
59392
|
return kebab + "s";
|
|
59393
59393
|
}
|
|
59394
59394
|
|
|
59395
|
+
// packages/core/src/llm-providers.ts
|
|
59396
|
+
var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
|
|
59397
|
+
var PROVIDERS = {
|
|
59398
|
+
anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
|
|
59399
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
|
|
59400
|
+
deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
|
|
59401
|
+
gemini: {
|
|
59402
|
+
label: "Gemini",
|
|
59403
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
59404
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
59405
|
+
defaultModel: "gemini-3-flash-preview",
|
|
59406
|
+
jsonMode: "schema",
|
|
59407
|
+
maxTokensParam: "max_tokens"
|
|
59408
|
+
},
|
|
59409
|
+
"openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
|
|
59410
|
+
};
|
|
59411
|
+
function resolveLlm(s, env) {
|
|
59412
|
+
if (!s.enabled) return void 0;
|
|
59413
|
+
const info = PROVIDERS[s.provider];
|
|
59414
|
+
const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
|
|
59415
|
+
const apiKey = env[keyEnv] || void 0;
|
|
59416
|
+
if (s.provider === "anthropic") {
|
|
59417
|
+
if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
|
|
59418
|
+
} else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
|
|
59419
|
+
return void 0;
|
|
59420
|
+
}
|
|
59421
|
+
const baseUrl = s.baseUrl ?? info.baseUrl;
|
|
59422
|
+
if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
|
|
59423
|
+
return {
|
|
59424
|
+
provider: s.provider,
|
|
59425
|
+
label: info.label,
|
|
59426
|
+
apiKey,
|
|
59427
|
+
baseUrl,
|
|
59428
|
+
jsonMode: info.jsonMode,
|
|
59429
|
+
maxTokensParam: info.maxTokensParam,
|
|
59430
|
+
model: (configured) => {
|
|
59431
|
+
if (s.model) return s.model;
|
|
59432
|
+
if (s.provider === "anthropic") return configured || info.defaultModel;
|
|
59433
|
+
if (configured && !configured.startsWith("claude-")) return configured;
|
|
59434
|
+
if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
|
|
59435
|
+
return info.defaultModel;
|
|
59436
|
+
}
|
|
59437
|
+
};
|
|
59438
|
+
}
|
|
59439
|
+
async function chatCompletion(r, body, f = fetch) {
|
|
59440
|
+
if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
|
|
59441
|
+
const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
59442
|
+
method: "POST",
|
|
59443
|
+
headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
|
|
59444
|
+
body: JSON.stringify(body)
|
|
59445
|
+
});
|
|
59446
|
+
if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
59447
|
+
return await res.json();
|
|
59448
|
+
}
|
|
59449
|
+
var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
|
|
59450
|
+
function jsonSchemaOf(schema) {
|
|
59451
|
+
const clean = (v) => {
|
|
59452
|
+
if (Array.isArray(v)) return v.map(clean);
|
|
59453
|
+
if (!v || typeof v !== "object") return v;
|
|
59454
|
+
const out = {};
|
|
59455
|
+
for (const [k, val] of Object.entries(v)) {
|
|
59456
|
+
if (k === "$schema") continue;
|
|
59457
|
+
if (k === "type" && Array.isArray(val)) {
|
|
59458
|
+
const types = val.filter((t) => t !== "null");
|
|
59459
|
+
out.type = types[0] ?? "string";
|
|
59460
|
+
if (types.length < val.length) out.nullable = true;
|
|
59461
|
+
continue;
|
|
59462
|
+
}
|
|
59463
|
+
out[k] = clean(val);
|
|
59464
|
+
}
|
|
59465
|
+
return out;
|
|
59466
|
+
};
|
|
59467
|
+
return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
|
|
59468
|
+
}
|
|
59469
|
+
var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
|
|
59470
|
+
async function extractJson(r, args, f = fetch) {
|
|
59471
|
+
const schemaJson = jsonSchemaOf(args.schema);
|
|
59472
|
+
const system = r.jsonMode === "schema" ? args.system : `${args.system}
|
|
59473
|
+
|
|
59474
|
+
Reply with one JSON object only, matching this JSON Schema:
|
|
59475
|
+
${JSON.stringify(schemaJson)}`;
|
|
59476
|
+
const messages = [
|
|
59477
|
+
{ role: "system", content: system },
|
|
59478
|
+
{ role: "user", content: args.content }
|
|
59479
|
+
];
|
|
59480
|
+
const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
|
|
59481
|
+
let problem = "";
|
|
59482
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
59483
|
+
const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
|
|
59484
|
+
const choice = res.choices[0];
|
|
59485
|
+
if (!choice) throw new Error(`${r.label} returned no answer.`);
|
|
59486
|
+
if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
|
|
59487
|
+
const text2 = choice.message.content ?? "";
|
|
59488
|
+
let value;
|
|
59489
|
+
try {
|
|
59490
|
+
value = parseJson(text2);
|
|
59491
|
+
} catch {
|
|
59492
|
+
problem = "The reply wasn't valid JSON.";
|
|
59493
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
|
|
59494
|
+
continue;
|
|
59495
|
+
}
|
|
59496
|
+
const parsed = args.schema.safeParse(value);
|
|
59497
|
+
if (parsed.success) return parsed.data;
|
|
59498
|
+
problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
59499
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
|
|
59500
|
+
}
|
|
59501
|
+
throw new Error(`${r.label} didn't return valid data: ${problem}`);
|
|
59502
|
+
}
|
|
59503
|
+
|
|
59395
59504
|
// packages/core/src/config.ts
|
|
59396
59505
|
var ActionOverride = external_exports.object({
|
|
59397
59506
|
name: external_exports.string(),
|
|
@@ -59408,6 +59517,7 @@ var ActionOverride = external_exports.object({
|
|
|
59408
59517
|
var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
|
|
59409
59518
|
var llmDefaults = {
|
|
59410
59519
|
enabled: true,
|
|
59520
|
+
provider: "anthropic",
|
|
59411
59521
|
classifyModel: "claude-opus-5-5",
|
|
59412
59522
|
extractModel: "claude-opus-5-5",
|
|
59413
59523
|
actionsModel: "claude-opus-5-5",
|
|
@@ -59449,6 +59559,14 @@ var ProjectConfig = external_exports.object({
|
|
|
59449
59559
|
actions: external_exports.array(ActionOverride).default([]),
|
|
59450
59560
|
llm: external_exports.object({
|
|
59451
59561
|
enabled: external_exports.boolean().default(true),
|
|
59562
|
+
/** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
|
|
59563
|
+
provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
|
|
59564
|
+
/** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
|
|
59565
|
+
baseUrl: external_exports.string().url().optional(),
|
|
59566
|
+
/** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
|
|
59567
|
+
apiKeyEnv: external_exports.string().optional(),
|
|
59568
|
+
/** One model for every step. Without it, non-Claude providers use their own default model. */
|
|
59569
|
+
model: external_exports.string().optional(),
|
|
59452
59570
|
classifyModel: external_exports.string().default(llmDefaults.classifyModel),
|
|
59453
59571
|
extractModel: external_exports.string().default(llmDefaults.extractModel),
|
|
59454
59572
|
actionsModel: external_exports.string().default(llmDefaults.actionsModel),
|
|
@@ -67666,6 +67784,15 @@ async function createClaudeAsker(opts = {}) {
|
|
|
67666
67784
|
}
|
|
67667
67785
|
};
|
|
67668
67786
|
}
|
|
67787
|
+
async function askerFor(s, env, f) {
|
|
67788
|
+
const r = resolveLlm(s, env);
|
|
67789
|
+
if (!r) return void 0;
|
|
67790
|
+
if (r.provider === "anthropic") {
|
|
67791
|
+
const claude = await createClaudeAsker({ apiKey: r.apiKey });
|
|
67792
|
+
return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
|
|
67793
|
+
}
|
|
67794
|
+
return { label: r.label, extract: (a) => extractJson(r, a, f) };
|
|
67795
|
+
}
|
|
67669
67796
|
|
|
67670
67797
|
// packages/actions/src/types.ts
|
|
67671
67798
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -68628,7 +68755,7 @@ var edge_worker_default = {
|
|
|
68628
68755
|
const { AGENTICWEB_KV, ...vars } = env;
|
|
68629
68756
|
const store = AGENTICWEB_KV ? new KvStore(AGENTICWEB_KV) : new MemoryStore();
|
|
68630
68757
|
const engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env: vars, store }) : void 0;
|
|
68631
|
-
const llm = config2.llm
|
|
68758
|
+
const llm = await askerFor(config2.llm, vars);
|
|
68632
68759
|
edge = createEdge(
|
|
68633
68760
|
{
|
|
68634
68761
|
graph,
|
package/dist/runtime/server.mjs
CHANGED
|
@@ -57879,9 +57879,9 @@ var init_client = __esm({
|
|
|
57879
57879
|
}
|
|
57880
57880
|
return url2.toString();
|
|
57881
57881
|
}
|
|
57882
|
-
_calculateNonstreamingTimeout(
|
|
57882
|
+
_calculateNonstreamingTimeout(maxTokens2) {
|
|
57883
57883
|
const defaultTimeout = 10 * 60;
|
|
57884
|
-
const expectedTimeout = 60 * 60 *
|
|
57884
|
+
const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
|
|
57885
57885
|
if (expectedTimeout > defaultTimeout) {
|
|
57886
57886
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
|
|
57887
57887
|
}
|
|
@@ -58231,11 +58231,11 @@ var init_client = __esm({
|
|
|
58231
58231
|
const jitter2 = 1 - Math.random() * 0.25;
|
|
58232
58232
|
return sleepSeconds * jitter2 * 1e3;
|
|
58233
58233
|
}
|
|
58234
|
-
calculateNonstreamingTimeout(
|
|
58234
|
+
calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
|
|
58235
58235
|
const maxTime = 60 * 60 * 1e3;
|
|
58236
58236
|
const defaultTime = 60 * 10 * 1e3;
|
|
58237
|
-
const expectedTime = maxTime *
|
|
58238
|
-
if (expectedTime > defaultTime || maxNonstreamingTokens != null &&
|
|
58237
|
+
const expectedTime = maxTime * maxTokens2 / 128e3;
|
|
58238
|
+
if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
|
|
58239
58239
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
|
|
58240
58240
|
}
|
|
58241
58241
|
return defaultTime;
|
|
@@ -59398,6 +59398,115 @@ function entityFileName(type) {
|
|
|
59398
59398
|
return kebab + "s";
|
|
59399
59399
|
}
|
|
59400
59400
|
|
|
59401
|
+
// packages/core/src/llm-providers.ts
|
|
59402
|
+
var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
|
|
59403
|
+
var PROVIDERS = {
|
|
59404
|
+
anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
|
|
59405
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
|
|
59406
|
+
deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
|
|
59407
|
+
gemini: {
|
|
59408
|
+
label: "Gemini",
|
|
59409
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
59410
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
59411
|
+
defaultModel: "gemini-3-flash-preview",
|
|
59412
|
+
jsonMode: "schema",
|
|
59413
|
+
maxTokensParam: "max_tokens"
|
|
59414
|
+
},
|
|
59415
|
+
"openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
|
|
59416
|
+
};
|
|
59417
|
+
function resolveLlm(s, env2) {
|
|
59418
|
+
if (!s.enabled) return void 0;
|
|
59419
|
+
const info = PROVIDERS[s.provider];
|
|
59420
|
+
const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
|
|
59421
|
+
const apiKey = env2[keyEnv] || void 0;
|
|
59422
|
+
if (s.provider === "anthropic") {
|
|
59423
|
+
if (!apiKey && !env2.ANTHROPIC_AUTH_TOKEN && !env2.ANTHROPIC_PROFILE) return void 0;
|
|
59424
|
+
} else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
|
|
59425
|
+
return void 0;
|
|
59426
|
+
}
|
|
59427
|
+
const baseUrl = s.baseUrl ?? info.baseUrl;
|
|
59428
|
+
if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
|
|
59429
|
+
return {
|
|
59430
|
+
provider: s.provider,
|
|
59431
|
+
label: info.label,
|
|
59432
|
+
apiKey,
|
|
59433
|
+
baseUrl,
|
|
59434
|
+
jsonMode: info.jsonMode,
|
|
59435
|
+
maxTokensParam: info.maxTokensParam,
|
|
59436
|
+
model: (configured) => {
|
|
59437
|
+
if (s.model) return s.model;
|
|
59438
|
+
if (s.provider === "anthropic") return configured || info.defaultModel;
|
|
59439
|
+
if (configured && !configured.startsWith("claude-")) return configured;
|
|
59440
|
+
if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
|
|
59441
|
+
return info.defaultModel;
|
|
59442
|
+
}
|
|
59443
|
+
};
|
|
59444
|
+
}
|
|
59445
|
+
async function chatCompletion(r, body, f = fetch) {
|
|
59446
|
+
if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
|
|
59447
|
+
const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
59448
|
+
method: "POST",
|
|
59449
|
+
headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
|
|
59450
|
+
body: JSON.stringify(body)
|
|
59451
|
+
});
|
|
59452
|
+
if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
59453
|
+
return await res.json();
|
|
59454
|
+
}
|
|
59455
|
+
var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
|
|
59456
|
+
function jsonSchemaOf(schema) {
|
|
59457
|
+
const clean = (v) => {
|
|
59458
|
+
if (Array.isArray(v)) return v.map(clean);
|
|
59459
|
+
if (!v || typeof v !== "object") return v;
|
|
59460
|
+
const out = {};
|
|
59461
|
+
for (const [k, val] of Object.entries(v)) {
|
|
59462
|
+
if (k === "$schema") continue;
|
|
59463
|
+
if (k === "type" && Array.isArray(val)) {
|
|
59464
|
+
const types = val.filter((t) => t !== "null");
|
|
59465
|
+
out.type = types[0] ?? "string";
|
|
59466
|
+
if (types.length < val.length) out.nullable = true;
|
|
59467
|
+
continue;
|
|
59468
|
+
}
|
|
59469
|
+
out[k] = clean(val);
|
|
59470
|
+
}
|
|
59471
|
+
return out;
|
|
59472
|
+
};
|
|
59473
|
+
return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
|
|
59474
|
+
}
|
|
59475
|
+
var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
|
|
59476
|
+
async function extractJson(r, args, f = fetch) {
|
|
59477
|
+
const schemaJson = jsonSchemaOf(args.schema);
|
|
59478
|
+
const system = r.jsonMode === "schema" ? args.system : `${args.system}
|
|
59479
|
+
|
|
59480
|
+
Reply with one JSON object only, matching this JSON Schema:
|
|
59481
|
+
${JSON.stringify(schemaJson)}`;
|
|
59482
|
+
const messages = [
|
|
59483
|
+
{ role: "system", content: system },
|
|
59484
|
+
{ role: "user", content: args.content }
|
|
59485
|
+
];
|
|
59486
|
+
const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
|
|
59487
|
+
let problem = "";
|
|
59488
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
59489
|
+
const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
|
|
59490
|
+
const choice = res.choices[0];
|
|
59491
|
+
if (!choice) throw new Error(`${r.label} returned no answer.`);
|
|
59492
|
+
if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
|
|
59493
|
+
const text2 = choice.message.content ?? "";
|
|
59494
|
+
let value;
|
|
59495
|
+
try {
|
|
59496
|
+
value = parseJson(text2);
|
|
59497
|
+
} catch {
|
|
59498
|
+
problem = "The reply wasn't valid JSON.";
|
|
59499
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
|
|
59500
|
+
continue;
|
|
59501
|
+
}
|
|
59502
|
+
const parsed = args.schema.safeParse(value);
|
|
59503
|
+
if (parsed.success) return parsed.data;
|
|
59504
|
+
problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
59505
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
|
|
59506
|
+
}
|
|
59507
|
+
throw new Error(`${r.label} didn't return valid data: ${problem}`);
|
|
59508
|
+
}
|
|
59509
|
+
|
|
59401
59510
|
// packages/core/src/config.ts
|
|
59402
59511
|
var ActionOverride = external_exports.object({
|
|
59403
59512
|
name: external_exports.string(),
|
|
@@ -59414,6 +59523,7 @@ var ActionOverride = external_exports.object({
|
|
|
59414
59523
|
var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
|
|
59415
59524
|
var llmDefaults = {
|
|
59416
59525
|
enabled: true,
|
|
59526
|
+
provider: "anthropic",
|
|
59417
59527
|
classifyModel: "claude-opus-5-5",
|
|
59418
59528
|
extractModel: "claude-opus-5-5",
|
|
59419
59529
|
actionsModel: "claude-opus-5-5",
|
|
@@ -59455,6 +59565,14 @@ var ProjectConfig = external_exports.object({
|
|
|
59455
59565
|
actions: external_exports.array(ActionOverride).default([]),
|
|
59456
59566
|
llm: external_exports.object({
|
|
59457
59567
|
enabled: external_exports.boolean().default(true),
|
|
59568
|
+
/** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
|
|
59569
|
+
provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
|
|
59570
|
+
/** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
|
|
59571
|
+
baseUrl: external_exports.string().url().optional(),
|
|
59572
|
+
/** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
|
|
59573
|
+
apiKeyEnv: external_exports.string().optional(),
|
|
59574
|
+
/** One model for every step. Without it, non-Claude providers use their own default model. */
|
|
59575
|
+
model: external_exports.string().optional(),
|
|
59458
59576
|
classifyModel: external_exports.string().default(llmDefaults.classifyModel),
|
|
59459
59577
|
extractModel: external_exports.string().default(llmDefaults.extractModel),
|
|
59460
59578
|
actionsModel: external_exports.string().default(llmDefaults.actionsModel),
|
|
@@ -67672,6 +67790,15 @@ async function createClaudeAsker(opts = {}) {
|
|
|
67672
67790
|
}
|
|
67673
67791
|
};
|
|
67674
67792
|
}
|
|
67793
|
+
async function askerFor(s, env2, f) {
|
|
67794
|
+
const r = resolveLlm(s, env2);
|
|
67795
|
+
if (!r) return void 0;
|
|
67796
|
+
if (r.provider === "anthropic") {
|
|
67797
|
+
const claude = await createClaudeAsker({ apiKey: r.apiKey });
|
|
67798
|
+
return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
|
|
67799
|
+
}
|
|
67800
|
+
return { label: r.label, extract: (a) => extractJson(r, a, f) };
|
|
67801
|
+
}
|
|
67675
67802
|
|
|
67676
67803
|
// node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
|
|
67677
67804
|
var X_ALREADY_SENT = "x-hono-already-sent";
|
|
@@ -69991,7 +70118,7 @@ var env = process.env;
|
|
|
69991
70118
|
var graph = SiteGraph.parse(JSON.parse(readFileSync2(path5.join(here, "site-graph.json"), "utf8")));
|
|
69992
70119
|
var config2 = ProjectConfig.parse(JSON.parse(readFileSync2(path5.join(here, "config.json"), "utf8")));
|
|
69993
70120
|
var engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env, store: new FileStore2(path5.join(env.DATA_DIR ?? here, "actions.json")) }) : void 0;
|
|
69994
|
-
var llm = config2.llm
|
|
70121
|
+
var llm = await askerFor(config2.llm, env);
|
|
69995
70122
|
var edgeOrigin = env.EDGE_ORIGIN || (env.EDGE_MODE === "1" ? config2.edge.origin : void 0);
|
|
69996
70123
|
var server = await serveNode({
|
|
69997
70124
|
graph,
|
package/dist/runtime/worker.js
CHANGED
|
@@ -57878,9 +57878,9 @@ var init_client = __esm({
|
|
|
57878
57878
|
}
|
|
57879
57879
|
return url2.toString();
|
|
57880
57880
|
}
|
|
57881
|
-
_calculateNonstreamingTimeout(
|
|
57881
|
+
_calculateNonstreamingTimeout(maxTokens2) {
|
|
57882
57882
|
const defaultTimeout = 10 * 60;
|
|
57883
|
-
const expectedTimeout = 60 * 60 *
|
|
57883
|
+
const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
|
|
57884
57884
|
if (expectedTimeout > defaultTimeout) {
|
|
57885
57885
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
|
|
57886
57886
|
}
|
|
@@ -58230,11 +58230,11 @@ var init_client = __esm({
|
|
|
58230
58230
|
const jitter2 = 1 - Math.random() * 0.25;
|
|
58231
58231
|
return sleepSeconds * jitter2 * 1e3;
|
|
58232
58232
|
}
|
|
58233
|
-
calculateNonstreamingTimeout(
|
|
58233
|
+
calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
|
|
58234
58234
|
const maxTime = 60 * 60 * 1e3;
|
|
58235
58235
|
const defaultTime = 60 * 10 * 1e3;
|
|
58236
|
-
const expectedTime = maxTime *
|
|
58237
|
-
if (expectedTime > defaultTime || maxNonstreamingTokens != null &&
|
|
58236
|
+
const expectedTime = maxTime * maxTokens2 / 128e3;
|
|
58237
|
+
if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
|
|
58238
58238
|
throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
|
|
58239
58239
|
}
|
|
58240
58240
|
return defaultTime;
|
|
@@ -59392,6 +59392,115 @@ function entityFileName(type) {
|
|
|
59392
59392
|
return kebab + "s";
|
|
59393
59393
|
}
|
|
59394
59394
|
|
|
59395
|
+
// packages/core/src/llm-providers.ts
|
|
59396
|
+
var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
|
|
59397
|
+
var PROVIDERS = {
|
|
59398
|
+
anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
|
|
59399
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
|
|
59400
|
+
deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
|
|
59401
|
+
gemini: {
|
|
59402
|
+
label: "Gemini",
|
|
59403
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
59404
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
59405
|
+
defaultModel: "gemini-3-flash-preview",
|
|
59406
|
+
jsonMode: "schema",
|
|
59407
|
+
maxTokensParam: "max_tokens"
|
|
59408
|
+
},
|
|
59409
|
+
"openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
|
|
59410
|
+
};
|
|
59411
|
+
function resolveLlm(s, env) {
|
|
59412
|
+
if (!s.enabled) return void 0;
|
|
59413
|
+
const info = PROVIDERS[s.provider];
|
|
59414
|
+
const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
|
|
59415
|
+
const apiKey = env[keyEnv] || void 0;
|
|
59416
|
+
if (s.provider === "anthropic") {
|
|
59417
|
+
if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
|
|
59418
|
+
} else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
|
|
59419
|
+
return void 0;
|
|
59420
|
+
}
|
|
59421
|
+
const baseUrl = s.baseUrl ?? info.baseUrl;
|
|
59422
|
+
if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
|
|
59423
|
+
return {
|
|
59424
|
+
provider: s.provider,
|
|
59425
|
+
label: info.label,
|
|
59426
|
+
apiKey,
|
|
59427
|
+
baseUrl,
|
|
59428
|
+
jsonMode: info.jsonMode,
|
|
59429
|
+
maxTokensParam: info.maxTokensParam,
|
|
59430
|
+
model: (configured) => {
|
|
59431
|
+
if (s.model) return s.model;
|
|
59432
|
+
if (s.provider === "anthropic") return configured || info.defaultModel;
|
|
59433
|
+
if (configured && !configured.startsWith("claude-")) return configured;
|
|
59434
|
+
if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
|
|
59435
|
+
return info.defaultModel;
|
|
59436
|
+
}
|
|
59437
|
+
};
|
|
59438
|
+
}
|
|
59439
|
+
async function chatCompletion(r, body, f = fetch) {
|
|
59440
|
+
if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
|
|
59441
|
+
const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
59442
|
+
method: "POST",
|
|
59443
|
+
headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
|
|
59444
|
+
body: JSON.stringify(body)
|
|
59445
|
+
});
|
|
59446
|
+
if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
59447
|
+
return await res.json();
|
|
59448
|
+
}
|
|
59449
|
+
var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
|
|
59450
|
+
function jsonSchemaOf(schema) {
|
|
59451
|
+
const clean = (v) => {
|
|
59452
|
+
if (Array.isArray(v)) return v.map(clean);
|
|
59453
|
+
if (!v || typeof v !== "object") return v;
|
|
59454
|
+
const out = {};
|
|
59455
|
+
for (const [k, val] of Object.entries(v)) {
|
|
59456
|
+
if (k === "$schema") continue;
|
|
59457
|
+
if (k === "type" && Array.isArray(val)) {
|
|
59458
|
+
const types = val.filter((t) => t !== "null");
|
|
59459
|
+
out.type = types[0] ?? "string";
|
|
59460
|
+
if (types.length < val.length) out.nullable = true;
|
|
59461
|
+
continue;
|
|
59462
|
+
}
|
|
59463
|
+
out[k] = clean(val);
|
|
59464
|
+
}
|
|
59465
|
+
return out;
|
|
59466
|
+
};
|
|
59467
|
+
return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
|
|
59468
|
+
}
|
|
59469
|
+
var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
|
|
59470
|
+
async function extractJson(r, args, f = fetch) {
|
|
59471
|
+
const schemaJson = jsonSchemaOf(args.schema);
|
|
59472
|
+
const system = r.jsonMode === "schema" ? args.system : `${args.system}
|
|
59473
|
+
|
|
59474
|
+
Reply with one JSON object only, matching this JSON Schema:
|
|
59475
|
+
${JSON.stringify(schemaJson)}`;
|
|
59476
|
+
const messages = [
|
|
59477
|
+
{ role: "system", content: system },
|
|
59478
|
+
{ role: "user", content: args.content }
|
|
59479
|
+
];
|
|
59480
|
+
const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
|
|
59481
|
+
let problem = "";
|
|
59482
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
59483
|
+
const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
|
|
59484
|
+
const choice = res.choices[0];
|
|
59485
|
+
if (!choice) throw new Error(`${r.label} returned no answer.`);
|
|
59486
|
+
if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
|
|
59487
|
+
const text2 = choice.message.content ?? "";
|
|
59488
|
+
let value;
|
|
59489
|
+
try {
|
|
59490
|
+
value = parseJson(text2);
|
|
59491
|
+
} catch {
|
|
59492
|
+
problem = "The reply wasn't valid JSON.";
|
|
59493
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
|
|
59494
|
+
continue;
|
|
59495
|
+
}
|
|
59496
|
+
const parsed = args.schema.safeParse(value);
|
|
59497
|
+
if (parsed.success) return parsed.data;
|
|
59498
|
+
problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
59499
|
+
messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
|
|
59500
|
+
}
|
|
59501
|
+
throw new Error(`${r.label} didn't return valid data: ${problem}`);
|
|
59502
|
+
}
|
|
59503
|
+
|
|
59395
59504
|
// packages/core/src/config.ts
|
|
59396
59505
|
var ActionOverride = external_exports.object({
|
|
59397
59506
|
name: external_exports.string(),
|
|
@@ -59408,6 +59517,7 @@ var ActionOverride = external_exports.object({
|
|
|
59408
59517
|
var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
|
|
59409
59518
|
var llmDefaults = {
|
|
59410
59519
|
enabled: true,
|
|
59520
|
+
provider: "anthropic",
|
|
59411
59521
|
classifyModel: "claude-opus-5-5",
|
|
59412
59522
|
extractModel: "claude-opus-5-5",
|
|
59413
59523
|
actionsModel: "claude-opus-5-5",
|
|
@@ -59449,6 +59559,14 @@ var ProjectConfig = external_exports.object({
|
|
|
59449
59559
|
actions: external_exports.array(ActionOverride).default([]),
|
|
59450
59560
|
llm: external_exports.object({
|
|
59451
59561
|
enabled: external_exports.boolean().default(true),
|
|
59562
|
+
/** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
|
|
59563
|
+
provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
|
|
59564
|
+
/** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
|
|
59565
|
+
baseUrl: external_exports.string().url().optional(),
|
|
59566
|
+
/** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
|
|
59567
|
+
apiKeyEnv: external_exports.string().optional(),
|
|
59568
|
+
/** One model for every step. Without it, non-Claude providers use their own default model. */
|
|
59569
|
+
model: external_exports.string().optional(),
|
|
59452
59570
|
classifyModel: external_exports.string().default(llmDefaults.classifyModel),
|
|
59453
59571
|
extractModel: external_exports.string().default(llmDefaults.extractModel),
|
|
59454
59572
|
actionsModel: external_exports.string().default(llmDefaults.actionsModel),
|
|
@@ -67461,6 +67579,15 @@ async function createClaudeAsker(opts = {}) {
|
|
|
67461
67579
|
}
|
|
67462
67580
|
};
|
|
67463
67581
|
}
|
|
67582
|
+
async function askerFor(s, env, f) {
|
|
67583
|
+
const r = resolveLlm(s, env);
|
|
67584
|
+
if (!r) return void 0;
|
|
67585
|
+
if (r.provider === "anthropic") {
|
|
67586
|
+
const claude = await createClaudeAsker({ apiKey: r.apiKey });
|
|
67587
|
+
return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
|
|
67588
|
+
}
|
|
67589
|
+
return { label: r.label, extract: (a) => extractJson(r, a, f) };
|
|
67590
|
+
}
|
|
67464
67591
|
|
|
67465
67592
|
// packages/actions/src/types.ts
|
|
67466
67593
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -68423,7 +68550,7 @@ var worker_default = {
|
|
|
68423
68550
|
const { AGENTICWEB_KV, ...vars } = env;
|
|
68424
68551
|
const store = AGENTICWEB_KV ? new KvStore(AGENTICWEB_KV) : new MemoryStore();
|
|
68425
68552
|
const engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env: vars, store }) : void 0;
|
|
68426
|
-
const llm = config2.llm
|
|
68553
|
+
const llm = await askerFor(config2.llm, vars);
|
|
68427
68554
|
app = createApp({
|
|
68428
68555
|
graph,
|
|
68429
68556
|
engine,
|