@agentprojectcontext/apx 1.31.2 → 1.32.2

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 (229) hide show
  1. package/package.json +6 -1
  2. package/skills/apc-context/SKILL.md +5 -2
  3. package/skills/apx/SKILL.md +3 -3
  4. package/skills/apx-agency-agents/SKILL.md +5 -5
  5. package/skills/apx-agent/SKILL.md +7 -7
  6. package/skills/apx-mcp/SKILL.md +6 -4
  7. package/skills/apx-mcp-builder/SKILL.md +4 -7
  8. package/skills/apx-project/SKILL.md +4 -5
  9. package/skills/apx-routine/SKILL.md +14 -12
  10. package/skills/apx-runtime/SKILL.md +5 -3
  11. package/skills/apx-sessions/SKILL.md +5 -5
  12. package/skills/apx-skill-builder/SKILL.md +10 -6
  13. package/skills/apx-task/SKILL.md +8 -8
  14. package/skills/apx-telegram/SKILL.md +23 -7
  15. package/skills/apx-voice/SKILL.md +8 -6
  16. package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
  17. package/src/core/agent/constants.js +5 -0
  18. package/src/core/agent/index.js +0 -2
  19. package/src/core/{agent-memory.js → agent/memory.js} +2 -2
  20. package/src/core/agent/model-router.js +21 -43
  21. package/src/core/agent/prompt-builder.js +17 -63
  22. package/src/core/agent/prompts/action-discipline.md +17 -0
  23. package/src/core/agent/prompts/channels/code.md +8 -12
  24. package/src/core/agent/prompts/channels/desktop.md +6 -4
  25. package/src/core/agent/prompts/channels/routine.md +10 -1
  26. package/src/core/agent/prompts/channels/telegram.md +5 -0
  27. package/src/core/agent/prompts/channels/web_code.md +20 -0
  28. package/src/core/agent/prompts/modes/voice.md +2 -2
  29. package/src/core/agent/prompts/super-agent-base.md +2 -2
  30. package/src/core/agent/run-agent.js +66 -36
  31. package/src/core/agent/runtime-bridge.js +42 -0
  32. package/src/core/agent/self-memory.js +19 -9
  33. package/src/core/agent/skills/catalog.js +65 -0
  34. package/src/core/agent/skills/index.js +6 -0
  35. package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
  36. package/src/core/agent/skills/rag.js +91 -0
  37. package/src/core/agent/skills/trigger.js +71 -0
  38. package/src/{host/daemon → core/agent}/super-agent.js +5 -5
  39. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
  40. package/src/core/agent/tools/handlers/ask-questions.js +115 -0
  41. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
  42. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
  43. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
  44. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
  45. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
  46. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
  47. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
  48. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
  49. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
  50. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
  51. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
  52. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
  53. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
  54. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
  55. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
  56. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
  57. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
  58. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
  59. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
  60. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
  61. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
  62. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
  63. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
  64. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
  65. package/src/core/agent/tools/helpers.js +74 -0
  66. package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
  67. package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
  68. package/src/core/apc/agents-vault.js +37 -0
  69. package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
  70. package/src/core/{config.js → config/index.js} +21 -27
  71. package/src/core/config/paths.js +32 -0
  72. package/src/core/constants/actors.js +8 -0
  73. package/src/core/constants/channels.js +19 -0
  74. package/src/core/constants/index.js +5 -0
  75. package/src/core/constants/permissions.js +17 -0
  76. package/src/core/constants/roles.js +9 -0
  77. package/src/core/engines/_streaming.js +63 -0
  78. package/src/core/engines/anthropic.js +11 -22
  79. package/src/core/engines/ollama.js +7 -16
  80. package/src/core/identity/index.js +8 -0
  81. package/src/core/{identity.js → identity/self.js} +5 -5
  82. package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
  83. package/src/core/logging.js +1 -1
  84. package/src/core/mascot.js +1 -1
  85. package/src/core/memory/active-threads.js +10 -10
  86. package/src/core/memory/broker.js +9 -9
  87. package/src/core/memory/compactor.js +2 -2
  88. package/src/core/memory/index.js +2 -2
  89. package/src/core/memory/indexer.js +1 -1
  90. package/src/core/{code-sessions-store.js → stores/code-sessions.js} +3 -7
  91. package/src/core/{messages-store.js → stores/messages.js} +6 -4
  92. package/src/core/stores/routine-memory.js +71 -0
  93. package/src/core/{routines-store.js → stores/routines.js} +1 -3
  94. package/src/core/stores/runtime-sessions.js +99 -0
  95. package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
  96. package/src/core/update-check.js +1 -1
  97. package/src/core/util/ids.js +14 -0
  98. package/src/core/util/index.js +2 -0
  99. package/src/core/util/text-similarity.js +52 -0
  100. package/src/core/util/time.js +9 -0
  101. package/src/core/voice/tts.js +1 -1
  102. package/src/host/daemon/api/admin-config.js +4 -3
  103. package/src/host/daemon/api/admin.js +1 -1
  104. package/src/host/daemon/api/agents.js +4 -25
  105. package/src/host/daemon/api/artifacts.js +118 -1
  106. package/src/host/daemon/api/code.js +60 -16
  107. package/src/host/daemon/api/confirm.js +1 -1
  108. package/src/host/daemon/api/connections.js +2 -2
  109. package/src/host/daemon/api/conversations.js +2 -2
  110. package/src/host/daemon/api/deck.js +1 -1
  111. package/src/host/daemon/api/desktop.js +1 -1
  112. package/src/host/daemon/api/embeddings.js +4 -4
  113. package/src/host/daemon/api/engines.js +2 -2
  114. package/src/host/daemon/api/exec.js +3 -3
  115. package/src/host/daemon/api/identity.js +1 -1
  116. package/src/host/daemon/api/mcps.js +1 -1
  117. package/src/host/daemon/api/messages.js +1 -1
  118. package/src/host/daemon/api/runtimes.js +9 -8
  119. package/src/host/daemon/api/sessions-search.js +1 -1
  120. package/src/host/daemon/api/sessions.js +2 -2
  121. package/src/host/daemon/api/shared.js +5 -4
  122. package/src/host/daemon/api/skills.js +30 -0
  123. package/src/host/daemon/api/super-agent.js +29 -9
  124. package/src/host/daemon/api/tasks.js +2 -2
  125. package/src/host/daemon/api/telegram.js +1 -1
  126. package/src/host/daemon/api/tools.js +6 -6
  127. package/src/host/daemon/api/tts.js +2 -2
  128. package/src/host/daemon/api/voice.js +14 -12
  129. package/src/host/daemon/api.js +2 -0
  130. package/src/host/daemon/compact.js +1 -1
  131. package/src/host/daemon/db.js +4 -4
  132. package/src/host/daemon/desktop-ws.js +1 -1
  133. package/src/host/daemon/index.js +4 -4
  134. package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +45 -6
  135. package/src/host/daemon/plugins/index.js +2 -2
  136. package/src/host/daemon/plugins/telegram/ask.js +309 -0
  137. package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +390 -191
  138. package/src/host/daemon/plugins/telegram/media.js +162 -0
  139. package/src/host/daemon/projects-helpers.js +54 -0
  140. package/src/host/daemon/routines.js +28 -12
  141. package/src/host/daemon/smoke.js +2 -2
  142. package/src/host/daemon/token-store.js +1 -1
  143. package/src/host/daemon/transcription.js +2 -2
  144. package/src/host/daemon/wakeup.js +2 -2
  145. package/src/interfaces/cli/commands/agent.js +3 -3
  146. package/src/interfaces/cli/commands/artifact.js +99 -0
  147. package/src/interfaces/cli/commands/command.js +1 -1
  148. package/src/interfaces/cli/commands/config.js +3 -2
  149. package/src/interfaces/cli/commands/desktop.js +1 -1
  150. package/src/interfaces/cli/commands/exec.js +2 -1
  151. package/src/interfaces/cli/commands/identity.js +2 -2
  152. package/src/interfaces/cli/commands/init.js +1 -1
  153. package/src/interfaces/cli/commands/mcp.js +1 -1
  154. package/src/interfaces/cli/commands/memory.js +2 -2
  155. package/src/interfaces/cli/commands/model.js +16 -6
  156. package/src/interfaces/cli/commands/project.js +1 -1
  157. package/src/interfaces/cli/commands/routine.js +58 -0
  158. package/src/interfaces/cli/commands/search.js +1 -1
  159. package/src/interfaces/cli/commands/session.js +4 -4
  160. package/src/interfaces/cli/commands/setup.js +4 -3
  161. package/src/interfaces/cli/commands/skills.js +25 -4
  162. package/src/interfaces/cli/commands/status.js +1 -1
  163. package/src/interfaces/cli/commands/sys.js +11 -4
  164. package/src/interfaces/cli/commands/update.js +1 -1
  165. package/src/interfaces/cli/index.js +8 -4
  166. package/src/interfaces/cli/postinstall.js +2 -2
  167. package/src/interfaces/cli/terminal-chat/renderer.js +22 -2
  168. package/src/interfaces/mcp-server/index.js +1 -1
  169. package/src/interfaces/tui/component/prompt/index.tsx +3 -1
  170. package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
  171. package/src/interfaces/tui/context/sync-apx.tsx +20 -2
  172. package/src/interfaces/tui/context/sync.tsx +2 -1
  173. package/src/interfaces/tui/routes/session/index.tsx +151 -136
  174. package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
  175. package/src/interfaces/tui/run.ts +2 -0
  176. package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +1 -0
  177. package/src/interfaces/web/dist/assets/index-BkybwwRn.js +570 -0
  178. package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +1 -0
  179. package/src/interfaces/web/dist/index.html +2 -2
  180. package/src/interfaces/web/package-lock.json +9 -9
  181. package/src/interfaces/web/src/App.tsx +51 -32
  182. package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
  183. package/src/interfaces/web/src/components/UiSelect.tsx +1 -1
  184. package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +72 -0
  185. package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +399 -0
  186. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  187. package/src/interfaces/web/src/components/chat/MessageList.tsx +2 -1
  188. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
  189. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +230 -0
  190. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
  191. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +40 -17
  192. package/src/interfaces/web/src/components/common/TabLayout.tsx +9 -5
  193. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
  194. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
  195. package/src/interfaces/web/src/hooks/useChat.ts +47 -2
  196. package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +59 -0
  197. package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
  198. package/src/interfaces/web/src/i18n/en.ts +27 -7
  199. package/src/interfaces/web/src/i18n/es.ts +27 -7
  200. package/src/interfaces/web/src/lib/api/artifacts.ts +47 -0
  201. package/src/interfaces/web/src/lib/api/skills.ts +25 -0
  202. package/src/interfaces/web/src/lib/api.ts +2 -0
  203. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +41 -20
  204. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
  205. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
  206. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
  207. package/src/interfaces/web/src/screens/project/ChatTab.tsx +27 -9
  208. package/src/skills/apc-context/SKILL.md +159 -0
  209. package/src/core/agent/ghost-guard.js +0 -24
  210. package/src/core/agent/prompts/channels/terminal.md +0 -16
  211. package/src/host/daemon/apc-runtime-context.js +0 -124
  212. package/src/host/daemon/super-agent-tools/helpers.js +0 -124
  213. package/src/host/daemon/super-agent-tools/tools/ask-questions.js +0 -32
  214. package/src/host/daemon/tool-call-parser.js +0 -2
  215. package/src/interfaces/web/dist/assets/index-BDUsA6L6.css +0 -1
  216. package/src/interfaces/web/dist/assets/index-BV615I9p.js +0 -548
  217. package/src/interfaces/web/dist/assets/index-BV615I9p.js.map +0 -1
  218. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
  219. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
  220. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
  221. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
  222. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
  223. /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
  224. /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
  225. /package/src/core/{parser.js → apc/parser.js} +0 -0
  226. /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
  227. /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
  228. /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
  229. /package/src/core/{session-store.js → stores/sessions.js} +0 -0
@@ -1,3 +1,8 @@
1
1
  export const MAX_TOOL_ITERS = 6;
2
2
  export const ACK_ONLY_TOOLS = new Set(["send_telegram"]);
3
3
  export const MAX_CONSECUTIVE_ACKS = 2;
4
+ // Tools whose semantics REQUIRE handing control back to the user. After the
5
+ // tool runs we break the loop — even under completionContract — because the
6
+ // task literally cannot advance without a human reply. Without this, models
7
+ // under forced toolChoice spam the same question across iterations.
8
+ export const TURN_ENDING_TOOLS = new Set(["ask_questions"]);
@@ -20,9 +20,7 @@ export {
20
20
  resolveActiveModel,
21
21
  checkProviderHealth,
22
22
  probeAllProviders,
23
- fallbackOrder,
24
23
  fallbackModels,
25
- modelForProvider,
26
24
  isFallbackEnabled,
27
25
  DEFAULT_FALLBACK_ORDER,
28
26
  DEFAULT_FALLBACK_MODELS,
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { projectStorageRoot } from "./config.js";
4
- import { getOrCreateApxId } from "./scaffold.js";
3
+ import { projectStorageRoot } from "../config/index.js";
4
+ import { getOrCreateApxId } from "../apc/scaffold.js";
5
5
 
6
6
  const EMPTY_MEMORY = (slug) =>
7
7
  `# Memory — ${slug}\n\n` +
@@ -128,42 +128,6 @@ export function fallbackModels(globalConfig) {
128
128
  .filter((m) => typeof m === "string" && m.includes(":"));
129
129
  }
130
130
 
131
- /**
132
- * @deprecated use fallbackModels(). Kept for tests / external callers that
133
- * still ask "what provider to try after Ollama?". Derives the answer from the
134
- * resolved model chain.
135
- */
136
- export function fallbackOrder(globalConfig) {
137
- const models = fallbackModels(globalConfig);
138
- const providers = [];
139
- for (const m of models) {
140
- try {
141
- const p = parseModelId(m).provider;
142
- if (!providers.includes(p)) providers.push(p);
143
- } catch { /* skip malformed entries */ }
144
- }
145
- return providers.length ? providers : [...DEFAULT_FALLBACK_ORDER];
146
- }
147
-
148
- /**
149
- * @deprecated use fallbackModels(). Looks up a single provider's model in
150
- * the resolved chain. Returns "" if the provider isn't in the fallback list.
151
- */
152
- export function modelForProvider(globalConfig, provider) {
153
- const p = String(provider).toLowerCase();
154
- const sa = globalConfig?.super_agent || {};
155
- const models = fallbackModels(globalConfig);
156
- const match = models.find((m) => {
157
- try { return parseModelId(m).provider === p; } catch { return false; }
158
- });
159
- if (match) return match;
160
- // Ollama gets a special fallback to the primary model (legacy behavior).
161
- if (p === "ollama" && typeof sa.model === "string" && /^ollama:/i.test(sa.model)) {
162
- return sa.model;
163
- }
164
- return DEFAULT_FALLBACK_MODELS[p] || "";
165
- }
166
-
167
131
  export function isFallbackEnabled(globalConfig) {
168
132
  const fb = globalConfig?.super_agent?.model_fallback || {};
169
133
  return fb.enabled !== false;
@@ -243,15 +207,29 @@ export async function resolveActiveModel(globalConfig, { overrideModel = null, t
243
207
  }
244
208
 
245
209
  export async function probeAllProviders(globalConfig, timeoutMs) {
246
- const order = fallbackOrder(globalConfig);
210
+ const models = fallbackModels(globalConfig);
211
+ // Build a deduped list of {provider, model} in chain order. Fall back to
212
+ // DEFAULT_FALLBACK_ORDER + DEFAULT_FALLBACK_MODELS when nothing is configured.
213
+ const entries = [];
214
+ const seen = new Set();
215
+ for (const m of models) {
216
+ let provider;
217
+ try { provider = parseModelId(m).provider; } catch { continue; }
218
+ if (seen.has(provider)) continue;
219
+ seen.add(provider);
220
+ entries.push({ provider, model: m });
221
+ }
222
+ if (entries.length === 0) {
223
+ for (const provider of DEFAULT_FALLBACK_ORDER) {
224
+ const m = DEFAULT_FALLBACK_MODELS[provider];
225
+ entries.push({ provider, model: m || "(not set)" });
226
+ }
227
+ }
228
+
247
229
  const out = [];
248
- for (const provider of order) {
230
+ for (const { provider, model } of entries) {
249
231
  const health = await checkProviderHealth(provider, globalConfig, timeoutMs);
250
- out.push({
251
- provider,
252
- model: modelForProvider(globalConfig, provider) || "(not set)",
253
- ...health,
254
- });
232
+ out.push({ provider, model, ...health });
255
233
  }
256
234
  return out;
257
235
  }
@@ -7,13 +7,20 @@
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { readIdentity } from "../identity.js";
10
+ import { readIdentity } from "../identity/index.js";
11
11
  import { readSelfMemoryForPrompt } from "./self-memory.js";
12
+ import { buildSkillsHintBlock } from "./skills/catalog.js";
12
13
 
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  const PROMPTS_DIR = path.join(__dirname, "prompts");
15
16
  const BASE_PROMPT_PATH = path.join(PROMPTS_DIR, "super-agent-base.md");
16
17
 
18
+ // Action discipline + chit-chat rules — same file used by project agents
19
+ // (buildAgentSystem in agent-system.js), loaded once here for the super-agent.
20
+ const ACTION_DISCIPLINE = fs
21
+ .readFileSync(path.join(PROMPTS_DIR, "action-discipline.md"), "utf8")
22
+ .trimEnd();
23
+
17
24
  const promptCache = new Map();
18
25
 
19
26
  /** @deprecated use super-agent-base.md */
@@ -24,15 +31,15 @@ const LEGACY_PROMPT_PATH = path.join(PROMPTS_DIR, "super-agent-default.md");
24
31
  // turn is channel "deck" + voice mode, not its own channel.
25
32
  const CHANNEL_PROMPT_FILES = {
26
33
  telegram: "channels/telegram.md",
27
- terminal: "channels/terminal.md",
28
34
  cli: "channels/cli.md",
29
35
  routine: "channels/routine.md",
30
36
  api: "channels/api.md",
31
37
  web: "channels/web.md", // web big chat (long-form, full tools)
32
38
  web_sidebar: "channels/web_sidebar.md", // web quick chat (short, lightweight)
39
+ web_code: "channels/web_code.md", // web Code module (rich coding session)
33
40
  deck: "channels/deck.md", // cockpit dashboard
34
41
  desktop: "channels/desktop.md", // PC floating module (was "overlay")
35
- code: "channels/code.md", // web Code module (rich coding session)
42
+ code: "channels/code.md", // `apx code` terminal coding session
36
43
  };
37
44
 
38
45
  // Voice mode: spoken-reply rules layered on any surface when the turn will be
@@ -135,7 +142,7 @@ export function buildIdentityBlock(identity, userLang = "en") {
135
142
  }
136
143
 
137
144
  // "Who you're talking to" block. Agent-agnostic: built once from the resolved
138
- // sender (see core/telegram-identity.js) and injected into BOTH the super-agent
145
+ // sender (see core/identity/telegram.js) and injected into BOTH the super-agent
139
146
  // prompt and any routed project-agent prompt, so identification doesn't depend
140
147
  // on which agent answers. Returns "" when there's no sender info.
141
148
  export function buildRelationshipBlock(sender) {
@@ -173,8 +180,8 @@ export function buildRelationshipBlock(sender) {
173
180
  return lines.join("\n");
174
181
  }
175
182
 
176
- // Roby's own notebook (~/.apx/memory.md), bounded for the prompt. Returns ""
177
- // when empty so the block is dropped entirely.
183
+ // The super-agent's own notebook (~/.apx/memory.md), bounded for the prompt.
184
+ // Returns "" when empty so the block is dropped entirely.
178
185
  export function buildSelfMemoryBlock() {
179
186
  const slice = readSelfMemoryForPrompt();
180
187
  if (!slice) return "";
@@ -193,59 +200,6 @@ export function isSuperAgentEnabled(cfg) {
193
200
  return sa.enabled !== false;
194
201
  }
195
202
 
196
- function buildPermissionBlock(sa) {
197
- const permissionMode = sa.permission_mode || "automatico";
198
- const allowedTools = Array.isArray(sa.allowed_tools) ? sa.allowed_tools : [];
199
- return [
200
- "# Permission mode",
201
- `mode: ${permissionMode}`,
202
- `allowed_tools: ${allowedTools.join(", ") || "(none)"}`,
203
- "When a tool schema has confirmed, set confirmed=true only after explicit user confirmation for that exact action.",
204
- ].join("\n");
205
- }
206
-
207
- // Skill descriptions are authored for Claude Code's skill matcher, so many end
208
- // with verbose "Trigger on: …" / "Activate when …" lists and multi-sentence
209
- // usage notes. Inside Roby's prompt those tails are pure noise (he matches
210
- // semantically, not by trigger string). Keep the first sentence only, drop the
211
- // trigger/activation tail, and cap length — this is the single biggest
212
- // signal-per-token win in the prompt (~1k tokens recovered per turn).
213
- function condenseSkillDescription(desc) {
214
- if (!desc) return "(no description)";
215
- const full = String(desc).replace(/\s+/g, " ").trim();
216
- const MARKER =
217
- /\s*(?:Trigger(?:s)? on|Triggers|TRIGGER|Activate (?:on|when|only)|Use this skill (?:whenever|when)|Use (?:it )?when|Triggers include|SKIP|Also (?:use|triggers))\b/i;
218
- // Prefer the gist before any trigger/activation marker; but if a skill leads
219
- // straight into "Activate ONLY when…" (no gist first), that head is empty —
220
- // fall back to the first sentence of the full text so we keep real info.
221
- let d = full.split(MARKER)[0].trim();
222
- if (d.length < 15) d = full;
223
- // First sentence only, then cap length.
224
- const firstStop = d.search(/\.(\s|$)/);
225
- if (firstStop > 0) d = d.slice(0, firstStop + 1);
226
- d = d.trim();
227
- if (d.length > 160) d = d.slice(0, 157).trimEnd() + "…";
228
- return d || "(no description)";
229
- }
230
-
231
- function buildSkillsCatalog(listSkills) {
232
- let list = [];
233
- try {
234
- list = listSkills();
235
- } catch {
236
- /* empty */
237
- }
238
- if (!list.length) return "";
239
- return [
240
- "# Available skills (load on demand)",
241
- "Catalog (slug + one-line gist). Bodies are NOT loaded. When the user needs",
242
- "knowledge or syntax matching one (match semantically, any language), call",
243
- "load_skill({slug}).",
244
- "",
245
- ...list.map((s) => `- **${s.slug}**: ${condenseSkillDescription(s.description)}`),
246
- ].join("\n");
247
- }
248
-
249
203
  export function buildSuperAgentSystem({
250
204
  globalConfig,
251
205
  projects,
@@ -263,12 +217,12 @@ export function buildSuperAgentSystem({
263
217
  // at the end of the system prompt (where format directives belong),
264
218
  // not mixed in with situational context.
265
219
  systemSuffix = "",
266
- // Pre-rendered output of the Memory Broker (Pieza 4): a [MEMORIA RELEVANTE]
220
+ // Pre-rendered output of the Memory Broker (Pieza 4): a [RELEVANT MEMORY]
267
221
  // block built from the RAG retriever + recent memory.md entries. When
268
222
  // provided it REPLACES the always-on self-memory slice (it already includes
269
223
  // the latest notebook entries). "" falls back to the plain notebook slice.
270
224
  memoryBlock = "",
271
- // Pre-rendered "# Hilos activos en otros canales" block (recency-based
225
+ // Pre-rendered "# Active threads on other channels" block (recency-based
272
226
  // cross-channel awareness; see core/memory/active-threads.js). "" → omitted.
273
227
  activeThreadsBlock = "",
274
228
  // Compact "# Tools adicionales (activación on-demand)" block: instructions +
@@ -304,14 +258,14 @@ export function buildSuperAgentSystem({
304
258
  memoryBlock || buildSelfMemoryBlock(),
305
259
  activeThreadsBlock,
306
260
  relationshipBlock,
307
- buildPermissionBlock(sa),
308
261
  extraContext,
309
262
  "# Registered projects (just the index — call tools for details)",
310
263
  projectIndex || "(no projects registered)",
311
264
  buildProjectAgentsBlock(channelMeta?.projectPath),
312
- buildSkillsCatalog(listSkills),
265
+ buildSkillsHintBlock(listSkills),
313
266
  lazyToolsBlock,
314
267
  voiceModeBlock,
268
+ ACTION_DISCIPLINE,
315
269
  systemSuffix,
316
270
  ]
317
271
  .filter(Boolean)
@@ -0,0 +1,17 @@
1
+ ## Action Discipline (mandatory)
2
+ - NEVER acknowledge an action without executing it in the same turn. If you are going to do something, call the tool FIRST, then report the result.
3
+ - NEVER use empty acknowledgments like "Ok", "Got it", "Sure", "Understood", "On it", "Give me a moment", "I'll do that now" as standalone responses when a tool call is expected. These are invalid responses.
4
+ - Action first, report after. Produce the tool call in the same response as your acknowledgment.
5
+ - If you cannot execute the action (missing permission, unclear params, tool not available), explain WHY — do not promise and disappear.
6
+ - If the user asks you to do multiple things, do them all in the same turn using sequential tool calls if needed.
7
+
8
+ ## One reply per turn — no repeated greetings (mandatory)
9
+ - A single turn can produce SEVERAL text segments: a short narration you write BEFORE calling a tool, and the final answer that comes AFTER the tool runs. On some surfaces each segment is shown separately.
10
+ - Greet AT MOST ONCE per turn. If you already said "hola"/"hi" in an early segment, do NOT greet again in the final answer — start it with the actual content.
11
+ - NEVER repeat the same sentence, greeting, or summary across segments of the same turn. Each segment is shown in full.
12
+ - On simple requests, SKIP the intro entirely: go straight to the work, then give the result once. Only add a short intro when the work will clearly take more than a single quick tool call, and keep it to a few words ("un momento…", "reviso eso…").
13
+
14
+ ## Chit-chat & greetings (only path out of a forced tool turn)
15
+ - If the user is just greeting, chatting, or thanking you with NO actionable request ("hola", "hi", "buenas", "gracias", "👍", "ok"), you must STILL satisfy the tool-choice contract: call `finish` with a brief friendly reply in the user's language. Do NOT call any other tool just because tools are available — `finish` is the correct tool for chit-chat.
16
+ - A greeting that piggybacks a real request ("hola, listame las rutinas") is NOT chit-chat — handle the request normally with the right tool.
17
+ - When in doubt between chit-chat and a vague request, ask ONE short clarifying question via `finish` — never invent a topic or run an unrelated tool to "be useful".
@@ -1,20 +1,16 @@
1
1
  # Channel context
2
- Channel: **code** (the Code module in the web admin panel) — a rich, OpenCode-style coding session scoped to a single project. The user sees every tool call, argument, file edit, and result rendered in the UI, plus a live "changes" diff and a token/context panel.
2
+ Channel: **code** (`apx code`, the interactive APX coding session in the terminal) — the same OpenCode-style coding surface as the web Code module, just rendered in the terminal. You can read, search, edit files and run shell commands.
3
3
 
4
- Working project: **{{projectName}}** (id {{projectId}})
5
- Path: `{{projectPath}}`
6
- All file and shell tools resolve relative to that project path — operate inside it unless told otherwise.
4
+ - CWD: {{cwd}}
5
+ - References to "this directory", "this project", "here", "current folder" mean the CWD above — use it as the path argument; do not ask for a path
7
6
 
8
- {{modeGuidance}}
9
-
10
- Working style — KEEP GOING UNTIL THE TASK IS DONE:
7
+ Working style — KEEP GOING UNTIL THE TASK IS DONE (build mode):
11
8
  - You are an autonomous coding agent. Once the user gives a task, complete the WHOLE thing in this turn: chain as many tool calls as needed (read → edit → run → verify), do not stop after one or two steps.
12
- - NEVER stop to ask "do you want me to…?" / "¿confirmás…?" / "should I continue?". You already have permission on this surface — just do it. Only ask a question if the task is truly ambiguous and you genuinely cannot proceed.
9
+ - NEVER stop to ask "do you want me to…?" / "should I continue?" / "is that OK?". You already have permission on this surface — just do it. Only ask if the task is truly ambiguous and you genuinely cannot proceed.
13
10
  - NEVER announce an action and then end your turn ("now I'll edit the file." → stop). If you say you will do something, immediately call the tool and actually do it in the same turn.
14
- - After each tool result, decide the next concrete step and take it. Keep iterating until the user's request is fully satisfied; only then write your final summary.
11
+ - After each tool result, decide the next concrete step and take it. Keep iterating until the request is fully satisfied; only then write your final summary.
15
12
  - If something fails, read the error, fix it, and retry — don't hand the problem back to the user.
16
13
 
17
14
  Formatting:
18
- - Markdown with code fences is expected. Narrate what you're doing naturally; don't re-paste full tool output the user can already see in the UI.
19
- - Lead with the result. Keep prose tight this is a working surface, not a chat.
20
- - When you edit files, prefer small, surgical `edit_file` changes over rewriting whole files, and explain the intent of each change briefly.
15
+ - Markdown OK; keep readable; use code diffs when editing.
16
+ - Lead with the result; keep prose tight. Don't re-paste full tool output the user can already see.
@@ -16,8 +16,9 @@ Formatting:
16
16
  done and a tiny confirmation, not an explanation.
17
17
  - No markdown tables, no code fences, no bulleted lists unless the user
18
18
  explicitly asks. Plain prose only — these get read aloud verbatim.
19
- - No URLs / file paths spelled out — refer to them by name ("abrí Voces en
20
- la admin web" rather than "http://localhost:7430/m/voice").
19
+ - No URLs / file paths spelled out — refer to them by name (e.g. "open
20
+ Voices in the web admin" rather than "http://localhost:7430/m/voice").
21
+ Use the user's language when phrasing it.
21
22
  - If a Voice mode block is present below, its rules win over anything here.
22
23
  - Bias hard toward DOING the action and reporting the result in one breath,
23
24
  rather than asking back. Confirm-after, not confirm-before, for
@@ -27,8 +28,9 @@ Don't repeat yourself (this matters — your messages are shown AND spoken):
27
28
  - Greet AT MOST once per conversation. If you already said hi, never greet
28
29
  again — jump straight to the answer.
29
30
  - When you call a tool, any line BEFORE it must be a 2–4 word filler only
30
- ("Dame un segundo…", "Ya lo busco…"). NEVER state the answer, the list, or
31
- the result before the tool has run — you don't have it yet.
31
+ (e.g. "one moment…", "checking that…", in the user's language). NEVER
32
+ state the answer, the list, or the result before the tool has run — you
33
+ don't have it yet.
32
34
  - After the tool returns, give the result ONCE. Do not re-announce it, do not
33
35
  re-greet, do not restate the filler. One clean reply.
34
36
  - Never say the same thing twice across a single turn.
@@ -1,9 +1,18 @@
1
1
  # Channel context
2
2
  Channel: **routine** (autonomous scheduled run — not an interactive chat).
3
3
 
4
- Routine: `{{routineName}}`
4
+ Routine name: `{{routineName}}`
5
+ Routine ID: `{{routineId}}`
6
+ Schedule: `{{routineSchedule}}`
7
+ Last run: {{routineLastRun}}
8
+ Routine memory: `{{routineMemoryPath}}`
5
9
  Project path: {{projectPath}}
6
10
 
11
+ ## Routine memory (durable notes for this routine)
12
+ {{routineMemory}}
13
+
7
14
  Formatting:
8
15
  - Execute the task fully; no conversational filler
9
16
  - This is not Telegram — do not optimize for chat brevity unless the routine prompt says so
17
+ - "Last run" is when this routine fired previously — use it to know what's already been done; never repeat the same work blindly
18
+ - "Routine memory" above is everything this specific routine has decided to remember across runs. Treat as known facts. The file exists at the path shown — read it in full with `read_file` if the slice looks truncated.
@@ -7,3 +7,8 @@ Formatting:
7
7
  - Previous turns are conversational context only; re-call tools for facts
8
8
 
9
9
  What the user sees here: ONLY your final text reply. They do NOT see your tool calls, args, or intermediate results — those never reach Telegram. So if a request needs real work (running something, searching, editing, a multi-step task), the channel sends a short "on it" heads-up for you; you still must report what you actually did in plain words at the end. Never assume they saw what you ran.
10
+
11
+ Segments policy: when you write any prose BEFORE calling a tool (an intro like "voy a revisar…") it lands as its OWN Telegram message — separate from the final answer that comes AFTER the tool runs. So:
12
+ - Greet at most ONCE per turn. If you already said "Hola" in the intro segment, do NOT greet again in the final answer. Start the final answer with the actual content.
13
+ - Prefer to skip the intro entirely on simple requests — go straight to the work, then answer. Only add an intro when the work will take noticeably longer than a single tool call.
14
+ - Never repeat the same sentence across segments — each message is shown in full to the user.
@@ -0,0 +1,20 @@
1
+ # Channel context
2
+ Channel: **web_code** (the Code module in the web admin panel at `/m/code`) — a rich, OpenCode-style coding session scoped to a single project. The user sees every tool call, argument, file edit, and result rendered in the UI, plus a live "changes" diff and a token/context panel.
3
+
4
+ Working project: **{{projectName}}** (id {{projectId}})
5
+ Path: `{{projectPath}}`
6
+ All file and shell tools resolve relative to that project path — operate inside it unless told otherwise.
7
+
8
+ {{modeGuidance}}
9
+
10
+ Working style — KEEP GOING UNTIL THE TASK IS DONE:
11
+ - You are an autonomous coding agent. Once the user gives a task, complete the WHOLE thing in this turn: chain as many tool calls as needed (read → edit → run → verify), do not stop after one or two steps.
12
+ - NEVER stop to ask "do you want me to…?" / "should I continue?" / "is that OK?". You already have permission on this surface — just do it. Only ask a question if the task is truly ambiguous and you genuinely cannot proceed.
13
+ - NEVER announce an action and then end your turn ("now I'll edit the file." → stop). If you say you will do something, immediately call the tool and actually do it in the same turn.
14
+ - After each tool result, decide the next concrete step and take it. Keep iterating until the user's request is fully satisfied; only then write your final summary.
15
+ - If something fails, read the error, fix it, and retry — don't hand the problem back to the user.
16
+
17
+ Formatting:
18
+ - Markdown with code fences is expected. Narrate what you're doing naturally; don't re-paste full tool output the user can already see in the UI.
19
+ - Lead with the result. Keep prose tight — this is a working surface, not a chat.
20
+ - When you edit files, prefer small, surgical `edit_file` changes over rewriting whole files, and explain the intent of each change briefly.
@@ -1,4 +1,4 @@
1
1
  # Voice mode (your reply will be SPOKEN by a TTS engine)
2
2
  - Two short sentences max. No markdown, no bullet lists, no code fences, no URLs — it gets read aloud.
3
- - Lead with the outcome ("Listo, anoté la tarea"), then at most one detail.
4
- - Spell things out for the ear: say "punto" not ".", avoid long ids/paths — summarize them ("el proyecto apx").
3
+ - Lead with the outcome (e.g. "Done, the task is noted"), then at most one detail. Phrase it in the user's language.
4
+ - Spell things out for the ear: say "dot" instead of ".", avoid long ids/paths — summarize them (e.g. "the apx project").
@@ -19,8 +19,8 @@ You also ship **apx-\* skills** with the exact syntax for multi-step APX operati
19
19
 
20
20
  # Memory & history
21
21
  You have durable memory across all channels — never deny it. Two sources:
22
- - **Sessions & chat logs**: when asked what you worked on, about a "previous/last session", or "what we talked about", call `search_sessions` (defaults to your own apx sessions — pass `engine` only when the user names claude/codex, `all:true` only when they want every engine; pass `id` to open a transcript) and/or `search_messages`. Answer didactically in prose ("la última vez hicimos X e Y"), not as a raw list of titles. If your sessions are thin, say so and offer to look across engines — never conclude you "have no history".
23
- - **Your notebook (self-memory)**: `~/.apx/memory.md`, a bounded slice injected above (as "# Your notebook" or folded into "# Memoria relevante"). At the end of any turn where something durable happened (a decision, a completed task, an agreed fact), save the gist with `remember` so your other channels know it too. Keep notes to one self-contained sentence. Use `create_task` for one-off TODOs and project-agent memory for project-scoped facts. When a "# Memoria relevante" block is present, treat its bullets as known facts; if a fresh chat opens and something there is still open, bring it up naturally ("ayer estuvimos con X, ¿seguimos?") — weave in only what's relevant, don't dump the block.
22
+ - **Sessions & chat logs**: when asked what you worked on, about a "previous/last session", or "what we talked about", call `search_sessions` (defaults to your own apx sessions — pass `engine` only when the user names claude/codex, `all:true` only when they want every engine; pass `id` to open a transcript) and/or `search_messages`. Answer didactically in prose (in the user's language — e.g. "last time we worked on X and Y"), not as a raw list of titles. If your sessions are thin, say so and offer to look across engines — never conclude you "have no history".
23
+ - **Your notebook (self-memory)**: `~/.apx/memory.md`, a bounded slice injected above (as "# Your notebook" or folded into "# Relevant memory"). At the end of any turn where something durable happened (a decision, a completed task, an agreed fact), save the gist with `remember` so your other channels know it too. Keep notes to one self-contained sentence. Use `create_task` for one-off TODOs and project-agent memory for project-scoped facts. When a "# Relevant memory" block is present, treat its bullets as known facts; if a fresh chat opens and something there is still open, bring it up naturally in the user's language (e.g. "yesterday we were on X — shall we continue?") — weave in only what's relevant, don't dump the block.
24
24
 
25
25
  # How you operate
26
26
  - APC projects live anywhere on disk; the default workspace is APX home, not a user repo. Registered projects appear below as a tiny index — call tools for details.
@@ -2,16 +2,10 @@ import { callEngine } from "../engines/index.js";
2
2
  import {
3
3
  extractPseudoToolCalls,
4
4
  cleanTextOfPseudoToolCalls,
5
- } from "./tool-call-parser.js";
5
+ } from "./tools/tool-call-parser.js";
6
6
  import { resolveActiveModel, fallbackModels } from "./model-router.js";
7
- import { MAX_TOOL_ITERS, ACK_ONLY_TOOLS, MAX_CONSECUTIVE_ACKS } from "./constants.js";
8
- import {
9
- isShortConfirmation,
10
- lastAssistantAskedForConfirmation,
11
- isGhostResponse,
12
- looksLikeActionRequest,
13
- } from "./ghost-guard.js";
14
- import { pseudoToolSystem, shouldRetryWithPseudoTools } from "./pseudo-tools.js";
7
+ import { MAX_TOOL_ITERS, ACK_ONLY_TOOLS, MAX_CONSECUTIVE_ACKS, TURN_ENDING_TOOLS } from "./constants.js";
8
+ import { pseudoToolSystem, shouldRetryWithPseudoTools } from "./tools/pseudo-tools.js";
15
9
  import { filterToolSchemas } from "./tools-overlap.js";
16
10
  import { isRetryableEngineError, shortRetryReason } from "./retry.js";
17
11
 
@@ -39,6 +33,19 @@ function fallbackFinalText(trace, error) {
39
33
  return lines.join("\n");
40
34
  }
41
35
 
36
+ // A leading greeting clause: "¡Hola Manu!", "Hola,", "Hi there!", "Buenas tardes…".
37
+ // Intentionally narrow — only the opening salutation up to its first terminator —
38
+ // so we never eat real content.
39
+ const LEADING_GREETING_RE =
40
+ /^\s*[¡!]*\s*(hola+|holis?|buenas|buen[oa]s?\s+(d[ií]as|tardes|noches)|hey|hi|hello)\b[^.!?¡\n]*[.!?¡]*[\s,]*/i;
41
+
42
+ /** If `text` opens with a greeting, return it with that greeting removed; else null. */
43
+ function stripLeadingGreeting(text) {
44
+ const m = String(text).match(LEADING_GREETING_RE);
45
+ if (!m) return null;
46
+ return String(text).slice(m[0].length).replace(/^\s+/, "");
47
+ }
48
+
42
49
  function previewTraceResult(result) {
43
50
  if (result === null || result === undefined) return "ok";
44
51
  if (typeof result === "string") return result.slice(0, 180);
@@ -160,11 +167,7 @@ export async function runAgent({
160
167
  effectiveSchemas = [...effectiveSchemas, FINISH_TOOL_SCHEMA];
161
168
  }
162
169
 
163
- const rawHandlers = makeToolHandlers({
164
- ...toolHandlerCtx,
165
- implicitConfirmation:
166
- isShortConfirmation(prompt) && lastAssistantAskedForConfirmation(previousMessages),
167
- });
170
+ const rawHandlers = makeToolHandlers(toolHandlerCtx);
168
171
  const handlers = suppressed.size > 0
169
172
  ? new Proxy(rawHandlers, {
170
173
  get(target, name) {
@@ -201,6 +204,22 @@ export async function runAgent({
201
204
  const trace = [];
202
205
  let totalUsage = { input_tokens: 0, output_tokens: 0 };
203
206
  let lastText = "";
207
+
208
+ // Collapse repeated greetings within a single turn. A turn can produce several
209
+ // text segments (pre-tool narration + final answer) and weaker models greet in
210
+ // each one, so the user sees "¡Hola Manu!" twice. Keep the first greeting,
211
+ // strip any later one. Belt-and-suspenders over the action-discipline prompt
212
+ // rule (which strong models follow but gemini-flash et al. often ignore).
213
+ let greetedThisTurn = false;
214
+ const dedupeGreeting = (text) => {
215
+ if (!text) return text;
216
+ if (greetedThisTurn) {
217
+ const stripped = stripLeadingGreeting(text);
218
+ return stripped == null ? text : stripped;
219
+ }
220
+ if (LEADING_GREETING_RE.test(text)) greetedThisTurn = true;
221
+ return text;
222
+ };
204
223
  let usePseudoTools = false;
205
224
  let ackOnlyStreak = 0;
206
225
  // Side-effect dedupe. Weaker models (Gemini especially) sometimes
@@ -258,16 +277,9 @@ export async function runAgent({
258
277
  // Merge any tools activated via discover_tools on the previous iteration.
259
278
  drainPendingTools();
260
279
  await emitProgress(onEvent, { type: "model_start", iteration: iter + 1, model: activeModel });
261
- // Force a tool call on iter 0 ONLY when the user message looks like a real
262
- // action request ("listame…", "mandá…", "buscá…"). For chit-chat ("hola",
263
- // "qué tal") forcing a tool makes weaker models (llama-3.3 via Groq,
264
- // qwen3-32b) emit a malformed tool_calls payload — Groq then rejects the
265
- // whole turn with 400 "Failed to call a function". Better: let the model
266
- // choose between text and tool when the prompt is conversational.
267
280
  const forceTool =
268
281
  effectiveSchemas.length > 0 &&
269
282
  (useContract ||
270
- (iter === 0 && looksLikeActionRequest(prompt)) ||
271
283
  (ackOnlyStreak > 0 && ackOnlyStreak <= MAX_CONSECUTIVE_ACKS));
272
284
  let result;
273
285
  try {
@@ -320,22 +332,11 @@ export async function runAgent({
320
332
  }
321
333
 
322
334
  if (!toolCalls || toolCalls.length === 0) {
323
- if (iter === 0 && isGhostResponse(lastText) && looksLikeActionRequest(prompt)) {
324
- await emitProgress(onEvent, { type: "ghost_response_detected", text: lastText });
325
- conversation.push({ role: "assistant", content: lastText });
326
- conversation.push({
327
- role: "user",
328
- content:
329
- "Remember: you must execute the action, not just confirm it. " +
330
- "Call the tool now — action first, report after.",
331
- });
332
- continue;
333
- }
334
335
  lastText = cleanTextOfPseudoToolCalls(lastText) || lastText;
335
336
  break;
336
337
  }
337
338
 
338
- const visibleText = cleanTextOfPseudoToolCalls(lastText).trim();
339
+ const visibleText = dedupeGreeting(cleanTextOfPseudoToolCalls(lastText).trim());
339
340
  if (visibleText) {
340
341
  await emitProgress(onEvent, { type: "assistant_text", text: visibleText, iteration: iter + 1 });
341
342
  }
@@ -347,6 +348,7 @@ export async function runAgent({
347
348
  });
348
349
 
349
350
  let finishSummary = null;
351
+ let turnEndingQuestions = null;
350
352
  for (const tc of toolCalls) {
351
353
  const fn = tc.function || tc;
352
354
  const name = fn.name;
@@ -409,18 +411,45 @@ export async function runAgent({
409
411
  tool_name: name,
410
412
  content: JSON.stringify(toolResult),
411
413
  });
414
+
415
+ // Capture turn-ending intents (e.g. ask_questions). The loop cannot
416
+ // legitimately advance without a user reply; under completionContract
417
+ // forcing another tool call just produces ask_questions spam.
418
+ if (TURN_ENDING_TOOLS.has(name) && !turnEndingQuestions) {
419
+ // Questions may be plain strings (legacy) or {question, options, ...}.
420
+ // For the assistant_text fallback we only need the prompt strings.
421
+ const qs = Array.isArray(args.questions)
422
+ ? args.questions
423
+ .map((q) => (typeof q === "string" ? q : q && typeof q.question === "string" ? q.question : null))
424
+ .filter(Boolean)
425
+ : [];
426
+ turnEndingQuestions = qs;
427
+ }
412
428
  }
413
429
 
414
430
  // Task declared complete via the contract — emit the summary as the final
415
431
  // assistant text and exit the loop.
416
432
  if (finishSummary !== null) {
417
433
  if (finishSummary) {
418
- lastText = finishSummary;
419
- await emitProgress(onEvent, { type: "assistant_text", text: finishSummary, iteration: iter + 1 });
434
+ lastText = dedupeGreeting(finishSummary) || "";
435
+ if (lastText) await emitProgress(onEvent, { type: "assistant_text", text: lastText, iteration: iter + 1 });
420
436
  }
421
437
  break;
422
438
  }
423
439
 
440
+ // ask_questions (or future turn-ending tools): the task is genuinely
441
+ // blocked on user input. Exit the loop — completionContract or not,
442
+ // asking again gets us nowhere. We deliberately do NOT emit a synthetic
443
+ // assistant_text and we leave lastText empty so persistence and one-shot
444
+ // API callers don't end up with a duplicate bullet list next to the
445
+ // rendering surfaces' own UI (web AskQuestionsCard, terminal renderer,
446
+ // telegram inline keyboard). The structured questions live on the tool
447
+ // trace — that's the canonical source.
448
+ if (turnEndingQuestions) {
449
+ if (!lastText) lastText = "";
450
+ break;
451
+ }
452
+
424
453
  const allAckOnly = toolCalls.every((tc) => {
425
454
  const n = (tc.function?.name) || tc.name;
426
455
  return ACK_ONLY_TOOLS.has(n);
@@ -434,7 +463,8 @@ export async function runAgent({
434
463
  }
435
464
 
436
465
  return {
437
- text: lastText,
466
+ // Strip a final greeting if an earlier segment in this turn already greeted.
467
+ text: dedupeGreeting(lastText),
438
468
  usage: totalUsage,
439
469
  name: agentName,
440
470
  trace,
@@ -0,0 +1,42 @@
1
+ // Prompt-shaped hint injected into external runtimes when APX delegates work
2
+ // to them (Claude Code, Codex, OpenCode, Aider, Cursor Agent, Gemini CLI,
3
+ // Qwen Code). Tells the runtime two things only:
4
+ //
5
+ // 1. "You're being orchestrated by APX as the super-agent (or as agent X
6
+ // via APX delegation)" — equivalent to a one-shot a2a hand-off.
7
+ // 2. The session id APX created on disk so the runtime can echo it back
8
+ // via `apx session close <id> --result "..."` if its shell allows it.
9
+ //
10
+ // We intentionally do NOT re-explain APC: every runtime that can read this
11
+ // hint already has the apc-context skill (or equivalent rule) installed in
12
+ // its own config, and that rule covers the project context. Repeating it
13
+ // here just bloats the prompt. The bridge is APX-specific glue.
14
+ //
15
+ // (Was `buildApfHint` in host/daemon/apc-runtime-context.js. "APF" was the
16
+ // old internal name; renamed for clarity. Lifecycle helpers
17
+ // — createRuntimeSession / closeRuntimeSession — live in
18
+ // core/stores/runtime-sessions.js.)
19
+
20
+ const RUNTIME_BRIDGE_HINT = `
21
+ # APX runtime delegation
22
+
23
+ You are being run by APX. The APX super-agent (or the named agent below) handed this turn to you as a one-shot delegation — think of it as an a2a (agent-to-agent) call where you are the callee. APX is the parent process; the project's apc-context already explains the codebase.
24
+
25
+ - Project: {{name}}
26
+ - Delegating agent: {{agent}}
27
+ - APX session id: {{session_id}}
28
+
29
+ When you finish, if you can shell out, leave a short trace for APX:
30
+ apx session close {{session_id}} --result "<one-line summary>"
31
+ `.trim();
32
+
33
+ export function buildRuntimeBridgeHint({ projectName, projectPath, agentSlug, sessionId }) {
34
+ return RUNTIME_BRIDGE_HINT
35
+ .replace(/\{\{name\}\}/g, projectName)
36
+ .replace(/\{\{path\}\}/g, projectPath)
37
+ .replace(/\{\{agent\}\}/g, agentSlug)
38
+ .replace(/\{\{session_id\}\}/g, sessionId);
39
+ }
40
+
41
+ // Back-compat alias — callers can migrate at their own pace.
42
+ export const buildApfHint = buildRuntimeBridgeHint;