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/cli.js CHANGED
@@ -21,7 +21,7 @@ var package_default;
21
21
  var init_package = __esm(() => {
22
22
  package_default = {
23
23
  name: "wasper-cli",
24
- version: "0.2.0",
24
+ version: "0.3.0",
25
25
  description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
26
26
  type: "module",
27
27
  homepage: "https://wasper.site",
@@ -680,6 +680,14 @@ var SCHEMA = `
680
680
  name TEXT NOT NULL DEFAULT '',
681
681
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
682
682
  );
683
+
684
+ CREATE TABLE IF NOT EXISTS chat_memory (
685
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
686
+ role TEXT NOT NULL,
687
+ content TEXT NOT NULL,
688
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
689
+ );
690
+ CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
683
691
  `;
684
692
 
685
693
  // src/db/index.ts
@@ -859,6 +867,19 @@ var init_db = __esm(() => {
859
867
  getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
860
868
  setSetting: (key, value) => {
861
869
  db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
870
+ },
871
+ saveMemory: (role, content) => {
872
+ db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
873
+ },
874
+ getMemory: (limit = 20) => {
875
+ const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
876
+ return rows.reverse();
877
+ },
878
+ clearMemory: () => {
879
+ db.query("DELETE FROM chat_memory").run();
880
+ },
881
+ trimMemory: (keepLast = 40) => {
882
+ db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
862
883
  }
863
884
  };
864
885
  });
@@ -5100,6 +5121,517 @@ var init_engine2 = __esm(() => {
5100
5121
  init_state();
5101
5122
  });
5102
5123
 
5124
+ // src/agent/harness.ts
5125
+ function mergeSignals(a, b) {
5126
+ if (!a && !b)
5127
+ return new AbortController().signal;
5128
+ if (!a)
5129
+ return b;
5130
+ if (!b)
5131
+ return a;
5132
+ const ctrl = new AbortController;
5133
+ const abort = () => ctrl.abort();
5134
+ a.addEventListener("abort", abort, { once: true });
5135
+ b.addEventListener("abort", abort, { once: true });
5136
+ return ctrl.signal;
5137
+ }
5138
+ async function fetchWithRetry(url, opts, emit, signal, maxRetries = 4) {
5139
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
5140
+ const stepSignal = signal;
5141
+ let res;
5142
+ try {
5143
+ res = await fetch(url, { ...opts, signal: stepSignal });
5144
+ } catch (e) {
5145
+ const msg = e instanceof Error ? e.message : String(e);
5146
+ if (signal?.aborted)
5147
+ throw e;
5148
+ if (attempt === maxRetries)
5149
+ throw e;
5150
+ const isNetwork = msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("network") || msg.includes("fetch");
5151
+ if (!isNetwork)
5152
+ throw e;
5153
+ const delay2 = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 15000);
5154
+ emit({ type: "info", message: `Network error, retrying in ${Math.round(delay2 / 1000)}s\u2026` });
5155
+ await new Promise((r) => setTimeout(r, delay2));
5156
+ continue;
5157
+ }
5158
+ if (!RETRYABLE_STATUS.has(res.status) || attempt === maxRetries)
5159
+ return res;
5160
+ const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
5161
+ const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 30000);
5162
+ const label = res.status === 429 ? "Rate limited" : `Server error ${res.status}`;
5163
+ emit({ type: "info", message: `${label} \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
5164
+ await new Promise((r) => setTimeout(r, delay));
5165
+ }
5166
+ return fetch(url, opts);
5167
+ }
5168
+ function trimContext(messages) {
5169
+ if (JSON.stringify(messages).length <= MAX_CONTEXT_CHARS)
5170
+ return { messages, trimmed: false };
5171
+ const result = [...messages];
5172
+ while (JSON.stringify(result).length > MAX_CONTEXT_CHARS && result.length > 2) {
5173
+ let removed = false;
5174
+ const toolIdx = result.findIndex((m) => m.role === "tool");
5175
+ if (toolIdx !== -1) {
5176
+ result.splice(toolIdx, 1);
5177
+ if (toolIdx > 0) {
5178
+ const prev = result[toolIdx - 1];
5179
+ if (prev?.role === "assistant") {
5180
+ const tc = prev.tool_calls;
5181
+ if (Array.isArray(tc) && tc.length)
5182
+ result.splice(toolIdx - 1, 1);
5183
+ }
5184
+ }
5185
+ removed = true;
5186
+ }
5187
+ if (!removed) {
5188
+ const anthropicIdx = result.findIndex((m) => {
5189
+ if (m.role !== "user")
5190
+ return false;
5191
+ const c = m.content;
5192
+ return Array.isArray(c) && c.some((b) => b.type === "tool_result");
5193
+ });
5194
+ if (anthropicIdx !== -1) {
5195
+ result.splice(anthropicIdx, 1);
5196
+ if (anthropicIdx > 0 && result[anthropicIdx - 1]?.role === "assistant") {
5197
+ result.splice(anthropicIdx - 1, 1);
5198
+ }
5199
+ removed = true;
5200
+ }
5201
+ }
5202
+ if (!removed)
5203
+ break;
5204
+ }
5205
+ return { messages: result, trimmed: true };
5206
+ }
5207
+ async function* readSSE(body) {
5208
+ const reader = body.getReader();
5209
+ const decoder = new TextDecoder;
5210
+ let buf = "";
5211
+ try {
5212
+ while (true) {
5213
+ const { done, value } = await reader.read();
5214
+ if (done)
5215
+ break;
5216
+ buf += decoder.decode(value, { stream: true });
5217
+ const parts = buf.split(`
5218
+
5219
+ `);
5220
+ buf = parts.pop() ?? "";
5221
+ for (const part of parts) {
5222
+ let data = "";
5223
+ for (const line of part.split(`
5224
+ `)) {
5225
+ if (line.startsWith("data: ")) {
5226
+ data = line.slice(6);
5227
+ break;
5228
+ }
5229
+ }
5230
+ if (!data || data === "[DONE]")
5231
+ continue;
5232
+ try {
5233
+ yield JSON.parse(data);
5234
+ } catch {}
5235
+ }
5236
+ }
5237
+ } finally {
5238
+ reader.releaseLock();
5239
+ }
5240
+ }
5241
+ function buildAnthropicTools(schemas) {
5242
+ return schemas.map((s) => ({
5243
+ name: s.name,
5244
+ description: s.description,
5245
+ input_schema: { type: "object", properties: s.params, required: s.required }
5246
+ }));
5247
+ }
5248
+ async function streamAnthropic(cfg, system, messages, tools, emit, signal) {
5249
+ const base = (cfg.baseUrl || "https://api.anthropic.com").replace(/\/$/, "");
5250
+ const systemContent = cfg.enablePromptCache ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : system;
5251
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5252
+ const res = await fetchWithRetry(`${base}/v1/messages`, {
5253
+ method: "POST",
5254
+ headers: {
5255
+ "Content-Type": "application/json",
5256
+ "x-api-key": cfg.apiKey,
5257
+ "anthropic-version": "2023-06-01",
5258
+ ...cfg.enablePromptCache ? { "anthropic-beta": "prompt-caching-1-0" } : {},
5259
+ ...cfg.extraHeaders
5260
+ },
5261
+ body: JSON.stringify({
5262
+ model: cfg.model,
5263
+ max_tokens: cfg.maxTokens,
5264
+ temperature: cfg.temperature,
5265
+ ...cfg.topK > 0 ? { top_k: cfg.topK } : {},
5266
+ system: systemContent,
5267
+ messages,
5268
+ tools,
5269
+ stream: true
5270
+ })
5271
+ }, emit, stepSignal);
5272
+ if (!res.ok) {
5273
+ const body = await res.text();
5274
+ const retryable = RETRYABLE_STATUS.has(res.status);
5275
+ throw Object.assign(new Error(`Anthropic ${res.status}: ${body}`), { retryable });
5276
+ }
5277
+ const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5278
+ const blocks = [];
5279
+ const inputAccum = {};
5280
+ for await (const ev of readSSE(res.body)) {
5281
+ if (signal.aborted)
5282
+ break;
5283
+ const evType = ev.type;
5284
+ if (evType === "message_start") {
5285
+ const usage = ev.message?.usage;
5286
+ if (usage) {
5287
+ result.usage.input = usage.input_tokens ?? 0;
5288
+ result.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
5289
+ result.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
5290
+ }
5291
+ } else if (evType === "content_block_start") {
5292
+ const idx = ev.index;
5293
+ const cb = ev.content_block;
5294
+ blocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
5295
+ if (cb.type === "tool_use")
5296
+ inputAccum[idx] = "";
5297
+ } else if (evType === "content_block_delta") {
5298
+ const idx = ev.index;
5299
+ const delta = ev.delta;
5300
+ if (delta.type === "text_delta" && delta.text) {
5301
+ result.text += delta.text;
5302
+ if (!blocks[idx])
5303
+ blocks[idx] = { type: "text" };
5304
+ blocks[idx].text = (blocks[idx].text ?? "") + delta.text;
5305
+ emit({ type: "text_delta", text: delta.text });
5306
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
5307
+ result.thinking += delta.thinking;
5308
+ emit({ type: "thinking", text: delta.thinking });
5309
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
5310
+ inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
5311
+ }
5312
+ } else if (evType === "content_block_stop") {
5313
+ const idx = ev.index;
5314
+ if (blocks[idx]?.type === "tool_use") {
5315
+ try {
5316
+ blocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
5317
+ } catch {
5318
+ blocks[idx].input = {};
5319
+ }
5320
+ }
5321
+ } else if (evType === "message_delta") {
5322
+ const delta = ev.delta;
5323
+ const usage = ev.usage;
5324
+ if (delta.stop_reason)
5325
+ result.stopReason = delta.stop_reason;
5326
+ if (usage?.output_tokens)
5327
+ result.usage.output = usage.output_tokens;
5328
+ }
5329
+ }
5330
+ for (const b of blocks) {
5331
+ if (b.type === "tool_use" && b.id && b.name) {
5332
+ result.toolUses.push({ id: b.id, name: b.name, input: b.input ?? {} });
5333
+ }
5334
+ }
5335
+ result._anthropicBlocks = blocks;
5336
+ return result;
5337
+ }
5338
+ function buildOpenAITools(schemas) {
5339
+ return schemas.map((s) => ({
5340
+ type: "function",
5341
+ function: { name: s.name, description: s.description, parameters: { type: "object", properties: s.params, required: s.required } }
5342
+ }));
5343
+ }
5344
+ async function streamOpenAI(cfg, system, messages, tools, emit, signal) {
5345
+ const providerBases = {
5346
+ openai: "https://api.openai.com",
5347
+ mistral: "https://api.mistral.ai",
5348
+ groq: "https://api.groq.com/openai",
5349
+ "github-copilot": "https://api.githubcopilot.com"
5350
+ };
5351
+ const base = (cfg.baseUrl || providerBases[cfg.provider] || "https://api.openai.com").replace(/\/$/, "");
5352
+ const providerHeaders = cfg.provider === "github-copilot" ? { "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "vscode/1.85.0" } : {};
5353
+ const authHeaders = cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {};
5354
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5355
+ const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
5356
+ method: "POST",
5357
+ headers: { "Content-Type": "application/json", ...authHeaders, ...providerHeaders, ...cfg.extraHeaders },
5358
+ body: JSON.stringify({
5359
+ model: cfg.model,
5360
+ max_tokens: cfg.maxTokens,
5361
+ temperature: cfg.temperature,
5362
+ messages: [{ role: "system", content: system }, ...messages],
5363
+ tools,
5364
+ tool_choice: "auto",
5365
+ stream: true,
5366
+ stream_options: { include_usage: true }
5367
+ })
5368
+ }, emit, stepSignal);
5369
+ if (!res.ok) {
5370
+ const body = await res.text();
5371
+ const retryable = RETRYABLE_STATUS.has(res.status);
5372
+ throw Object.assign(new Error(body), { retryable });
5373
+ }
5374
+ const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5375
+ const tcAccum = {};
5376
+ for await (const ev of readSSE(res.body)) {
5377
+ if (signal.aborted)
5378
+ break;
5379
+ if (ev.object === "error") {
5380
+ const retryable = ev.code === "1300" || ev.raw_status_code === 429 || ev.raw_status_code === 503;
5381
+ throw Object.assign(new Error(JSON.stringify(ev)), { retryable });
5382
+ }
5383
+ if (ev.usage) {
5384
+ const u = ev.usage;
5385
+ result.usage.input = u.prompt_tokens ?? 0;
5386
+ result.usage.output = u.completion_tokens ?? 0;
5387
+ }
5388
+ const choices = ev.choices;
5389
+ const choice = choices?.[0];
5390
+ if (!choice)
5391
+ continue;
5392
+ const fr = choice.finish_reason;
5393
+ if (fr)
5394
+ result.stopReason = fr;
5395
+ const delta = choice.delta;
5396
+ if (!delta)
5397
+ continue;
5398
+ if (typeof delta.content === "string" && delta.content) {
5399
+ result.text += delta.content;
5400
+ emit({ type: "text_delta", text: delta.content });
5401
+ }
5402
+ const tcDeltas = delta.tool_calls;
5403
+ if (tcDeltas) {
5404
+ for (const tc of tcDeltas) {
5405
+ if (!tcAccum[tc.index])
5406
+ tcAccum[tc.index] = { id: "", name: "", args: "" };
5407
+ const e = tcAccum[tc.index];
5408
+ if (tc.id)
5409
+ e.id += tc.id;
5410
+ if (tc.function?.name)
5411
+ e.name += tc.function.name;
5412
+ if (tc.function?.arguments)
5413
+ e.args += tc.function.arguments;
5414
+ }
5415
+ }
5416
+ }
5417
+ for (const tc of Object.values(tcAccum)) {
5418
+ let input = {};
5419
+ try {
5420
+ input = JSON.parse(tc.args);
5421
+ } catch {}
5422
+ result.toolUses.push({ id: tc.id, name: tc.name, input });
5423
+ }
5424
+ result._openaiTcAccum = tcAccum;
5425
+ return result;
5426
+ }
5427
+ async function callOllama(cfg, system, messages, emit, signal) {
5428
+ const base = (cfg.baseUrl || "http://localhost:11434").replace(/\/$/, "");
5429
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5430
+ const res = await fetchWithRetry(`${base}/api/chat`, {
5431
+ method: "POST",
5432
+ headers: { "Content-Type": "application/json" },
5433
+ body: JSON.stringify({ model: cfg.model, messages: [{ role: "system", content: system }, ...messages], stream: false })
5434
+ }, emit, stepSignal);
5435
+ if (!res.ok)
5436
+ throw new Error(`Ollama ${res.status}: ${await res.text()}`);
5437
+ const d = await res.json();
5438
+ const text = d.message?.content ?? "";
5439
+ emit({ type: "text_delta", text });
5440
+ return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5441
+ }
5442
+ async function callGemini(cfg, system, messages, emit, signal) {
5443
+ const base = (cfg.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
5444
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5445
+ const res = await fetchWithRetry(`${base}/v1beta/models/${cfg.model}:generateContent?key=${cfg.apiKey}`, {
5446
+ method: "POST",
5447
+ headers: { "Content-Type": "application/json" },
5448
+ body: JSON.stringify({
5449
+ systemInstruction: { parts: [{ text: system }] },
5450
+ contents: messages.map((m) => ({
5451
+ role: m.role === "assistant" ? "model" : "user",
5452
+ parts: [{ text: m.content }]
5453
+ })),
5454
+ generationConfig: { maxOutputTokens: cfg.maxTokens }
5455
+ })
5456
+ }, emit, stepSignal);
5457
+ if (!res.ok)
5458
+ throw new Error(`Gemini ${res.status}: ${await res.text()}`);
5459
+ const d = await res.json();
5460
+ const text = d.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
5461
+ emit({ type: "text_delta", text });
5462
+ return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5463
+ }
5464
+ async function runAgentLoop(config2, system, initialMessages, toolSchemas, executeTool, emit, signal = new AbortController().signal, toolCache = new Map) {
5465
+ const cfg = { ...DEFAULTS, ...config2 };
5466
+ const isAnthropic = cfg.provider === "anthropic";
5467
+ const isOllama = cfg.provider === "ollama";
5468
+ const isGemini = cfg.provider === "gemini";
5469
+ const anthropicTools = buildAnthropicTools(toolSchemas);
5470
+ const openaiTools = buildOpenAITools(toolSchemas);
5471
+ const messages = [...initialMessages];
5472
+ const allToolCalls = [];
5473
+ const totalTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
5474
+ let totalToolsUsed = 0;
5475
+ let consecutiveErrors = 0;
5476
+ const endpointErrors = {};
5477
+ for (let iter = 0;iter < cfg.maxIterations; iter++) {
5478
+ if (signal.aborted) {
5479
+ return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
5480
+ }
5481
+ const { messages: trimmed, trimmed: didTrim } = trimContext(messages);
5482
+ if (didTrim) {
5483
+ emit({ type: "info", message: "Context trimmed to fit within limits." });
5484
+ messages.splice(0, messages.length, ...trimmed);
5485
+ }
5486
+ let turn;
5487
+ try {
5488
+ if (isAnthropic) {
5489
+ turn = await streamAnthropic(cfg, system, messages, anthropicTools, emit, signal);
5490
+ } else if (isOllama) {
5491
+ turn = await callOllama(cfg, system, messages, emit, signal);
5492
+ } else if (isGemini) {
5493
+ turn = await callGemini(cfg, system, messages, emit, signal);
5494
+ } else {
5495
+ turn = await streamOpenAI(cfg, system, messages, openaiTools, emit, signal);
5496
+ }
5497
+ } catch (e) {
5498
+ if (signal.aborted) {
5499
+ return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
5500
+ }
5501
+ const msg = e instanceof Error ? e.message : String(e);
5502
+ const retryable = e.retryable ?? false;
5503
+ emit({ type: "error", message: msg, retryable });
5504
+ throw e;
5505
+ }
5506
+ totalTokens.input += turn.usage.input;
5507
+ totalTokens.output += turn.usage.output;
5508
+ totalTokens.cacheRead += turn.usage.cacheRead;
5509
+ totalTokens.cacheWrite += turn.usage.cacheWrite;
5510
+ if (turn.usage.input || turn.usage.output) {
5511
+ emit({ type: "token_usage", ...turn.usage });
5512
+ }
5513
+ const wantsTools = isAnthropic ? turn.stopReason === "tool_use" : turn.stopReason === "tool_calls";
5514
+ if (!wantsTools || turn.toolUses.length === 0) {
5515
+ return { content: turn.text, toolCalls: allToolCalls, stopReason: "end_turn", tokens: totalTokens };
5516
+ }
5517
+ if (totalToolsUsed >= cfg.maxTotalTools) {
5518
+ return {
5519
+ content: `Agent stopped: reached ${cfg.maxTotalTools} tool calls. Break your request into smaller steps.`,
5520
+ toolCalls: allToolCalls,
5521
+ stopReason: "max_tools",
5522
+ tokens: totalTokens
5523
+ };
5524
+ }
5525
+ const dedupeKey = (name, input) => `${name}:${JSON.stringify(input)}`;
5526
+ const turnResults = [];
5527
+ const executeOne = async (use) => {
5528
+ totalToolsUsed++;
5529
+ const key = dedupeKey(use.name, use.input);
5530
+ const cachedResult = toolCache.get(key);
5531
+ const isCached = !!cachedResult;
5532
+ emit({ type: "tool_start", id: use.id, tool: use.name, input: use.input, cached: isCached });
5533
+ let result;
5534
+ let ms = 0;
5535
+ if (isCached) {
5536
+ result = cachedResult;
5537
+ } else {
5538
+ const t0 = Date.now();
5539
+ result = await executeTool(use.name, use.input);
5540
+ ms = Date.now() - t0;
5541
+ if (!result.isError && use.name !== "execute_api_request" && use.name !== "fetch_url") {
5542
+ toolCache.set(key, result);
5543
+ }
5544
+ }
5545
+ emit({ type: "tool_done", id: use.id, tool: use.name, output: result.text, isError: result.isError, ms, cached: isCached });
5546
+ return { id: use.id, name: use.name, text: result.text, isError: result.isError, ms, cached: isCached };
5547
+ };
5548
+ const toolUses = turn.toolUses;
5549
+ if (cfg.parallelTools && toolUses.length > 1) {
5550
+ const pure = toolUses.filter((u) => u.name !== "execute_api_request" && u.name !== "fetch_url");
5551
+ const sideEffect = toolUses.filter((u) => u.name === "execute_api_request" || u.name === "fetch_url");
5552
+ const pureResults = await Promise.all(pure.map((u) => executeOne(u)));
5553
+ const sideEffectResults = [];
5554
+ for (const u of sideEffect)
5555
+ sideEffectResults.push(await executeOne(u));
5556
+ const resultMap = new Map([...pureResults, ...sideEffectResults].map((r) => [r.id, r]));
5557
+ for (const u of toolUses) {
5558
+ const r = resultMap.get(u.id);
5559
+ if (r)
5560
+ turnResults.push(r);
5561
+ }
5562
+ } else {
5563
+ for (const u of toolUses)
5564
+ turnResults.push(await executeOne(u));
5565
+ }
5566
+ for (const r of turnResults) {
5567
+ 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 });
5568
+ if (r.isError) {
5569
+ consecutiveErrors++;
5570
+ if (r.name === "execute_api_request") {
5571
+ const eid = String(turn.toolUses.find((u) => u.id === r.id)?.input?.operationId ?? r.id);
5572
+ endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5573
+ if (endpointErrors[eid] >= cfg.maxEndpointErrors) {
5574
+ return {
5575
+ content: `Endpoint "${eid}" failed ${cfg.maxEndpointErrors} times. Last error: ${r.text}`,
5576
+ toolCalls: allToolCalls,
5577
+ stopReason: "max_endpoint_errors",
5578
+ tokens: totalTokens
5579
+ };
5580
+ }
5581
+ }
5582
+ if (consecutiveErrors >= cfg.maxConsecutiveErrors) {
5583
+ return {
5584
+ content: `Stopped after ${cfg.maxConsecutiveErrors} consecutive errors. Last: ${r.text}`,
5585
+ toolCalls: allToolCalls,
5586
+ stopReason: "max_errors",
5587
+ tokens: totalTokens
5588
+ };
5589
+ }
5590
+ } else {
5591
+ consecutiveErrors = 0;
5592
+ }
5593
+ }
5594
+ if (isAnthropic) {
5595
+ const blocks = turn._anthropicBlocks ?? [];
5596
+ messages.push({ role: "assistant", content: blocks });
5597
+ messages.push({
5598
+ role: "user",
5599
+ content: turnResults.map((r) => ({ type: "tool_result", tool_use_id: r.id, content: r.text }))
5600
+ });
5601
+ } else {
5602
+ const tc = turn._openaiTcAccum ?? {};
5603
+ messages.push({
5604
+ role: "assistant",
5605
+ content: turn.text || null,
5606
+ tool_calls: Object.values(tc).map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: t.args } }))
5607
+ });
5608
+ for (const r of turnResults) {
5609
+ messages.push({ role: "tool", tool_call_id: r.id, content: r.text });
5610
+ }
5611
+ }
5612
+ }
5613
+ return { content: "(max iterations reached)", toolCalls: allToolCalls, stopReason: "max_iterations", tokens: totalTokens };
5614
+ }
5615
+ var RETRYABLE_STATUS, MAX_CONTEXT_CHARS = 300000, DEFAULTS;
5616
+ var init_harness = __esm(() => {
5617
+ RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
5618
+ DEFAULTS = {
5619
+ apiKey: "",
5620
+ baseUrl: "",
5621
+ extraHeaders: {},
5622
+ maxTokens: 4096,
5623
+ temperature: 1,
5624
+ topK: 0,
5625
+ maxIterations: 40,
5626
+ maxTotalTools: 40,
5627
+ maxConsecutiveErrors: 5,
5628
+ maxEndpointErrors: 3,
5629
+ stepTimeoutMs: 60000,
5630
+ parallelTools: true,
5631
+ enablePromptCache: true
5632
+ };
5633
+ });
5634
+
5103
5635
  // src/api/routes.ts
5104
5636
  import dns from "dns/promises";
5105
5637
  function json(data, status = 200) {
@@ -5163,6 +5695,12 @@ async function apiRouter(req) {
5163
5695
  return handleDeleteRule(path);
5164
5696
  if (path === "/api/ai/chat" && method === "POST")
5165
5697
  return handleAiChat(req);
5698
+ if (path === "/api/ai/memory" && method === "GET")
5699
+ return json({ memory: dbQueries.getMemory(40) });
5700
+ if (path === "/api/ai/memory" && method === "DELETE") {
5701
+ dbQueries.clearMemory();
5702
+ return json({ success: true });
5703
+ }
5166
5704
  if (path === "/api/debug/dns" && method === "GET")
5167
5705
  return handleDnsQuery(searchParams);
5168
5706
  if (path === "/api/debug/ping" && method === "GET")
@@ -5388,39 +5926,54 @@ async function handleSetSettings(req) {
5388
5926
  dbQueries.setSettings(body);
5389
5927
  return json(body);
5390
5928
  }
5391
- async function executeTool(name, args) {
5929
+ async function executeTool(name, args, cache = new Map) {
5392
5930
  const { operations, spec } = getState();
5393
5931
  if (name === "search_endpoints") {
5932
+ const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
5933
+ const hit = cache.get(cacheKey2);
5934
+ if (hit)
5935
+ return hit;
5394
5936
  const q = String(args.query ?? "").toLowerCase();
5395
5937
  const terms = q.split(/\s+/).filter(Boolean);
5396
5938
  const matches = operations.filter((op) => {
5397
5939
  const hay = [op.operationId, op.path, op.method, ...op.tags ?? [], op.summary ?? "", op.description ?? ""].join(" ").toLowerCase();
5398
5940
  return terms.every((t) => hay.includes(t));
5399
5941
  }).slice(0, 30).map((op) => ({ operationId: op.operationId, method: op.method.toUpperCase(), path: op.path, summary: op.summary ?? null, tags: op.tags }));
5400
- if (!matches.length)
5401
- return { text: `No endpoints found matching "${args.query}". Total: ${operations.length}.`, isError: false };
5402
- return { text: JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2), isError: false };
5942
+ 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);
5943
+ const result = { text, isError: false };
5944
+ cache.set(cacheKey2, result);
5945
+ return result;
5403
5946
  }
5404
5947
  if (name === "get_endpoint_schema") {
5948
+ const cacheKey2 = `schema:${String(args.operationId ?? "")}`;
5949
+ const hit = cache.get(cacheKey2);
5950
+ if (hit)
5951
+ return hit;
5405
5952
  const op = operations.find((o) => o.operationId === args.operationId);
5406
5953
  if (!op)
5407
5954
  return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
5408
- return {
5409
- text: JSON.stringify({
5410
- operationId: op.operationId,
5411
- method: op.method.toUpperCase(),
5412
- path: op.path,
5413
- summary: op.summary ?? null,
5414
- description: op.description ?? null,
5415
- tags: op.tags,
5416
- parameters: op.parameters,
5417
- requestBody: op.requestBody ?? null,
5418
- responses: op.responses
5419
- }, null, 2),
5420
- isError: false
5421
- };
5955
+ const text = JSON.stringify({
5956
+ operationId: op.operationId,
5957
+ method: op.method.toUpperCase(),
5958
+ path: op.path,
5959
+ summary: op.summary ?? null,
5960
+ description: op.description ?? null,
5961
+ tags: op.tags,
5962
+ parameters: op.parameters,
5963
+ requestBody: op.requestBody ?? null,
5964
+ responses: op.responses
5965
+ }, null, 2);
5966
+ const result = { text, isError: false };
5967
+ cache.set(cacheKey2, result);
5968
+ return result;
5422
5969
  }
5423
5970
  if (name === "execute_api_request") {
5971
+ const now = Date.now();
5972
+ const gap = now - _lastApiCallMs;
5973
+ if (gap < MIN_API_CALL_INTERVAL_MS) {
5974
+ await new Promise((r) => setTimeout(r, MIN_API_CALL_INTERVAL_MS - gap));
5975
+ }
5976
+ _lastApiCallMs = Date.now();
5424
5977
  const op = operations.find((o) => o.operationId === args.operationId);
5425
5978
  if (!op)
5426
5979
  return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
@@ -5451,20 +6004,81 @@ async function executeTool(name, args) {
5451
6004
  const bodyStr = reqBody !== undefined ? typeof reqBody === "string" ? reqBody : JSON.stringify(reqBody) : null;
5452
6005
  if (bodyStr !== null && op.requestBody?.contentType)
5453
6006
  authedHeaders["Content-Type"] = op.requestBody.contentType;
6007
+ const logId = randomUUID();
5454
6008
  try {
5455
6009
  const start = Date.now();
5456
6010
  const res = await fetch(authedUrl, { method: op.method.toUpperCase(), headers: authedHeaders, body: bodyStr ?? undefined });
5457
- const text = await res.text();
6011
+ const responseText = await res.text();
5458
6012
  const latency = Date.now() - start;
5459
- let pretty = text;
6013
+ const resHeaders = Object.fromEntries(res.headers.entries());
6014
+ dbQueries.insertLog({
6015
+ id: logId,
6016
+ source: "ai",
6017
+ tool_name: String(args.operationId ?? op.operationId),
6018
+ method: op.method.toUpperCase(),
6019
+ url: authedUrl,
6020
+ request_headers: JSON.stringify(authedHeaders),
6021
+ request_body: bodyStr,
6022
+ status_code: res.status,
6023
+ response_headers: JSON.stringify(resHeaders),
6024
+ response_body: responseText.slice(0, 8192),
6025
+ latency_ms: latency,
6026
+ error: null
6027
+ });
6028
+ logBus.emit({
6029
+ id: logId,
6030
+ source: "ai",
6031
+ tool_name: String(args.operationId ?? op.operationId),
6032
+ method: op.method.toUpperCase(),
6033
+ url: authedUrl,
6034
+ request_headers: JSON.stringify(authedHeaders),
6035
+ request_body: bodyStr,
6036
+ status_code: res.status,
6037
+ response_headers: JSON.stringify(resHeaders),
6038
+ response_body: responseText.slice(0, 2048),
6039
+ latency_ms: latency,
6040
+ error: null,
6041
+ created_at: Date.now()
6042
+ });
6043
+ let pretty = responseText;
5460
6044
  try {
5461
- pretty = JSON.stringify(JSON.parse(text), null, 2);
6045
+ pretty = JSON.stringify(JSON.parse(responseText), null, 2);
5462
6046
  } catch {}
5463
6047
  return { text: `HTTP ${res.status} (${latency}ms)
5464
6048
 
5465
6049
  ${pretty}`, isError: !res.ok };
5466
6050
  } catch (e) {
5467
- return { text: `Network error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
6051
+ const errMsg = e instanceof Error ? e.message : String(e);
6052
+ dbQueries.insertLog({
6053
+ id: logId,
6054
+ source: "ai",
6055
+ tool_name: String(args.operationId ?? op.operationId),
6056
+ method: op.method.toUpperCase(),
6057
+ url: authedUrl,
6058
+ request_headers: JSON.stringify(authedHeaders),
6059
+ request_body: bodyStr,
6060
+ status_code: null,
6061
+ response_headers: null,
6062
+ response_body: null,
6063
+ latency_ms: null,
6064
+ error: errMsg
6065
+ });
6066
+ logBus.emit({
6067
+ id: logId,
6068
+ source: "ai",
6069
+ tool_name: String(args.operationId ?? op.operationId),
6070
+ method: op.method.toUpperCase(),
6071
+ url: authedUrl,
6072
+ request_headers: null,
6073
+ request_body: bodyStr,
6074
+ status_code: null,
6075
+ response_headers: null,
6076
+ response_body: null,
6077
+ latency_ms: null,
6078
+ error: errMsg,
6079
+ created_at: Date.now()
6080
+ });
6081
+ return { text: `Network error: ${errMsg}`, isError: true };
5468
6082
  }
5469
6083
  }
5470
6084
  if (name === "fetch_url") {
@@ -5614,10 +6228,25 @@ ${stripped}`, isError: !res.ok };
5614
6228
  }
5615
6229
  if (name === "save_auth_token") {
5616
6230
  const profileName = String(args.name ?? "AI Login").trim();
6231
+ const tokenType = String(args.token_type ?? "bearer");
6232
+ if (tokenType === "basic" || args.username && args.password) {
6233
+ const username = String(args.username ?? "").trim();
6234
+ const password = String(args.password ?? "").trim();
6235
+ if (!username || !password)
6236
+ return { text: "Error: username and password are required for basic auth", isError: true };
6237
+ const authConfig2 = { type: "basic", username, password };
6238
+ const profileId2 = randomUUID();
6239
+ try {
6240
+ dbQueries.insertProfile({ id: profileId2, name: profileName, description: "Saved by AI", type: "basic", config: JSON.stringify(authConfig2), token_cache: null, is_active: 0 });
6241
+ dbQueries.activateProfile(profileId2);
6242
+ return { text: JSON.stringify({ success: true, message: `Saved and activated basic auth profile "${profileName}"`, id: profileId2 }), isError: false };
6243
+ } catch (e) {
6244
+ return { text: `Error saving profile: ${e instanceof Error ? e.message : String(e)}`, isError: true };
6245
+ }
6246
+ }
5617
6247
  const token = String(args.token ?? "").trim();
5618
6248
  if (!token)
5619
- return { text: "Error: token is required", isError: true };
5620
- const tokenType = String(args.token_type ?? "bearer");
6249
+ return { text: "Error: token is required for bearer/apikey auth", isError: true };
5621
6250
  const headerName = String(args.header_name ?? "X-Api-Key");
5622
6251
  let authConfig;
5623
6252
  let type;
@@ -5642,274 +6271,6 @@ ${stripped}`, isError: !res.ok };
5642
6271
  }
5643
6272
  return { text: `Unknown tool: ${name}`, isError: true };
5644
6273
  }
5645
- async function fetchWithRetry(url, opts, emit, maxRetries = 3) {
5646
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
5647
- const res = await fetch(url, opts);
5648
- if (res.status !== 429 || attempt === maxRetries)
5649
- return res;
5650
- const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
5651
- const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
5652
- emit({ type: "info", message: `Rate limited \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
5653
- await new Promise((r) => setTimeout(r, delay));
5654
- }
5655
- return fetch(url, opts);
5656
- }
5657
- async function anthropicAgentLoop(apiKey, model, system, initialMessages, emit) {
5658
- const msgs = [...initialMessages];
5659
- const toolCalls = [];
5660
- let totalTools = 0;
5661
- let consecutiveErrors = 0;
5662
- const endpointErrors = {};
5663
- for (let iter = 0;iter < 40; iter++) {
5664
- const res = await fetchWithRetry("https://api.anthropic.com/v1/messages", {
5665
- method: "POST",
5666
- headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
5667
- body: JSON.stringify({ model, max_tokens: 4096, system, messages: msgs, tools: ANTHROPIC_TOOLS, stream: true })
5668
- }, emit);
5669
- if (!res.ok)
5670
- throw new Error(`Anthropic error: ${await res.text()}`);
5671
- let fullText = "";
5672
- let stopReason = "";
5673
- const contentBlocks = [];
5674
- const inputAccum = {};
5675
- const reader = res.body.getReader();
5676
- const decoder = new TextDecoder;
5677
- let buf = "";
5678
- while (true) {
5679
- const { done, value } = await reader.read();
5680
- if (done)
5681
- break;
5682
- buf += decoder.decode(value, { stream: true });
5683
- const parts = buf.split(`
5684
-
5685
- `);
5686
- buf = parts.pop() ?? "";
5687
- for (const part of parts) {
5688
- let dataLine = "";
5689
- for (const line of part.split(`
5690
- `)) {
5691
- if (line.startsWith("data: ")) {
5692
- dataLine = line.slice(6);
5693
- break;
5694
- }
5695
- }
5696
- if (!dataLine || dataLine === "[DONE]")
5697
- continue;
5698
- let ev;
5699
- try {
5700
- ev = JSON.parse(dataLine);
5701
- } catch {
5702
- continue;
5703
- }
5704
- const type = ev.type;
5705
- if (type === "content_block_start") {
5706
- const idx = ev.index;
5707
- const cb = ev.content_block;
5708
- contentBlocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
5709
- if (cb.type === "tool_use")
5710
- inputAccum[idx] = "";
5711
- } else if (type === "content_block_delta") {
5712
- const idx = ev.index;
5713
- const delta = ev.delta;
5714
- if (delta.type === "text_delta" && delta.text) {
5715
- fullText += delta.text;
5716
- if (!contentBlocks[idx])
5717
- contentBlocks[idx] = { type: "text", text: "" };
5718
- contentBlocks[idx].text = (contentBlocks[idx].text ?? "") + delta.text;
5719
- emit({ type: "text_delta", text: delta.text });
5720
- } else if (delta.type === "input_json_delta" && delta.partial_json) {
5721
- inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
5722
- }
5723
- } else if (type === "content_block_stop") {
5724
- const idx = ev.index;
5725
- if (contentBlocks[idx]?.type === "tool_use") {
5726
- try {
5727
- contentBlocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
5728
- } catch {
5729
- contentBlocks[idx].input = {};
5730
- }
5731
- }
5732
- } else if (type === "message_delta") {
5733
- const delta = ev.delta;
5734
- if (delta.stop_reason)
5735
- stopReason = delta.stop_reason;
5736
- }
5737
- }
5738
- }
5739
- if (stopReason !== "tool_use")
5740
- return { content: fullText, toolCalls };
5741
- if (totalTools >= MAX_TOTAL_TOOLS) {
5742
- return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
5743
- }
5744
- msgs.push({ role: "assistant", content: contentBlocks });
5745
- const toolResults = [];
5746
- for (const block of contentBlocks) {
5747
- if (block.type !== "tool_use" || !block.id || !block.name)
5748
- continue;
5749
- totalTools++;
5750
- emit({ type: "tool_start", tool: block.name, input: block.input ?? {} });
5751
- const result = await executeTool(block.name, block.input ?? {});
5752
- emit({ type: "tool_done", tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
5753
- toolCalls.push({ tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
5754
- if (result.isError) {
5755
- consecutiveErrors++;
5756
- if (block.name === "execute_api_request" && block.input?.operationId) {
5757
- const eid = String(block.input.operationId);
5758
- endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5759
- if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
5760
- const stopContent = result.text + `
5761
-
5762
- [AGENT LOOP STOPPED: endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times \u2014 stopping to avoid loop]`;
5763
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
5764
- msgs.push({ role: "user", content: toolResults });
5765
- return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
5766
- }
5767
- }
5768
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
5769
- const stopMsg = `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`;
5770
- const stopContent = result.text + `
5771
-
5772
- [AGENT LOOP STOPPED: ${stopMsg}]`;
5773
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
5774
- msgs.push({ role: "user", content: toolResults });
5775
- return { content: stopMsg, toolCalls };
5776
- }
5777
- } else {
5778
- consecutiveErrors = 0;
5779
- }
5780
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result.text });
5781
- }
5782
- if (!toolResults.length)
5783
- return { content: fullText, toolCalls };
5784
- msgs.push({ role: "user", content: toolResults });
5785
- }
5786
- return { content: "(max iterations reached)", toolCalls };
5787
- }
5788
- async function openaiCompatibleLoop(base, apiKey, model, extraHeaders, system, initialMessages, emit) {
5789
- const msgs = [{ role: "system", content: system }, ...initialMessages];
5790
- const toolCalls = [];
5791
- const authHeaders = {};
5792
- if (apiKey)
5793
- authHeaders["Authorization"] = `Bearer ${apiKey}`;
5794
- let totalTools = 0;
5795
- let consecutiveErrors = 0;
5796
- const endpointErrors = {};
5797
- for (let iter = 0;iter < 40; iter++) {
5798
- const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
5799
- method: "POST",
5800
- headers: { "Content-Type": "application/json", ...authHeaders, ...extraHeaders },
5801
- body: JSON.stringify({ model, messages: msgs, tools: OPENAI_TOOLS, tool_choice: "auto", stream: true })
5802
- }, emit);
5803
- if (!res.ok)
5804
- throw new Error(await res.text());
5805
- let fullContent = "";
5806
- let finishReason = "";
5807
- const tcAccum = {};
5808
- const reader = res.body.getReader();
5809
- const dec = new TextDecoder;
5810
- let buf = "";
5811
- outer:
5812
- while (true) {
5813
- const { done, value } = await reader.read();
5814
- if (done)
5815
- break;
5816
- buf += dec.decode(value, { stream: true });
5817
- const parts = buf.split(`
5818
-
5819
- `);
5820
- buf = parts.pop() ?? "";
5821
- for (const part of parts) {
5822
- let data = "";
5823
- for (const line of part.split(`
5824
- `)) {
5825
- if (line.startsWith("data: ")) {
5826
- data = line.slice(6);
5827
- break;
5828
- }
5829
- }
5830
- if (!data)
5831
- continue;
5832
- if (data === "[DONE]")
5833
- break outer;
5834
- let ev;
5835
- try {
5836
- ev = JSON.parse(data);
5837
- } catch {
5838
- continue;
5839
- }
5840
- if (ev.object === "error")
5841
- throw new Error(JSON.stringify(ev));
5842
- const choices = ev.choices;
5843
- const choice = choices?.[0];
5844
- if (!choice)
5845
- continue;
5846
- const fr = choice.finish_reason;
5847
- if (fr)
5848
- finishReason = fr;
5849
- const delta = choice.delta;
5850
- if (!delta)
5851
- continue;
5852
- if (typeof delta.content === "string" && delta.content) {
5853
- fullContent += delta.content;
5854
- emit({ type: "text_delta", text: delta.content });
5855
- }
5856
- const tcDeltas = delta.tool_calls;
5857
- if (tcDeltas) {
5858
- for (const tc of tcDeltas) {
5859
- if (!tcAccum[tc.index])
5860
- tcAccum[tc.index] = { id: "", name: "", args: "" };
5861
- const entry = tcAccum[tc.index];
5862
- if (tc.id)
5863
- entry.id += tc.id;
5864
- if (tc.function?.name)
5865
- entry.name += tc.function.name;
5866
- if (tc.function?.arguments)
5867
- entry.args += tc.function.arguments;
5868
- }
5869
- }
5870
- }
5871
- }
5872
- if (finishReason !== "tool_calls")
5873
- return { content: fullContent, toolCalls };
5874
- if (totalTools >= MAX_TOTAL_TOOLS) {
5875
- return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
5876
- }
5877
- const msgToolCalls = Object.values(tcAccum).map((tc) => ({
5878
- id: tc.id,
5879
- type: "function",
5880
- function: { name: tc.name, arguments: tc.args }
5881
- }));
5882
- msgs.push({ role: "assistant", content: fullContent || null, tool_calls: msgToolCalls });
5883
- for (const tc of Object.values(tcAccum)) {
5884
- let args = {};
5885
- try {
5886
- args = JSON.parse(tc.args);
5887
- } catch {}
5888
- totalTools++;
5889
- emit({ type: "tool_start", tool: tc.name, input: args });
5890
- const result = await executeTool(tc.name, args);
5891
- emit({ type: "tool_done", tool: tc.name, input: args, output: result.text, isError: result.isError });
5892
- toolCalls.push({ tool: tc.name, input: args, output: result.text, isError: result.isError });
5893
- msgs.push({ role: "tool", tool_call_id: tc.id, content: result.text });
5894
- if (result.isError) {
5895
- consecutiveErrors++;
5896
- if (tc.name === "execute_api_request" && args.operationId) {
5897
- const eid = String(args.operationId);
5898
- endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5899
- if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
5900
- return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
5901
- }
5902
- }
5903
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
5904
- return { content: `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`, toolCalls };
5905
- }
5906
- } else {
5907
- consecutiveErrors = 0;
5908
- }
5909
- }
5910
- }
5911
- return { content: "(max iterations reached)", toolCalls };
5912
- }
5913
6274
  async function handleAiChat(req) {
5914
6275
  let body;
5915
6276
  try {
@@ -5920,43 +6281,58 @@ async function handleAiChat(req) {
5920
6281
  const settingsRow = dbQueries.getSettings();
5921
6282
  const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
5922
6283
  const ai = settings.ai ?? {};
6284
+ const provider = ai.provider ?? "anthropic";
6285
+ const providerDefaults = PROVIDER_DEFAULTS[provider] ?? { model: "" };
6286
+ const requiresKey = provider !== "ollama" && provider !== "custom";
6287
+ if (requiresKey && !ai.apiKey) {
6288
+ return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
6289
+ }
6290
+ if (!hasState())
6291
+ return json({ error: "No spec loaded." }, 400);
5923
6292
  const { spec, operations } = getState();
5924
6293
  const preview = operations.slice(0, 40).map((op) => `- ${op.method.toUpperCase()} ${op.path}${op.summary ? `: ${op.summary}` : ""}`).join(`
5925
6294
  `);
5926
6295
  const activeAuth = dbQueries.getActiveProfile();
5927
- 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.";
6296
+ 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.";
6297
+ const memory = dbQueries.getMemory(20);
6298
+ const memorySection = memory.length ? `
6299
+ ## Memory from previous sessions
6300
+ ${memory.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 300)}${m.content.length > 300 ? "\u2026" : ""}`).join(`
6301
+ `)}
6302
+ ` : "";
5928
6303
  const system = `You are an AI assistant for the "${spec.title}" API (v${spec.version}). Base URL: ${spec.baseUrl}.
5929
6304
  Total endpoints: ${operations.length}. Sample:
5930
6305
  ${preview}${operations.length > 40 ? `
5931
6306
  ... and ${operations.length - 40} more` : ""}
5932
6307
 
5933
6308
  ${authLine}
6309
+ ${memorySection}
6310
+ Tools:
6311
+ - search_endpoints / get_endpoint_schema \u2014 explore API structure (results cached; never repeat the same query)
6312
+ - execute_api_request \u2014 call an endpoint
6313
+ - list_auth_profiles / set_active_auth / save_auth_token \u2014 manage credentials
6314
+ \u2022 save_auth_token supports token_type="basic" with username+password for HTTP Basic auth
6315
+ - fetch_url \u2014 external docs
6316
+ - dns_lookup \u2014 connectivity diagnostics
6317
+ - get_recent_logs \u2014 proxy traffic history
6318
+ - run_security_check \u2014 static security analysis
5934
6319
 
5935
- Tools available:
5936
- - search_endpoints / get_endpoint_schema: explore API structure
5937
- - execute_api_request: call an endpoint
5938
- - list_auth_profiles: list all saved auth profiles (name, type, active)
5939
- - set_active_auth(name): switch to a saved profile before making requests
5940
- - 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
5941
- - fetch_url: fetch external docs
5942
- - dns_lookup: DNS resolution / connectivity
5943
- - get_recent_logs: recent request/response traffic
5944
- - run_security_check: security analysis on an endpoint
6320
+ Auth workflow: 401/403 \u2192 list_auth_profiles \u2192 set_active_auth OR find login endpoint \u2192 save_auth_token \u2192 retry.
5945
6321
 
5946
- 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.
6322
+ Rules:
6323
+ - Never repeat a search you already ran \u2014 results are cached.
6324
+ - Diagnose errors before retrying. Three failures on the same endpoint stops the agent.
6325
+ - Do not fire rapid successive API requests.
5947
6326
 
5948
- 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.
6327
+ Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
5949
6328
 
5950
- Be concise and practical. Format code and JSON in code blocks.${body.extra_context ? `
6329
+ ---
6330
+ ## Custom instructions
6331
+ ${ai.customInstructions}` : ""}${body.extra_context ? `
5951
6332
 
5952
6333
  ---
5953
- ## Current context
6334
+ ## Context
5954
6335
  ${body.extra_context}` : ""}`;
5955
- const provider = ai.provider ?? "anthropic";
5956
- const requiresKey = provider !== "ollama" && provider !== "custom";
5957
- if (requiresKey && !ai.apiKey) {
5958
- return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
5959
- }
5960
6336
  const { readable, writable } = new TransformStream;
5961
6337
  const writer = writable.getWriter();
5962
6338
  const enc = new TextEncoder;
@@ -5966,62 +6342,39 @@ ${body.extra_context}` : ""}`;
5966
6342
  `)).catch(() => {});
5967
6343
  };
5968
6344
  const msgs = body.messages;
6345
+ const toolCache = new Map;
6346
+ const abortCtrl = new AbortController;
6347
+ const lastUserMsg = [...msgs].reverse().find((m) => m.role === "user");
6348
+ const userMemoryContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : null;
5969
6349
  (async () => {
5970
6350
  try {
5971
- let result;
5972
- if (provider === "anthropic") {
5973
- result = await anthropicAgentLoop(ai.apiKey, ai.model || "claude-haiku-4-5-20251001", system, msgs, emit);
5974
- } else if (provider === "openai") {
5975
- const base = (ai.baseUrl || "https://api.openai.com").replace(/\/$/, "");
5976
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o-mini", {}, system, msgs, emit);
5977
- } else if (provider === "mistral") {
5978
- const base = (ai.baseUrl || "https://api.mistral.ai").replace(/\/$/, "");
5979
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "mistral-small-latest", {}, system, msgs, emit);
5980
- } else if (provider === "github-copilot") {
5981
- const base = (ai.baseUrl || "https://api.githubcopilot.com").replace(/\/$/, "");
5982
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o", {
5983
- "Copilot-Integration-Id": "vscode-chat",
5984
- "Editor-Version": "vscode/1.85.0"
5985
- }, system, msgs, emit);
5986
- } else if (provider === "groq") {
5987
- const base = (ai.baseUrl || "https://api.groq.com/openai").replace(/\/$/, "");
5988
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "llama-3.1-70b-versatile", {}, system, msgs, emit);
5989
- } else if (provider === "custom") {
5990
- if (!ai.baseUrl) {
5991
- emit({ type: "error", message: "Custom provider requires a Base URL." });
5992
- await writer.close();
5993
- return;
5994
- }
5995
- result = await openaiCompatibleLoop(ai.baseUrl.replace(/\/$/, ""), ai.apiKey, ai.model || "", {}, system, msgs, emit);
5996
- } else if (provider === "ollama") {
5997
- const base = (ai.baseUrl || "http://localhost:11434").replace(/\/$/, "");
5998
- const res = await fetch(`${base}/api/chat`, {
5999
- method: "POST",
6000
- headers: { "Content-Type": "application/json" },
6001
- body: JSON.stringify({ model: ai.model || "llama3", messages: [{ role: "system", content: system }, ...msgs], stream: false })
6002
- });
6003
- const d = await res.json();
6004
- result = { content: d.message.content ?? "", toolCalls: [] };
6005
- } else if (provider === "gemini") {
6006
- const model = ai.model || "gemini-1.5-flash";
6007
- const base = (ai.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
6008
- const res = await fetch(`${base}/v1beta/models/${model}:generateContent?key=${ai.apiKey}`, {
6009
- method: "POST",
6010
- headers: { "Content-Type": "application/json" },
6011
- body: JSON.stringify({
6012
- systemInstruction: { parts: [{ text: system }] },
6013
- contents: msgs.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] })),
6014
- generationConfig: { maxOutputTokens: 4096 }
6015
- })
6016
- });
6017
- const d = await res.json();
6018
- result = { content: d.candidates[0]?.content.parts[0]?.text ?? "", toolCalls: [] };
6019
- } else {
6020
- emit({ type: "error", message: `Unknown provider: ${provider}` });
6021
- await writer.close();
6022
- return;
6351
+ const result = await runAgentLoop({
6352
+ provider,
6353
+ apiKey: ai.apiKey,
6354
+ model: ai.model || providerDefaults.model,
6355
+ baseUrl: ai.baseUrl || providerDefaults.baseUrl,
6356
+ maxTokens: ai.maxTokens ?? 4096,
6357
+ stepTimeoutMs: ai.stepTimeoutMs ?? 60000,
6358
+ temperature: ai.temperature,
6359
+ topK: ai.topK && ai.topK > 0 ? ai.topK : undefined,
6360
+ parallelTools: true,
6361
+ enablePromptCache: true
6362
+ }, system, msgs, TOOL_SCHEMAS, (name, args) => executeTool(name, args, toolCache), emit, abortCtrl.signal, toolCache);
6363
+ if (result.content && result.stopReason !== "max_iterations") {
6364
+ try {
6365
+ if (userMemoryContent)
6366
+ dbQueries.saveMemory("user", userMemoryContent.slice(0, 1000));
6367
+ dbQueries.saveMemory("assistant", result.content.slice(0, 1000));
6368
+ dbQueries.trimMemory(40);
6369
+ } catch {}
6023
6370
  }
6024
- emit({ type: "done", content: result.content, toolCalls: result.toolCalls });
6371
+ emit({
6372
+ type: "done",
6373
+ content: result.content,
6374
+ toolCalls: result.toolCalls,
6375
+ stopReason: result.stopReason,
6376
+ tokens: result.tokens
6377
+ });
6025
6378
  } catch (e) {
6026
6379
  emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
6027
6380
  } finally {
@@ -6031,11 +6384,7 @@ ${body.extra_context}` : ""}`;
6031
6384
  }
6032
6385
  })();
6033
6386
  return new Response(readable, {
6034
- headers: {
6035
- "Content-Type": "text/event-stream",
6036
- "Cache-Control": "no-cache",
6037
- ...CORS4
6038
- }
6387
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
6039
6388
  });
6040
6389
  }
6041
6390
  function handleGetProfiles() {
@@ -6642,7 +6991,7 @@ function handleDeleteCaptureBin(path) {
6642
6991
  dbQueries.deleteCaptureBin(id);
6643
6992
  return json({ ok: true });
6644
6993
  }
6645
- var CORS4, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
6994
+ var CORS4, TOOL_DEFS, _lastApiCallMs = 0, MIN_API_CALL_INTERVAL_MS = 400, TOOL_SCHEMAS, PROVIDER_DEFAULTS;
6646
6995
  var init_routes = __esm(() => {
6647
6996
  init_db();
6648
6997
  init_engine();
@@ -6652,6 +7001,7 @@ var init_routes = __esm(() => {
6652
7001
  init_config();
6653
7002
  init_version();
6654
7003
  init_engine2();
7004
+ init_harness();
6655
7005
  CORS4 = {
6656
7006
  "Access-Control-Allow-Origin": "*",
6657
7007
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
@@ -6720,25 +7070,34 @@ var init_routes = __esm(() => {
6720
7070
  required: ["name"]
6721
7071
  },
6722
7072
  save_auth_token: {
6723
- 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.",
7073
+ 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.",
6724
7074
  params: {
6725
7075
  name: { type: "string", description: 'Profile name, e.g. "user session" or the username' },
6726
- token: { type: "string", description: "The bearer token or API key value to save" },
6727
- token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query"], description: "Token type (default: bearer)" },
6728
- header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" }
7076
+ token: { type: "string", description: "The bearer token or API key value (omit for basic auth)" },
7077
+ token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query", "basic"], description: "Token type (default: bearer)" },
7078
+ header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" },
7079
+ username: { type: "string", description: "Username for basic auth" },
7080
+ password: { type: "string", description: "Password for basic auth" }
6729
7081
  },
6730
- required: ["name", "token"]
7082
+ required: ["name"]
6731
7083
  }
6732
7084
  };
6733
- ANTHROPIC_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
7085
+ TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6734
7086
  name,
6735
7087
  description: def.description,
6736
- input_schema: { type: "object", properties: def.params, required: def.required }
6737
- }));
6738
- OPENAI_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6739
- type: "function",
6740
- function: { name, description: def.description, parameters: { type: "object", properties: def.params, required: def.required } }
7088
+ params: def.params,
7089
+ required: def.required
6741
7090
  }));
7091
+ PROVIDER_DEFAULTS = {
7092
+ anthropic: { model: "claude-haiku-4-5-20251001" },
7093
+ openai: { model: "gpt-4o-mini", baseUrl: "https://api.openai.com" },
7094
+ mistral: { model: "mistral-small-latest", baseUrl: "https://api.mistral.ai" },
7095
+ groq: { model: "llama-3.1-70b-versatile", baseUrl: "https://api.groq.com/openai" },
7096
+ "github-copilot": { model: "gpt-4o", baseUrl: "https://api.githubcopilot.com" },
7097
+ ollama: { model: "llama3", baseUrl: "http://localhost:11434" },
7098
+ gemini: { model: "gemini-1.5-flash" },
7099
+ custom: { model: "" }
7100
+ };
6742
7101
  });
6743
7102
 
6744
7103
  // src/repl.ts