@agentprojectcontext/apx 1.15.6 → 1.17.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 +46 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent-langchain.js +296 -0
- package/src/daemon/super-agent.js +115 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
package/src/daemon/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { PluginManager } from "./plugins/index.js";
|
|
|
21
21
|
import { RoutineScheduler } from "./routines.js";
|
|
22
22
|
import { buildApi } from "./api.js";
|
|
23
23
|
import { triggerWakeup } from "./wakeup.js";
|
|
24
|
+
import { registerOverlayClient } from "./overlay-ws.js";
|
|
25
|
+
import { log as logToUnified } from "../core/logging.js";
|
|
24
26
|
|
|
25
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
28
|
const __dirname = path.dirname(__filename);
|
|
@@ -32,8 +34,41 @@ const PKG = JSON.parse(
|
|
|
32
34
|
// to ~/.apx/daemon.log via `stdio: ["ignore", out, out]`. So a single
|
|
33
35
|
// process.stdout.write reaches the file once. In foreground (npm start), it
|
|
34
36
|
// still prints to the console. No double-append.
|
|
37
|
+
//
|
|
38
|
+
// Beyond the legacy stdout sink we also fan out every line to the unified
|
|
39
|
+
// ~/.apx/logs/apx.log via core/logging.js so `apx log` and `apx log -f`
|
|
40
|
+
// see everything that any plugin/module writes through the daemon's log fn.
|
|
41
|
+
//
|
|
42
|
+
// Heuristic for level/module inference: messages prefixed "fatal:" /
|
|
43
|
+
// "uncaughtException:" / "error:" are ERROR; "warn:" / "could not" / "skipping"
|
|
44
|
+
// are WARN; everything else INFO. Plugins normally pass `plugin <id> ...` or
|
|
45
|
+
// `<id>[<name>] ...` — we use the first bracketed token (or first word before
|
|
46
|
+
// ":") as the module tag.
|
|
47
|
+
function inferLevel(msg) {
|
|
48
|
+
if (/^fatal:|^uncaughtException:|^error:|failed|crash/i.test(msg)) return "ERROR";
|
|
49
|
+
if (/^warn:|could not|skipping|orphan|broken pipe/i.test(msg)) return "WARN";
|
|
50
|
+
return "INFO";
|
|
51
|
+
}
|
|
52
|
+
function inferModule(msg) {
|
|
53
|
+
// "plugin telegram initialized" → telegram
|
|
54
|
+
const plug = msg.match(/^plugin\s+([a-z_-]+)/i);
|
|
55
|
+
if (plug) return plug[1];
|
|
56
|
+
// "telegram[default] ..." → telegram
|
|
57
|
+
const bracket = msg.match(/^([a-z_-]+)\[/i);
|
|
58
|
+
if (bracket) return bracket[1];
|
|
59
|
+
// "whisper: preloading ..." → whisper
|
|
60
|
+
const colon = msg.match(/^([a-z_-]+):\s/i);
|
|
61
|
+
if (colon) return colon[1];
|
|
62
|
+
// "overlay: ..." caught above; "loaded project ..." → daemon
|
|
63
|
+
return "daemon";
|
|
64
|
+
}
|
|
35
65
|
const log = (msg) => {
|
|
36
66
|
process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
67
|
+
try {
|
|
68
|
+
logToUnified(inferLevel(msg), inferModule(msg), msg);
|
|
69
|
+
} catch {
|
|
70
|
+
// logger is best-effort, never throw
|
|
71
|
+
}
|
|
37
72
|
};
|
|
38
73
|
|
|
39
74
|
function ensureHome() {
|
|
@@ -170,6 +205,25 @@ async function main() {
|
|
|
170
205
|
scheduler.start();
|
|
171
206
|
// Fire wake-up message after a short delay so plugins (Telegram) are ready
|
|
172
207
|
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
208
|
+
// Preload whisper-server in the background so first overlay transcription is fast.
|
|
209
|
+
// Adopts an existing one if already on the port; otherwise spawns fresh.
|
|
210
|
+
import("./transcription.js").then(({ preloadWhisperServer }) => {
|
|
211
|
+
preloadWhisperServer((m) => log(m));
|
|
212
|
+
}).catch(() => {});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Attach WebSocket upgrade for overlay channel on /overlay/ws
|
|
216
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
217
|
+
if (req.url !== "/overlay/ws") { socket.destroy(); return; }
|
|
218
|
+
// Lazy-import ws to avoid hard dep on startup
|
|
219
|
+
let WebSocketServer;
|
|
220
|
+
try { ({ WebSocketServer } = await import("ws")); } catch {
|
|
221
|
+
socket.destroy(); return;
|
|
222
|
+
}
|
|
223
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
224
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
225
|
+
registerOverlayClient(ws);
|
|
226
|
+
});
|
|
173
227
|
});
|
|
174
228
|
|
|
175
229
|
server.on("error", (e) => {
|
|
@@ -185,6 +239,10 @@ async function main() {
|
|
|
185
239
|
scheduler.stop();
|
|
186
240
|
plugins.stopAll();
|
|
187
241
|
registries.shutdown();
|
|
242
|
+
// Best-effort shutdown of whisper-server subprocess.
|
|
243
|
+
import("./transcription.js").then(({ shutdownWhisperServer }) => {
|
|
244
|
+
shutdownWhisperServer().catch(() => {});
|
|
245
|
+
}).catch(() => {});
|
|
188
246
|
server.close(() => {
|
|
189
247
|
clearPid();
|
|
190
248
|
process.exit(0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Singleton WebSocket hub for the overlay channel.
|
|
2
|
+
// Imported by api.js (to register connections) and by plugins/overlay.js (to broadcast).
|
|
3
|
+
|
|
4
|
+
const _clients = new Set(); // Set<WebSocket>
|
|
5
|
+
let _messageHandler = null; // (ws, data) => void — set by overlay plugin
|
|
6
|
+
|
|
7
|
+
export const overlayClients = _clients;
|
|
8
|
+
|
|
9
|
+
export function setOverlayMessageHandler(fn) {
|
|
10
|
+
_messageHandler = fn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerOverlayClient(ws) {
|
|
14
|
+
_clients.add(ws);
|
|
15
|
+
ws.on("close", () => _clients.delete(ws));
|
|
16
|
+
ws.on("error", () => _clients.delete(ws));
|
|
17
|
+
ws.on("message", (raw) => {
|
|
18
|
+
if (typeof _messageHandler === "function") {
|
|
19
|
+
let data;
|
|
20
|
+
try { data = JSON.parse(raw.toString()); } catch { data = { type: "raw", raw: raw.toString() }; }
|
|
21
|
+
_messageHandler(ws, data);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function broadcastOverlay(msg) {
|
|
27
|
+
const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
28
|
+
for (const ws of _clients) {
|
|
29
|
+
try {
|
|
30
|
+
if (ws.readyState === 1) ws.send(payload); // 1 = OPEN
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sendToClient(ws, msg) {
|
|
36
|
+
const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
37
|
+
try {
|
|
38
|
+
if (ws.readyState === 1) ws.send(payload);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// Plugins are discovered by static import here. Adding a new plugin = importing
|
|
14
14
|
// it and pushing into PLUGINS.
|
|
15
15
|
import telegramPlugin from "./telegram.js";
|
|
16
|
+
import overlayPlugin from "./overlay.js";
|
|
16
17
|
|
|
17
|
-
export const PLUGINS = [telegramPlugin];
|
|
18
|
+
export const PLUGINS = [telegramPlugin, overlayPlugin];
|
|
18
19
|
|
|
19
20
|
export class PluginManager {
|
|
20
21
|
constructor({ projects, config, log, registries }) {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Overlay plugin — voice/floating-window channel for the APX daemon.
|
|
2
|
+
//
|
|
3
|
+
// This plugin:
|
|
4
|
+
// 1. Registers as a super-agent channel (type "overlay")
|
|
5
|
+
// 2. Routes inbound messages (POST /overlay/message) to the super-agent
|
|
6
|
+
// 3. Streams tokens + tool events back to overlay clients via WebSocket
|
|
7
|
+
//
|
|
8
|
+
// Overlay history is kept in-memory per session (not persisted to disk).
|
|
9
|
+
// Each new overlay window starts a fresh session.
|
|
10
|
+
//
|
|
11
|
+
// Config (in ~/.apx/config.json):
|
|
12
|
+
// "overlay": {
|
|
13
|
+
// "enabled": true,
|
|
14
|
+
// "route_to_agent": "", // leave empty = use super-agent
|
|
15
|
+
// "model": "", // override model; leave empty = super-agent.model
|
|
16
|
+
// "max_history": 20 // turns to keep in context
|
|
17
|
+
// }
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
broadcastOverlay,
|
|
21
|
+
sendToClient,
|
|
22
|
+
setOverlayMessageHandler,
|
|
23
|
+
} from "../overlay-ws.js";
|
|
24
|
+
import { runSuperAgent, isSuperAgentEnabled } from "../super-agent.js";
|
|
25
|
+
import { appendGlobalMessage } from "../../core/messages-store.js";
|
|
26
|
+
|
|
27
|
+
const CHANNEL = "overlay";
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
id: "overlay",
|
|
31
|
+
|
|
32
|
+
init({ projects, config, log, plugins }) {
|
|
33
|
+
const cfg = config.overlay || {};
|
|
34
|
+
const enabled = cfg.enabled !== false; // enabled by default
|
|
35
|
+
|
|
36
|
+
// In-memory conversation history per connected client.
|
|
37
|
+
// Map<WebSocket, Array<{role, content}>>
|
|
38
|
+
const histories = new WeakMap();
|
|
39
|
+
|
|
40
|
+
function getHistory(ws) {
|
|
41
|
+
if (!histories.has(ws)) histories.set(ws, []);
|
|
42
|
+
return histories.get(ws);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle messages sent from the overlay renderer via WebSocket
|
|
46
|
+
setOverlayMessageHandler(async (ws, data) => {
|
|
47
|
+
if (data.type === "message") {
|
|
48
|
+
await _handleMessage({ ws, text: data.text, previousMessages: getHistory(ws) }, { projects, config, log, plugins, cfg, histories });
|
|
49
|
+
} else if (data.type === "cancel") {
|
|
50
|
+
// Signal to abort current generation (handled via AbortController below)
|
|
51
|
+
ws._overlayAbort?.abort();
|
|
52
|
+
} else if (data.type === "ping") {
|
|
53
|
+
sendToClient(ws, { type: "pong" });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const instance = {
|
|
58
|
+
start() {
|
|
59
|
+
if (enabled) log("overlay: plugin started");
|
|
60
|
+
},
|
|
61
|
+
stop() {},
|
|
62
|
+
status() { return { enabled }; },
|
|
63
|
+
|
|
64
|
+
// Called by the /overlay/message REST endpoint
|
|
65
|
+
async handleMessage({ text, previousMessages = [] }) {
|
|
66
|
+
if (!enabled) throw new Error("overlay plugin not enabled");
|
|
67
|
+
broadcastOverlay({ type: "user_message", text });
|
|
68
|
+
await _handleMessage({ ws: null, text, previousMessages }, { projects, config, log, plugins, cfg, histories });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return instance;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Core message handler
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
async function _handleMessage({ ws, text, previousMessages }, { projects, config, log, plugins, cfg, histories }) {
|
|
81
|
+
// Append user turn to history
|
|
82
|
+
if (ws && histories) {
|
|
83
|
+
const hist = _getHistory(ws, histories);
|
|
84
|
+
hist.push({ role: "user", content: text });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const maxHistory = cfg.max_history ?? 20;
|
|
88
|
+
const history = ws ? _getHistory(ws, histories).slice(-(maxHistory)) : previousMessages.slice(-(maxHistory));
|
|
89
|
+
|
|
90
|
+
// AbortController for cancel support
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
if (ws) ws._overlayAbort = controller;
|
|
93
|
+
|
|
94
|
+
// Emit "thinking" indicator
|
|
95
|
+
_send(ws, { type: "thinking" });
|
|
96
|
+
|
|
97
|
+
// Persist to overlay message log
|
|
98
|
+
try {
|
|
99
|
+
await appendGlobalMessage(CHANNEL, { role: "user", content: text, ts: new Date().toISOString() });
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
let fullResponse = "";
|
|
103
|
+
let toolsExecuted = [];
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
if (!isSuperAgentEnabled(config)) {
|
|
107
|
+
throw new Error("super-agent not enabled — set super_agent.enabled + super_agent.model in ~/.apx/config.json");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result = await runSuperAgent({
|
|
111
|
+
globalConfig: config,
|
|
112
|
+
projects,
|
|
113
|
+
plugins,
|
|
114
|
+
prompt: text,
|
|
115
|
+
contextNote: "# Channel context\nChannel: overlay (floating voice window). Reply concisely.",
|
|
116
|
+
previousMessages: history.slice(0, -1),
|
|
117
|
+
overrideModel: cfg.model || null,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
onToken: (chunk) => {
|
|
120
|
+
fullResponse += chunk;
|
|
121
|
+
_send(ws, { type: "token", text: chunk });
|
|
122
|
+
},
|
|
123
|
+
onEvent: async (event) => {
|
|
124
|
+
if (event.type === "tool_start") {
|
|
125
|
+
const t = event.trace;
|
|
126
|
+
toolsExecuted.push(t.tool);
|
|
127
|
+
_send(ws, { type: "tool_start", name: t.tool, args: t.args });
|
|
128
|
+
} else if (event.type === "tool_result") {
|
|
129
|
+
_send(ws, { type: "tool_done", name: event.trace.tool });
|
|
130
|
+
} else if (event.type === "assistant_text" && event.text && !fullResponse) {
|
|
131
|
+
_send(ws, { type: "token", text: event.text });
|
|
132
|
+
fullResponse += event.text;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const finalText = fullResponse || result.text || "";
|
|
137
|
+
|
|
138
|
+
// Emit done with full text
|
|
139
|
+
_send(ws, { type: "done", text: finalText });
|
|
140
|
+
|
|
141
|
+
// Append assistant turn to history
|
|
142
|
+
if (ws && histories) {
|
|
143
|
+
const hist = _getHistory(ws, histories);
|
|
144
|
+
hist.push({ role: "assistant", content: finalText });
|
|
145
|
+
// Trim history
|
|
146
|
+
if (hist.length > (cfg.max_history ?? 20)) {
|
|
147
|
+
hist.splice(0, hist.length - (cfg.max_history ?? 20));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Persist assistant response
|
|
152
|
+
try {
|
|
153
|
+
await appendGlobalMessage(CHANNEL, { role: "assistant", content: finalText, ts: new Date().toISOString() });
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e.name === "AbortError") {
|
|
158
|
+
_send(ws, { type: "cancelled" });
|
|
159
|
+
} else {
|
|
160
|
+
log(`overlay: error — ${e.message}`);
|
|
161
|
+
_send(ws, { type: "error", message: e.message });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _send(ws, msg) {
|
|
167
|
+
if (ws) {
|
|
168
|
+
sendToClient(ws, msg);
|
|
169
|
+
} else {
|
|
170
|
+
broadcastOverlay(msg);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _getHistory(ws, histories) {
|
|
175
|
+
if (!histories.has(ws)) histories.set(ws, []);
|
|
176
|
+
return histories.get(ws);
|
|
177
|
+
}
|
|
@@ -554,7 +554,13 @@ class ChannelPoller {
|
|
|
554
554
|
},
|
|
555
555
|
});
|
|
556
556
|
|
|
557
|
-
|
|
557
|
+
// Super-agent is ALWAYS active on Telegram: respond_with_engine === false
|
|
558
|
+
// used to silently drop user messages, which looked to the user like the
|
|
559
|
+
// bot ignored them. Honour the legacy flag only as a soft hint (skip the
|
|
560
|
+
// routed-agent shortcut so we fall straight to super-agent) but never let
|
|
561
|
+
// it short-circuit the whole reply. To genuinely silence the bot, disable
|
|
562
|
+
// the channel entirely (telegram.enabled = false in config).
|
|
563
|
+
const skipRoutedAgent = this.channel.respond_with_engine === false;
|
|
558
564
|
if (!text) return;
|
|
559
565
|
|
|
560
566
|
// Short-circuit /reset / /new: send an ack and don't engage the engine.
|
|
@@ -586,8 +592,9 @@ class ChannelPoller {
|
|
|
586
592
|
let replyAuthor;
|
|
587
593
|
const projectCfg = target.config || this.globalConfig;
|
|
588
594
|
|
|
589
|
-
// Try the project's chosen agent first
|
|
590
|
-
|
|
595
|
+
// Try the project's chosen agent first (skipped if the legacy
|
|
596
|
+
// respond_with_engine === false hint asked to bypass routed agents).
|
|
597
|
+
const routeSlug = skipRoutedAgent ? null : this.channel.route_to_agent;
|
|
591
598
|
if (routeSlug) {
|
|
592
599
|
const agent = readAgents(target.path).find((a) => a.slug === routeSlug);
|
|
593
600
|
if (agent && agent.fields.Model) {
|
|
@@ -642,6 +649,11 @@ class ChannelPoller {
|
|
|
642
649
|
return; // don't send reply if aborted
|
|
643
650
|
}
|
|
644
651
|
this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
|
|
652
|
+
// Surface the failure to the user instead of silently dropping the
|
|
653
|
+
// turn — otherwise from the chat side it looks like the bot ignored
|
|
654
|
+
// the message. Keep the message short and non-leaking.
|
|
655
|
+
replyText = `⚠️ Could not generate a reply right now (${e.message || "internal error"}).`;
|
|
656
|
+
replyAuthor = "apx";
|
|
645
657
|
}
|
|
646
658
|
}
|
|
647
659
|
|
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
}
|