wasper-cli 0.2.0 → 0.3.1
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/dist/cli.js +742 -383
- package/dist/index.js +742 -383
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3158,6 +3158,14 @@ var SCHEMA = `
|
|
|
3158
3158
|
name TEXT NOT NULL DEFAULT '',
|
|
3159
3159
|
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3160
3160
|
);
|
|
3161
|
+
|
|
3162
|
+
CREATE TABLE IF NOT EXISTS chat_memory (
|
|
3163
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3164
|
+
role TEXT NOT NULL,
|
|
3165
|
+
content TEXT NOT NULL,
|
|
3166
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3167
|
+
);
|
|
3168
|
+
CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
|
|
3161
3169
|
`;
|
|
3162
3170
|
|
|
3163
3171
|
// src/db/index.ts
|
|
@@ -3337,6 +3345,19 @@ var init_db = __esm(() => {
|
|
|
3337
3345
|
getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
|
|
3338
3346
|
setSetting: (key, value) => {
|
|
3339
3347
|
db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
|
|
3348
|
+
},
|
|
3349
|
+
saveMemory: (role, content) => {
|
|
3350
|
+
db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
|
|
3351
|
+
},
|
|
3352
|
+
getMemory: (limit = 20) => {
|
|
3353
|
+
const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
3354
|
+
return rows.reverse();
|
|
3355
|
+
},
|
|
3356
|
+
clearMemory: () => {
|
|
3357
|
+
db.query("DELETE FROM chat_memory").run();
|
|
3358
|
+
},
|
|
3359
|
+
trimMemory: (keepLast = 40) => {
|
|
3360
|
+
db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
|
|
3340
3361
|
}
|
|
3341
3362
|
};
|
|
3342
3363
|
});
|
|
@@ -3586,7 +3607,7 @@ var package_default;
|
|
|
3586
3607
|
var init_package = __esm(() => {
|
|
3587
3608
|
package_default = {
|
|
3588
3609
|
name: "wasper-cli",
|
|
3589
|
-
version: "0.
|
|
3610
|
+
version: "0.3.1",
|
|
3590
3611
|
description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
|
|
3591
3612
|
type: "module",
|
|
3592
3613
|
homepage: "https://wasper.site",
|
|
@@ -4578,6 +4599,517 @@ var init_engine2 = __esm(() => {
|
|
|
4578
4599
|
init_state();
|
|
4579
4600
|
});
|
|
4580
4601
|
|
|
4602
|
+
// src/agent/harness.ts
|
|
4603
|
+
function mergeSignals(a, b) {
|
|
4604
|
+
if (!a && !b)
|
|
4605
|
+
return new AbortController().signal;
|
|
4606
|
+
if (!a)
|
|
4607
|
+
return b;
|
|
4608
|
+
if (!b)
|
|
4609
|
+
return a;
|
|
4610
|
+
const ctrl = new AbortController;
|
|
4611
|
+
const abort = () => ctrl.abort();
|
|
4612
|
+
a.addEventListener("abort", abort, { once: true });
|
|
4613
|
+
b.addEventListener("abort", abort, { once: true });
|
|
4614
|
+
return ctrl.signal;
|
|
4615
|
+
}
|
|
4616
|
+
async function fetchWithRetry(url, opts, emit, signal, maxRetries = 4) {
|
|
4617
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
4618
|
+
const stepSignal = signal;
|
|
4619
|
+
let res;
|
|
4620
|
+
try {
|
|
4621
|
+
res = await fetch(url, { ...opts, signal: stepSignal });
|
|
4622
|
+
} catch (e) {
|
|
4623
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4624
|
+
if (signal?.aborted)
|
|
4625
|
+
throw e;
|
|
4626
|
+
if (attempt === maxRetries)
|
|
4627
|
+
throw e;
|
|
4628
|
+
const isNetwork = msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("network") || msg.includes("fetch");
|
|
4629
|
+
if (!isNetwork)
|
|
4630
|
+
throw e;
|
|
4631
|
+
const delay2 = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 15000);
|
|
4632
|
+
emit({ type: "info", message: `Network error, retrying in ${Math.round(delay2 / 1000)}s\u2026` });
|
|
4633
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
if (!RETRYABLE_STATUS.has(res.status) || attempt === maxRetries)
|
|
4637
|
+
return res;
|
|
4638
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
|
|
4639
|
+
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 30000);
|
|
4640
|
+
const label = res.status === 429 ? "Rate limited" : `Server error ${res.status}`;
|
|
4641
|
+
emit({ type: "info", message: `${label} \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
|
|
4642
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
4643
|
+
}
|
|
4644
|
+
return fetch(url, opts);
|
|
4645
|
+
}
|
|
4646
|
+
function trimContext(messages) {
|
|
4647
|
+
if (JSON.stringify(messages).length <= MAX_CONTEXT_CHARS)
|
|
4648
|
+
return { messages, trimmed: false };
|
|
4649
|
+
const result = [...messages];
|
|
4650
|
+
while (JSON.stringify(result).length > MAX_CONTEXT_CHARS && result.length > 2) {
|
|
4651
|
+
let removed = false;
|
|
4652
|
+
const toolIdx = result.findIndex((m) => m.role === "tool");
|
|
4653
|
+
if (toolIdx !== -1) {
|
|
4654
|
+
result.splice(toolIdx, 1);
|
|
4655
|
+
if (toolIdx > 0) {
|
|
4656
|
+
const prev = result[toolIdx - 1];
|
|
4657
|
+
if (prev?.role === "assistant") {
|
|
4658
|
+
const tc = prev.tool_calls;
|
|
4659
|
+
if (Array.isArray(tc) && tc.length)
|
|
4660
|
+
result.splice(toolIdx - 1, 1);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
removed = true;
|
|
4664
|
+
}
|
|
4665
|
+
if (!removed) {
|
|
4666
|
+
const anthropicIdx = result.findIndex((m) => {
|
|
4667
|
+
if (m.role !== "user")
|
|
4668
|
+
return false;
|
|
4669
|
+
const c = m.content;
|
|
4670
|
+
return Array.isArray(c) && c.some((b) => b.type === "tool_result");
|
|
4671
|
+
});
|
|
4672
|
+
if (anthropicIdx !== -1) {
|
|
4673
|
+
result.splice(anthropicIdx, 1);
|
|
4674
|
+
if (anthropicIdx > 0 && result[anthropicIdx - 1]?.role === "assistant") {
|
|
4675
|
+
result.splice(anthropicIdx - 1, 1);
|
|
4676
|
+
}
|
|
4677
|
+
removed = true;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
if (!removed)
|
|
4681
|
+
break;
|
|
4682
|
+
}
|
|
4683
|
+
return { messages: result, trimmed: true };
|
|
4684
|
+
}
|
|
4685
|
+
async function* readSSE(body) {
|
|
4686
|
+
const reader = body.getReader();
|
|
4687
|
+
const decoder = new TextDecoder;
|
|
4688
|
+
let buf = "";
|
|
4689
|
+
try {
|
|
4690
|
+
while (true) {
|
|
4691
|
+
const { done, value } = await reader.read();
|
|
4692
|
+
if (done)
|
|
4693
|
+
break;
|
|
4694
|
+
buf += decoder.decode(value, { stream: true });
|
|
4695
|
+
const parts = buf.split(`
|
|
4696
|
+
|
|
4697
|
+
`);
|
|
4698
|
+
buf = parts.pop() ?? "";
|
|
4699
|
+
for (const part of parts) {
|
|
4700
|
+
let data = "";
|
|
4701
|
+
for (const line of part.split(`
|
|
4702
|
+
`)) {
|
|
4703
|
+
if (line.startsWith("data: ")) {
|
|
4704
|
+
data = line.slice(6);
|
|
4705
|
+
break;
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (!data || data === "[DONE]")
|
|
4709
|
+
continue;
|
|
4710
|
+
try {
|
|
4711
|
+
yield JSON.parse(data);
|
|
4712
|
+
} catch {}
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
} finally {
|
|
4716
|
+
reader.releaseLock();
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
function buildAnthropicTools(schemas) {
|
|
4720
|
+
return schemas.map((s) => ({
|
|
4721
|
+
name: s.name,
|
|
4722
|
+
description: s.description,
|
|
4723
|
+
input_schema: { type: "object", properties: s.params, required: s.required }
|
|
4724
|
+
}));
|
|
4725
|
+
}
|
|
4726
|
+
async function streamAnthropic(cfg, system, messages, tools, emit, signal) {
|
|
4727
|
+
const base = (cfg.baseUrl || "https://api.anthropic.com").replace(/\/$/, "");
|
|
4728
|
+
const systemContent = cfg.enablePromptCache ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : system;
|
|
4729
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4730
|
+
const res = await fetchWithRetry(`${base}/v1/messages`, {
|
|
4731
|
+
method: "POST",
|
|
4732
|
+
headers: {
|
|
4733
|
+
"Content-Type": "application/json",
|
|
4734
|
+
"x-api-key": cfg.apiKey,
|
|
4735
|
+
"anthropic-version": "2023-06-01",
|
|
4736
|
+
...cfg.enablePromptCache ? { "anthropic-beta": "prompt-caching-1-0" } : {},
|
|
4737
|
+
...cfg.extraHeaders
|
|
4738
|
+
},
|
|
4739
|
+
body: JSON.stringify({
|
|
4740
|
+
model: cfg.model,
|
|
4741
|
+
max_tokens: cfg.maxTokens,
|
|
4742
|
+
temperature: cfg.temperature,
|
|
4743
|
+
...cfg.topK > 0 ? { top_k: cfg.topK } : {},
|
|
4744
|
+
system: systemContent,
|
|
4745
|
+
messages,
|
|
4746
|
+
tools,
|
|
4747
|
+
stream: true
|
|
4748
|
+
})
|
|
4749
|
+
}, emit, stepSignal);
|
|
4750
|
+
if (!res.ok) {
|
|
4751
|
+
const body = await res.text();
|
|
4752
|
+
const retryable = RETRYABLE_STATUS.has(res.status);
|
|
4753
|
+
throw Object.assign(new Error(`Anthropic ${res.status}: ${body}`), { retryable });
|
|
4754
|
+
}
|
|
4755
|
+
const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4756
|
+
const blocks = [];
|
|
4757
|
+
const inputAccum = {};
|
|
4758
|
+
for await (const ev of readSSE(res.body)) {
|
|
4759
|
+
if (signal.aborted)
|
|
4760
|
+
break;
|
|
4761
|
+
const evType = ev.type;
|
|
4762
|
+
if (evType === "message_start") {
|
|
4763
|
+
const usage = ev.message?.usage;
|
|
4764
|
+
if (usage) {
|
|
4765
|
+
result.usage.input = usage.input_tokens ?? 0;
|
|
4766
|
+
result.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
4767
|
+
result.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
4768
|
+
}
|
|
4769
|
+
} else if (evType === "content_block_start") {
|
|
4770
|
+
const idx = ev.index;
|
|
4771
|
+
const cb = ev.content_block;
|
|
4772
|
+
blocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
|
|
4773
|
+
if (cb.type === "tool_use")
|
|
4774
|
+
inputAccum[idx] = "";
|
|
4775
|
+
} else if (evType === "content_block_delta") {
|
|
4776
|
+
const idx = ev.index;
|
|
4777
|
+
const delta = ev.delta;
|
|
4778
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
4779
|
+
result.text += delta.text;
|
|
4780
|
+
if (!blocks[idx])
|
|
4781
|
+
blocks[idx] = { type: "text" };
|
|
4782
|
+
blocks[idx].text = (blocks[idx].text ?? "") + delta.text;
|
|
4783
|
+
emit({ type: "text_delta", text: delta.text });
|
|
4784
|
+
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
4785
|
+
result.thinking += delta.thinking;
|
|
4786
|
+
emit({ type: "thinking", text: delta.thinking });
|
|
4787
|
+
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
4788
|
+
inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
|
|
4789
|
+
}
|
|
4790
|
+
} else if (evType === "content_block_stop") {
|
|
4791
|
+
const idx = ev.index;
|
|
4792
|
+
if (blocks[idx]?.type === "tool_use") {
|
|
4793
|
+
try {
|
|
4794
|
+
blocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
|
|
4795
|
+
} catch {
|
|
4796
|
+
blocks[idx].input = {};
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
} else if (evType === "message_delta") {
|
|
4800
|
+
const delta = ev.delta;
|
|
4801
|
+
const usage = ev.usage;
|
|
4802
|
+
if (delta.stop_reason)
|
|
4803
|
+
result.stopReason = delta.stop_reason;
|
|
4804
|
+
if (usage?.output_tokens)
|
|
4805
|
+
result.usage.output = usage.output_tokens;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
for (const b of blocks) {
|
|
4809
|
+
if (b.type === "tool_use" && b.id && b.name) {
|
|
4810
|
+
result.toolUses.push({ id: b.id, name: b.name, input: b.input ?? {} });
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
result._anthropicBlocks = blocks;
|
|
4814
|
+
return result;
|
|
4815
|
+
}
|
|
4816
|
+
function buildOpenAITools(schemas) {
|
|
4817
|
+
return schemas.map((s) => ({
|
|
4818
|
+
type: "function",
|
|
4819
|
+
function: { name: s.name, description: s.description, parameters: { type: "object", properties: s.params, required: s.required } }
|
|
4820
|
+
}));
|
|
4821
|
+
}
|
|
4822
|
+
async function streamOpenAI(cfg, system, messages, tools, emit, signal) {
|
|
4823
|
+
const providerBases = {
|
|
4824
|
+
openai: "https://api.openai.com",
|
|
4825
|
+
mistral: "https://api.mistral.ai",
|
|
4826
|
+
groq: "https://api.groq.com/openai",
|
|
4827
|
+
"github-copilot": "https://api.githubcopilot.com"
|
|
4828
|
+
};
|
|
4829
|
+
const base = (cfg.baseUrl || providerBases[cfg.provider] || "https://api.openai.com").replace(/\/$/, "");
|
|
4830
|
+
const providerHeaders = cfg.provider === "github-copilot" ? { "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "vscode/1.85.0" } : {};
|
|
4831
|
+
const authHeaders = cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {};
|
|
4832
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4833
|
+
const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
|
|
4834
|
+
method: "POST",
|
|
4835
|
+
headers: { "Content-Type": "application/json", ...authHeaders, ...providerHeaders, ...cfg.extraHeaders },
|
|
4836
|
+
body: JSON.stringify({
|
|
4837
|
+
model: cfg.model,
|
|
4838
|
+
max_tokens: cfg.maxTokens,
|
|
4839
|
+
temperature: cfg.temperature,
|
|
4840
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
4841
|
+
tools,
|
|
4842
|
+
tool_choice: "auto",
|
|
4843
|
+
stream: true,
|
|
4844
|
+
stream_options: { include_usage: true }
|
|
4845
|
+
})
|
|
4846
|
+
}, emit, stepSignal);
|
|
4847
|
+
if (!res.ok) {
|
|
4848
|
+
const body = await res.text();
|
|
4849
|
+
const retryable = RETRYABLE_STATUS.has(res.status);
|
|
4850
|
+
throw Object.assign(new Error(body), { retryable });
|
|
4851
|
+
}
|
|
4852
|
+
const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4853
|
+
const tcAccum = {};
|
|
4854
|
+
for await (const ev of readSSE(res.body)) {
|
|
4855
|
+
if (signal.aborted)
|
|
4856
|
+
break;
|
|
4857
|
+
if (ev.object === "error") {
|
|
4858
|
+
const retryable = ev.code === "1300" || ev.raw_status_code === 429 || ev.raw_status_code === 503;
|
|
4859
|
+
throw Object.assign(new Error(JSON.stringify(ev)), { retryable });
|
|
4860
|
+
}
|
|
4861
|
+
if (ev.usage) {
|
|
4862
|
+
const u = ev.usage;
|
|
4863
|
+
result.usage.input = u.prompt_tokens ?? 0;
|
|
4864
|
+
result.usage.output = u.completion_tokens ?? 0;
|
|
4865
|
+
}
|
|
4866
|
+
const choices = ev.choices;
|
|
4867
|
+
const choice = choices?.[0];
|
|
4868
|
+
if (!choice)
|
|
4869
|
+
continue;
|
|
4870
|
+
const fr = choice.finish_reason;
|
|
4871
|
+
if (fr)
|
|
4872
|
+
result.stopReason = fr;
|
|
4873
|
+
const delta = choice.delta;
|
|
4874
|
+
if (!delta)
|
|
4875
|
+
continue;
|
|
4876
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
4877
|
+
result.text += delta.content;
|
|
4878
|
+
emit({ type: "text_delta", text: delta.content });
|
|
4879
|
+
}
|
|
4880
|
+
const tcDeltas = delta.tool_calls;
|
|
4881
|
+
if (tcDeltas) {
|
|
4882
|
+
for (const tc of tcDeltas) {
|
|
4883
|
+
if (!tcAccum[tc.index])
|
|
4884
|
+
tcAccum[tc.index] = { id: "", name: "", args: "" };
|
|
4885
|
+
const e = tcAccum[tc.index];
|
|
4886
|
+
if (tc.id)
|
|
4887
|
+
e.id += tc.id;
|
|
4888
|
+
if (tc.function?.name)
|
|
4889
|
+
e.name += tc.function.name;
|
|
4890
|
+
if (tc.function?.arguments)
|
|
4891
|
+
e.args += tc.function.arguments;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
for (const tc of Object.values(tcAccum)) {
|
|
4896
|
+
let input = {};
|
|
4897
|
+
try {
|
|
4898
|
+
input = JSON.parse(tc.args);
|
|
4899
|
+
} catch {}
|
|
4900
|
+
result.toolUses.push({ id: tc.id, name: tc.name, input });
|
|
4901
|
+
}
|
|
4902
|
+
result._openaiTcAccum = tcAccum;
|
|
4903
|
+
return result;
|
|
4904
|
+
}
|
|
4905
|
+
async function callOllama(cfg, system, messages, emit, signal) {
|
|
4906
|
+
const base = (cfg.baseUrl || "http://localhost:11434").replace(/\/$/, "");
|
|
4907
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4908
|
+
const res = await fetchWithRetry(`${base}/api/chat`, {
|
|
4909
|
+
method: "POST",
|
|
4910
|
+
headers: { "Content-Type": "application/json" },
|
|
4911
|
+
body: JSON.stringify({ model: cfg.model, messages: [{ role: "system", content: system }, ...messages], stream: false })
|
|
4912
|
+
}, emit, stepSignal);
|
|
4913
|
+
if (!res.ok)
|
|
4914
|
+
throw new Error(`Ollama ${res.status}: ${await res.text()}`);
|
|
4915
|
+
const d = await res.json();
|
|
4916
|
+
const text = d.message?.content ?? "";
|
|
4917
|
+
emit({ type: "text_delta", text });
|
|
4918
|
+
return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4919
|
+
}
|
|
4920
|
+
async function callGemini(cfg, system, messages, emit, signal) {
|
|
4921
|
+
const base = (cfg.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
|
|
4922
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4923
|
+
const res = await fetchWithRetry(`${base}/v1beta/models/${cfg.model}:generateContent?key=${cfg.apiKey}`, {
|
|
4924
|
+
method: "POST",
|
|
4925
|
+
headers: { "Content-Type": "application/json" },
|
|
4926
|
+
body: JSON.stringify({
|
|
4927
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
4928
|
+
contents: messages.map((m) => ({
|
|
4929
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
4930
|
+
parts: [{ text: m.content }]
|
|
4931
|
+
})),
|
|
4932
|
+
generationConfig: { maxOutputTokens: cfg.maxTokens }
|
|
4933
|
+
})
|
|
4934
|
+
}, emit, stepSignal);
|
|
4935
|
+
if (!res.ok)
|
|
4936
|
+
throw new Error(`Gemini ${res.status}: ${await res.text()}`);
|
|
4937
|
+
const d = await res.json();
|
|
4938
|
+
const text = d.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
4939
|
+
emit({ type: "text_delta", text });
|
|
4940
|
+
return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4941
|
+
}
|
|
4942
|
+
async function runAgentLoop(config2, system, initialMessages, toolSchemas, executeTool, emit, signal = new AbortController().signal, toolCache = new Map) {
|
|
4943
|
+
const cfg = { ...DEFAULTS, ...config2 };
|
|
4944
|
+
const isAnthropic = cfg.provider === "anthropic";
|
|
4945
|
+
const isOllama = cfg.provider === "ollama";
|
|
4946
|
+
const isGemini = cfg.provider === "gemini";
|
|
4947
|
+
const anthropicTools = buildAnthropicTools(toolSchemas);
|
|
4948
|
+
const openaiTools = buildOpenAITools(toolSchemas);
|
|
4949
|
+
const messages = [...initialMessages];
|
|
4950
|
+
const allToolCalls = [];
|
|
4951
|
+
const totalTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
4952
|
+
let totalToolsUsed = 0;
|
|
4953
|
+
let consecutiveErrors = 0;
|
|
4954
|
+
const endpointErrors = {};
|
|
4955
|
+
for (let iter = 0;iter < cfg.maxIterations; iter++) {
|
|
4956
|
+
if (signal.aborted) {
|
|
4957
|
+
return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
|
|
4958
|
+
}
|
|
4959
|
+
const { messages: trimmed, trimmed: didTrim } = trimContext(messages);
|
|
4960
|
+
if (didTrim) {
|
|
4961
|
+
emit({ type: "info", message: "Context trimmed to fit within limits." });
|
|
4962
|
+
messages.splice(0, messages.length, ...trimmed);
|
|
4963
|
+
}
|
|
4964
|
+
let turn;
|
|
4965
|
+
try {
|
|
4966
|
+
if (isAnthropic) {
|
|
4967
|
+
turn = await streamAnthropic(cfg, system, messages, anthropicTools, emit, signal);
|
|
4968
|
+
} else if (isOllama) {
|
|
4969
|
+
turn = await callOllama(cfg, system, messages, emit, signal);
|
|
4970
|
+
} else if (isGemini) {
|
|
4971
|
+
turn = await callGemini(cfg, system, messages, emit, signal);
|
|
4972
|
+
} else {
|
|
4973
|
+
turn = await streamOpenAI(cfg, system, messages, openaiTools, emit, signal);
|
|
4974
|
+
}
|
|
4975
|
+
} catch (e) {
|
|
4976
|
+
if (signal.aborted) {
|
|
4977
|
+
return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
|
|
4978
|
+
}
|
|
4979
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4980
|
+
const retryable = e.retryable ?? false;
|
|
4981
|
+
emit({ type: "error", message: msg, retryable });
|
|
4982
|
+
throw e;
|
|
4983
|
+
}
|
|
4984
|
+
totalTokens.input += turn.usage.input;
|
|
4985
|
+
totalTokens.output += turn.usage.output;
|
|
4986
|
+
totalTokens.cacheRead += turn.usage.cacheRead;
|
|
4987
|
+
totalTokens.cacheWrite += turn.usage.cacheWrite;
|
|
4988
|
+
if (turn.usage.input || turn.usage.output) {
|
|
4989
|
+
emit({ type: "token_usage", ...turn.usage });
|
|
4990
|
+
}
|
|
4991
|
+
const wantsTools = isAnthropic ? turn.stopReason === "tool_use" : turn.stopReason === "tool_calls";
|
|
4992
|
+
if (!wantsTools || turn.toolUses.length === 0) {
|
|
4993
|
+
return { content: turn.text, toolCalls: allToolCalls, stopReason: "end_turn", tokens: totalTokens };
|
|
4994
|
+
}
|
|
4995
|
+
if (totalToolsUsed >= cfg.maxTotalTools) {
|
|
4996
|
+
return {
|
|
4997
|
+
content: `Agent stopped: reached ${cfg.maxTotalTools} tool calls. Break your request into smaller steps.`,
|
|
4998
|
+
toolCalls: allToolCalls,
|
|
4999
|
+
stopReason: "max_tools",
|
|
5000
|
+
tokens: totalTokens
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
const dedupeKey = (name, input) => `${name}:${JSON.stringify(input)}`;
|
|
5004
|
+
const turnResults = [];
|
|
5005
|
+
const executeOne = async (use) => {
|
|
5006
|
+
totalToolsUsed++;
|
|
5007
|
+
const key = dedupeKey(use.name, use.input);
|
|
5008
|
+
const cachedResult = toolCache.get(key);
|
|
5009
|
+
const isCached = !!cachedResult;
|
|
5010
|
+
emit({ type: "tool_start", id: use.id, tool: use.name, input: use.input, cached: isCached });
|
|
5011
|
+
let result;
|
|
5012
|
+
let ms = 0;
|
|
5013
|
+
if (isCached) {
|
|
5014
|
+
result = cachedResult;
|
|
5015
|
+
} else {
|
|
5016
|
+
const t0 = Date.now();
|
|
5017
|
+
result = await executeTool(use.name, use.input);
|
|
5018
|
+
ms = Date.now() - t0;
|
|
5019
|
+
if (!result.isError && use.name !== "execute_api_request" && use.name !== "fetch_url") {
|
|
5020
|
+
toolCache.set(key, result);
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
emit({ type: "tool_done", id: use.id, tool: use.name, output: result.text, isError: result.isError, ms, cached: isCached });
|
|
5024
|
+
return { id: use.id, name: use.name, text: result.text, isError: result.isError, ms, cached: isCached };
|
|
5025
|
+
};
|
|
5026
|
+
const toolUses = turn.toolUses;
|
|
5027
|
+
if (cfg.parallelTools && toolUses.length > 1) {
|
|
5028
|
+
const pure = toolUses.filter((u) => u.name !== "execute_api_request" && u.name !== "fetch_url");
|
|
5029
|
+
const sideEffect = toolUses.filter((u) => u.name === "execute_api_request" || u.name === "fetch_url");
|
|
5030
|
+
const pureResults = await Promise.all(pure.map((u) => executeOne(u)));
|
|
5031
|
+
const sideEffectResults = [];
|
|
5032
|
+
for (const u of sideEffect)
|
|
5033
|
+
sideEffectResults.push(await executeOne(u));
|
|
5034
|
+
const resultMap = new Map([...pureResults, ...sideEffectResults].map((r) => [r.id, r]));
|
|
5035
|
+
for (const u of toolUses) {
|
|
5036
|
+
const r = resultMap.get(u.id);
|
|
5037
|
+
if (r)
|
|
5038
|
+
turnResults.push(r);
|
|
5039
|
+
}
|
|
5040
|
+
} else {
|
|
5041
|
+
for (const u of toolUses)
|
|
5042
|
+
turnResults.push(await executeOne(u));
|
|
5043
|
+
}
|
|
5044
|
+
for (const r of turnResults) {
|
|
5045
|
+
allToolCalls.push({ id: r.id, tool: r.name, input: turn.toolUses.find((u) => u.id === r.id)?.input ?? {}, output: r.text, isError: r.isError, ms: r.ms, cached: r.cached });
|
|
5046
|
+
if (r.isError) {
|
|
5047
|
+
consecutiveErrors++;
|
|
5048
|
+
if (r.name === "execute_api_request") {
|
|
5049
|
+
const eid = String(turn.toolUses.find((u) => u.id === r.id)?.input?.operationId ?? r.id);
|
|
5050
|
+
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
5051
|
+
if (endpointErrors[eid] >= cfg.maxEndpointErrors) {
|
|
5052
|
+
return {
|
|
5053
|
+
content: `Endpoint "${eid}" failed ${cfg.maxEndpointErrors} times. Last error: ${r.text}`,
|
|
5054
|
+
toolCalls: allToolCalls,
|
|
5055
|
+
stopReason: "max_endpoint_errors",
|
|
5056
|
+
tokens: totalTokens
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
if (consecutiveErrors >= cfg.maxConsecutiveErrors) {
|
|
5061
|
+
return {
|
|
5062
|
+
content: `Stopped after ${cfg.maxConsecutiveErrors} consecutive errors. Last: ${r.text}`,
|
|
5063
|
+
toolCalls: allToolCalls,
|
|
5064
|
+
stopReason: "max_errors",
|
|
5065
|
+
tokens: totalTokens
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
} else {
|
|
5069
|
+
consecutiveErrors = 0;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
if (isAnthropic) {
|
|
5073
|
+
const blocks = turn._anthropicBlocks ?? [];
|
|
5074
|
+
messages.push({ role: "assistant", content: blocks });
|
|
5075
|
+
messages.push({
|
|
5076
|
+
role: "user",
|
|
5077
|
+
content: turnResults.map((r) => ({ type: "tool_result", tool_use_id: r.id, content: r.text }))
|
|
5078
|
+
});
|
|
5079
|
+
} else {
|
|
5080
|
+
const tc = turn._openaiTcAccum ?? {};
|
|
5081
|
+
messages.push({
|
|
5082
|
+
role: "assistant",
|
|
5083
|
+
content: turn.text || null,
|
|
5084
|
+
tool_calls: Object.values(tc).map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: t.args } }))
|
|
5085
|
+
});
|
|
5086
|
+
for (const r of turnResults) {
|
|
5087
|
+
messages.push({ role: "tool", tool_call_id: r.id, content: r.text });
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
return { content: "(max iterations reached)", toolCalls: allToolCalls, stopReason: "max_iterations", tokens: totalTokens };
|
|
5092
|
+
}
|
|
5093
|
+
var RETRYABLE_STATUS, MAX_CONTEXT_CHARS = 300000, DEFAULTS;
|
|
5094
|
+
var init_harness = __esm(() => {
|
|
5095
|
+
RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
5096
|
+
DEFAULTS = {
|
|
5097
|
+
apiKey: "",
|
|
5098
|
+
baseUrl: "",
|
|
5099
|
+
extraHeaders: {},
|
|
5100
|
+
maxTokens: 4096,
|
|
5101
|
+
temperature: 1,
|
|
5102
|
+
topK: 0,
|
|
5103
|
+
maxIterations: 40,
|
|
5104
|
+
maxTotalTools: 40,
|
|
5105
|
+
maxConsecutiveErrors: 5,
|
|
5106
|
+
maxEndpointErrors: 3,
|
|
5107
|
+
stepTimeoutMs: 60000,
|
|
5108
|
+
parallelTools: true,
|
|
5109
|
+
enablePromptCache: true
|
|
5110
|
+
};
|
|
5111
|
+
});
|
|
5112
|
+
|
|
4581
5113
|
// src/api/routes.ts
|
|
4582
5114
|
import dns from "dns/promises";
|
|
4583
5115
|
function json(data, status = 200) {
|
|
@@ -4641,6 +5173,12 @@ async function apiRouter(req) {
|
|
|
4641
5173
|
return handleDeleteRule(path);
|
|
4642
5174
|
if (path === "/api/ai/chat" && method === "POST")
|
|
4643
5175
|
return handleAiChat(req);
|
|
5176
|
+
if (path === "/api/ai/memory" && method === "GET")
|
|
5177
|
+
return json({ memory: dbQueries.getMemory(40) });
|
|
5178
|
+
if (path === "/api/ai/memory" && method === "DELETE") {
|
|
5179
|
+
dbQueries.clearMemory();
|
|
5180
|
+
return json({ success: true });
|
|
5181
|
+
}
|
|
4644
5182
|
if (path === "/api/debug/dns" && method === "GET")
|
|
4645
5183
|
return handleDnsQuery(searchParams);
|
|
4646
5184
|
if (path === "/api/debug/ping" && method === "GET")
|
|
@@ -4866,39 +5404,54 @@ async function handleSetSettings(req) {
|
|
|
4866
5404
|
dbQueries.setSettings(body);
|
|
4867
5405
|
return json(body);
|
|
4868
5406
|
}
|
|
4869
|
-
async function executeTool(name, args) {
|
|
5407
|
+
async function executeTool(name, args, cache = new Map) {
|
|
4870
5408
|
const { operations, spec } = getState();
|
|
4871
5409
|
if (name === "search_endpoints") {
|
|
5410
|
+
const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
|
|
5411
|
+
const hit = cache.get(cacheKey2);
|
|
5412
|
+
if (hit)
|
|
5413
|
+
return hit;
|
|
4872
5414
|
const q = String(args.query ?? "").toLowerCase();
|
|
4873
5415
|
const terms = q.split(/\s+/).filter(Boolean);
|
|
4874
5416
|
const matches = operations.filter((op) => {
|
|
4875
5417
|
const hay = [op.operationId, op.path, op.method, ...op.tags ?? [], op.summary ?? "", op.description ?? ""].join(" ").toLowerCase();
|
|
4876
5418
|
return terms.every((t) => hay.includes(t));
|
|
4877
5419
|
}).slice(0, 30).map((op) => ({ operationId: op.operationId, method: op.method.toUpperCase(), path: op.path, summary: op.summary ?? null, tags: op.tags }));
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
5420
|
+
const text = !matches.length ? `No endpoints found matching "${args.query}". Total: ${operations.length}.` : JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2);
|
|
5421
|
+
const result = { text, isError: false };
|
|
5422
|
+
cache.set(cacheKey2, result);
|
|
5423
|
+
return result;
|
|
4881
5424
|
}
|
|
4882
5425
|
if (name === "get_endpoint_schema") {
|
|
5426
|
+
const cacheKey2 = `schema:${String(args.operationId ?? "")}`;
|
|
5427
|
+
const hit = cache.get(cacheKey2);
|
|
5428
|
+
if (hit)
|
|
5429
|
+
return hit;
|
|
4883
5430
|
const op = operations.find((o) => o.operationId === args.operationId);
|
|
4884
5431
|
if (!op)
|
|
4885
5432
|
return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5433
|
+
const text = JSON.stringify({
|
|
5434
|
+
operationId: op.operationId,
|
|
5435
|
+
method: op.method.toUpperCase(),
|
|
5436
|
+
path: op.path,
|
|
5437
|
+
summary: op.summary ?? null,
|
|
5438
|
+
description: op.description ?? null,
|
|
5439
|
+
tags: op.tags,
|
|
5440
|
+
parameters: op.parameters,
|
|
5441
|
+
requestBody: op.requestBody ?? null,
|
|
5442
|
+
responses: op.responses
|
|
5443
|
+
}, null, 2);
|
|
5444
|
+
const result = { text, isError: false };
|
|
5445
|
+
cache.set(cacheKey2, result);
|
|
5446
|
+
return result;
|
|
4900
5447
|
}
|
|
4901
5448
|
if (name === "execute_api_request") {
|
|
5449
|
+
const now = Date.now();
|
|
5450
|
+
const gap = now - _lastApiCallMs;
|
|
5451
|
+
if (gap < MIN_API_CALL_INTERVAL_MS) {
|
|
5452
|
+
await new Promise((r) => setTimeout(r, MIN_API_CALL_INTERVAL_MS - gap));
|
|
5453
|
+
}
|
|
5454
|
+
_lastApiCallMs = Date.now();
|
|
4902
5455
|
const op = operations.find((o) => o.operationId === args.operationId);
|
|
4903
5456
|
if (!op)
|
|
4904
5457
|
return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
|
|
@@ -4929,20 +5482,81 @@ async function executeTool(name, args) {
|
|
|
4929
5482
|
const bodyStr = reqBody !== undefined ? typeof reqBody === "string" ? reqBody : JSON.stringify(reqBody) : null;
|
|
4930
5483
|
if (bodyStr !== null && op.requestBody?.contentType)
|
|
4931
5484
|
authedHeaders["Content-Type"] = op.requestBody.contentType;
|
|
5485
|
+
const logId = randomUUID2();
|
|
4932
5486
|
try {
|
|
4933
5487
|
const start = Date.now();
|
|
4934
5488
|
const res = await fetch(authedUrl, { method: op.method.toUpperCase(), headers: authedHeaders, body: bodyStr ?? undefined });
|
|
4935
|
-
const
|
|
5489
|
+
const responseText = await res.text();
|
|
4936
5490
|
const latency = Date.now() - start;
|
|
4937
|
-
|
|
5491
|
+
const resHeaders = Object.fromEntries(res.headers.entries());
|
|
5492
|
+
dbQueries.insertLog({
|
|
5493
|
+
id: logId,
|
|
5494
|
+
source: "ai",
|
|
5495
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5496
|
+
method: op.method.toUpperCase(),
|
|
5497
|
+
url: authedUrl,
|
|
5498
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5499
|
+
request_body: bodyStr,
|
|
5500
|
+
status_code: res.status,
|
|
5501
|
+
response_headers: JSON.stringify(resHeaders),
|
|
5502
|
+
response_body: responseText.slice(0, 8192),
|
|
5503
|
+
latency_ms: latency,
|
|
5504
|
+
error: null
|
|
5505
|
+
});
|
|
5506
|
+
logBus.emit({
|
|
5507
|
+
id: logId,
|
|
5508
|
+
source: "ai",
|
|
5509
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5510
|
+
method: op.method.toUpperCase(),
|
|
5511
|
+
url: authedUrl,
|
|
5512
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5513
|
+
request_body: bodyStr,
|
|
5514
|
+
status_code: res.status,
|
|
5515
|
+
response_headers: JSON.stringify(resHeaders),
|
|
5516
|
+
response_body: responseText.slice(0, 2048),
|
|
5517
|
+
latency_ms: latency,
|
|
5518
|
+
error: null,
|
|
5519
|
+
created_at: Date.now()
|
|
5520
|
+
});
|
|
5521
|
+
let pretty = responseText;
|
|
4938
5522
|
try {
|
|
4939
|
-
pretty = JSON.stringify(JSON.parse(
|
|
5523
|
+
pretty = JSON.stringify(JSON.parse(responseText), null, 2);
|
|
4940
5524
|
} catch {}
|
|
4941
5525
|
return { text: `HTTP ${res.status} (${latency}ms)
|
|
4942
5526
|
|
|
4943
5527
|
${pretty}`, isError: !res.ok };
|
|
4944
5528
|
} catch (e) {
|
|
4945
|
-
|
|
5529
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
5530
|
+
dbQueries.insertLog({
|
|
5531
|
+
id: logId,
|
|
5532
|
+
source: "ai",
|
|
5533
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5534
|
+
method: op.method.toUpperCase(),
|
|
5535
|
+
url: authedUrl,
|
|
5536
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5537
|
+
request_body: bodyStr,
|
|
5538
|
+
status_code: null,
|
|
5539
|
+
response_headers: null,
|
|
5540
|
+
response_body: null,
|
|
5541
|
+
latency_ms: null,
|
|
5542
|
+
error: errMsg
|
|
5543
|
+
});
|
|
5544
|
+
logBus.emit({
|
|
5545
|
+
id: logId,
|
|
5546
|
+
source: "ai",
|
|
5547
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5548
|
+
method: op.method.toUpperCase(),
|
|
5549
|
+
url: authedUrl,
|
|
5550
|
+
request_headers: null,
|
|
5551
|
+
request_body: bodyStr,
|
|
5552
|
+
status_code: null,
|
|
5553
|
+
response_headers: null,
|
|
5554
|
+
response_body: null,
|
|
5555
|
+
latency_ms: null,
|
|
5556
|
+
error: errMsg,
|
|
5557
|
+
created_at: Date.now()
|
|
5558
|
+
});
|
|
5559
|
+
return { text: `Network error: ${errMsg}`, isError: true };
|
|
4946
5560
|
}
|
|
4947
5561
|
}
|
|
4948
5562
|
if (name === "fetch_url") {
|
|
@@ -5092,10 +5706,25 @@ ${stripped}`, isError: !res.ok };
|
|
|
5092
5706
|
}
|
|
5093
5707
|
if (name === "save_auth_token") {
|
|
5094
5708
|
const profileName = String(args.name ?? "AI Login").trim();
|
|
5709
|
+
const tokenType = String(args.token_type ?? "bearer");
|
|
5710
|
+
if (tokenType === "basic" || args.username && args.password) {
|
|
5711
|
+
const username = String(args.username ?? "").trim();
|
|
5712
|
+
const password = String(args.password ?? "").trim();
|
|
5713
|
+
if (!username || !password)
|
|
5714
|
+
return { text: "Error: username and password are required for basic auth", isError: true };
|
|
5715
|
+
const authConfig2 = { type: "basic", username, password };
|
|
5716
|
+
const profileId2 = randomUUID2();
|
|
5717
|
+
try {
|
|
5718
|
+
dbQueries.insertProfile({ id: profileId2, name: profileName, description: "Saved by AI", type: "basic", config: JSON.stringify(authConfig2), token_cache: null, is_active: 0 });
|
|
5719
|
+
dbQueries.activateProfile(profileId2);
|
|
5720
|
+
return { text: JSON.stringify({ success: true, message: `Saved and activated basic auth profile "${profileName}"`, id: profileId2 }), isError: false };
|
|
5721
|
+
} catch (e) {
|
|
5722
|
+
return { text: `Error saving profile: ${e instanceof Error ? e.message : String(e)}`, isError: true };
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5095
5725
|
const token = String(args.token ?? "").trim();
|
|
5096
5726
|
if (!token)
|
|
5097
|
-
return { text: "Error: token is required", isError: true };
|
|
5098
|
-
const tokenType = String(args.token_type ?? "bearer");
|
|
5727
|
+
return { text: "Error: token is required for bearer/apikey auth", isError: true };
|
|
5099
5728
|
const headerName = String(args.header_name ?? "X-Api-Key");
|
|
5100
5729
|
let authConfig;
|
|
5101
5730
|
let type;
|
|
@@ -5120,274 +5749,6 @@ ${stripped}`, isError: !res.ok };
|
|
|
5120
5749
|
}
|
|
5121
5750
|
return { text: `Unknown tool: ${name}`, isError: true };
|
|
5122
5751
|
}
|
|
5123
|
-
async function fetchWithRetry(url, opts, emit, maxRetries = 3) {
|
|
5124
|
-
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
5125
|
-
const res = await fetch(url, opts);
|
|
5126
|
-
if (res.status !== 429 || attempt === maxRetries)
|
|
5127
|
-
return res;
|
|
5128
|
-
const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
|
|
5129
|
-
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
|
|
5130
|
-
emit({ type: "info", message: `Rate limited \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
|
|
5131
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
5132
|
-
}
|
|
5133
|
-
return fetch(url, opts);
|
|
5134
|
-
}
|
|
5135
|
-
async function anthropicAgentLoop(apiKey, model, system, initialMessages, emit) {
|
|
5136
|
-
const msgs = [...initialMessages];
|
|
5137
|
-
const toolCalls = [];
|
|
5138
|
-
let totalTools = 0;
|
|
5139
|
-
let consecutiveErrors = 0;
|
|
5140
|
-
const endpointErrors = {};
|
|
5141
|
-
for (let iter = 0;iter < 40; iter++) {
|
|
5142
|
-
const res = await fetchWithRetry("https://api.anthropic.com/v1/messages", {
|
|
5143
|
-
method: "POST",
|
|
5144
|
-
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
5145
|
-
body: JSON.stringify({ model, max_tokens: 4096, system, messages: msgs, tools: ANTHROPIC_TOOLS, stream: true })
|
|
5146
|
-
}, emit);
|
|
5147
|
-
if (!res.ok)
|
|
5148
|
-
throw new Error(`Anthropic error: ${await res.text()}`);
|
|
5149
|
-
let fullText = "";
|
|
5150
|
-
let stopReason = "";
|
|
5151
|
-
const contentBlocks = [];
|
|
5152
|
-
const inputAccum = {};
|
|
5153
|
-
const reader = res.body.getReader();
|
|
5154
|
-
const decoder = new TextDecoder;
|
|
5155
|
-
let buf = "";
|
|
5156
|
-
while (true) {
|
|
5157
|
-
const { done, value } = await reader.read();
|
|
5158
|
-
if (done)
|
|
5159
|
-
break;
|
|
5160
|
-
buf += decoder.decode(value, { stream: true });
|
|
5161
|
-
const parts = buf.split(`
|
|
5162
|
-
|
|
5163
|
-
`);
|
|
5164
|
-
buf = parts.pop() ?? "";
|
|
5165
|
-
for (const part of parts) {
|
|
5166
|
-
let dataLine = "";
|
|
5167
|
-
for (const line of part.split(`
|
|
5168
|
-
`)) {
|
|
5169
|
-
if (line.startsWith("data: ")) {
|
|
5170
|
-
dataLine = line.slice(6);
|
|
5171
|
-
break;
|
|
5172
|
-
}
|
|
5173
|
-
}
|
|
5174
|
-
if (!dataLine || dataLine === "[DONE]")
|
|
5175
|
-
continue;
|
|
5176
|
-
let ev;
|
|
5177
|
-
try {
|
|
5178
|
-
ev = JSON.parse(dataLine);
|
|
5179
|
-
} catch {
|
|
5180
|
-
continue;
|
|
5181
|
-
}
|
|
5182
|
-
const type = ev.type;
|
|
5183
|
-
if (type === "content_block_start") {
|
|
5184
|
-
const idx = ev.index;
|
|
5185
|
-
const cb = ev.content_block;
|
|
5186
|
-
contentBlocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
|
|
5187
|
-
if (cb.type === "tool_use")
|
|
5188
|
-
inputAccum[idx] = "";
|
|
5189
|
-
} else if (type === "content_block_delta") {
|
|
5190
|
-
const idx = ev.index;
|
|
5191
|
-
const delta = ev.delta;
|
|
5192
|
-
if (delta.type === "text_delta" && delta.text) {
|
|
5193
|
-
fullText += delta.text;
|
|
5194
|
-
if (!contentBlocks[idx])
|
|
5195
|
-
contentBlocks[idx] = { type: "text", text: "" };
|
|
5196
|
-
contentBlocks[idx].text = (contentBlocks[idx].text ?? "") + delta.text;
|
|
5197
|
-
emit({ type: "text_delta", text: delta.text });
|
|
5198
|
-
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
5199
|
-
inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
|
|
5200
|
-
}
|
|
5201
|
-
} else if (type === "content_block_stop") {
|
|
5202
|
-
const idx = ev.index;
|
|
5203
|
-
if (contentBlocks[idx]?.type === "tool_use") {
|
|
5204
|
-
try {
|
|
5205
|
-
contentBlocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
|
|
5206
|
-
} catch {
|
|
5207
|
-
contentBlocks[idx].input = {};
|
|
5208
|
-
}
|
|
5209
|
-
}
|
|
5210
|
-
} else if (type === "message_delta") {
|
|
5211
|
-
const delta = ev.delta;
|
|
5212
|
-
if (delta.stop_reason)
|
|
5213
|
-
stopReason = delta.stop_reason;
|
|
5214
|
-
}
|
|
5215
|
-
}
|
|
5216
|
-
}
|
|
5217
|
-
if (stopReason !== "tool_use")
|
|
5218
|
-
return { content: fullText, toolCalls };
|
|
5219
|
-
if (totalTools >= MAX_TOTAL_TOOLS) {
|
|
5220
|
-
return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
|
|
5221
|
-
}
|
|
5222
|
-
msgs.push({ role: "assistant", content: contentBlocks });
|
|
5223
|
-
const toolResults = [];
|
|
5224
|
-
for (const block of contentBlocks) {
|
|
5225
|
-
if (block.type !== "tool_use" || !block.id || !block.name)
|
|
5226
|
-
continue;
|
|
5227
|
-
totalTools++;
|
|
5228
|
-
emit({ type: "tool_start", tool: block.name, input: block.input ?? {} });
|
|
5229
|
-
const result = await executeTool(block.name, block.input ?? {});
|
|
5230
|
-
emit({ type: "tool_done", tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
|
|
5231
|
-
toolCalls.push({ tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
|
|
5232
|
-
if (result.isError) {
|
|
5233
|
-
consecutiveErrors++;
|
|
5234
|
-
if (block.name === "execute_api_request" && block.input?.operationId) {
|
|
5235
|
-
const eid = String(block.input.operationId);
|
|
5236
|
-
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
5237
|
-
if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
|
|
5238
|
-
const stopContent = result.text + `
|
|
5239
|
-
|
|
5240
|
-
[AGENT LOOP STOPPED: endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times \u2014 stopping to avoid loop]`;
|
|
5241
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
|
|
5242
|
-
msgs.push({ role: "user", content: toolResults });
|
|
5243
|
-
return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
|
|
5244
|
-
}
|
|
5245
|
-
}
|
|
5246
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
5247
|
-
const stopMsg = `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`;
|
|
5248
|
-
const stopContent = result.text + `
|
|
5249
|
-
|
|
5250
|
-
[AGENT LOOP STOPPED: ${stopMsg}]`;
|
|
5251
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
|
|
5252
|
-
msgs.push({ role: "user", content: toolResults });
|
|
5253
|
-
return { content: stopMsg, toolCalls };
|
|
5254
|
-
}
|
|
5255
|
-
} else {
|
|
5256
|
-
consecutiveErrors = 0;
|
|
5257
|
-
}
|
|
5258
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result.text });
|
|
5259
|
-
}
|
|
5260
|
-
if (!toolResults.length)
|
|
5261
|
-
return { content: fullText, toolCalls };
|
|
5262
|
-
msgs.push({ role: "user", content: toolResults });
|
|
5263
|
-
}
|
|
5264
|
-
return { content: "(max iterations reached)", toolCalls };
|
|
5265
|
-
}
|
|
5266
|
-
async function openaiCompatibleLoop(base, apiKey, model, extraHeaders, system, initialMessages, emit) {
|
|
5267
|
-
const msgs = [{ role: "system", content: system }, ...initialMessages];
|
|
5268
|
-
const toolCalls = [];
|
|
5269
|
-
const authHeaders = {};
|
|
5270
|
-
if (apiKey)
|
|
5271
|
-
authHeaders["Authorization"] = `Bearer ${apiKey}`;
|
|
5272
|
-
let totalTools = 0;
|
|
5273
|
-
let consecutiveErrors = 0;
|
|
5274
|
-
const endpointErrors = {};
|
|
5275
|
-
for (let iter = 0;iter < 40; iter++) {
|
|
5276
|
-
const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
|
|
5277
|
-
method: "POST",
|
|
5278
|
-
headers: { "Content-Type": "application/json", ...authHeaders, ...extraHeaders },
|
|
5279
|
-
body: JSON.stringify({ model, messages: msgs, tools: OPENAI_TOOLS, tool_choice: "auto", stream: true })
|
|
5280
|
-
}, emit);
|
|
5281
|
-
if (!res.ok)
|
|
5282
|
-
throw new Error(await res.text());
|
|
5283
|
-
let fullContent = "";
|
|
5284
|
-
let finishReason = "";
|
|
5285
|
-
const tcAccum = {};
|
|
5286
|
-
const reader = res.body.getReader();
|
|
5287
|
-
const dec = new TextDecoder;
|
|
5288
|
-
let buf = "";
|
|
5289
|
-
outer:
|
|
5290
|
-
while (true) {
|
|
5291
|
-
const { done, value } = await reader.read();
|
|
5292
|
-
if (done)
|
|
5293
|
-
break;
|
|
5294
|
-
buf += dec.decode(value, { stream: true });
|
|
5295
|
-
const parts = buf.split(`
|
|
5296
|
-
|
|
5297
|
-
`);
|
|
5298
|
-
buf = parts.pop() ?? "";
|
|
5299
|
-
for (const part of parts) {
|
|
5300
|
-
let data = "";
|
|
5301
|
-
for (const line of part.split(`
|
|
5302
|
-
`)) {
|
|
5303
|
-
if (line.startsWith("data: ")) {
|
|
5304
|
-
data = line.slice(6);
|
|
5305
|
-
break;
|
|
5306
|
-
}
|
|
5307
|
-
}
|
|
5308
|
-
if (!data)
|
|
5309
|
-
continue;
|
|
5310
|
-
if (data === "[DONE]")
|
|
5311
|
-
break outer;
|
|
5312
|
-
let ev;
|
|
5313
|
-
try {
|
|
5314
|
-
ev = JSON.parse(data);
|
|
5315
|
-
} catch {
|
|
5316
|
-
continue;
|
|
5317
|
-
}
|
|
5318
|
-
if (ev.object === "error")
|
|
5319
|
-
throw new Error(JSON.stringify(ev));
|
|
5320
|
-
const choices = ev.choices;
|
|
5321
|
-
const choice = choices?.[0];
|
|
5322
|
-
if (!choice)
|
|
5323
|
-
continue;
|
|
5324
|
-
const fr = choice.finish_reason;
|
|
5325
|
-
if (fr)
|
|
5326
|
-
finishReason = fr;
|
|
5327
|
-
const delta = choice.delta;
|
|
5328
|
-
if (!delta)
|
|
5329
|
-
continue;
|
|
5330
|
-
if (typeof delta.content === "string" && delta.content) {
|
|
5331
|
-
fullContent += delta.content;
|
|
5332
|
-
emit({ type: "text_delta", text: delta.content });
|
|
5333
|
-
}
|
|
5334
|
-
const tcDeltas = delta.tool_calls;
|
|
5335
|
-
if (tcDeltas) {
|
|
5336
|
-
for (const tc of tcDeltas) {
|
|
5337
|
-
if (!tcAccum[tc.index])
|
|
5338
|
-
tcAccum[tc.index] = { id: "", name: "", args: "" };
|
|
5339
|
-
const entry = tcAccum[tc.index];
|
|
5340
|
-
if (tc.id)
|
|
5341
|
-
entry.id += tc.id;
|
|
5342
|
-
if (tc.function?.name)
|
|
5343
|
-
entry.name += tc.function.name;
|
|
5344
|
-
if (tc.function?.arguments)
|
|
5345
|
-
entry.args += tc.function.arguments;
|
|
5346
|
-
}
|
|
5347
|
-
}
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
|
-
if (finishReason !== "tool_calls")
|
|
5351
|
-
return { content: fullContent, toolCalls };
|
|
5352
|
-
if (totalTools >= MAX_TOTAL_TOOLS) {
|
|
5353
|
-
return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
|
|
5354
|
-
}
|
|
5355
|
-
const msgToolCalls = Object.values(tcAccum).map((tc) => ({
|
|
5356
|
-
id: tc.id,
|
|
5357
|
-
type: "function",
|
|
5358
|
-
function: { name: tc.name, arguments: tc.args }
|
|
5359
|
-
}));
|
|
5360
|
-
msgs.push({ role: "assistant", content: fullContent || null, tool_calls: msgToolCalls });
|
|
5361
|
-
for (const tc of Object.values(tcAccum)) {
|
|
5362
|
-
let args = {};
|
|
5363
|
-
try {
|
|
5364
|
-
args = JSON.parse(tc.args);
|
|
5365
|
-
} catch {}
|
|
5366
|
-
totalTools++;
|
|
5367
|
-
emit({ type: "tool_start", tool: tc.name, input: args });
|
|
5368
|
-
const result = await executeTool(tc.name, args);
|
|
5369
|
-
emit({ type: "tool_done", tool: tc.name, input: args, output: result.text, isError: result.isError });
|
|
5370
|
-
toolCalls.push({ tool: tc.name, input: args, output: result.text, isError: result.isError });
|
|
5371
|
-
msgs.push({ role: "tool", tool_call_id: tc.id, content: result.text });
|
|
5372
|
-
if (result.isError) {
|
|
5373
|
-
consecutiveErrors++;
|
|
5374
|
-
if (tc.name === "execute_api_request" && args.operationId) {
|
|
5375
|
-
const eid = String(args.operationId);
|
|
5376
|
-
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
5377
|
-
if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
|
|
5378
|
-
return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
5382
|
-
return { content: `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`, toolCalls };
|
|
5383
|
-
}
|
|
5384
|
-
} else {
|
|
5385
|
-
consecutiveErrors = 0;
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5388
|
-
}
|
|
5389
|
-
return { content: "(max iterations reached)", toolCalls };
|
|
5390
|
-
}
|
|
5391
5752
|
async function handleAiChat(req) {
|
|
5392
5753
|
let body;
|
|
5393
5754
|
try {
|
|
@@ -5398,43 +5759,58 @@ async function handleAiChat(req) {
|
|
|
5398
5759
|
const settingsRow = dbQueries.getSettings();
|
|
5399
5760
|
const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
|
|
5400
5761
|
const ai = settings.ai ?? {};
|
|
5762
|
+
const provider = ai.provider ?? "anthropic";
|
|
5763
|
+
const providerDefaults = PROVIDER_DEFAULTS[provider] ?? { model: "" };
|
|
5764
|
+
const requiresKey = provider !== "ollama" && provider !== "custom";
|
|
5765
|
+
if (requiresKey && !ai.apiKey) {
|
|
5766
|
+
return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
|
|
5767
|
+
}
|
|
5768
|
+
if (!hasState())
|
|
5769
|
+
return json({ error: "No spec loaded." }, 400);
|
|
5401
5770
|
const { spec, operations } = getState();
|
|
5402
5771
|
const preview = operations.slice(0, 40).map((op) => `- ${op.method.toUpperCase()} ${op.path}${op.summary ? `: ${op.summary}` : ""}`).join(`
|
|
5403
5772
|
`);
|
|
5404
5773
|
const activeAuth = dbQueries.getActiveProfile();
|
|
5405
|
-
const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile.
|
|
5774
|
+
const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile. Call list_auth_profiles, then set_active_auth or save_auth_token.";
|
|
5775
|
+
const memory = dbQueries.getMemory(20);
|
|
5776
|
+
const memorySection = memory.length ? `
|
|
5777
|
+
## Memory from previous sessions
|
|
5778
|
+
${memory.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 300)}${m.content.length > 300 ? "\u2026" : ""}`).join(`
|
|
5779
|
+
`)}
|
|
5780
|
+
` : "";
|
|
5406
5781
|
const system = `You are an AI assistant for the "${spec.title}" API (v${spec.version}). Base URL: ${spec.baseUrl}.
|
|
5407
5782
|
Total endpoints: ${operations.length}. Sample:
|
|
5408
5783
|
${preview}${operations.length > 40 ? `
|
|
5409
5784
|
... and ${operations.length - 40} more` : ""}
|
|
5410
5785
|
|
|
5411
5786
|
${authLine}
|
|
5787
|
+
${memorySection}
|
|
5788
|
+
Tools:
|
|
5789
|
+
- search_endpoints / get_endpoint_schema \u2014 explore API structure (results cached; never repeat the same query)
|
|
5790
|
+
- execute_api_request \u2014 call an endpoint
|
|
5791
|
+
- list_auth_profiles / set_active_auth / save_auth_token \u2014 manage credentials
|
|
5792
|
+
\u2022 save_auth_token supports token_type="basic" with username+password for HTTP Basic auth
|
|
5793
|
+
- fetch_url \u2014 external docs
|
|
5794
|
+
- dns_lookup \u2014 connectivity diagnostics
|
|
5795
|
+
- get_recent_logs \u2014 proxy traffic history
|
|
5796
|
+
- run_security_check \u2014 static security analysis
|
|
5412
5797
|
|
|
5413
|
-
|
|
5414
|
-
- search_endpoints / get_endpoint_schema: explore API structure
|
|
5415
|
-
- execute_api_request: call an endpoint
|
|
5416
|
-
- list_auth_profiles: list all saved auth profiles (name, type, active)
|
|
5417
|
-
- set_active_auth(name): switch to a saved profile before making requests
|
|
5418
|
-
- save_auth_token(name, token): IMMEDIATELY call this after a successful login that returns a token \u2014 saves the token as a named profile and activates it so subsequent requests are authenticated
|
|
5419
|
-
- fetch_url: fetch external docs
|
|
5420
|
-
- dns_lookup: DNS resolution / connectivity
|
|
5421
|
-
- get_recent_logs: recent request/response traffic
|
|
5422
|
-
- run_security_check: security analysis on an endpoint
|
|
5798
|
+
Auth workflow: 401/403 \u2192 list_auth_profiles \u2192 set_active_auth OR find login endpoint \u2192 save_auth_token \u2192 retry.
|
|
5423
5799
|
|
|
5424
|
-
|
|
5800
|
+
Rules:
|
|
5801
|
+
- Never repeat a search you already ran \u2014 results are cached.
|
|
5802
|
+
- Diagnose errors before retrying. Three failures on the same endpoint stops the agent.
|
|
5803
|
+
- Do not fire rapid successive API requests.
|
|
5425
5804
|
|
|
5426
|
-
|
|
5805
|
+
Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
|
|
5427
5806
|
|
|
5428
|
-
|
|
5807
|
+
---
|
|
5808
|
+
## Custom instructions
|
|
5809
|
+
${ai.customInstructions}` : ""}${body.extra_context ? `
|
|
5429
5810
|
|
|
5430
5811
|
---
|
|
5431
|
-
##
|
|
5812
|
+
## Context
|
|
5432
5813
|
${body.extra_context}` : ""}`;
|
|
5433
|
-
const provider = ai.provider ?? "anthropic";
|
|
5434
|
-
const requiresKey = provider !== "ollama" && provider !== "custom";
|
|
5435
|
-
if (requiresKey && !ai.apiKey) {
|
|
5436
|
-
return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
|
|
5437
|
-
}
|
|
5438
5814
|
const { readable, writable } = new TransformStream;
|
|
5439
5815
|
const writer = writable.getWriter();
|
|
5440
5816
|
const enc = new TextEncoder;
|
|
@@ -5444,62 +5820,39 @@ ${body.extra_context}` : ""}`;
|
|
|
5444
5820
|
`)).catch(() => {});
|
|
5445
5821
|
};
|
|
5446
5822
|
const msgs = body.messages;
|
|
5823
|
+
const toolCache = new Map;
|
|
5824
|
+
const abortCtrl = new AbortController;
|
|
5825
|
+
const lastUserMsg = [...msgs].reverse().find((m) => m.role === "user");
|
|
5826
|
+
const userMemoryContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : null;
|
|
5447
5827
|
(async () => {
|
|
5448
5828
|
try {
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
if (!ai.baseUrl) {
|
|
5469
|
-
emit({ type: "error", message: "Custom provider requires a Base URL." });
|
|
5470
|
-
await writer.close();
|
|
5471
|
-
return;
|
|
5472
|
-
}
|
|
5473
|
-
result = await openaiCompatibleLoop(ai.baseUrl.replace(/\/$/, ""), ai.apiKey, ai.model || "", {}, system, msgs, emit);
|
|
5474
|
-
} else if (provider === "ollama") {
|
|
5475
|
-
const base = (ai.baseUrl || "http://localhost:11434").replace(/\/$/, "");
|
|
5476
|
-
const res = await fetch(`${base}/api/chat`, {
|
|
5477
|
-
method: "POST",
|
|
5478
|
-
headers: { "Content-Type": "application/json" },
|
|
5479
|
-
body: JSON.stringify({ model: ai.model || "llama3", messages: [{ role: "system", content: system }, ...msgs], stream: false })
|
|
5480
|
-
});
|
|
5481
|
-
const d = await res.json();
|
|
5482
|
-
result = { content: d.message.content ?? "", toolCalls: [] };
|
|
5483
|
-
} else if (provider === "gemini") {
|
|
5484
|
-
const model = ai.model || "gemini-1.5-flash";
|
|
5485
|
-
const base = (ai.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
|
|
5486
|
-
const res = await fetch(`${base}/v1beta/models/${model}:generateContent?key=${ai.apiKey}`, {
|
|
5487
|
-
method: "POST",
|
|
5488
|
-
headers: { "Content-Type": "application/json" },
|
|
5489
|
-
body: JSON.stringify({
|
|
5490
|
-
systemInstruction: { parts: [{ text: system }] },
|
|
5491
|
-
contents: msgs.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] })),
|
|
5492
|
-
generationConfig: { maxOutputTokens: 4096 }
|
|
5493
|
-
})
|
|
5494
|
-
});
|
|
5495
|
-
const d = await res.json();
|
|
5496
|
-
result = { content: d.candidates[0]?.content.parts[0]?.text ?? "", toolCalls: [] };
|
|
5497
|
-
} else {
|
|
5498
|
-
emit({ type: "error", message: `Unknown provider: ${provider}` });
|
|
5499
|
-
await writer.close();
|
|
5500
|
-
return;
|
|
5829
|
+
const result = await runAgentLoop({
|
|
5830
|
+
provider,
|
|
5831
|
+
apiKey: ai.apiKey,
|
|
5832
|
+
model: ai.model || providerDefaults.model,
|
|
5833
|
+
baseUrl: ai.baseUrl || providerDefaults.baseUrl,
|
|
5834
|
+
maxTokens: ai.maxTokens ?? 4096,
|
|
5835
|
+
stepTimeoutMs: ai.stepTimeoutMs ?? 60000,
|
|
5836
|
+
temperature: ai.temperature,
|
|
5837
|
+
topK: ai.topK && ai.topK > 0 ? ai.topK : undefined,
|
|
5838
|
+
parallelTools: true,
|
|
5839
|
+
enablePromptCache: true
|
|
5840
|
+
}, system, msgs, TOOL_SCHEMAS, (name, args) => executeTool(name, args, toolCache), emit, abortCtrl.signal, toolCache);
|
|
5841
|
+
if (result.content && result.stopReason !== "max_iterations") {
|
|
5842
|
+
try {
|
|
5843
|
+
if (userMemoryContent)
|
|
5844
|
+
dbQueries.saveMemory("user", userMemoryContent.slice(0, 1000));
|
|
5845
|
+
dbQueries.saveMemory("assistant", result.content.slice(0, 1000));
|
|
5846
|
+
dbQueries.trimMemory(40);
|
|
5847
|
+
} catch {}
|
|
5501
5848
|
}
|
|
5502
|
-
emit({
|
|
5849
|
+
emit({
|
|
5850
|
+
type: "done",
|
|
5851
|
+
content: result.content,
|
|
5852
|
+
toolCalls: result.toolCalls,
|
|
5853
|
+
stopReason: result.stopReason,
|
|
5854
|
+
tokens: result.tokens
|
|
5855
|
+
});
|
|
5503
5856
|
} catch (e) {
|
|
5504
5857
|
emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
5505
5858
|
} finally {
|
|
@@ -5509,11 +5862,7 @@ ${body.extra_context}` : ""}`;
|
|
|
5509
5862
|
}
|
|
5510
5863
|
})();
|
|
5511
5864
|
return new Response(readable, {
|
|
5512
|
-
headers: {
|
|
5513
|
-
"Content-Type": "text/event-stream",
|
|
5514
|
-
"Cache-Control": "no-cache",
|
|
5515
|
-
...CORS4
|
|
5516
|
-
}
|
|
5865
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
|
|
5517
5866
|
});
|
|
5518
5867
|
}
|
|
5519
5868
|
function handleGetProfiles() {
|
|
@@ -6120,7 +6469,7 @@ function handleDeleteCaptureBin(path) {
|
|
|
6120
6469
|
dbQueries.deleteCaptureBin(id);
|
|
6121
6470
|
return json({ ok: true });
|
|
6122
6471
|
}
|
|
6123
|
-
var CORS4, TOOL_DEFS,
|
|
6472
|
+
var CORS4, TOOL_DEFS, _lastApiCallMs = 0, MIN_API_CALL_INTERVAL_MS = 400, TOOL_SCHEMAS, PROVIDER_DEFAULTS;
|
|
6124
6473
|
var init_routes = __esm(() => {
|
|
6125
6474
|
init_db();
|
|
6126
6475
|
init_engine();
|
|
@@ -6130,6 +6479,7 @@ var init_routes = __esm(() => {
|
|
|
6130
6479
|
init_config();
|
|
6131
6480
|
init_version();
|
|
6132
6481
|
init_engine2();
|
|
6482
|
+
init_harness();
|
|
6133
6483
|
CORS4 = {
|
|
6134
6484
|
"Access-Control-Allow-Origin": "*",
|
|
6135
6485
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
@@ -6198,25 +6548,34 @@ var init_routes = __esm(() => {
|
|
|
6198
6548
|
required: ["name"]
|
|
6199
6549
|
},
|
|
6200
6550
|
save_auth_token: {
|
|
6201
|
-
description: "Save a bearer token
|
|
6551
|
+
description: "Save a bearer token, API key, or basic auth credentials as a named auth profile and immediately activate it. Call this right after a successful login endpoint returns a token so all subsequent API requests are authenticated.",
|
|
6202
6552
|
params: {
|
|
6203
6553
|
name: { type: "string", description: 'Profile name, e.g. "user session" or the username' },
|
|
6204
|
-
token: { type: "string", description: "The bearer token or API key value
|
|
6205
|
-
token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query"], description: "Token type (default: bearer)" },
|
|
6206
|
-
header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" }
|
|
6554
|
+
token: { type: "string", description: "The bearer token or API key value (omit for basic auth)" },
|
|
6555
|
+
token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query", "basic"], description: "Token type (default: bearer)" },
|
|
6556
|
+
header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" },
|
|
6557
|
+
username: { type: "string", description: "Username for basic auth" },
|
|
6558
|
+
password: { type: "string", description: "Password for basic auth" }
|
|
6207
6559
|
},
|
|
6208
|
-
required: ["name"
|
|
6560
|
+
required: ["name"]
|
|
6209
6561
|
}
|
|
6210
6562
|
};
|
|
6211
|
-
|
|
6563
|
+
TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
6212
6564
|
name,
|
|
6213
6565
|
description: def.description,
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
OPENAI_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
6217
|
-
type: "function",
|
|
6218
|
-
function: { name, description: def.description, parameters: { type: "object", properties: def.params, required: def.required } }
|
|
6566
|
+
params: def.params,
|
|
6567
|
+
required: def.required
|
|
6219
6568
|
}));
|
|
6569
|
+
PROVIDER_DEFAULTS = {
|
|
6570
|
+
anthropic: { model: "claude-haiku-4-5-20251001" },
|
|
6571
|
+
openai: { model: "gpt-4o-mini", baseUrl: "https://api.openai.com" },
|
|
6572
|
+
mistral: { model: "mistral-small-latest", baseUrl: "https://api.mistral.ai" },
|
|
6573
|
+
groq: { model: "llama-3.1-70b-versatile", baseUrl: "https://api.groq.com/openai" },
|
|
6574
|
+
"github-copilot": { model: "gpt-4o", baseUrl: "https://api.githubcopilot.com" },
|
|
6575
|
+
ollama: { model: "llama3", baseUrl: "http://localhost:11434" },
|
|
6576
|
+
gemini: { model: "gemini-1.5-flash" },
|
|
6577
|
+
custom: { model: "" }
|
|
6578
|
+
};
|
|
6220
6579
|
});
|
|
6221
6580
|
|
|
6222
6581
|
// src/daemon.ts
|