@agentprojectcontext/apx 1.30.2 → 1.31.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/package.json +1 -1
- package/skills/apx-agency-agents/SKILL.md +1 -1
- package/skills/apx-agent/SKILL.md +6 -6
- package/skills/apx-project/SKILL.md +1 -2
- package/src/core/agent/prompt-builder.js +6 -0
- package/src/core/agent/run-agent.js +21 -0
- package/src/core/agent/self-memory.js +1 -1
- package/src/core/agent-memory.js +64 -0
- package/src/core/agent-system.js +3 -2
- package/src/core/scaffold.js +43 -18
- package/src/core/tools/browser.js +169 -75
- package/src/core/tools/registry.js +13 -8
- package/src/core/tools/search.js +35 -7
- package/src/host/daemon/api/agents.js +19 -21
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/shared.js +5 -8
- package/src/host/daemon/super-agent-tools/index.js +232 -43
- package/src/host/daemon/super-agent-tools/registry-bridge.js +30 -1
- package/src/host/daemon/super-agent-tools/tools/discover-tools.js +67 -0
- package/src/host/daemon/super-agent-tools/tools/import-agent.js +2 -0
- package/src/host/daemon/super-agent-tools/tools/read-agent-memory.js +5 -4
- package/src/host/daemon/super-agent.js +15 -17
- package/src/interfaces/cli/commands/agent.js +4 -1
- package/src/interfaces/cli/commands/memory.js +9 -10
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js → index-BV615I9p.js} +5 -5
- package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js.map → index-BV615I9p.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/package-lock.json +100 -211
- package/src/interfaces/web/src/i18n/en.ts +6 -6
- package/src/interfaces/web/src/i18n/es.ts +6 -6
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
|
@@ -350,12 +350,17 @@ const TOOL_DEFINITIONS = [
|
|
|
350
350
|
{
|
|
351
351
|
name: "browser_navigate",
|
|
352
352
|
category: "browser",
|
|
353
|
-
description: "Navigate the headless browser to a URL. Launches Chromium lazily on first call.",
|
|
353
|
+
description: "Navigate the headless browser to a URL. Launches Chromium lazily on first call. Auto-retries and falls back to a more permissive wait strategy on redirect-heavy sites.",
|
|
354
354
|
endpoint: { method: "POST", path: "/tools/browser/navigate" },
|
|
355
355
|
parameters: {
|
|
356
356
|
type: "object",
|
|
357
357
|
properties: {
|
|
358
358
|
url: { type: "string" },
|
|
359
|
+
wait_until: {
|
|
360
|
+
type: "string",
|
|
361
|
+
enum: ["load", "domcontentloaded", "networkidle0", "networkidle2"],
|
|
362
|
+
description: "Puppeteer wait strategy (default networkidle2). Use 'domcontentloaded' for slow/redirect-heavy sites; navigate auto-falls back to it on failure anyway.",
|
|
363
|
+
},
|
|
359
364
|
launch_options: { type: "object", description: "Puppeteer launch overrides (headless, args, defaultViewport, etc.)." },
|
|
360
365
|
allow_dangerous: { type: "boolean", description: "Allow dangerous launch args (--no-sandbox, --single-process, etc.)." },
|
|
361
366
|
},
|
|
@@ -614,6 +619,8 @@ function makeInlineHandlers({ projects, registries }) {
|
|
|
614
619
|
memory_list: async (body) => {
|
|
615
620
|
const { default: fs } = await import("node:fs");
|
|
616
621
|
const { default: path } = await import("node:path");
|
|
622
|
+
const { readAgents } = await import("../parser.js");
|
|
623
|
+
const { agentMemoryPath } = await import("../agent-memory.js");
|
|
617
624
|
// Find the project
|
|
618
625
|
const all = projects.list();
|
|
619
626
|
let p = null;
|
|
@@ -624,15 +631,13 @@ function makeInlineHandlers({ projects, registries }) {
|
|
|
624
631
|
}
|
|
625
632
|
if (!p) p = projects.get(all.filter((x) => x.id !== 0)[0]?.id) || projects.get(0);
|
|
626
633
|
if (!p) throw new Error("no project registered");
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}).map((slug) => {
|
|
632
|
-
const memPath = path.join(agentsDir, slug, "memory.md");
|
|
634
|
+
const result = readAgents(p.path).map((agent) => {
|
|
635
|
+
const slug = agent.slug;
|
|
636
|
+
const memPath = agentMemoryPath(p, slug);
|
|
637
|
+
if (!fs.existsSync(memPath)) return null;
|
|
633
638
|
const stat = fs.statSync(memPath);
|
|
634
639
|
return { agent: slug, path: memPath, size: stat.size, mtime: stat.mtime };
|
|
635
|
-
});
|
|
640
|
+
}).filter(Boolean);
|
|
636
641
|
return { project: p.path, agents_with_memory: result };
|
|
637
642
|
},
|
|
638
643
|
};
|
package/src/core/tools/search.js
CHANGED
|
@@ -45,26 +45,54 @@ function extractText(html) {
|
|
|
45
45
|
.replace(/</g, "<")
|
|
46
46
|
.replace(/>/g, ">")
|
|
47
47
|
.replace(/"/g, '"')
|
|
48
|
-
.replace(/&#
|
|
48
|
+
.replace(/�?39;/g, "'")
|
|
49
49
|
.replace(/ /g, " ")
|
|
50
|
+
// Generic numeric entities (decimal \ and hex ') DDG sprinkles into
|
|
51
|
+
// titles/snippets — decode so results read cleanly.
|
|
52
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
53
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
|
|
50
54
|
.replace(/\s{2,}/g, " ")
|
|
51
55
|
.trim();
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Unwrap DuckDuckGo's result redirect. DDG no longer exposes the target URL
|
|
60
|
+
* directly: every result href is `//duckduckgo.com/l/?uddg=<urlencoded real
|
|
61
|
+
* url>&rut=...`. We pull the `uddg` param out and decode it back to the real
|
|
62
|
+
* destination. Plain/protocol-relative URLs are normalized to https.
|
|
63
|
+
*/
|
|
64
|
+
export function unwrapDdgUrl(href) {
|
|
65
|
+
if (!href) return href;
|
|
66
|
+
const m = href.match(/[?&]uddg=([^&]+)/);
|
|
67
|
+
if (m) {
|
|
68
|
+
try {
|
|
69
|
+
return decodeURIComponent(m[1].replace(/&/g, "&"));
|
|
70
|
+
} catch {
|
|
71
|
+
/* fall through to raw href */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (href.startsWith("//")) return "https:" + href;
|
|
75
|
+
return href;
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
/** Parse DuckDuckGo HTML results */
|
|
55
|
-
function parseDdgResults(html, limit) {
|
|
79
|
+
export function parseDdgResults(html, limit) {
|
|
56
80
|
const results = [];
|
|
57
|
-
// Match result blocks: each has a link (.result__a) and snippet (.result__snippet)
|
|
58
|
-
|
|
81
|
+
// Match result blocks: each has a link (.result__a) and snippet (.result__snippet).
|
|
82
|
+
// Attribute order varies (rel/class/href), so don't assume class precedes href.
|
|
83
|
+
const blockRe = /<a[^>]+class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
59
84
|
const snippetRe = /<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
60
85
|
|
|
61
86
|
const links = [];
|
|
62
87
|
let m;
|
|
63
88
|
while ((m = blockRe.exec(html)) !== null && links.length < limit * 2) {
|
|
64
|
-
|
|
89
|
+
// DDG wraps every external link in a //duckduckgo.com/l/?uddg= redirect —
|
|
90
|
+
// decode it to the real target instead of discarding it (the old code
|
|
91
|
+
// dropped everything containing "duckduckgo.com", yielding zero results).
|
|
92
|
+
const url = unwrapDdgUrl(m[1]);
|
|
65
93
|
const title = extractText(m[2]).trim();
|
|
66
|
-
if (
|
|
67
|
-
links.push({ url
|
|
94
|
+
if (url && title && !/^https?:\/\/(?:[a-z]+\.)?duckduckgo\.com\//i.test(url) && !url.startsWith("//duckduckgo")) {
|
|
95
|
+
links.push({ url, title });
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
|
|
@@ -14,6 +14,13 @@ import {
|
|
|
14
14
|
restoreVaultAgent,
|
|
15
15
|
ensureAgentDir,
|
|
16
16
|
} from "../../../core/scaffold.js";
|
|
17
|
+
import {
|
|
18
|
+
ensureAgentRuntimeDir,
|
|
19
|
+
readAgentMemory,
|
|
20
|
+
writeAgentMemory,
|
|
21
|
+
agentMemoryPath,
|
|
22
|
+
legacyAgentMemoryPath,
|
|
23
|
+
} from "../../../core/agent-memory.js";
|
|
17
24
|
import { agentToResponse } from "./shared.js";
|
|
18
25
|
|
|
19
26
|
// Lowercase the patch keys we accept on the vault and turn skills/tools into
|
|
@@ -114,6 +121,7 @@ export function register(app, { projects, project }) {
|
|
|
114
121
|
try {
|
|
115
122
|
writeAgentFile(p.path, slug, vault.fields || {}, vault.body || "");
|
|
116
123
|
ensureAgentDir(p.path, slug);
|
|
124
|
+
ensureAgentRuntimeDir(p, slug);
|
|
117
125
|
projects.rebuild(p.id);
|
|
118
126
|
res.status(201).json(agentToResponse(readAgents(p.path).find((a) => a.slug === slug)));
|
|
119
127
|
} catch (e) {
|
|
@@ -133,10 +141,7 @@ export function register(app, { projects, project }) {
|
|
|
133
141
|
const agents = readAgents(p.path);
|
|
134
142
|
const a = agents.find((x) => x.slug === req.params.slug);
|
|
135
143
|
if (!a) return res.status(404).json({ error: "agent not found" });
|
|
136
|
-
const
|
|
137
|
-
const memory = fs.existsSync(memPath)
|
|
138
|
-
? fs.readFileSync(memPath, "utf8")
|
|
139
|
-
: "";
|
|
144
|
+
const memory = readAgentMemory(p, a.slug);
|
|
140
145
|
res.json({ ...agentToResponse(a), memory, system: a.body || "" });
|
|
141
146
|
});
|
|
142
147
|
|
|
@@ -163,6 +168,7 @@ export function register(app, { projects, project }) {
|
|
|
163
168
|
Parent: parent || null,
|
|
164
169
|
});
|
|
165
170
|
ensureAgentDir(p.path, slug);
|
|
171
|
+
ensureAgentRuntimeDir(p, slug);
|
|
166
172
|
projects.rebuild(p.id);
|
|
167
173
|
const created = readAgents(p.path).find((a) => a.slug === slug);
|
|
168
174
|
res.status(201).json(agentToResponse(created));
|
|
@@ -203,6 +209,7 @@ export function register(app, { projects, project }) {
|
|
|
203
209
|
try {
|
|
204
210
|
writeAgentFile(p.path, slug, fields, body);
|
|
205
211
|
ensureAgentDir(p.path, slug);
|
|
212
|
+
ensureAgentRuntimeDir(p, slug);
|
|
206
213
|
projects.rebuild(p.id);
|
|
207
214
|
const updated = readAgents(p.path).find((a) => a.slug === slug);
|
|
208
215
|
res.json(agentToResponse(updated));
|
|
@@ -211,18 +218,20 @@ export function register(app, { projects, project }) {
|
|
|
211
218
|
}
|
|
212
219
|
});
|
|
213
220
|
|
|
214
|
-
// Delete an agent: removes .apc/agents/<slug>.md and
|
|
221
|
+
// Delete an agent: removes .apc/agents/<slug>.md and runtime data dir.
|
|
215
222
|
app.delete("/projects/:pid/agents/:slug", (req, res) => {
|
|
216
223
|
const p = project(req, res);
|
|
217
224
|
if (!p) return;
|
|
218
225
|
const slug = req.params.slug;
|
|
219
226
|
const file = path.join(p.path, ".apc", "agents", `${slug}.md`);
|
|
220
|
-
const
|
|
221
|
-
|
|
227
|
+
const runtimeDir = path.dirname(agentMemoryPath(p, slug));
|
|
228
|
+
const legacyDir = path.dirname(legacyAgentMemoryPath(p.path, slug));
|
|
229
|
+
if (!fs.existsSync(file) && !fs.existsSync(runtimeDir) && !fs.existsSync(legacyDir))
|
|
222
230
|
return res.status(404).json({ error: "agent not found" });
|
|
223
231
|
try {
|
|
224
232
|
if (fs.existsSync(file)) fs.rmSync(file);
|
|
225
|
-
if (fs.existsSync(
|
|
233
|
+
if (fs.existsSync(runtimeDir)) fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
234
|
+
if (fs.existsSync(legacyDir)) fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
226
235
|
projects.rebuild(p.id);
|
|
227
236
|
res.json({ ok: true });
|
|
228
237
|
} catch (e) {
|
|
@@ -257,15 +266,7 @@ export function register(app, { projects, project }) {
|
|
|
257
266
|
app.get("/projects/:pid/agents/:slug/memory", (req, res) => {
|
|
258
267
|
const p = project(req, res);
|
|
259
268
|
if (!p) return;
|
|
260
|
-
|
|
261
|
-
p.path,
|
|
262
|
-
".apc",
|
|
263
|
-
"agents",
|
|
264
|
-
req.params.slug,
|
|
265
|
-
"memory.md"
|
|
266
|
-
);
|
|
267
|
-
if (!fs.existsSync(memPath)) return res.json({ body: "" });
|
|
268
|
-
res.json({ body: fs.readFileSync(memPath, "utf8") });
|
|
269
|
+
res.json({ body: readAgentMemory(p, req.params.slug) });
|
|
269
270
|
});
|
|
270
271
|
|
|
271
272
|
app.put("/projects/:pid/agents/:slug/memory", (req, res) => {
|
|
@@ -274,10 +275,7 @@ export function register(app, { projects, project }) {
|
|
|
274
275
|
const { body } = req.body || {};
|
|
275
276
|
if (typeof body !== "string")
|
|
276
277
|
return res.status(400).json({ error: "body must be string" });
|
|
277
|
-
|
|
278
|
-
fs.mkdirSync(path.join(dir, "sessions"), { recursive: true });
|
|
279
|
-
const memPath = path.join(dir, "memory.md");
|
|
280
|
-
fs.writeFileSync(memPath, body);
|
|
278
|
+
writeAgentMemory(p, req.params.slug, body);
|
|
281
279
|
projects.rebuild(p.id);
|
|
282
280
|
res.json({ ok: true, bytes: Buffer.byteLength(body, "utf8") });
|
|
283
281
|
});
|
|
@@ -31,7 +31,7 @@ export function register(app, { projects, config }) {
|
|
|
31
31
|
for (const p of targetProjects) {
|
|
32
32
|
if (!p) continue;
|
|
33
33
|
|
|
34
|
-
// 1)
|
|
34
|
+
// 1) Legacy session files in the repo (.apc/agents/<slug>/sessions/)
|
|
35
35
|
const sessionAgentsDir = path.join(p.path, ".apc", "agents");
|
|
36
36
|
if (fs.existsSync(sessionAgentsDir)) {
|
|
37
37
|
for (const slug of fs.readdirSync(sessionAgentsDir)) {
|
|
@@ -6,6 +6,8 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { appendErrorTrace, previewText } from "../../../core/logging.js";
|
|
9
|
+
import { readAgents } from "../../../core/parser.js";
|
|
10
|
+
import { agentMemoryPath } from "../../../core/agent-memory.js";
|
|
9
11
|
|
|
10
12
|
export const nowIso = () =>
|
|
11
13
|
new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
@@ -110,15 +112,10 @@ export function makeTopProjectResolver(projects) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// Pick the memory.md to use when /memory is called without an agent ref.
|
|
113
|
-
// Prefer the first
|
|
115
|
+
// Prefer the first agent's runtime-local memory; else project-level .apc/memory.md.
|
|
114
116
|
export function resolveMemoryPath(p) {
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
const slugs = fs.readdirSync(agentsDir).filter((s) =>
|
|
118
|
-
fs.statSync(path.join(agentsDir, s)).isDirectory()
|
|
119
|
-
);
|
|
120
|
-
if (slugs.length) return path.join(agentsDir, slugs[0], "memory.md");
|
|
121
|
-
}
|
|
117
|
+
const firstAgent = readAgents(p.path)[0];
|
|
118
|
+
if (firstAgent) return agentMemoryPath(p, firstAgent.slug);
|
|
122
119
|
return path.join(p.path, ".apc", "memory.md");
|
|
123
120
|
}
|
|
124
121
|
|
|
@@ -28,6 +28,7 @@ import transcribeAudio from "./tools/transcribe-audio.js";
|
|
|
28
28
|
import askQuestions from "./tools/ask-questions.js";
|
|
29
29
|
import createTask from "./tools/create-task.js";
|
|
30
30
|
import listTasks from "./tools/list-tasks.js";
|
|
31
|
+
import discoverTools from "./tools/discover-tools.js";
|
|
31
32
|
import { createPermissionGuard } from "./helpers.js";
|
|
32
33
|
import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
|
|
33
34
|
|
|
@@ -62,6 +63,7 @@ const NATIVE_TOOLS = [
|
|
|
62
63
|
askQuestions,
|
|
63
64
|
createTask,
|
|
64
65
|
listTasks,
|
|
66
|
+
discoverTools,
|
|
65
67
|
];
|
|
66
68
|
|
|
67
69
|
// Registry-backed bridges. Categories can be overridden per-process via env
|
|
@@ -78,67 +80,254 @@ const TOOLS = [...NATIVE_TOOLS, ...BRIDGED_TOOLS];
|
|
|
78
80
|
|
|
79
81
|
export const TOOL_SCHEMAS = TOOLS.map((tool) => tool.schema);
|
|
80
82
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// free tier caps you at 6-12 K TPM.
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Lazy tools: base set (always loaded) + on-demand set (revealed via
|
|
85
|
+
// discover_tools). Motivation: full TOOL_SCHEMAS is ~25 KB / ~6.3 K tokens —
|
|
86
|
+
// too much when Groq's free tier caps you at 6-12 K TPM. The base set is
|
|
87
|
+
// ~24 tools (the ones a Telegram chat actually reaches for); everything else
|
|
88
|
+
// (browser/Puppeteer, fetch, web_search, runtime delegation, voice, …) stays
|
|
89
|
+
// off the wire until the model asks for it with discover_tools().
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
// Always loaded on lightweight channels. Covers messages, files, memory,
|
|
93
|
+
// sessions, projects/inventory, basic shell, tasks, skills, and discovery.
|
|
94
|
+
export const BASE_TOOL_NAMES = new Set([
|
|
95
|
+
// Discovery — the entry point to everything not loaded here.
|
|
96
|
+
"discover_tools",
|
|
97
|
+
// Inventory — the model needs these to know what exists.
|
|
88
98
|
"list_projects",
|
|
89
99
|
"list_agents",
|
|
90
100
|
"list_mcps",
|
|
91
101
|
"list_skills",
|
|
92
|
-
|
|
102
|
+
"load_skill",
|
|
103
|
+
// Memory + identity.
|
|
93
104
|
"read_agent_memory",
|
|
94
|
-
"
|
|
95
|
-
// Self-memory: jot durable facts so they survive across sessions.
|
|
105
|
+
"read_self_memory",
|
|
96
106
|
"remember",
|
|
97
|
-
|
|
107
|
+
"set_identity",
|
|
108
|
+
// Sessions + messages (self-recall + channel history).
|
|
98
109
|
"search_sessions",
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
//
|
|
102
|
-
"load_skill",
|
|
103
|
-
// Channels the user expects out of any super-agent turn.
|
|
110
|
+
"search_messages",
|
|
111
|
+
"tail_messages",
|
|
112
|
+
// Channels + conversation control + lightweight delegation.
|
|
104
113
|
"send_telegram",
|
|
105
|
-
|
|
114
|
+
"ask_questions",
|
|
106
115
|
"call_agent",
|
|
107
|
-
//
|
|
116
|
+
// Tasks (very common ask via chat).
|
|
108
117
|
"create_task",
|
|
109
118
|
"list_tasks",
|
|
119
|
+
// Files + basic shell — frequent enough on chat to keep hot.
|
|
120
|
+
"read_file",
|
|
121
|
+
"write_file",
|
|
122
|
+
"edit_file",
|
|
123
|
+
"list_files",
|
|
124
|
+
"search_files",
|
|
125
|
+
"run_shell",
|
|
110
126
|
]);
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
// Channels that get the FULL registry up front (deliberate, user-picked model,
|
|
129
|
+
// no cheap-tier TPM cap). Everything else is a "lightweight" channel and starts
|
|
130
|
+
// on BASE_TOOL_NAMES with discover_tools to expand.
|
|
131
|
+
const FULL_CHANNELS = new Set(["routine", "api", "web", "code", "terminal"]);
|
|
132
|
+
|
|
133
|
+
// Category labels for grouping the discover_tools catalog. Native tools have no
|
|
134
|
+
// registry category, so we assign one here; bridged tools carry their own
|
|
135
|
+
// (browser/fetch/search/file) from registry-bridge.js.
|
|
136
|
+
const NATIVE_CATEGORY = {
|
|
137
|
+
discover_tools: "system",
|
|
138
|
+
set_permission_mode: "system",
|
|
139
|
+
list_projects: "inventory",
|
|
140
|
+
list_agents: "inventory",
|
|
141
|
+
list_vault_agents: "inventory",
|
|
142
|
+
list_mcps: "inventory",
|
|
143
|
+
list_skills: "inventory",
|
|
144
|
+
load_skill: "skills",
|
|
145
|
+
import_agent: "agents",
|
|
146
|
+
add_project: "projects",
|
|
147
|
+
call_agent: "agents",
|
|
148
|
+
call_runtime: "runtime",
|
|
149
|
+
call_mcp: "mcp",
|
|
150
|
+
read_agent_memory: "memory",
|
|
151
|
+
read_self_memory: "memory",
|
|
152
|
+
remember: "memory",
|
|
153
|
+
set_identity: "identity",
|
|
154
|
+
search_sessions: "sessions",
|
|
155
|
+
search_messages: "messages",
|
|
156
|
+
tail_messages: "messages",
|
|
157
|
+
send_telegram: "messages",
|
|
158
|
+
ask_questions: "conversation",
|
|
159
|
+
create_task: "tasks",
|
|
160
|
+
list_tasks: "tasks",
|
|
161
|
+
transcribe_audio: "voice",
|
|
162
|
+
read_file: "files",
|
|
163
|
+
write_file: "files",
|
|
164
|
+
edit_file: "files",
|
|
165
|
+
list_files: "files",
|
|
166
|
+
search_files: "files",
|
|
167
|
+
run_shell: "shell",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function categoryOf(tool) {
|
|
171
|
+
return tool.category || NATIVE_CATEGORY[tool.name] || "other";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function oneLine(desc = "") {
|
|
175
|
+
const flat = String(desc).replace(/\s+/g, " ").trim();
|
|
176
|
+
if (flat.length <= 120) return flat;
|
|
177
|
+
return flat.slice(0, 117).trimEnd() + "…";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Static metadata index for every tool — name, schema, category, short blurb.
|
|
181
|
+
// Used by the per-turn tool session for the catalog and activation lookups.
|
|
182
|
+
const TOOL_META = TOOLS.map((t) => ({
|
|
183
|
+
name: t.name,
|
|
184
|
+
schema: t.schema,
|
|
185
|
+
category: categoryOf(t),
|
|
186
|
+
description: oneLine(t.schema?.function?.description),
|
|
187
|
+
}));
|
|
188
|
+
const META_BY_NAME = new Map(TOOL_META.map((m) => [m.name, m]));
|
|
189
|
+
|
|
190
|
+
export const BASE_TOOL_SCHEMAS = TOOLS
|
|
191
|
+
.filter((t) => BASE_TOOL_NAMES.has(t.name))
|
|
114
192
|
.map((t) => t.schema);
|
|
115
193
|
|
|
194
|
+
// Back-compat alias: a few callers/tests historically referenced the "core"
|
|
195
|
+
// subset. The base set supersedes it.
|
|
196
|
+
export const CORE_TOOL_SCHEMAS = BASE_TOOL_SCHEMAS;
|
|
197
|
+
|
|
198
|
+
const schemaName = (s) => s?.function?.name || s?.name;
|
|
199
|
+
|
|
116
200
|
/**
|
|
117
|
-
* Choose the tool schema list for a
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
201
|
+
* Choose the INITIAL tool schema list for a channel. Full channels get the
|
|
202
|
+
* whole registry; lightweight channels (telegram/desktop/deck/web_sidebar) get
|
|
203
|
+
* the base set and expand on demand via discover_tools. `full: true` forces the
|
|
204
|
+
* complete registry regardless of channel.
|
|
121
205
|
*/
|
|
122
206
|
export function schemasForChannel(channel, { full = false } = {}) {
|
|
123
|
-
if (full) return TOOL_SCHEMAS;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
if (full || FULL_CHANNELS.has(channel)) return TOOL_SCHEMAS;
|
|
208
|
+
return BASE_TOOL_SCHEMAS;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Per-turn tool session: tracks which tools are live, exposes the catalog of
|
|
213
|
+
* not-yet-loaded tools, and activates more on demand. The agent loop reads
|
|
214
|
+
* `pending` after each iteration and merges the new schemas into the live set,
|
|
215
|
+
* so activated tools become callable on the model's next step.
|
|
216
|
+
*
|
|
217
|
+
* `allowedTools` mirrors the role gate: "*" = unrestricted, [] = nothing, an
|
|
218
|
+
* array = allowlist. Both the initial set AND any activation respect it, so a
|
|
219
|
+
* limited sender can't discover its way past the gate.
|
|
220
|
+
*/
|
|
221
|
+
export function createToolSession(channel, { full = false, allowedTools = "*" } = {}) {
|
|
222
|
+
const allowAll = allowedTools === "*";
|
|
223
|
+
const allow = allowAll || !Array.isArray(allowedTools) ? null : new Set(allowedTools);
|
|
224
|
+
const permits = (name) => allowAll || (allow ? allow.has(name) : false);
|
|
225
|
+
|
|
226
|
+
// If the role gate is "[]" (no tools), start empty and stay empty.
|
|
227
|
+
const gateEmpty = Array.isArray(allowedTools) && allowedTools.length === 0;
|
|
228
|
+
|
|
229
|
+
const initial = (gateEmpty ? [] : schemasForChannel(channel, { full }))
|
|
230
|
+
.filter((s) => permits(schemaName(s)));
|
|
231
|
+
const activeNames = new Set(initial.map(schemaName));
|
|
232
|
+
|
|
233
|
+
const session = {
|
|
234
|
+
channel,
|
|
235
|
+
initialSchemas: initial,
|
|
236
|
+
pending: [],
|
|
237
|
+
activeNames,
|
|
238
|
+
|
|
239
|
+
// Tools that exist but aren't loaded yet (and are permitted by the gate).
|
|
240
|
+
notLoaded() {
|
|
241
|
+
return TOOL_META.filter((m) => !activeNames.has(m.name) && permits(m.name));
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Catalog response for discover_tools() with no args: grouped by category.
|
|
245
|
+
catalogResponse() {
|
|
246
|
+
const pool = session.notLoaded();
|
|
247
|
+
const byCategory = {};
|
|
248
|
+
for (const m of pool) {
|
|
249
|
+
(byCategory[m.category] ||= []).push({ name: m.name, description: m.description });
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
loaded_count: activeNames.size,
|
|
254
|
+
available_count: pool.length,
|
|
255
|
+
categories: byCategory,
|
|
256
|
+
hint:
|
|
257
|
+
"Activá lo que necesites con discover_tools({ category: \"<cat>\" }) o " +
|
|
258
|
+
"discover_tools({ names: [\"tool_a\", \"tool_b\"] }). Quedan disponibles desde tu próximo paso.",
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// Activate by exact names and/or whole category. Pushes new schemas to
|
|
263
|
+
// `pending` for the agent loop to merge.
|
|
264
|
+
activate({ names, category } = {}) {
|
|
265
|
+
const targets = new Set();
|
|
266
|
+
if (Array.isArray(names)) for (const n of names) targets.add(n);
|
|
267
|
+
if (typeof category === "string" && category.trim()) {
|
|
268
|
+
const cat = category.trim();
|
|
269
|
+
for (const m of TOOL_META) if (m.category === cat) targets.add(m.name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const activated = [];
|
|
273
|
+
const alreadyLoaded = [];
|
|
274
|
+
const unknown = [];
|
|
275
|
+
const denied = [];
|
|
276
|
+
for (const name of targets) {
|
|
277
|
+
const meta = META_BY_NAME.get(name);
|
|
278
|
+
if (!meta) { unknown.push(name); continue; }
|
|
279
|
+
if (!permits(name)) { denied.push(name); continue; }
|
|
280
|
+
if (activeNames.has(name)) { alreadyLoaded.push(name); continue; }
|
|
281
|
+
activeNames.add(name);
|
|
282
|
+
session.pending.push(meta.schema);
|
|
283
|
+
activated.push(name);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
ok: activated.length > 0 || (unknown.length === 0 && denied.length === 0),
|
|
288
|
+
activated,
|
|
289
|
+
already_loaded: alreadyLoaded,
|
|
290
|
+
...(unknown.length ? { unknown } : {}),
|
|
291
|
+
...(denied.length ? { denied } : {}),
|
|
292
|
+
note: activated.length
|
|
293
|
+
? `Activé ${activated.length} tool(s): ${activated.join(", ")}. Ya las podés usar desde tu próximo paso.`
|
|
294
|
+
: "No se activó ninguna tool nueva.",
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return session;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Compact "tools you can activate" block for the system prompt: instructions +
|
|
304
|
+
* just the NAMES (no schemas) of not-loaded tools, grouped by category. Returns
|
|
305
|
+
* "" when nothing is pending (full channels), so it's omitted from the prompt.
|
|
306
|
+
*/
|
|
307
|
+
export function buildLazyToolsBlock(session) {
|
|
308
|
+
if (!session) return "";
|
|
309
|
+
const pool = session.notLoaded();
|
|
310
|
+
if (pool.length === 0) return "";
|
|
311
|
+
|
|
312
|
+
const byCategory = {};
|
|
313
|
+
for (const m of pool) (byCategory[m.category] ||= []).push(m.name);
|
|
314
|
+
const lines = Object.keys(byCategory)
|
|
315
|
+
.sort()
|
|
316
|
+
.map((cat) => `- ${cat}: ${byCategory[cat].join(", ")}`);
|
|
317
|
+
|
|
318
|
+
return [
|
|
319
|
+
"# Tools adicionales (activación on-demand)",
|
|
320
|
+
"Tenés las tools base siempre cargadas. Estas otras EXISTEN pero no están",
|
|
321
|
+
"cargadas (para ahorrar tokens). Activalas cuando las necesites con",
|
|
322
|
+
"discover_tools — quedan disponibles desde tu próximo paso:",
|
|
323
|
+
' • discover_tools() → catálogo completo (nombre + descripción)',
|
|
324
|
+
' • discover_tools({ category: "browser" }) → activa toda una categoría',
|
|
325
|
+
' • discover_tools({ names: ["browser_navigate"] })→ activa tools puntuales',
|
|
326
|
+
"Si no encontrás la tool que buscás, llamá discover_tools() sin argumentos.",
|
|
327
|
+
"",
|
|
328
|
+
`Tools no cargadas (solo nombres, ${pool.length} en total):`,
|
|
329
|
+
...lines,
|
|
330
|
+
].join("\n");
|
|
142
331
|
}
|
|
143
332
|
|
|
144
333
|
export function makeToolHandlers(ctx) {
|
|
@@ -18,7 +18,29 @@
|
|
|
18
18
|
// Net result: adding a tool = adding one entry to registry.js. No file in
|
|
19
19
|
// super-agent-tools/tools/, no import in index.js.
|
|
20
20
|
|
|
21
|
+
import fs from "node:fs";
|
|
21
22
|
import { TOOL_DEFINITIONS } from "../../../core/tools/registry.js";
|
|
23
|
+
import { TOKEN_PATH } from "../../../core/config.js";
|
|
24
|
+
|
|
25
|
+
// The bridge POSTs to the daemon's OWN HTTP server, which is behind the bearer
|
|
26
|
+
// auth middleware (see api/shared.js). Without a token every bridged tool call
|
|
27
|
+
// (web_search, browser_*, http_*, glob, grep) comes back 401 "unauthorized" —
|
|
28
|
+
// which is exactly what Roby hit. We read the daemon's master token from
|
|
29
|
+
// ~/.apx/daemon.token (the same file the CLI authenticates with) and cache it.
|
|
30
|
+
let cachedToken = null;
|
|
31
|
+
function daemonToken() {
|
|
32
|
+
if (cachedToken !== null) return cachedToken;
|
|
33
|
+
cachedToken =
|
|
34
|
+
process.env.APX_TOKEN ||
|
|
35
|
+
(() => {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(TOKEN_PATH, "utf8").trim();
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
return cachedToken;
|
|
43
|
+
}
|
|
22
44
|
|
|
23
45
|
// Native handlers in super-agent-tools/tools/ that own these names. The bridge
|
|
24
46
|
// MUST skip them or the registry version (HTTP roundtrip) would shadow the
|
|
@@ -56,9 +78,13 @@ function buildHandler(entry) {
|
|
|
56
78
|
const method = String(entry.endpoint?.method || "POST").toUpperCase();
|
|
57
79
|
let url = `http://127.0.0.1:${port}${entry.endpoint?.path || ""}`;
|
|
58
80
|
|
|
81
|
+
const token = daemonToken();
|
|
59
82
|
const opts = {
|
|
60
83
|
method,
|
|
61
|
-
headers: {
|
|
84
|
+
headers: {
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
87
|
+
},
|
|
62
88
|
};
|
|
63
89
|
|
|
64
90
|
if (method === "GET" || method === "HEAD") {
|
|
@@ -114,6 +140,9 @@ export function buildBridgedTools(opts = {}) {
|
|
|
114
140
|
.filter(e => e.endpoint?.path)
|
|
115
141
|
.map(entry => ({
|
|
116
142
|
name: entry.name,
|
|
143
|
+
// Carried through so the lazy-tools catalog can group on-demand tools by
|
|
144
|
+
// their registry category (browser/fetch/search/file) for discover_tools.
|
|
145
|
+
category: entry.category,
|
|
117
146
|
schema: buildSchema(entry),
|
|
118
147
|
makeHandler: buildHandler(entry),
|
|
119
148
|
}));
|