@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.
Files changed (222) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/wakeup.js +14 -19
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. 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
- if (!this.channel.respond_with_engine) return;
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
- const routeSlug = this.channel.route_to_agent;
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
- const DEFAULT_SYSTEM = `You are the **APX dispatcher** the daemon-level agent that runs above all APC projects.
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 the user's language unless they request another language. The system prompt stays English. Plain text, no markdown formatting for Telegram.
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 EMPTY RESPONSES**: Never respond with only text when you have tools available and the user is asking you to DO something. Call the tool FIRST, then explain. Never say "I'll do X" without immediately calling the tool. Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking", "stand by") without a tool call are invalid responses they will be re-prompted and waste a turn.
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
- return !!(cfg && cfg.super_agent && cfg.super_agent.enabled && cfg.super_agent.model);
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
- // On the first iteration, force a tool call. This prevents the model from
253
- // returning a bare acknowledgment ("ok", "dame un segundo") instead of
254
- // acting on an action request. On later iterations (after tool results
255
- // have been fed back) tool_choice is "auto" so the model can produce its
256
- // final text summary.
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 : (iter === 0 ? "required" : "auto"),
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 {