@agentprojectcontext/apx 1.10.4 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/src/cli/commands/search.js +62 -0
- package/src/cli/index.js +21 -0
- package/src/core/agent-system.js +15 -0
- package/src/daemon/api.js +229 -0
- package/src/daemon/engines/anthropic.js +19 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/openai.js +22 -2
- package/src/daemon/plugins/telegram.js +248 -2
- package/src/daemon/super-agent.js +42 -1
- package/src/daemon/tools/browser.js +424 -0
- package/src/daemon/tools/fetch.js +138 -0
- package/src/daemon/tools/glob.js +165 -0
- package/src/daemon/tools/grep.js +218 -0
- package/src/daemon/tools/registry.js +729 -0
- package/src/daemon/tools/search.js +290 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"express": "^4.21.0",
|
|
36
36
|
"node-fetch": "^3.3.2"
|
|
37
37
|
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"fast-glob": "^3.3.2",
|
|
40
|
+
"puppeteer": "^22.0.0"
|
|
41
|
+
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@semantic-release/changelog": "^6.0.3",
|
|
40
44
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// cli/commands/search.js
|
|
2
|
+
// `apx search "query"` — web search from the terminal.
|
|
3
|
+
// Uses the daemon's tools/search.js module directly (no HTTP roundtrip,
|
|
4
|
+
// no need to have `apx daemon start` running).
|
|
5
|
+
|
|
6
|
+
import { webSearch } from "../../daemon/tools/search.js";
|
|
7
|
+
|
|
8
|
+
const DIM = "\x1b[2m";
|
|
9
|
+
const BOLD = "\x1b[1m";
|
|
10
|
+
const BLUE = "\x1b[34m";
|
|
11
|
+
const CYAN = "\x1b[36m";
|
|
12
|
+
const RESET = "\x1b[0m";
|
|
13
|
+
|
|
14
|
+
export async function cmdSearch(args) {
|
|
15
|
+
const query = (args._ || []).join(" ").trim();
|
|
16
|
+
if (!query) {
|
|
17
|
+
console.error("apx search: missing <query>");
|
|
18
|
+
console.error("usage: apx search \"<query>\" [--mode auto|ddg|brave|browser] [-n N]");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mode = args.flags?.mode || "auto";
|
|
23
|
+
const limit = parseInt(args.flags?.n || args.flags?.limit || "5", 10) || 5;
|
|
24
|
+
const json = !!args.flags?.json;
|
|
25
|
+
|
|
26
|
+
process.stderr.write(`${DIM}Searching... (${mode})${RESET}\n`);
|
|
27
|
+
|
|
28
|
+
let r;
|
|
29
|
+
try {
|
|
30
|
+
r = await webSearch({ query, mode, limit });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(`${DIM}error:${RESET} ${e.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (json) {
|
|
37
|
+
console.log(JSON.stringify(r, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const results = r.results || [];
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
console.log(`${DIM}(no results from ${r.mode})${RESET}`);
|
|
44
|
+
if (r.raw_excerpt) console.log(`${DIM}excerpt:${RESET} ${r.raw_excerpt.slice(0, 400)}…`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`${DIM}${results.length} result${results.length === 1 ? "" : "s"} via ${r.mode}${RESET}\n`);
|
|
49
|
+
results.forEach((item, i) => {
|
|
50
|
+
const num = `[${i + 1}]`;
|
|
51
|
+
const title = item.title || "(no title)";
|
|
52
|
+
console.log(`${BOLD}${num} ${title}${RESET} ${DIM}— ${hostname(item.url)}${RESET}`);
|
|
53
|
+
if (item.snippet) console.log(` ${item.snippet}`);
|
|
54
|
+
console.log(` ${BLUE}${item.url}${RESET}`);
|
|
55
|
+
if (i < results.length - 1) console.log("");
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hostname(url) {
|
|
60
|
+
try { return new URL(url).hostname.replace(/^www\./, ""); }
|
|
61
|
+
catch { return url; }
|
|
62
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
cmdTelegramSetup,
|
|
55
55
|
} from "./commands/telegram.js";
|
|
56
56
|
import { cmdMessagesTail, cmdMessagesSearch, cmdMessagesChat } from "./commands/messages.js";
|
|
57
|
+
import { cmdSearch } from "./commands/search.js";
|
|
57
58
|
import { cmdExec } from "./commands/exec.js";
|
|
58
59
|
import {
|
|
59
60
|
cmdChat,
|
|
@@ -653,6 +654,21 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
653
654
|
options: [["--project <name|id|path>", "Pin command to a specific project."]],
|
|
654
655
|
examples: ["apx messages search \"deploy\""],
|
|
655
656
|
}),
|
|
657
|
+
search: topic({
|
|
658
|
+
title: "apx search",
|
|
659
|
+
summary: "Web search from the terminal (DuckDuckGo / Brave / Puppeteer fallback).",
|
|
660
|
+
usage: ["apx search \"<query>\" [--mode auto|ddg|brave|browser] [-n N] [--json]"],
|
|
661
|
+
options: [
|
|
662
|
+
["--mode <m>", "auto (default) | ddg | brave | browser. Brave needs BRAVE_API_KEY."],
|
|
663
|
+
["-n N", "Number of results (default 5, max 20)."],
|
|
664
|
+
["--json", "Output raw JSON instead of the formatted list."],
|
|
665
|
+
],
|
|
666
|
+
examples: [
|
|
667
|
+
"apx search \"agent project context\"",
|
|
668
|
+
"apx search \"node 22 release notes\" --mode ddg -n 10",
|
|
669
|
+
"apx search \"weather buenos aires\" --json",
|
|
670
|
+
],
|
|
671
|
+
}),
|
|
656
672
|
"messages chat": topic({
|
|
657
673
|
title: "apx messages chat",
|
|
658
674
|
summary: "Print APX messages as a chat transcript with user, agent, tool, or system type.",
|
|
@@ -1129,6 +1145,7 @@ function buildHelp(version) {
|
|
|
1129
1145
|
hCmd("apx code", 36, "APX terminal coding assistant"),
|
|
1130
1146
|
hCmd("apx exec <agent> \"prompt\"",36, "one-shot agent call --model <id> --max-tokens N"),
|
|
1131
1147
|
hCmd("apx chat <agent>", 36, "interactive agent REPL --conversation <id>"),
|
|
1148
|
+
hCmd("apx search \"query\"", 36, "web search (ddg | brave | browser) --mode <m> -n N"),
|
|
1132
1149
|
hCmd("apx conversations list", 36, "stored exec/chat conversations for <agent>"),
|
|
1133
1150
|
hCmd("apx conversations get", 36, "<agent> <id>"),
|
|
1134
1151
|
|
|
@@ -1433,6 +1450,10 @@ async function dispatch(cmd, rest) {
|
|
|
1433
1450
|
await cmdExec(parseArgs(rest));
|
|
1434
1451
|
break;
|
|
1435
1452
|
|
|
1453
|
+
case "search":
|
|
1454
|
+
await cmdSearch(parseArgs(rest));
|
|
1455
|
+
break;
|
|
1456
|
+
|
|
1436
1457
|
case "chat":
|
|
1437
1458
|
await cmdChat(parseArgs(rest));
|
|
1438
1459
|
break;
|
package/src/core/agent-system.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Anti-ghost-response rules injected into every agent system prompt.
|
|
6
|
+
// Prevents agents from saying "Ok, I'll do that" and then doing nothing.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const ACTION_DISCIPLINE_RULES = `## Action Discipline (mandatory)
|
|
9
|
+
- NEVER acknowledge an action without executing it in the same turn. If you are going to do something, call the tool FIRST, then report the result.
|
|
10
|
+
- NEVER use empty acknowledgments like "Ok", "Got it", "Sure", "Understood", "On it", "Give me a moment", "I'll do that now" as standalone responses when a tool call is expected. These are invalid responses.
|
|
11
|
+
- Action first, report after. Produce the tool call in the same response as your acknowledgment.
|
|
12
|
+
- If you cannot execute the action (missing permission, unclear params, tool not available), explain WHY — do not promise and disappear.
|
|
13
|
+
- If the user asks you to do multiple things, do them all in the same turn using sequential tool calls if needed.`;
|
|
14
|
+
|
|
4
15
|
function listField(value) {
|
|
5
16
|
if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
6
17
|
return String(value || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -66,6 +77,10 @@ export function buildAgentSystem(project, agent, {
|
|
|
66
77
|
if (ep) parts.push(ep);
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
// Always append action discipline rules last so they are close to the end
|
|
81
|
+
// of the system prompt and harder for the model to "forget".
|
|
82
|
+
parts.push(ACTION_DISCIPLINE_RULES);
|
|
83
|
+
|
|
69
84
|
return parts.join("\n\n");
|
|
70
85
|
}
|
|
71
86
|
|
package/src/daemon/api.js
CHANGED
|
@@ -3,6 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import express from "express";
|
|
6
|
+
import { buildBrowserRouter } from "./tools/browser.js";
|
|
7
|
+
import { buildFetchRouter } from "./tools/fetch.js";
|
|
8
|
+
import { buildSearchRouter } from "./tools/search.js";
|
|
9
|
+
import { buildRegistryRouter } from "./tools/registry.js";
|
|
10
|
+
import { buildGlobRouter } from "./tools/glob.js";
|
|
11
|
+
import { buildGrepRouter } from "./tools/grep.js";
|
|
6
12
|
import { readApfMcps, writeApfMcps, SOURCES } from "./mcp-sources.js";
|
|
7
13
|
import { callEngine, ENGINE_IDS } from "./engines/index.js";
|
|
8
14
|
import { getRuntime, RUNTIME_IDS } from "./runtimes/index.js";
|
|
@@ -58,6 +64,18 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
58
64
|
const app = express();
|
|
59
65
|
app.use(express.json({ limit: "2mb" }));
|
|
60
66
|
|
|
67
|
+
// ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
|
|
68
|
+
// fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
|
|
69
|
+
// browser = Puppeteer-backed → heavy, lazy-launched, for JS-rendered pages
|
|
70
|
+
app.use("/tools/fetch", buildFetchRouter(express));
|
|
71
|
+
app.use("/tools/browser", buildBrowserRouter(express));
|
|
72
|
+
app.use("/tools/search", buildSearchRouter(express));
|
|
73
|
+
app.use("/tools/glob", buildGlobRouter(express));
|
|
74
|
+
app.use("/tools/grep", buildGrepRouter(express));
|
|
75
|
+
// Registry MUST be mounted after specific routers so /:name wildcard
|
|
76
|
+
// doesn't shadow /tools/browser, /tools/fetch, /tools/search, etc.
|
|
77
|
+
app.use("/tools", buildRegistryRouter(express, { projects, registries }));
|
|
78
|
+
|
|
61
79
|
// ---- Health -------------------------------------------------------
|
|
62
80
|
app.get("/health", (_req, res) => {
|
|
63
81
|
res.json({
|
|
@@ -399,6 +417,60 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
399
417
|
}
|
|
400
418
|
});
|
|
401
419
|
|
|
420
|
+
// POST /telegram/send_photo { chat_id?, photo (path|url), caption?, channel? }
|
|
421
|
+
app.post("/telegram/send_photo", async (req, res) => {
|
|
422
|
+
const { chat_id, photo, caption, parse_mode, channel } = req.body || {};
|
|
423
|
+
if (!photo) return res.status(400).json({ error: "photo required (path or url)" });
|
|
424
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
425
|
+
try {
|
|
426
|
+
const r = await telegram.sendPhoto({ chat_id, photo, caption, parse_mode, channel });
|
|
427
|
+
res.status(202).json({ ok: true, message_id: r?.message_id });
|
|
428
|
+
} catch (e) {
|
|
429
|
+
res.status(502).json({ error: e.message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// POST /telegram/send_voice { chat_id?, audio (path), caption?, duration?, channel? }
|
|
434
|
+
app.post("/telegram/send_voice", async (req, res) => {
|
|
435
|
+
const { chat_id, audio, caption, duration, channel } = req.body || {};
|
|
436
|
+
if (!audio) return res.status(400).json({ error: "audio required (path)" });
|
|
437
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
438
|
+
try {
|
|
439
|
+
const r = await telegram.sendVoice({ chat_id, audio, caption, duration, channel });
|
|
440
|
+
res.status(202).json({ ok: true, message_id: r?.message_id });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
res.status(502).json({ error: e.message });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// POST /telegram/send_audio { chat_id?, audio (path), caption?, title?, performer?, channel? }
|
|
447
|
+
app.post("/telegram/send_audio", async (req, res) => {
|
|
448
|
+
const { chat_id, audio, caption, title, performer, channel } = req.body || {};
|
|
449
|
+
if (!audio) return res.status(400).json({ error: "audio required (path)" });
|
|
450
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
451
|
+
try {
|
|
452
|
+
const r = await telegram.sendAudio({ chat_id, audio, caption, title, performer, channel });
|
|
453
|
+
res.status(202).json({ ok: true, message_id: r?.message_id });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
res.status(502).json({ error: e.message });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// POST /telegram/notify — alias for /telegram/send for proactive daemon notifications
|
|
460
|
+
// Any internal daemon code (routines, error handlers, MCP failure hooks) can POST here
|
|
461
|
+
// to push a message to the user without waiting for a user-initiated request.
|
|
462
|
+
app.post("/telegram/notify", async (req, res) => {
|
|
463
|
+
const { chat_id, text, channel } = req.body || {};
|
|
464
|
+
if (!text) return res.status(400).json({ error: "text required" });
|
|
465
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
466
|
+
try {
|
|
467
|
+
const r = await telegram.send({ chat_id, text, channel });
|
|
468
|
+
res.status(202).json({ ok: true, message_id: r.message_id, via: "notify" });
|
|
469
|
+
} catch (e) {
|
|
470
|
+
res.status(502).json({ error: e.message });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
402
474
|
// ---- Plugins -----------------------------------------------------
|
|
403
475
|
app.get("/plugins", (_req, res) => {
|
|
404
476
|
if (!plugins) return res.json({});
|
|
@@ -1174,6 +1246,163 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
1174
1246
|
}
|
|
1175
1247
|
});
|
|
1176
1248
|
|
|
1249
|
+
// ---- Session search (cross-agent, cross-conversation) ------------
|
|
1250
|
+
// GET /sessions/search?q=...&project=...&limit=20
|
|
1251
|
+
// Searches session files (.apc/agents/{slug}/sessions/*.md) and
|
|
1252
|
+
// conversation files (~/.apx/.../conversations/*.md) by text content.
|
|
1253
|
+
app.get("/sessions/search", (req, res) => {
|
|
1254
|
+
const { q, project: projectRef, limit = "20" } = req.query;
|
|
1255
|
+
if (!q) return res.status(400).json({ error: "q required" });
|
|
1256
|
+
const lim = Math.min(parseInt(limit, 10) || 20, 200);
|
|
1257
|
+
const needle = q.toLowerCase();
|
|
1258
|
+
|
|
1259
|
+
// Resolve project (or search all)
|
|
1260
|
+
const allProjects = projects.list();
|
|
1261
|
+
const targetProjects = (() => {
|
|
1262
|
+
if (projectRef != null) {
|
|
1263
|
+
const ref = String(projectRef);
|
|
1264
|
+
const found = allProjects.find((p) => String(p.id) === ref || p.path === path.resolve(ref));
|
|
1265
|
+
return found ? [projects.get(found.id)] : [];
|
|
1266
|
+
}
|
|
1267
|
+
return allProjects.map((p) => projects.get(p.id)).filter(Boolean);
|
|
1268
|
+
})();
|
|
1269
|
+
|
|
1270
|
+
const matches = [];
|
|
1271
|
+
|
|
1272
|
+
for (const p of targetProjects) {
|
|
1273
|
+
if (!p) continue;
|
|
1274
|
+
|
|
1275
|
+
// 1. Search session files in project (.apc/agents/{slug}/sessions/)
|
|
1276
|
+
const sessionAgentsDir = path.join(p.path, ".apc", "agents");
|
|
1277
|
+
if (fs.existsSync(sessionAgentsDir)) {
|
|
1278
|
+
for (const slug of fs.readdirSync(sessionAgentsDir)) {
|
|
1279
|
+
const sessionsDir = path.join(sessionAgentsDir, slug, "sessions");
|
|
1280
|
+
if (!fs.existsSync(sessionsDir)) continue;
|
|
1281
|
+
for (const f of fs.readdirSync(sessionsDir).filter((x) => x.endsWith(".md"))) {
|
|
1282
|
+
const filePath = path.join(sessionsDir, f);
|
|
1283
|
+
try {
|
|
1284
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
1285
|
+
if (text.toLowerCase().includes(needle)) {
|
|
1286
|
+
// Find matching excerpt
|
|
1287
|
+
const lines = text.split("\n");
|
|
1288
|
+
const matchLine = lines.findIndex((l) => l.toLowerCase().includes(needle));
|
|
1289
|
+
const excerpt = lines.slice(Math.max(0, matchLine - 1), matchLine + 3).join("\n");
|
|
1290
|
+
matches.push({
|
|
1291
|
+
type: "session",
|
|
1292
|
+
project: p.id,
|
|
1293
|
+
agent: slug,
|
|
1294
|
+
filename: f,
|
|
1295
|
+
path: filePath,
|
|
1296
|
+
excerpt: excerpt.slice(0, 300),
|
|
1297
|
+
});
|
|
1298
|
+
if (matches.length >= lim) break;
|
|
1299
|
+
}
|
|
1300
|
+
} catch {}
|
|
1301
|
+
}
|
|
1302
|
+
if (matches.length >= lim) break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (matches.length >= lim) break;
|
|
1307
|
+
|
|
1308
|
+
// 2. Search conversation files in daemon storage (~/.apx/.../conversations/)
|
|
1309
|
+
const convAgentsDir = path.join(p.storagePath, "agents");
|
|
1310
|
+
if (fs.existsSync(convAgentsDir)) {
|
|
1311
|
+
for (const slug of fs.readdirSync(convAgentsDir)) {
|
|
1312
|
+
const convDir = path.join(convAgentsDir, slug, "conversations");
|
|
1313
|
+
if (!fs.existsSync(convDir)) continue;
|
|
1314
|
+
for (const f of fs.readdirSync(convDir).filter((x) => x.endsWith(".md"))) {
|
|
1315
|
+
const filePath = path.join(convDir, f);
|
|
1316
|
+
try {
|
|
1317
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
1318
|
+
if (text.toLowerCase().includes(needle)) {
|
|
1319
|
+
const lines = text.split("\n");
|
|
1320
|
+
const matchLine = lines.findIndex((l) => l.toLowerCase().includes(needle));
|
|
1321
|
+
const excerpt = lines.slice(Math.max(0, matchLine - 1), matchLine + 3).join("\n");
|
|
1322
|
+
matches.push({
|
|
1323
|
+
type: "conversation",
|
|
1324
|
+
project: p.id,
|
|
1325
|
+
agent: slug,
|
|
1326
|
+
filename: f,
|
|
1327
|
+
path: filePath,
|
|
1328
|
+
excerpt: excerpt.slice(0, 300),
|
|
1329
|
+
});
|
|
1330
|
+
if (matches.length >= lim) break;
|
|
1331
|
+
}
|
|
1332
|
+
} catch {}
|
|
1333
|
+
}
|
|
1334
|
+
if (matches.length >= lim) break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (matches.length >= lim) break;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
res.json({ q, count: matches.length, results: matches });
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// POST /sessions/:id/compact
|
|
1345
|
+
// Shortcut: resolves which project/agent owns the session file,
|
|
1346
|
+
// then delegates to the existing compactConversation logic.
|
|
1347
|
+
// Body: { project?, model? }
|
|
1348
|
+
app.post("/sessions/:id/compact", async (req, res) => {
|
|
1349
|
+
const { id } = req.params;
|
|
1350
|
+
const { model: modelOverride, project: projectRef } = req.body || {};
|
|
1351
|
+
|
|
1352
|
+
// Find which project/agent owns this session ID
|
|
1353
|
+
const allProjects = projectRef != null
|
|
1354
|
+
? (() => {
|
|
1355
|
+
const ref = String(projectRef);
|
|
1356
|
+
const found = projects.list().find((p) => String(p.id) === ref || p.path === path.resolve(ref));
|
|
1357
|
+
return found ? [projects.get(found.id)] : [];
|
|
1358
|
+
})()
|
|
1359
|
+
: projects.list().map((p) => projects.get(p.id)).filter(Boolean);
|
|
1360
|
+
|
|
1361
|
+
let found = null;
|
|
1362
|
+
const filename = id.endsWith(".md") ? id : `${id}.md`;
|
|
1363
|
+
|
|
1364
|
+
for (const p of allProjects) {
|
|
1365
|
+
if (!p) continue;
|
|
1366
|
+
// Search in daemon conversation storage
|
|
1367
|
+
const agentsDir = path.join(p.storagePath, "agents");
|
|
1368
|
+
if (fs.existsSync(agentsDir)) {
|
|
1369
|
+
for (const slug of fs.readdirSync(agentsDir)) {
|
|
1370
|
+
const f = path.join(agentsDir, slug, "conversations", filename);
|
|
1371
|
+
if (fs.existsSync(f)) {
|
|
1372
|
+
found = { p, slug };
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (found) break;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (!found) {
|
|
1381
|
+
return res.status(404).json({ error: `session/conversation "${id}" not found` });
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const { p, slug } = found;
|
|
1385
|
+
const { readAgents: _readAgents } = await import("../core/parser.js");
|
|
1386
|
+
const agents = _readAgents(p.path);
|
|
1387
|
+
const agent = agents.find((a) => a.slug === slug);
|
|
1388
|
+
const modelId = modelOverride || agent?.fields?.Model;
|
|
1389
|
+
if (!modelId) return res.status(400).json({ error: "agent has no model; pass model in body" });
|
|
1390
|
+
|
|
1391
|
+
try {
|
|
1392
|
+
const { compactConversation } = await import("./compact.js");
|
|
1393
|
+
const result = await compactConversation({
|
|
1394
|
+
storagePath: p.storagePath,
|
|
1395
|
+
agentSlug: slug,
|
|
1396
|
+
filename,
|
|
1397
|
+
modelId,
|
|
1398
|
+
config: p.config || config,
|
|
1399
|
+
});
|
|
1400
|
+
res.json(result);
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
res.status(500).json({ error: e.message });
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1177
1406
|
// ---- Admin --------------------------------------------------------
|
|
1178
1407
|
app.post("/admin/shutdown", (_req, res) => {
|
|
1179
1408
|
res.json({ ok: true });
|
|
@@ -11,7 +11,7 @@ function getKey(config) {
|
|
|
11
11
|
export default {
|
|
12
12
|
id: "anthropic",
|
|
13
13
|
|
|
14
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {} }) {
|
|
14
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
15
15
|
const key = getKey(config);
|
|
16
16
|
if (!key) throw new Error("anthropic: no api_key (set ANTHROPIC_API_KEY or engines.anthropic.api_key)");
|
|
17
17
|
if (!model) throw new Error("anthropic: model required");
|
|
@@ -27,6 +27,18 @@ export default {
|
|
|
27
27
|
};
|
|
28
28
|
if (system) body.system = system;
|
|
29
29
|
|
|
30
|
+
// Tool use support — pass tools array and optional tool_choice.
|
|
31
|
+
// toolChoice="required" → { type: "any" } forces at least one tool call per turn,
|
|
32
|
+
// preventing the model from giving an empty acknowledgment instead of acting.
|
|
33
|
+
if (tools && tools.length > 0) {
|
|
34
|
+
body.tools = tools;
|
|
35
|
+
if (toolChoice === "required" || toolChoice === "any") {
|
|
36
|
+
body.tool_choice = { type: "any" };
|
|
37
|
+
} else if (toolChoice && typeof toolChoice === "object") {
|
|
38
|
+
body.tool_choice = toolChoice;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
const res = await fetch(API_BASE, {
|
|
31
43
|
method: "POST",
|
|
32
44
|
headers: {
|
|
@@ -42,12 +54,18 @@ export default {
|
|
|
42
54
|
`anthropic ${res.status}: ${json?.error?.message || JSON.stringify(json)}`
|
|
43
55
|
);
|
|
44
56
|
}
|
|
57
|
+
|
|
58
|
+
// Extract text blocks; also capture tool_use blocks for callers that need them
|
|
45
59
|
const text = (json.content || [])
|
|
46
60
|
.filter((b) => b.type === "text")
|
|
47
61
|
.map((b) => b.text)
|
|
48
62
|
.join("");
|
|
63
|
+
const toolUses = (json.content || []).filter((b) => b.type === "tool_use");
|
|
64
|
+
|
|
49
65
|
return {
|
|
50
66
|
text,
|
|
67
|
+
tool_uses: toolUses.length > 0 ? toolUses : undefined,
|
|
68
|
+
stop_reason: json.stop_reason,
|
|
51
69
|
usage: {
|
|
52
70
|
input_tokens: json.usage?.input_tokens || 0,
|
|
53
71
|
output_tokens: json.usage?.output_tokens || 0,
|
|
@@ -46,7 +46,7 @@ export function getAdapter(provider) {
|
|
|
46
46
|
return a;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools }) {
|
|
49
|
+
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice }) {
|
|
50
50
|
const { provider, model } = resolveProvider(modelId);
|
|
51
51
|
const adapter = getAdapter(provider);
|
|
52
52
|
const providerCfg =
|
|
@@ -58,6 +58,7 @@ export async function callEngine({ modelId, system, messages, config, temperatur
|
|
|
58
58
|
temperature,
|
|
59
59
|
maxTokens,
|
|
60
60
|
tools,
|
|
61
|
+
toolChoice,
|
|
61
62
|
config: providerCfg,
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -10,7 +10,7 @@ function getKey(config) {
|
|
|
10
10
|
export default {
|
|
11
11
|
id: "openai",
|
|
12
12
|
|
|
13
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {} }) {
|
|
13
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
14
14
|
const key = getKey(config);
|
|
15
15
|
if (!key) throw new Error("openai: no api_key (set OPENAI_API_KEY or engines.openai.api_key)");
|
|
16
16
|
if (!model) throw new Error("openai: model required");
|
|
@@ -31,6 +31,20 @@ export default {
|
|
|
31
31
|
max_tokens: maxTokens,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// Tool use support
|
|
35
|
+
if (tools && tools.length > 0) {
|
|
36
|
+
body.tools = tools;
|
|
37
|
+
// toolChoice="required" forces the model to call at least one tool,
|
|
38
|
+
// preventing empty acknowledgment responses.
|
|
39
|
+
if (toolChoice === "required") {
|
|
40
|
+
body.tool_choice = "required";
|
|
41
|
+
} else if (toolChoice === "any") {
|
|
42
|
+
body.tool_choice = "required"; // OpenAI uses "required" for "any"
|
|
43
|
+
} else if (toolChoice && typeof toolChoice === "object") {
|
|
44
|
+
body.tool_choice = toolChoice;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
const res = await fetch(API_BASE, {
|
|
35
49
|
method: "POST",
|
|
36
50
|
headers: {
|
|
@@ -45,9 +59,15 @@ export default {
|
|
|
45
59
|
`openai ${res.status}: ${json?.error?.message || JSON.stringify(json)}`
|
|
46
60
|
);
|
|
47
61
|
}
|
|
48
|
-
|
|
62
|
+
|
|
63
|
+
const choice = json.choices?.[0];
|
|
64
|
+
const text = choice?.message?.content || "";
|
|
65
|
+
const toolCalls = choice?.message?.tool_calls;
|
|
66
|
+
|
|
49
67
|
return {
|
|
50
68
|
text,
|
|
69
|
+
tool_calls: toolCalls?.length > 0 ? toolCalls : undefined,
|
|
70
|
+
finish_reason: choice?.finish_reason,
|
|
51
71
|
usage: {
|
|
52
72
|
input_tokens: json.usage?.prompt_tokens || 0,
|
|
53
73
|
output_tokens: json.usage?.completion_tokens || 0,
|