@agentprojectcontext/apx 1.15.5 → 1.16.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 +40 -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.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- 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
|
@@ -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
|
|
|
@@ -22,7 +22,43 @@ import { readIdentity } from "../core/identity.js";
|
|
|
22
22
|
|
|
23
23
|
const MAX_TOOL_ITERS = 6;
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Tools that, when they're the ONLY thing the model called in an iteration,
|
|
26
|
+
// don't count as "real work" — they're acknowledgements (telegram ping back
|
|
27
|
+
// to the user, log lines, etc). When the model emits an iteration that only
|
|
28
|
+
// contains acks, we DON'T let it leave the loop on iter N+1 with empty text:
|
|
29
|
+
// we force another required tool call so the actual task gets executed.
|
|
30
|
+
//
|
|
31
|
+
// This is the fix for the "agent sends 'ya te escucho 🎧' and then stops"
|
|
32
|
+
// bug. Without it, gemma4-class models sometimes consider the ack the
|
|
33
|
+
// complete reply on iter 0 and emit only "ok" on iter 1, breaking out.
|
|
34
|
+
const ACK_ONLY_TOOLS = new Set(["send_telegram"]);
|
|
35
|
+
// Hard cap so the model can't ack-ack-ack forever — after this many
|
|
36
|
+
// consecutive ack-only iterations we let the loop progress naturally
|
|
37
|
+
// (the model already had its chance to call a real tool).
|
|
38
|
+
const MAX_CONSECUTIVE_ACKS = 2;
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SYSTEM = `# Identity (override everything else)
|
|
41
|
+
You are **APX** — Manuel's personal assistant running on his Mac.
|
|
42
|
+
You are NOT a code analyzer, NOT a generic chatbot, NOT a tutor.
|
|
43
|
+
You are an **action agent**: you USE TOOLS to do real things on Manuel's system.
|
|
44
|
+
|
|
45
|
+
# Language — non-negotiable
|
|
46
|
+
ALWAYS reply in **Spanish (rioplatense, voseo when natural)** unless Manuel
|
|
47
|
+
explicitly writes to you in another language for that turn. The user is an
|
|
48
|
+
Argentinian developer; English replies feel broken to him. If you find
|
|
49
|
+
yourself writing English, stop and rewrite in Spanish before sending.
|
|
50
|
+
This rule beats every other formatting hint below.
|
|
51
|
+
|
|
52
|
+
# What you must NOT do
|
|
53
|
+
- Do NOT explain code or write essays about "the provided snippet".
|
|
54
|
+
- Do NOT describe what a tool *would* do — call it and report the result.
|
|
55
|
+
- Do NOT dump the tool catalog at the user.
|
|
56
|
+
- Do NOT respond with disclaimers ("as an AI…", "I'm just an assistant…").
|
|
57
|
+
- If a user message is short or ambiguous, ASK one short clarifying question
|
|
58
|
+
in Spanish — do not invent a topic.
|
|
59
|
+
|
|
60
|
+
# How you operate
|
|
61
|
+
You are the **APX dispatcher** — the daemon-level agent that runs above all APC projects.
|
|
26
62
|
|
|
27
63
|
APX is a local daemon + CLI for APC projects. User-level runtime state lives under ~/.apx/:
|
|
28
64
|
- ~/.apx/config.json: daemon config, engines, Telegram, super-agent settings
|
|
@@ -51,7 +87,7 @@ HARD RULES (do not deviate):
|
|
|
51
87
|
3. NEVER answer "specify a project" — instead, just call the tool with no argument and you'll get the full picture.
|
|
52
88
|
4. If a tool result has an error, retry with different arguments before falling back to asking the user.
|
|
53
89
|
5. Respect permission mode. total = execute requested actions without confirmation. automatico = read/list/safe shell actions run directly; destructive, external, runtime, MCP calls, outbound messages, config, and filesystem mutations need explicit user confirmation. permiso = only allowed tools run directly; everything else needs confirmation.
|
|
54
|
-
6. Write in
|
|
90
|
+
6. Write in **Spanish** by default (see "Language" section above). Plain text on Telegram — no markdown tables, no code fences unless quoting code. Keep replies under 6 sentences unless the user asks for detail.
|
|
55
91
|
7. Stay brief: under 6 sentences unless asked for detail.
|
|
56
92
|
8. You DO see recent prior turns of this chat as previous messages when applicable. **Use them ONLY to disambiguate references** (e.g. "el primero" → first project mentioned earlier). For ANY factual data — agent details, MCP details, file contents, memory — RE-CALL the tool. Past turns are context, not a cache. Models change, agents change, files change.
|
|
57
93
|
9. /reset or /new from the user means "forget previous turns and answer this one fresh" — if you see those prefixes the operator already cleared the context for you.
|
|
@@ -63,7 +99,7 @@ HARD RULES (do not deviate):
|
|
|
63
99
|
15. NO-PENDING RULE: never say "give me a second", "I will do it", or "I will try later" as a final answer. Either call the tool in this same turn or say what blocks you.
|
|
64
100
|
16. IDENTITY RULE: when the user asks you to change your name, call yourself something, or update your personality/language, call set_identity and persist the change. Then confirm with your new name.
|
|
65
101
|
17. ROUTINES RULE: NEVER create a routine in the default project (id=0). Routines MUST be tied to a specific registered project. Before adding a routine, call list_projects to find the correct project id or name. Then pass --project <id|name> to apx routine add. If no project fits, ask the user which project to use. Creating routines in project 0/default mixes unrelated projects' schedules and corrupts state.
|
|
66
|
-
18. **NO
|
|
102
|
+
18. **NO BARE ACKS AS FINAL ANSWER**: Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking") are invalid as a FINAL response when a tool was needed — they will be re-prompted. EXCEPTION: a short contextual ack sent via send_telegram BEFORE another tool call is encouraged on Telegram audio inputs and on tool calls that take more than a few seconds (browser_screenshot, web_search, run_shell, long file edits). The ack must be **contextual and varied** in Spanish — e.g. "Ya te escucho 🎧", "Dame un seg, transcribiendo…", "Buscando eso ahora", "Voy a revisar el repo…", "Un momento, ejecutando…". Never reuse the exact same ack twice in a row. The ack is the FIRST tool call in the turn; the actual work follows immediately in the SAME turn (do not return without doing the work).
|
|
67
103
|
19. **CWD RULE**: When the channel context includes a "CWD: <path>" line, that is the user's current working directory. References to "este directorio", "este proyecto", "esta carpeta", "acá", "aquí", "this directory", "this project", "current dir/folder" all mean that exact CWD path. Use it as the path argument directly — DO NOT ask the user "what's the path?" when CWD is already given. Example: if user says "agregá este proyecto a la lista", call add_project({path: <CWD>}) immediately.
|
|
68
104
|
20. **NO MANUAL SCAFFOLDING**: To register or scaffold a project, ALWAYS use add_project — it auto-creates AGENTS.md and .apc/project.json when missing (one call, atomic). NEVER write AGENTS.md, .apc/project.json, or any APC scaffold file by hand via run_shell / write_file / shell pipes. The schema must come from the official initApf scaffold, not improvised. If add_project errors, report the error to the user — don't try to work around it with shell hacks. Same for any other APC-managed file (.apc/agents/*, .apc/skills/*, etc.) — use the dedicated tool, never raw filesystem writes.
|
|
69
105
|
21. **SKILLS — ON DEMAND**: The "# Available skills" section below lists every skill available to you (slug + description, NO body). When the user asks about specific APX/APC commands, project structure, agent runtimes, or anything where exact syntax or detailed behavior matches a skill description (in ANY language — match semantically, not by keyword), call load_skill({slug}) to fetch the full markdown body. If a CWD is in the contextNote, pass it as project_path so project-scoped skills resolve. If the user explicitly asks "what skills do you have?", you can either read the catalog below directly OR call list_skills to get a fresh enumeration. Do NOT load skills for trivial / unrelated questions — that wastes tokens. Don't guess CLI syntax when a skill can tell you; load it.
|
|
@@ -143,8 +179,32 @@ function looksLikeActionRequest(text) {
|
|
|
143
179
|
return /\b(list|show|find|get|fetch|search|run|execute|create|add|make|start|stop|delete|update|send|check|read|write|look|tell me|dame|mostra|busca|ejecuta|crea|agrega|mandá|revisá|corré|borrá|arrancá)\b/.test(t);
|
|
144
180
|
}
|
|
145
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Build the identity block injected into every super-agent system prompt.
|
|
184
|
+
* Pure function — exported for unit tests.
|
|
185
|
+
*
|
|
186
|
+
* @param {object|null} identity result of readIdentity(), or a plain object for tests
|
|
187
|
+
* @param {string} userLang ISO 639-1 code from config.user.language (default "en")
|
|
188
|
+
*/
|
|
189
|
+
export function buildIdentityBlock(identity, userLang = "en") {
|
|
190
|
+
const lines = ["# Identity"];
|
|
191
|
+
if (identity?.agent_name) lines.push(`Your name is ${identity.agent_name}.`);
|
|
192
|
+
if (identity?.personality) lines.push(`Your personality: ${identity.personality}.`);
|
|
193
|
+
if (identity?.owner_name) lines.push(`Your owner is ${identity.owner_name}.`);
|
|
194
|
+
if (identity?.owner_context) lines.push(`Owner context: ${identity.owner_context}`);
|
|
195
|
+
lines.push(`Always reply in the language with ISO code "${userLang}" unless the user explicitly switches.`);
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
146
199
|
export function isSuperAgentEnabled(cfg) {
|
|
147
|
-
|
|
200
|
+
// The super-agent is the system's default reply path. It is considered
|
|
201
|
+
// enabled as soon as a model is configured — the legacy `.enabled` flag is
|
|
202
|
+
// honoured only when explicitly set to `false`. This prevents the bot
|
|
203
|
+
// from silently dropping Telegram messages just because someone forgot to
|
|
204
|
+
// set super_agent.enabled = true.
|
|
205
|
+
const sa = cfg && cfg.super_agent;
|
|
206
|
+
if (!sa || !sa.model) return false;
|
|
207
|
+
return sa.enabled !== false;
|
|
148
208
|
}
|
|
149
209
|
|
|
150
210
|
export async function runSuperAgent({
|
|
@@ -158,6 +218,7 @@ export async function runSuperAgent({
|
|
|
158
218
|
overrideModel = null,
|
|
159
219
|
onEvent = null,
|
|
160
220
|
signal,
|
|
221
|
+
onToken = null,
|
|
161
222
|
}) {
|
|
162
223
|
if (!isSuperAgentEnabled(globalConfig)) {
|
|
163
224
|
throw new Error("super-agent not enabled (set super_agent.enabled and .model in ~/.apx/config.json)");
|
|
@@ -206,15 +267,7 @@ export async function runSuperAgent({
|
|
|
206
267
|
// Language comes from config.user.language (ISO 639-1) so it stays in sync with transcription.
|
|
207
268
|
const identity = (() => { try { return readIdentity(); } catch { return null; } })();
|
|
208
269
|
const userLang = globalConfig?.user?.language || "en";
|
|
209
|
-
const identityBlock = (
|
|
210
|
-
const lines = ["# Identity"];
|
|
211
|
-
if (identity?.agent_name) lines.push(`Your name is ${identity.agent_name}.`);
|
|
212
|
-
if (identity?.personality) lines.push(`Your personality: ${identity.personality}.`);
|
|
213
|
-
if (identity?.owner_name) lines.push(`Your owner is ${identity.owner_name}.`);
|
|
214
|
-
if (identity?.owner_context) lines.push(`Owner context: ${identity.owner_context}`);
|
|
215
|
-
lines.push(`Always reply in the language with ISO code "${userLang}" unless the user explicitly switches.`);
|
|
216
|
-
return lines.join("\n");
|
|
217
|
-
})();
|
|
270
|
+
const identityBlock = buildIdentityBlock(identity, userLang);
|
|
218
271
|
|
|
219
272
|
const system = [
|
|
220
273
|
sa.system || DEFAULT_SYSTEM,
|
|
@@ -246,14 +299,21 @@ export async function runSuperAgent({
|
|
|
246
299
|
let totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
247
300
|
let lastText = "";
|
|
248
301
|
let usePseudoTools = false;
|
|
302
|
+
// Track how many consecutive iterations contained only ACK_ONLY tools.
|
|
303
|
+
// While this is > 0 we keep tool_choice="required" so the next iter has
|
|
304
|
+
// to do real work — otherwise gemma4-class models call send_telegram
|
|
305
|
+
// for the ack and then break out with empty text on iter N+1.
|
|
306
|
+
let ackOnlyStreak = 0;
|
|
249
307
|
|
|
250
308
|
for (let iter = 0; iter < MAX_TOOL_ITERS; iter++) {
|
|
251
309
|
await emitProgress(onEvent, { type: "model_start", iteration: iter + 1 });
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
310
|
+
// Force a tool call on iter 0 (no bare "ok dame un segundo" reply), AND
|
|
311
|
+
// on any iteration that immediately follows an ack-only iter (so the
|
|
312
|
+
// model can't ack and then stop). After at most MAX_CONSECUTIVE_ACKS
|
|
313
|
+
// forced rounds we let it fall back to "auto" so the model can finish.
|
|
314
|
+
const forceTool =
|
|
315
|
+
iter === 0 ||
|
|
316
|
+
(ackOnlyStreak > 0 && ackOnlyStreak <= MAX_CONSECUTIVE_ACKS);
|
|
257
317
|
let result;
|
|
258
318
|
try {
|
|
259
319
|
result = await callEngine({
|
|
@@ -262,9 +322,12 @@ export async function runSuperAgent({
|
|
|
262
322
|
messages: conversation,
|
|
263
323
|
config: globalConfig,
|
|
264
324
|
tools: usePseudoTools ? null : TOOL_SCHEMAS,
|
|
265
|
-
toolChoice: usePseudoTools ? null : (
|
|
325
|
+
toolChoice: usePseudoTools ? null : (forceTool ? "required" : "auto"),
|
|
266
326
|
maxTokens: 1024,
|
|
267
327
|
signal,
|
|
328
|
+
// Only stream tokens on non-forced iterations — on forced iters the
|
|
329
|
+
// model MUST emit a tool_call, streaming text would confuse the user.
|
|
330
|
+
onToken: (!forceTool && onToken) ? onToken : null,
|
|
268
331
|
});
|
|
269
332
|
} catch (e) {
|
|
270
333
|
if (usePseudoTools && /^ollama:/i.test(String(activeModel || "")) && /ollama\s+500/i.test(String(e?.message || "")) && trace.length > 0) {
|
|
@@ -284,6 +347,7 @@ export async function runSuperAgent({
|
|
|
284
347
|
toolChoice: null,
|
|
285
348
|
maxTokens: 1024,
|
|
286
349
|
signal,
|
|
350
|
+
onToken: (iter > 0 && onToken) ? onToken : null,
|
|
287
351
|
});
|
|
288
352
|
}
|
|
289
353
|
totalUsage.input_tokens += result.usage?.input_tokens || 0;
|
|
@@ -378,6 +442,25 @@ export async function runSuperAgent({
|
|
|
378
442
|
content: JSON.stringify(toolResult),
|
|
379
443
|
});
|
|
380
444
|
}
|
|
445
|
+
|
|
446
|
+
// Did this iteration consist of ONLY ack-style tool calls? If so we'll
|
|
447
|
+
// keep tool_choice forced on the next iter (see top of loop). A turn
|
|
448
|
+
// that mixes send_telegram + e.g. browser_screenshot counts as "real
|
|
449
|
+
// work" and resets the streak.
|
|
450
|
+
const allAckOnly = toolCalls.every((tc) => {
|
|
451
|
+
const n = (tc.function?.name) || tc.name;
|
|
452
|
+
return ACK_ONLY_TOOLS.has(n);
|
|
453
|
+
});
|
|
454
|
+
if (allAckOnly) {
|
|
455
|
+
ackOnlyStreak += 1;
|
|
456
|
+
await emitProgress(onEvent, {
|
|
457
|
+
type: "ack_only_iter",
|
|
458
|
+
iteration: iter + 1,
|
|
459
|
+
streak: ackOnlyStreak,
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
ackOnlyStreak = 0;
|
|
463
|
+
}
|
|
381
464
|
}
|
|
382
465
|
|
|
383
466
|
return {
|