wasper-cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +742 -383
- package/dist/index.js +742 -383
- package/package.json +1 -1
package/dist/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.
|
|
24
|
+
version: "0.3.1",
|
|
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
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
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
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
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
|
|
6011
|
+
const responseText = await res.text();
|
|
5458
6012
|
const latency = Date.now() - start;
|
|
5459
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6327
|
+
Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
|
|
5949
6328
|
|
|
5950
|
-
|
|
6329
|
+
---
|
|
6330
|
+
## Custom instructions
|
|
6331
|
+
${ai.customInstructions}` : ""}${body.extra_context ? `
|
|
5951
6332
|
|
|
5952
6333
|
---
|
|
5953
|
-
##
|
|
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
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
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({
|
|
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,
|
|
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
|
|
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
|
|
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"
|
|
7082
|
+
required: ["name"]
|
|
6731
7083
|
}
|
|
6732
7084
|
};
|
|
6733
|
-
|
|
7085
|
+
TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
6734
7086
|
name,
|
|
6735
7087
|
description: def.description,
|
|
6736
|
-
|
|
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
|