@agentprojectcontext/apx 1.17.0 → 1.19.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 +1 -6
- package/src/cli/commands/status.js +33 -12
- package/src/cli/commands/telegram.js +23 -0
- package/src/cli/index.js +20 -2
- package/src/core/config.js +1 -1
- package/src/daemon/api.js +22 -0
- package/src/daemon/plugins/telegram.js +2 -0
- package/src/daemon/super-agent.js +50 -22
- package/src/tui/context/sdk-apx.tsx +32 -1
- package/src/tui/context/sync-apx.tsx +61 -1
- package/src/tui/routes/session/index.tsx +41 -8
- package/src/daemon/super-agent-langchain.js +0 -296
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"packageManager": "pnpm@10.25.0",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@langchain/anthropic": "^0.3.34",
|
|
39
|
-
"@langchain/community": "^0.3.59",
|
|
40
|
-
"@langchain/core": "^0.3.80",
|
|
41
|
-
"@langchain/ollama": "^0.2.4",
|
|
42
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
39
|
"@opentui/core": "^0.2.8",
|
|
44
40
|
"@opentui/keymap": "^0.2.8",
|
|
@@ -56,7 +52,6 @@
|
|
|
56
52
|
"express": "^4.21.0",
|
|
57
53
|
"fuzzysort": "^3.1.0",
|
|
58
54
|
"jsonc-parser": "^3.3.1",
|
|
59
|
-
"langchain": "^0.3.37",
|
|
60
55
|
"node-fetch": "^3.3.2",
|
|
61
56
|
"open": "^11.0.0",
|
|
62
57
|
"opentui-spinner": "^0.0.6",
|
|
@@ -79,17 +79,43 @@ export async function cmdStatus() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ── Telegram ───────────────────────────────────────────────────────────────
|
|
82
|
+
// Prefer the daemon's live view (real polling state). Fall back to the
|
|
83
|
+
// config file only when the daemon is unreachable. The config check must
|
|
84
|
+
// honour BOTH the legacy top-level bot_token AND per-channel tokens.
|
|
82
85
|
console.log(sec("Telegram"));
|
|
83
86
|
const tg = cfg.telegram || {};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
let tgLive = null;
|
|
88
|
+
if (daemonOk) {
|
|
89
|
+
try { tgLive = await http.get("/telegram/status"); } catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tgLive) {
|
|
93
|
+
const channels = tgLive.channels || [];
|
|
94
|
+
const polling = channels.filter((c) => c.polling).length;
|
|
95
|
+
if (!tgLive.enabled) {
|
|
96
|
+
console.log(` ${off("disabled in config")} run: ${CY}apx setup${R}`);
|
|
97
|
+
} else if (channels.length === 0) {
|
|
98
|
+
console.log(` ${off("enabled, no channels")} run: ${CY}apx setup${R}`);
|
|
99
|
+
} else if (polling === 0) {
|
|
100
|
+
console.log(` ${err("enabled, not polling")} run: ${CY}apx telegram start${R}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(` ${ok("polling")} ${val(`${polling}/${channels.length} channel${channels.length !== 1 ? "s" : ""}`)}`);
|
|
103
|
+
}
|
|
104
|
+
for (const c of channels) {
|
|
105
|
+
const mark = c.polling ? `${GR}●${R}` : `${RE}○${R}`;
|
|
106
|
+
const tok = c.bot_token_present ? "" : ` ${DI}(no token)${R}`;
|
|
107
|
+
const lastErr = c.last_error ? ` ${RE}${c.last_error}${R}` : "";
|
|
108
|
+
console.log(`${key(c.name)}${mark} ${DI}chat ${c.chat_id || "(unset)"}${R}${tok}${lastErr}`);
|
|
90
109
|
}
|
|
91
110
|
} else {
|
|
92
|
-
|
|
111
|
+
// Daemon down — best-effort from config. Token may be top-level or per-channel.
|
|
112
|
+
const hasToken = !!tg.bot_token || (tg.channels || []).some((c) => c.bot_token);
|
|
113
|
+
if (tg.enabled && hasToken) {
|
|
114
|
+
console.log(` ${off("configured")} ${DI}daemon down — start it to see live status${R}`);
|
|
115
|
+
console.log(`${key("channels")}${val((tg.channels?.length || 1) + " configured")}`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(` ${off("disabled")} run: ${CY}apx setup${R}`);
|
|
118
|
+
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
// ── Projects ───────────────────────────────────────────────────────────────
|
|
@@ -114,11 +140,6 @@ export async function cmdStatus() {
|
|
|
114
140
|
console.log();
|
|
115
141
|
}
|
|
116
142
|
|
|
117
|
-
function maskToken(token) {
|
|
118
|
-
if (!token || token.length < 12) return "***";
|
|
119
|
-
return token.slice(0, 6) + "…" + token.slice(-4);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
143
|
function formatUptime(s) {
|
|
123
144
|
if (!s) return "?";
|
|
124
145
|
if (s < 60) return `${s}s`;
|
|
@@ -36,6 +36,29 @@ export async function cmdTelegramStatus() {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export async function cmdTelegramStart() {
|
|
40
|
+
const r = await http.post("/telegram/start", {});
|
|
41
|
+
const channels = r.status?.channels || [];
|
|
42
|
+
const polling = channels.filter((c) => c.polling).length;
|
|
43
|
+
if (channels.length === 0) {
|
|
44
|
+
console.log("⚠️ no telegram channels configured — run: apx telegram setup");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (polling === 0) {
|
|
48
|
+
console.log("⚠️ polling did not start — check telegram.enabled in ~/.apx/config.json and that a bot_token is set");
|
|
49
|
+
for (const c of channels) {
|
|
50
|
+
if (c.last_error) console.log(` ${c.name}: ${c.last_error}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log(`✅ telegram polling (${polling}/${channels.length} channel${channels.length !== 1 ? "s" : ""})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function cmdTelegramStop() {
|
|
58
|
+
await http.post("/telegram/stop", {});
|
|
59
|
+
console.log("⏹ telegram polling stopped (config unchanged — apx telegram start to resume)");
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
export function cmdTelegramSetup() {
|
|
40
63
|
console.log(`Edit ~/.apx/config.json — telegram section:
|
|
41
64
|
|
package/src/cli/index.js
CHANGED
|
@@ -52,6 +52,8 @@ import {
|
|
|
52
52
|
cmdTelegramSend,
|
|
53
53
|
cmdTelegramStatus,
|
|
54
54
|
cmdTelegramSetup,
|
|
55
|
+
cmdTelegramStart,
|
|
56
|
+
cmdTelegramStop,
|
|
55
57
|
} from "./commands/telegram.js";
|
|
56
58
|
import { cmdMessagesTail, cmdMessagesSearch, cmdMessagesChat } from "./commands/messages.js";
|
|
57
59
|
import { cmdLog } from "./commands/log.js";
|
|
@@ -598,13 +600,15 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
598
600
|
telegram: topic({
|
|
599
601
|
title: "apx telegram",
|
|
600
602
|
summary: "Configure, inspect, and send through the Telegram bridge.",
|
|
601
|
-
usage: ["apx telegram <send|status|setup> [args] [--flags]"],
|
|
603
|
+
usage: ["apx telegram <send|status|start|stop|setup> [args] [--flags]"],
|
|
602
604
|
commands: [
|
|
603
605
|
["send \"text\"", "Send a Telegram message."],
|
|
604
606
|
["status", "Show Telegram plugin status."],
|
|
607
|
+
["start", "Start polling on every configured channel."],
|
|
608
|
+
["stop", "Stop polling (config stays intact)."],
|
|
605
609
|
["setup", "Print setup guidance."],
|
|
606
610
|
],
|
|
607
|
-
examples: ["apx telegram status", "apx telegram send \"hello\" --chat 123456"],
|
|
611
|
+
examples: ["apx telegram status", "apx telegram start", "apx telegram send \"hello\" --chat 123456"],
|
|
608
612
|
}),
|
|
609
613
|
"telegram send": topic({
|
|
610
614
|
title: "apx telegram send",
|
|
@@ -626,6 +630,18 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
626
630
|
usage: ["apx telegram status"],
|
|
627
631
|
examples: ["apx telegram status"],
|
|
628
632
|
}),
|
|
633
|
+
"telegram start": topic({
|
|
634
|
+
title: "apx telegram start",
|
|
635
|
+
summary: "Start Telegram polling on every configured channel.",
|
|
636
|
+
usage: ["apx telegram start"],
|
|
637
|
+
examples: ["apx telegram start"],
|
|
638
|
+
}),
|
|
639
|
+
"telegram stop": topic({
|
|
640
|
+
title: "apx telegram stop",
|
|
641
|
+
summary: "Stop Telegram polling (config stays intact; resume with apx telegram start).",
|
|
642
|
+
usage: ["apx telegram stop"],
|
|
643
|
+
examples: ["apx telegram stop"],
|
|
644
|
+
}),
|
|
629
645
|
"telegram setup": topic({
|
|
630
646
|
title: "apx telegram setup",
|
|
631
647
|
summary: "Print Telegram setup guidance.",
|
|
@@ -1441,6 +1457,8 @@ async function dispatch(cmd, rest) {
|
|
|
1441
1457
|
const a = parseArgs(rest.slice(1));
|
|
1442
1458
|
if (sub === "send") await cmdTelegramSend(a);
|
|
1443
1459
|
else if (sub === "status") await cmdTelegramStatus();
|
|
1460
|
+
else if (sub === "start") await cmdTelegramStart();
|
|
1461
|
+
else if (sub === "stop") await cmdTelegramStop();
|
|
1444
1462
|
else if (sub === "setup") cmdTelegramSetup();
|
|
1445
1463
|
else die(`unknown telegram subcommand: ${sub || "(none)"}`);
|
|
1446
1464
|
break;
|
package/src/core/config.js
CHANGED
|
@@ -50,7 +50,7 @@ const DEFAULT_CONFIG = {
|
|
|
50
50
|
name: "apx",
|
|
51
51
|
model: "", // e.g. "ollama:llama3.2:3b"
|
|
52
52
|
system: "", // optional override; defaults baked into super-agent.js
|
|
53
|
-
permission_mode: "
|
|
53
|
+
permission_mode: "total", // total | automatico | permiso
|
|
54
54
|
allowed_tools: [], // used by permission_mode="permiso"
|
|
55
55
|
},
|
|
56
56
|
engines: {
|
package/src/daemon/api.js
CHANGED
|
@@ -442,6 +442,28 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
442
442
|
res.json(telegram.status());
|
|
443
443
|
});
|
|
444
444
|
|
|
445
|
+
// POST /telegram/start — (re)start polling for every configured channel.
|
|
446
|
+
app.post("/telegram/start", (_req, res) => {
|
|
447
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
448
|
+
try {
|
|
449
|
+
telegram.start();
|
|
450
|
+
res.json({ ok: true, status: telegram.status() });
|
|
451
|
+
} catch (e) {
|
|
452
|
+
res.status(502).json({ error: e.message });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// POST /telegram/stop — stop polling on every channel (config stays intact).
|
|
457
|
+
app.post("/telegram/stop", (_req, res) => {
|
|
458
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
459
|
+
try {
|
|
460
|
+
telegram.stop();
|
|
461
|
+
res.json({ ok: true, status: telegram.status() });
|
|
462
|
+
} catch (e) {
|
|
463
|
+
res.status(502).json({ error: e.message });
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
445
467
|
app.post("/telegram/send", async (req, res) => {
|
|
446
468
|
const { chat_id, text, channel } = req.body || {};
|
|
447
469
|
if (!text) return res.status(400).json({ error: "text required" });
|
|
@@ -344,6 +344,8 @@ class ChannelPoller {
|
|
|
344
344
|
while (this.polling) {
|
|
345
345
|
try {
|
|
346
346
|
const updates = await this._getUpdates();
|
|
347
|
+
// A successful poll clears any stale error so status reflects recovery.
|
|
348
|
+
this.lastError = null;
|
|
347
349
|
for (const u of updates) {
|
|
348
350
|
await this._handleUpdate(u);
|
|
349
351
|
this.offset = u.update_id + 1;
|
|
@@ -42,6 +42,11 @@ You are **APX** — Manuel's personal assistant running on his Mac.
|
|
|
42
42
|
You are NOT a code analyzer, NOT a generic chatbot, NOT a tutor.
|
|
43
43
|
You are an **action agent**: you USE TOOLS to do real things on Manuel's system.
|
|
44
44
|
|
|
45
|
+
# Sobre Manuel (el usuario)
|
|
46
|
+
- Se llama **Manuel**, es un desarrollador argentino.
|
|
47
|
+
- Está en **Argentina**, timezone **UTC-3**. Cuando hables de horarios, asumí UTC-3 salvo que diga otra cosa.
|
|
48
|
+
- Habla **español rioplatense** (voseo). Hablale así.
|
|
49
|
+
|
|
45
50
|
# Language — non-negotiable
|
|
46
51
|
ALWAYS reply in **Spanish (rioplatense, voseo when natural)** unless Manuel
|
|
47
52
|
explicitly writes to you in another language for that turn. The user is an
|
|
@@ -49,6 +54,17 @@ Argentinian developer; English replies feel broken to him. If you find
|
|
|
49
54
|
yourself writing English, stop and rewrite in Spanish before sending.
|
|
50
55
|
This rule beats every other formatting hint below.
|
|
51
56
|
|
|
57
|
+
# Cómo se reciben los mensajes de audio
|
|
58
|
+
Cuando el usuario manda un audio por Telegram, el sistema lo transcribe
|
|
59
|
+
automáticamente y te lo entrega en este formato:
|
|
60
|
+
[audio] <texto transcripto del audio>
|
|
61
|
+
|
|
62
|
+
Cuando veas "[audio]" al inicio del mensaje, significa que el usuario HABLÓ ese
|
|
63
|
+
mensaje — lo que viene después es la transcripción exacta de lo que dijo.
|
|
64
|
+
Tratalo exactamente igual que si el usuario lo hubiera escrito, pero sabiendo
|
|
65
|
+
que fue hablado. Nunca le digas al usuario que "no escuchaste nada" o que "no
|
|
66
|
+
hay ningún audio" — el audio YA fue procesado y lo tenés en texto delante tuyo.
|
|
67
|
+
|
|
52
68
|
# What you must NOT do
|
|
53
69
|
- Do NOT explain code or write essays about "the provided snippet".
|
|
54
70
|
- Do NOT describe what a tool *would* do — call it and report the result.
|
|
@@ -57,15 +73,39 @@ This rule beats every other formatting hint below.
|
|
|
57
73
|
- If a user message is short or ambiguous, ASK one short clarifying question
|
|
58
74
|
in Spanish — do not invent a topic.
|
|
59
75
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
- ~/.apx/
|
|
67
|
-
- ~/.apx/
|
|
76
|
+
# Qué es APX y qué sos vos
|
|
77
|
+
**Vos SOS el superagente de APX.** No sos un modelo genérico — sos el agente
|
|
78
|
+
dispatcher que corre dentro del daemon de APX, y el usuario te habla por Telegram.
|
|
79
|
+
|
|
80
|
+
APX es un daemon + CLI local para proyectos APC (Agent Project Context):
|
|
81
|
+
- El daemon corre en localhost:7430 y mantiene estado en ~/.apx/
|
|
82
|
+
- ~/.apx/config.json: config del daemon, engines, Telegram, ajustes del superagente
|
|
83
|
+
- ~/.apx/projects/default: tu workspace por defecto; usalo para trabajo de sistema cuando el usuario no nombra un proyecto
|
|
84
|
+
- ~/.apx/agents: vault de templates de agentes reutilizables
|
|
85
|
+
- ~/.apx/messages: logs de canales globales como Telegram
|
|
86
|
+
- Los **proyectos** son carpetas en disco con AGENTS.md y .apc/project.json (agentes, memorias, skills, hints de MCP, comandos, routines). Por ahora el único proyecto del usuario se llama \`default\`.
|
|
87
|
+
|
|
88
|
+
Comandos de la CLI de APX (por si el usuario pregunta cómo hacer algo):
|
|
89
|
+
- \`apx daemon start|stop|status|logs\` — controlar el daemon
|
|
90
|
+
- \`apx status\` — estado completo de un vistazo (daemon, superagente, engines, Telegram, proyectos)
|
|
91
|
+
- \`apx code\` — asistente de coding en terminal (TUI)
|
|
92
|
+
- \`apx log\` / \`apx log -f\` — ver/seguir el log unificado en ~/.apx/logs/apx.log
|
|
93
|
+
- \`apx update\` — actualizar APX a la última versión de npm
|
|
94
|
+
- \`apx search <query>\` — buscar en mensajes/proyectos
|
|
95
|
+
- \`apx project add <path>\` — registrar un proyecto
|
|
96
|
+
- \`apx telegram status|start|stop|send\` — controlar el canal de Telegram
|
|
97
|
+
- \`apx routine list|add|run\` — routines programadas
|
|
98
|
+
- \`apx permission show|set\` — modo de permisos
|
|
99
|
+
- \`apx setup\` — wizard de configuración inicial
|
|
100
|
+
|
|
101
|
+
Tus tools (resumen — usalas, no las describas): list_projects / list_agents /
|
|
102
|
+
list_mcps / list_skills para inventario; read_file / list_files / read_agent_memory
|
|
103
|
+
para leer; write_file / add_project / import_agent para mutar; run_shell para
|
|
104
|
+
comandos; call_agent / call_runtime para delegar; send_telegram para mandar
|
|
105
|
+
mensajes/fotos/audio; load_skill para traer docs; web_search / browser_screenshot
|
|
106
|
+
para la web; set_identity para cambiar tu nombre/personalidad.
|
|
68
107
|
|
|
108
|
+
# How you operate
|
|
69
109
|
APC projects are filesystem projects anywhere on disk with AGENTS.md and .apc/project.json. They contain agents, memories, skills, MCP hints, commands, and routines. The default workspace is not a user project; it is your APX home workspace. Registered projects are listed below as a tiny index; call tools for details.
|
|
70
110
|
|
|
71
111
|
Useful CLI facts:
|
|
@@ -74,6 +114,7 @@ Useful CLI facts:
|
|
|
74
114
|
- Routine design: if the user asks for an agent to think, decide, write, or reply, create an exec_agent routine with spec.agent and spec.prompt. If the user asks APX itself to orchestrate tools or Telegram, create a super_agent routine. If the request is only a deterministic command, create a shell routine. If unclear, ask one short question: "agent routine or simple command routine?"
|
|
75
115
|
- Routine schedules: APX supports standard cron expressions (e.g. '*/5 * * * *'), OR 'every:<number><s|m|h|d>' (e.g. 'every:60s'), OR 'once:<iso-8601>'.
|
|
76
116
|
- Safe read-only shell checks such as apx --help, apx routine list, docker ps, find, ls, rg, grep can run in automatico without asking.
|
|
117
|
+
- Búsquedas en el filesystem: usá herramientas específicas y eficientes — \`find <dir> -name <patrón>\`, \`fd <patrón>\`, \`rg <texto>\` / \`grep -rn <texto>\`, o glob patterns concretos. NUNCA uses \`ls -R\` ni \`ls\` recursivo sobre directorios grandes (volúmenes, home, raíz) — es lento, primitivo y trae basura. Acotá siempre el directorio de búsqueda y el patrón.
|
|
77
118
|
|
|
78
119
|
Channel context:
|
|
79
120
|
- If the context note says Telegram, you are replying through Telegram. Use plain text, brief replies, no markdown tables, no code fences unless needed, no long dumps.
|
|
@@ -226,19 +267,6 @@ export async function runSuperAgent({
|
|
|
226
267
|
const sa = globalConfig.super_agent;
|
|
227
268
|
const activeModel = overrideModel || sa.model;
|
|
228
269
|
|
|
229
|
-
// Engine toggle: if config.super_agent.engine === "langchain", delegate to
|
|
230
|
-
// the LangChain AgentExecutor adapter. Default stays "native" (this loop).
|
|
231
|
-
// The toggle exists so we can A/B the two paths on the user's actual chat
|
|
232
|
-
// without committing to a full migration. See super-agent-langchain.js.
|
|
233
|
-
if (sa.engine === "langchain") {
|
|
234
|
-
const { runSuperAgentLangChain } = await import("./super-agent-langchain.js");
|
|
235
|
-
return runSuperAgentLangChain({
|
|
236
|
-
globalConfig, projects, plugins, registries,
|
|
237
|
-
prompt, previousMessages, contextNote,
|
|
238
|
-
onEvent, onToken, signal,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
270
|
// Tiny project hint — JUST names + ids, no detail. The model is expected to
|
|
243
271
|
// call list_agents / list_mcps / read_agent_memory / etc. for everything
|
|
244
272
|
// else. Keeping this short forces actual tool use instead of letting the
|
|
@@ -248,7 +276,7 @@ export async function runSuperAgent({
|
|
|
248
276
|
.map((p) => ` ${p.id}: ${p.id === 0 ? "[default]" : "[project]"} "${p.name}" (${p.path})`)
|
|
249
277
|
.join("\n");
|
|
250
278
|
|
|
251
|
-
const permissionMode = sa.permission_mode || "
|
|
279
|
+
const permissionMode = sa.permission_mode || "total";
|
|
252
280
|
const allowedTools = Array.isArray(sa.allowed_tools) ? sa.allowed_tools : [];
|
|
253
281
|
const permissionNote = [
|
|
254
282
|
"# Permission mode",
|
|
@@ -4,6 +4,7 @@ import { onCleanup } from "solid-js"
|
|
|
4
4
|
import fs from "node:fs"
|
|
5
5
|
import os from "node:os"
|
|
6
6
|
import path from "node:path"
|
|
7
|
+
import { spawn } from "node:child_process"
|
|
7
8
|
|
|
8
9
|
const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token")
|
|
9
10
|
|
|
@@ -20,6 +21,9 @@ export type ApxEvent =
|
|
|
20
21
|
| { type: "chunk"; sessionID: string; chunk: string }
|
|
21
22
|
| { type: "final"; sessionID: string; text: string; usage?: { input_tokens: number; output_tokens: number } }
|
|
22
23
|
| { type: "error"; sessionID: string; error: string }
|
|
24
|
+
| { type: "shell.start"; sessionID: string; shellID: string; command: string; cwd: string }
|
|
25
|
+
| { type: "shell.output"; sessionID: string; shellID: string; stream: "stdout" | "stderr"; chunk: string }
|
|
26
|
+
| { type: "shell.done"; sessionID: string; shellID: string; exitCode: number | null; signal: NodeJS.Signals | null }
|
|
23
27
|
|
|
24
28
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
25
29
|
name: "SDK",
|
|
@@ -102,6 +106,27 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
|
102
106
|
return (data as any).id as string
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
function runShell(sessionID: string, command: string, cwd: string = process.cwd()): Promise<{ shellID: string; exitCode: number | null }> {
|
|
110
|
+
const shellID = `sh-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
|
111
|
+
emitter.emit("event", { type: "shell.start", sessionID, shellID, command, cwd })
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const child = spawn(command, { shell: true, cwd, env: process.env })
|
|
114
|
+
child.stdout?.on("data", (buf) => {
|
|
115
|
+
emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stdout", chunk: buf.toString() })
|
|
116
|
+
})
|
|
117
|
+
child.stderr?.on("data", (buf) => {
|
|
118
|
+
emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stderr", chunk: buf.toString() })
|
|
119
|
+
})
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stderr", chunk: `[spawn error] ${err.message}\n` })
|
|
122
|
+
})
|
|
123
|
+
child.on("close", (code, signal) => {
|
|
124
|
+
emitter.emit("event", { type: "shell.done", sessionID, shellID, exitCode: code, signal })
|
|
125
|
+
resolve({ shellID, exitCode: code })
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
async function listSessions(): Promise<Array<{ id: string; title: string; updatedAt?: number }>> {
|
|
106
131
|
try {
|
|
107
132
|
const token = readToken()
|
|
@@ -146,7 +171,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
|
146
171
|
fork: async (_opts: any) => ({ data: undefined, error: new Error("not supported") }),
|
|
147
172
|
abort: async (_opts: any) => {},
|
|
148
173
|
prompt: async (_opts: any) => {},
|
|
149
|
-
shell: async (
|
|
174
|
+
shell: async (opts: { sessionID?: string; command?: string; cwd?: string }) => {
|
|
175
|
+
if (!opts?.command) return { data: undefined }
|
|
176
|
+
const sid = opts.sessionID || (await createSession())
|
|
177
|
+
const r = await runShell(sid, opts.command, opts.cwd)
|
|
178
|
+
return { data: r }
|
|
179
|
+
},
|
|
150
180
|
command: async (_opts: any) => {},
|
|
151
181
|
refresh: async () => {},
|
|
152
182
|
update: async (_opts: any) => ({ data: undefined }),
|
|
@@ -180,6 +210,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
|
180
210
|
streamChat,
|
|
181
211
|
createSession,
|
|
182
212
|
listSessions,
|
|
213
|
+
runShell,
|
|
183
214
|
}
|
|
184
215
|
},
|
|
185
216
|
})
|
|
@@ -13,10 +13,15 @@ export type ApxSession = {
|
|
|
13
13
|
export type ApxMessage = {
|
|
14
14
|
id: string
|
|
15
15
|
sessionID: string
|
|
16
|
-
role: "user" | "assistant"
|
|
16
|
+
role: "user" | "assistant" | "shell"
|
|
17
17
|
text: string
|
|
18
18
|
streaming?: boolean
|
|
19
19
|
error?: boolean
|
|
20
|
+
// Shell-specific
|
|
21
|
+
shellID?: string
|
|
22
|
+
command?: string
|
|
23
|
+
cwd?: string
|
|
24
|
+
exitCode?: number | null
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContext({
|
|
@@ -78,6 +83,55 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
|
|
|
78
83
|
setStore("previousMessages", (prev) => [...prev, { role: "assistant", content: e.text }])
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
if (ev.type === "shell.start") {
|
|
87
|
+
const e = ev
|
|
88
|
+
setStore(
|
|
89
|
+
"messages",
|
|
90
|
+
produce((draft) => {
|
|
91
|
+
;(draft[e.sessionID] ??= []).push({
|
|
92
|
+
id: e.shellID,
|
|
93
|
+
sessionID: e.sessionID,
|
|
94
|
+
role: "shell",
|
|
95
|
+
text: "",
|
|
96
|
+
streaming: true,
|
|
97
|
+
shellID: e.shellID,
|
|
98
|
+
command: e.command,
|
|
99
|
+
cwd: e.cwd,
|
|
100
|
+
})
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (ev.type === "shell.output") {
|
|
106
|
+
const e = ev
|
|
107
|
+
setStore(
|
|
108
|
+
"messages",
|
|
109
|
+
produce((draft) => {
|
|
110
|
+
const msgs = draft[e.sessionID]
|
|
111
|
+
if (!msgs) return
|
|
112
|
+
const target = msgs.find((m) => m.role === "shell" && m.shellID === e.shellID)
|
|
113
|
+
if (target) target.text += e.chunk
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ev.type === "shell.done") {
|
|
119
|
+
const e = ev
|
|
120
|
+
setStore(
|
|
121
|
+
"messages",
|
|
122
|
+
produce((draft) => {
|
|
123
|
+
const msgs = draft[e.sessionID]
|
|
124
|
+
if (!msgs) return
|
|
125
|
+
const target = msgs.find((m) => m.role === "shell" && m.shellID === e.shellID)
|
|
126
|
+
if (target) {
|
|
127
|
+
target.streaming = false
|
|
128
|
+
target.exitCode = e.exitCode
|
|
129
|
+
if (e.signal) target.text += `\n[killed by signal ${e.signal}]`
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
81
135
|
if (ev.type === "error") {
|
|
82
136
|
const e = ev
|
|
83
137
|
setStore(
|
|
@@ -121,6 +175,11 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
|
|
|
121
175
|
return id
|
|
122
176
|
}
|
|
123
177
|
|
|
178
|
+
async function runShell(command: string, cwd?: string) {
|
|
179
|
+
const sessionID = await ensureSession()
|
|
180
|
+
await sdk.runShell(sessionID, command, cwd ?? process.cwd())
|
|
181
|
+
}
|
|
182
|
+
|
|
124
183
|
async function sendMessage(text: string) {
|
|
125
184
|
const sessionID = await ensureSession()
|
|
126
185
|
const userMsg: ApxMessage = {
|
|
@@ -172,6 +231,7 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
|
|
|
172
231
|
async refresh() {},
|
|
173
232
|
},
|
|
174
233
|
sendMessage,
|
|
234
|
+
runShell,
|
|
175
235
|
ensureSession,
|
|
176
236
|
}
|
|
177
237
|
},
|
|
@@ -47,6 +47,35 @@ function AssistantBubble(props: { msg: ApxMessage }) {
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function ShellBubble(props: { msg: ApxMessage }) {
|
|
51
|
+
const { theme } = useTheme()
|
|
52
|
+
const header = () => {
|
|
53
|
+
const code = props.msg.exitCode
|
|
54
|
+
const status = props.msg.streaming
|
|
55
|
+
? "running"
|
|
56
|
+
: code === 0
|
|
57
|
+
? "exit 0"
|
|
58
|
+
: code == null
|
|
59
|
+
? "ended"
|
|
60
|
+
: `exit ${code}`
|
|
61
|
+
return `$ ${props.msg.command ?? ""} · ${status}`
|
|
62
|
+
}
|
|
63
|
+
const body = () => props.msg.text || (props.msg.streaming ? "…" : "(no output)")
|
|
64
|
+
return (
|
|
65
|
+
<box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
|
|
66
|
+
<text color={theme.warning ?? theme.primary} bold>
|
|
67
|
+
{header()}
|
|
68
|
+
</text>
|
|
69
|
+
<text
|
|
70
|
+
color={props.msg.exitCode && props.msg.exitCode !== 0 ? theme.error : theme.text}
|
|
71
|
+
wrap
|
|
72
|
+
>
|
|
73
|
+
{body()}
|
|
74
|
+
</text>
|
|
75
|
+
</box>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
export function Session() {
|
|
51
80
|
const dims = useTerminalDimensions()
|
|
52
81
|
const { theme } = useTheme()
|
|
@@ -109,7 +138,11 @@ export function Session() {
|
|
|
109
138
|
inputEl.clear()
|
|
110
139
|
setSending(true)
|
|
111
140
|
try {
|
|
112
|
-
|
|
141
|
+
if (text.startsWith("!") && text.length > 1) {
|
|
142
|
+
await sync.runShell(text.slice(1).trim())
|
|
143
|
+
} else {
|
|
144
|
+
await sync.sendMessage(text)
|
|
145
|
+
}
|
|
113
146
|
} catch (e) {
|
|
114
147
|
toast.error(e instanceof Error ? e : new Error(String(e)))
|
|
115
148
|
} finally {
|
|
@@ -132,17 +165,17 @@ export function Session() {
|
|
|
132
165
|
fallback={
|
|
133
166
|
<box paddingLeft={2} paddingTop={2}>
|
|
134
167
|
<text color={theme.textMuted} italic>
|
|
135
|
-
Type a message
|
|
168
|
+
Type a message to chat, or prefix with ! to run a shell command (e.g. !ls).
|
|
136
169
|
</text>
|
|
137
170
|
</box>
|
|
138
171
|
}
|
|
139
172
|
>
|
|
140
173
|
<For each={messages()}>
|
|
141
|
-
{(msg) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
{(msg) => {
|
|
175
|
+
if (msg.role === "user") return <UserBubble msg={msg} />
|
|
176
|
+
if (msg.role === "shell") return <ShellBubble msg={msg} />
|
|
177
|
+
return <AssistantBubble msg={msg} />
|
|
178
|
+
}}
|
|
146
179
|
</For>
|
|
147
180
|
</Show>
|
|
148
181
|
<box height={1} />
|
|
@@ -163,7 +196,7 @@ export function Session() {
|
|
|
163
196
|
inputEl = r
|
|
164
197
|
promptRef.set(makeRef(r))
|
|
165
198
|
}}
|
|
166
|
-
placeholder={sending() ? "Waiting for response…" : "Ask anything... (
|
|
199
|
+
placeholder={sending() ? "Waiting for response…" : "Ask anything... (prefix ! to run shell, e.g. !ls)"}
|
|
167
200
|
placeholderColor={theme.textMuted}
|
|
168
201
|
textColor={theme.text}
|
|
169
202
|
focusedTextColor={theme.text}
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
// LangChain adapter for the APX super-agent.
|
|
2
|
-
//
|
|
3
|
-
// Lives alongside the native loop in super-agent.js. Selected via
|
|
4
|
-
// config.super_agent.engine === "langchain" (default: "native"). The two
|
|
5
|
-
// implementations expose the same shape:
|
|
6
|
-
//
|
|
7
|
-
// { text, usage, name, trace } ← return value
|
|
8
|
-
// { globalConfig, projects, plugins, registries, prompt,
|
|
9
|
-
// previousMessages, contextNote, onEvent, onToken, signal } ← input
|
|
10
|
-
//
|
|
11
|
-
// Why a toggle and not a replacement: the native loop carries APX-specific
|
|
12
|
-
// features (pseudo-tool fallback for Ollama 500, ghost-response detection,
|
|
13
|
-
// permission_mode gates wired through tool handlers, identity-block injection,
|
|
14
|
-
// ACK_ONLY_TOOLS streak guard). Re-implementing all of those inside LangChain
|
|
15
|
-
// is a large refactor; meanwhile the toggle lets us A/B both paths and pick
|
|
16
|
-
// the one that actually behaves better with gemma4-class models on the
|
|
17
|
-
// user's hardware.
|
|
18
|
-
//
|
|
19
|
-
// LangChain version compat: written against @langchain/core ^0.3 +
|
|
20
|
-
// langchain ^0.3 + @langchain/anthropic ^0.3 + @langchain/ollama ^0.2.
|
|
21
|
-
//
|
|
22
|
-
// Limitations vs native loop (acknowledged in v1):
|
|
23
|
-
// - permission_mode confirmations are still enforced inside each tool
|
|
24
|
-
// handler (they return {error: "requires_confirmation: ..."}), but
|
|
25
|
-
// the loop has no UI to ask the user mid-run, so confirmable tools
|
|
26
|
-
// just fail-fast as they do today.
|
|
27
|
-
// - Pseudo-tool fallback (for Ollama 500 on structured tools) is NOT
|
|
28
|
-
// implemented here — if the underlying engine fails, the call
|
|
29
|
-
// propagates. Use engine === "native" for that case.
|
|
30
|
-
|
|
31
|
-
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
|
|
32
|
-
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
|
|
33
|
-
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
34
|
-
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
|
|
35
|
-
import { z } from "zod";
|
|
36
|
-
|
|
37
|
-
import { TOOL_SCHEMAS, makeToolHandlers } from "./super-agent-tools.js";
|
|
38
|
-
import { readIdentity } from "../core/identity.js";
|
|
39
|
-
import { logInfo, logWarn, logError } from "../core/logging.js";
|
|
40
|
-
|
|
41
|
-
const MAX_ITER_DEFAULT = 15;
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// JSON-Schema → Zod converter
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// LangChain's DynamicStructuredTool wants a Zod schema. APX's tools ship JSON
|
|
47
|
-
// Schema (in the OpenAI function-calling shape). We translate just enough for
|
|
48
|
-
// the parameter types APX actually uses: string, number, boolean, object,
|
|
49
|
-
// array, enum, optional/required. Anything more exotic falls back to z.any().
|
|
50
|
-
function jsonSchemaToZod(schema) {
|
|
51
|
-
if (!schema || typeof schema !== "object") return z.any();
|
|
52
|
-
// OpenAI function shape: { type: "function", function: { parameters: {...} } }
|
|
53
|
-
const root = schema.function?.parameters || schema.parameters || schema;
|
|
54
|
-
return objectToZod(root);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function objectToZod(obj) {
|
|
58
|
-
if (!obj || obj.type !== "object" || !obj.properties) {
|
|
59
|
-
return z.object({}).passthrough();
|
|
60
|
-
}
|
|
61
|
-
const required = new Set(obj.required || []);
|
|
62
|
-
const shape = {};
|
|
63
|
-
for (const [key, prop] of Object.entries(obj.properties)) {
|
|
64
|
-
let s = propToZod(prop);
|
|
65
|
-
if (!required.has(key)) s = s.optional();
|
|
66
|
-
if (prop?.description) s = s.describe(prop.description);
|
|
67
|
-
shape[key] = s;
|
|
68
|
-
}
|
|
69
|
-
return z.object(shape);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function propToZod(prop) {
|
|
73
|
-
if (!prop || typeof prop !== "object") return z.any();
|
|
74
|
-
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
75
|
-
return z.enum(prop.enum);
|
|
76
|
-
}
|
|
77
|
-
switch (prop.type) {
|
|
78
|
-
case "string": return z.string();
|
|
79
|
-
case "number": return z.number();
|
|
80
|
-
case "integer": return z.number().int();
|
|
81
|
-
case "boolean": return z.boolean();
|
|
82
|
-
case "array": return z.array(prop.items ? propToZod(prop.items) : z.any());
|
|
83
|
-
case "object": return objectToZod(prop);
|
|
84
|
-
default: return z.any();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// APX tool → LangChain DynamicStructuredTool
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
function buildLangChainTools(handlers, schemas, { trace, onEvent }) {
|
|
92
|
-
return schemas.map((s) => {
|
|
93
|
-
const name = s.function.name;
|
|
94
|
-
const handler = handlers[name];
|
|
95
|
-
if (!handler) {
|
|
96
|
-
logWarn("super-agent-lc", `no handler for tool ${name} — skipping`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
return new DynamicStructuredTool({
|
|
100
|
-
name,
|
|
101
|
-
description: s.function.description || "",
|
|
102
|
-
schema: jsonSchemaToZod(s),
|
|
103
|
-
func: async (args) => {
|
|
104
|
-
const traceId = `lc:${trace.length + 1}`;
|
|
105
|
-
if (typeof onEvent === "function") {
|
|
106
|
-
try {
|
|
107
|
-
await onEvent({
|
|
108
|
-
type: "tool_start",
|
|
109
|
-
trace: { id: traceId, tool: name, args, pending: true },
|
|
110
|
-
});
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const result = await handler(args || {});
|
|
115
|
-
trace.push({ id: traceId, tool: name, args, result });
|
|
116
|
-
if (typeof onEvent === "function") {
|
|
117
|
-
try { await onEvent({ type: "tool_result", trace: { id: traceId, tool: name, args, result } }); } catch {}
|
|
118
|
-
}
|
|
119
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
const errObj = { error: e.message };
|
|
122
|
-
trace.push({ id: traceId, tool: name, args, result: errObj });
|
|
123
|
-
if (typeof onEvent === "function") {
|
|
124
|
-
try { await onEvent({ type: "tool_result", trace: { id: traceId, tool: name, args, result: errObj } }); } catch {}
|
|
125
|
-
}
|
|
126
|
-
return JSON.stringify(errObj);
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}).filter(Boolean);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Engine factory — picks an @langchain ChatModel based on modelId
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
async function makeLangChainModel(modelId, config) {
|
|
137
|
-
// modelId grammar matches engines/index.js: "<provider>:<model>" or
|
|
138
|
-
// an inferable bare model id ("claude-…" → anthropic, etc).
|
|
139
|
-
const [providerRaw, ...rest] = String(modelId || "").split(":");
|
|
140
|
-
let provider = providerRaw.toLowerCase();
|
|
141
|
-
let model = rest.join(":");
|
|
142
|
-
if (!model) {
|
|
143
|
-
// bare id — infer like engines/index.js
|
|
144
|
-
if (/^claude/i.test(providerRaw)) { provider = "anthropic"; model = providerRaw; }
|
|
145
|
-
else if (/^gpt|^o[134]/i.test(providerRaw)) { provider = "openai"; model = providerRaw; }
|
|
146
|
-
else if (/^gemini/i.test(providerRaw)) { provider = "gemini"; model = providerRaw; }
|
|
147
|
-
else { provider = "ollama"; model = providerRaw; }
|
|
148
|
-
}
|
|
149
|
-
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
150
|
-
|
|
151
|
-
if (provider === "anthropic") {
|
|
152
|
-
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
153
|
-
const apiKey = providerCfg.api_key || process.env.ANTHROPIC_API_KEY;
|
|
154
|
-
if (!apiKey) throw new Error("anthropic: no api_key set");
|
|
155
|
-
return new ChatAnthropic({ apiKey, model, temperature: 1.0, maxTokens: 1024 });
|
|
156
|
-
}
|
|
157
|
-
if (provider === "ollama") {
|
|
158
|
-
const { ChatOllama } = await import("@langchain/ollama");
|
|
159
|
-
const baseUrl = providerCfg.base_url || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
160
|
-
return new ChatOllama({ baseUrl, model, temperature: 0.7 });
|
|
161
|
-
}
|
|
162
|
-
if (provider === "openai") {
|
|
163
|
-
// Lazy import — only required if the user picks openai.
|
|
164
|
-
const { ChatOpenAI } = await import("@langchain/openai").catch(() => ({}));
|
|
165
|
-
if (!ChatOpenAI) throw new Error("openai: install @langchain/openai to use this provider with the langchain engine");
|
|
166
|
-
const apiKey = providerCfg.api_key || process.env.OPENAI_API_KEY;
|
|
167
|
-
if (!apiKey) throw new Error("openai: no api_key set");
|
|
168
|
-
return new ChatOpenAI({ apiKey, model, temperature: 1.0 });
|
|
169
|
-
}
|
|
170
|
-
throw new Error(`langchain engine: unknown provider "${provider}" (modelId="${modelId}")`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Convert APX "previousMessages" rows ({role, content}) → LangChain messages
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
function toLangChainHistory(previousMessages) {
|
|
177
|
-
return (previousMessages || []).map((m) => {
|
|
178
|
-
if (m.role === "user") return new HumanMessage(m.content || "");
|
|
179
|
-
if (m.role === "assistant") return new AIMessage(m.content || "");
|
|
180
|
-
if (m.role === "tool") {
|
|
181
|
-
// LangChain ToolMessage requires a tool_call_id; APX doesn't track ids
|
|
182
|
-
// in the FS history, so we use a synthetic one. The agent only sees
|
|
183
|
-
// the content anyway.
|
|
184
|
-
return new ToolMessage({ content: m.content || "", tool_call_id: m.tool_name || "tool" });
|
|
185
|
-
}
|
|
186
|
-
return new HumanMessage(m.content || "");
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
// Public entry — same contract as runSuperAgent in super-agent.js
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
export async function runSuperAgentLangChain({
|
|
194
|
-
globalConfig,
|
|
195
|
-
projects,
|
|
196
|
-
plugins,
|
|
197
|
-
registries,
|
|
198
|
-
prompt,
|
|
199
|
-
previousMessages = [],
|
|
200
|
-
contextNote = "",
|
|
201
|
-
systemOverride = null,
|
|
202
|
-
onEvent,
|
|
203
|
-
onToken,
|
|
204
|
-
signal,
|
|
205
|
-
}) {
|
|
206
|
-
const sa = globalConfig?.super_agent || {};
|
|
207
|
-
if (!sa.model) throw new Error("super-agent (langchain): no model configured");
|
|
208
|
-
|
|
209
|
-
const identity = (() => { try { return readIdentity(); } catch { return null; } })();
|
|
210
|
-
const userLang = globalConfig?.user?.language || "en";
|
|
211
|
-
|
|
212
|
-
// System prompt — we reuse the native module's DEFAULT_SYSTEM unless the
|
|
213
|
-
// caller passes systemOverride. This keeps the personality / language /
|
|
214
|
-
// hard-rules consistent across both engines.
|
|
215
|
-
const { DEFAULT_SYSTEM, buildIdentityBlock } = await import("./super-agent.js");
|
|
216
|
-
const identityBlock = buildIdentityBlock(identity, userLang);
|
|
217
|
-
const systemPieces = [
|
|
218
|
-
systemOverride || sa.system || DEFAULT_SYSTEM,
|
|
219
|
-
identityBlock,
|
|
220
|
-
contextNote,
|
|
221
|
-
].filter(Boolean);
|
|
222
|
-
// LangChain ChatPromptTemplate uses f-string formatting and will try to
|
|
223
|
-
// resolve any `{name}` it finds in the system text as an input variable.
|
|
224
|
-
// The APX prompt naturally contains literal `{path: <CWD>}` examples and
|
|
225
|
-
// JSON-like snippets, so we double every `{` and `}` to escape them.
|
|
226
|
-
const systemText = systemPieces.join("\n\n").replace(/[{}]/g, (c) => c + c);
|
|
227
|
-
|
|
228
|
-
const trace = [];
|
|
229
|
-
const handlers = makeToolHandlers({
|
|
230
|
-
projects, plugins, registries, globalConfig,
|
|
231
|
-
implicitConfirmation: false,
|
|
232
|
-
});
|
|
233
|
-
const tools = buildLangChainTools(handlers, TOOL_SCHEMAS, { trace, onEvent });
|
|
234
|
-
|
|
235
|
-
logInfo("super-agent-lc", "starting AgentExecutor", {
|
|
236
|
-
model: sa.model, tools: tools.length, prev: previousMessages.length,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const llm = await makeLangChainModel(sa.model, globalConfig);
|
|
240
|
-
|
|
241
|
-
const promptTemplate = ChatPromptTemplate.fromMessages([
|
|
242
|
-
["system", systemText],
|
|
243
|
-
new MessagesPlaceholder("chat_history"),
|
|
244
|
-
["human", "{input}"],
|
|
245
|
-
new MessagesPlaceholder("agent_scratchpad"),
|
|
246
|
-
]);
|
|
247
|
-
|
|
248
|
-
const agent = await createToolCallingAgent({ llm, tools, prompt: promptTemplate });
|
|
249
|
-
|
|
250
|
-
const executor = new AgentExecutor({
|
|
251
|
-
agent,
|
|
252
|
-
tools,
|
|
253
|
-
maxIterations: Number(sa.max_iterations) > 0 ? Number(sa.max_iterations) : MAX_ITER_DEFAULT,
|
|
254
|
-
returnIntermediateSteps: true,
|
|
255
|
-
handleParsingErrors: true,
|
|
256
|
-
// verbose is noisy; we already log via core/logging.js
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const t0 = Date.now();
|
|
260
|
-
let result;
|
|
261
|
-
try {
|
|
262
|
-
if (typeof onEvent === "function") {
|
|
263
|
-
try { await onEvent({ type: "model_start", iteration: 1 }); } catch {}
|
|
264
|
-
}
|
|
265
|
-
result = await executor.invoke({
|
|
266
|
-
input: prompt,
|
|
267
|
-
chat_history: toLangChainHistory(previousMessages),
|
|
268
|
-
}, { signal });
|
|
269
|
-
} catch (e) {
|
|
270
|
-
logError("super-agent-lc", `executor failed in ${Date.now() - t0}ms`, { error: e.message });
|
|
271
|
-
throw e;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// result.output is the final text. result.intermediateSteps is an array
|
|
275
|
-
// of { action, observation }; we already pushed each into `trace` from the
|
|
276
|
-
// DynamicStructuredTool wrappers, so we don't double-record them here.
|
|
277
|
-
const text = String(result.output || "");
|
|
278
|
-
logInfo("super-agent-lc", `done in ${Date.now() - t0}ms`, {
|
|
279
|
-
text_len: text.length,
|
|
280
|
-
tool_calls: trace.length,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
text,
|
|
285
|
-
// LangChain doesn't surface token counts uniformly across providers;
|
|
286
|
-
// leave 0/0 so the caller's bookkeeping doesn't break. Real values
|
|
287
|
-
// would require provider-specific callback handlers.
|
|
288
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
289
|
-
name: sa.name || "apx",
|
|
290
|
-
trace,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function isLangChainEngineSelected(cfg) {
|
|
295
|
-
return cfg?.super_agent?.engine === "langchain";
|
|
296
|
-
}
|