wasper-cli 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/dist/cli.js +742 -383
  2. package/dist/index.js +742 -383
  3. 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.2.0",
3610
+ version: "0.3.0",
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
- if (!matches.length)
4879
- return { text: `No endpoints found matching "${args.query}". Total: ${operations.length}.`, isError: false };
4880
- return { text: JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2), isError: false };
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
- return {
4887
- text: JSON.stringify({
4888
- operationId: op.operationId,
4889
- method: op.method.toUpperCase(),
4890
- path: op.path,
4891
- summary: op.summary ?? null,
4892
- description: op.description ?? null,
4893
- tags: op.tags,
4894
- parameters: op.parameters,
4895
- requestBody: op.requestBody ?? null,
4896
- responses: op.responses
4897
- }, null, 2),
4898
- isError: false
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 text = await res.text();
5489
+ const responseText = await res.text();
4936
5490
  const latency = Date.now() - start;
4937
- let pretty = text;
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(text), null, 2);
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
- return { text: `Network error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
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. If the API requires auth, call list_auth_profiles first, then set_active_auth, or login and call save_auth_token.";
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
- Tools available:
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
- Authentication workflow: if requests return 401/403, call list_auth_profiles first. If a profile exists, call set_active_auth. If none, find and call the login endpoint, extract the token from the response, then call save_auth_token immediately. After saving, retry the original request.
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
- IMPORTANT: if an endpoint returns an error, diagnose it (check the schema, check auth) and fix the root cause before retrying. If the same endpoint fails 3 times the agent will be forcibly stopped. Do not retry without changing something.
5805
+ Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
5427
5806
 
5428
- Be concise and practical. Format code and JSON in code blocks.${body.extra_context ? `
5807
+ ---
5808
+ ## Custom instructions
5809
+ ${ai.customInstructions}` : ""}${body.extra_context ? `
5429
5810
 
5430
5811
  ---
5431
- ## Current context
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
- let result;
5450
- if (provider === "anthropic") {
5451
- result = await anthropicAgentLoop(ai.apiKey, ai.model || "claude-haiku-4-5-20251001", system, msgs, emit);
5452
- } else if (provider === "openai") {
5453
- const base = (ai.baseUrl || "https://api.openai.com").replace(/\/$/, "");
5454
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o-mini", {}, system, msgs, emit);
5455
- } else if (provider === "mistral") {
5456
- const base = (ai.baseUrl || "https://api.mistral.ai").replace(/\/$/, "");
5457
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "mistral-small-latest", {}, system, msgs, emit);
5458
- } else if (provider === "github-copilot") {
5459
- const base = (ai.baseUrl || "https://api.githubcopilot.com").replace(/\/$/, "");
5460
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o", {
5461
- "Copilot-Integration-Id": "vscode-chat",
5462
- "Editor-Version": "vscode/1.85.0"
5463
- }, system, msgs, emit);
5464
- } else if (provider === "groq") {
5465
- const base = (ai.baseUrl || "https://api.groq.com/openai").replace(/\/$/, "");
5466
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "llama-3.1-70b-versatile", {}, system, msgs, emit);
5467
- } else if (provider === "custom") {
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({ type: "done", content: result.content, toolCalls: result.toolCalls });
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, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
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 or API key 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.",
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 to save" },
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", "token"]
6560
+ required: ["name"]
6209
6561
  }
6210
6562
  };
6211
- ANTHROPIC_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6563
+ TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6212
6564
  name,
6213
6565
  description: def.description,
6214
- input_schema: { type: "object", properties: def.params, required: def.required }
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