funolio-agent 1.0.7 → 1.0.48

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 (239) hide show
  1. package/dist/agent-config.d.ts +9 -1
  2. package/dist/agent-config.d.ts.map +1 -1
  3. package/dist/agent-config.js +4 -1
  4. package/dist/agent-config.js.map +1 -1
  5. package/dist/approval.d.ts +1 -0
  6. package/dist/approval.d.ts.map +1 -1
  7. package/dist/approval.js +12 -0
  8. package/dist/approval.js.map +1 -1
  9. package/dist/auth/auto-detect.d.ts +11 -3
  10. package/dist/auth/auto-detect.d.ts.map +1 -1
  11. package/dist/auth/auto-detect.js +136 -168
  12. package/dist/auth/auto-detect.js.map +1 -1
  13. package/dist/auth/subscription-runtime.js +1 -1
  14. package/dist/auth/subscription-runtime.js.map +1 -1
  15. package/dist/auto-organizer.d.ts.map +1 -1
  16. package/dist/auto-organizer.js +4 -3
  17. package/dist/auto-organizer.js.map +1 -1
  18. package/dist/backfill.d.ts.map +1 -1
  19. package/dist/backfill.js +34 -30
  20. package/dist/backfill.js.map +1 -1
  21. package/dist/bot-manager.d.ts +4 -8
  22. package/dist/bot-manager.d.ts.map +1 -1
  23. package/dist/bot-manager.js +31 -160
  24. package/dist/bot-manager.js.map +1 -1
  25. package/dist/clerk-model.d.ts +15 -7
  26. package/dist/clerk-model.d.ts.map +1 -1
  27. package/dist/clerk-model.js +78 -43
  28. package/dist/clerk-model.js.map +1 -1
  29. package/dist/cli-session-epoch.d.ts +10 -0
  30. package/dist/cli-session-epoch.d.ts.map +1 -0
  31. package/dist/cli-session-epoch.js +61 -0
  32. package/dist/cli-session-epoch.js.map +1 -0
  33. package/dist/cli.js +7 -2
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/import-history.js +5 -1
  36. package/dist/commands/import-history.js.map +1 -1
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js +30 -1
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/pool.js +1 -1
  41. package/dist/commands/pool.js.map +1 -1
  42. package/dist/commands/setup.d.ts +37 -0
  43. package/dist/commands/setup.d.ts.map +1 -1
  44. package/dist/commands/setup.js +146 -43
  45. package/dist/commands/setup.js.map +1 -1
  46. package/dist/commands/start.d.ts.map +1 -1
  47. package/dist/commands/start.js +117 -255
  48. package/dist/commands/start.js.map +1 -1
  49. package/dist/config-cleanup.d.ts.map +1 -1
  50. package/dist/config-cleanup.js +2 -1
  51. package/dist/config-cleanup.js.map +1 -1
  52. package/dist/config.d.ts +6 -9
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +7 -18
  55. package/dist/config.js.map +1 -1
  56. package/dist/context-window.d.ts +33 -5
  57. package/dist/context-window.d.ts.map +1 -1
  58. package/dist/context-window.js +122 -21
  59. package/dist/context-window.js.map +1 -1
  60. package/dist/eval/orchestrator-front-door-replay.js +1 -1
  61. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  62. package/dist/eval/policy-detection-replay.js +1 -1
  63. package/dist/eval/policy-detection-replay.js.map +1 -1
  64. package/dist/import-parser-core.d.ts.map +1 -1
  65. package/dist/import-parser-core.js +74 -8
  66. package/dist/import-parser-core.js.map +1 -1
  67. package/dist/integration-tokens.d.ts +1 -6
  68. package/dist/integration-tokens.d.ts.map +1 -1
  69. package/dist/integration-tokens.js +38 -40
  70. package/dist/integration-tokens.js.map +1 -1
  71. package/dist/local-cli-pty-manager.d.ts +50 -0
  72. package/dist/local-cli-pty-manager.d.ts.map +1 -0
  73. package/dist/local-cli-pty-manager.js +645 -0
  74. package/dist/local-cli-pty-manager.js.map +1 -0
  75. package/dist/local-data.d.ts +89 -6
  76. package/dist/local-data.d.ts.map +1 -1
  77. package/dist/local-data.js +600 -63
  78. package/dist/local-data.js.map +1 -1
  79. package/dist/local-db.d.ts.map +1 -1
  80. package/dist/local-db.js +74 -1
  81. package/dist/local-db.js.map +1 -1
  82. package/dist/local-funnel.d.ts +0 -7
  83. package/dist/local-funnel.d.ts.map +1 -1
  84. package/dist/local-funnel.js +22 -30
  85. package/dist/local-funnel.js.map +1 -1
  86. package/dist/local-import-worker.d.ts.map +1 -1
  87. package/dist/local-import-worker.js +49 -4
  88. package/dist/local-import-worker.js.map +1 -1
  89. package/dist/local-memory-search.d.ts +1 -0
  90. package/dist/local-memory-search.d.ts.map +1 -1
  91. package/dist/local-memory-search.js +107 -21
  92. package/dist/local-memory-search.js.map +1 -1
  93. package/dist/local-server.d.ts +21 -0
  94. package/dist/local-server.d.ts.map +1 -1
  95. package/dist/local-server.js +1057 -501
  96. package/dist/local-server.js.map +1 -1
  97. package/dist/mcp/bridge-server.d.ts.map +1 -1
  98. package/dist/mcp/bridge-server.js +2 -1
  99. package/dist/mcp/bridge-server.js.map +1 -1
  100. package/dist/mcp/local-memory-server.d.ts +6 -1
  101. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  102. package/dist/mcp/local-memory-server.js +38 -13
  103. package/dist/mcp/local-memory-server.js.map +1 -1
  104. package/dist/mcp/manager.d.ts +3 -22
  105. package/dist/mcp/manager.d.ts.map +1 -1
  106. package/dist/mcp/manager.js +66 -320
  107. package/dist/mcp/manager.js.map +1 -1
  108. package/dist/memory-extraction.d.ts +2 -0
  109. package/dist/memory-extraction.d.ts.map +1 -1
  110. package/dist/memory-extraction.js +3 -1
  111. package/dist/memory-extraction.js.map +1 -1
  112. package/dist/message-loop.d.ts +1 -3
  113. package/dist/message-loop.d.ts.map +1 -1
  114. package/dist/message-loop.js +220 -437
  115. package/dist/message-loop.js.map +1 -1
  116. package/dist/mqtt-client.d.ts +2 -28
  117. package/dist/mqtt-client.d.ts.map +1 -1
  118. package/dist/mqtt-client.js +2 -2
  119. package/dist/mqtt-client.js.map +1 -1
  120. package/dist/oauth.d.ts +6 -0
  121. package/dist/oauth.d.ts.map +1 -1
  122. package/dist/oauth.js +91 -0
  123. package/dist/oauth.js.map +1 -1
  124. package/dist/orchestration/front-door-policy.d.ts +5 -2
  125. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  126. package/dist/orchestration/front-door-policy.js +25 -28
  127. package/dist/orchestration/front-door-policy.js.map +1 -1
  128. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
  129. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
  130. package/dist/orchestration/orchestrator-blocked-prompt.js +12 -1
  131. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
  132. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
  133. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
  134. package/dist/orchestration/orchestrator-final-response-prompt.js +9 -7
  135. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
  136. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  137. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  138. package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
  139. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  140. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  141. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  142. package/dist/orchestration/worker-operating-prompt.js +41 -2
  143. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  144. package/dist/orchestrator.d.ts +17 -0
  145. package/dist/orchestrator.d.ts.map +1 -1
  146. package/dist/orchestrator.js +328 -166
  147. package/dist/orchestrator.js.map +1 -1
  148. package/dist/prompt-template.js +3 -3
  149. package/dist/prompt-template.js.map +1 -1
  150. package/dist/providers/anthropic.d.ts +0 -5
  151. package/dist/providers/anthropic.d.ts.map +1 -1
  152. package/dist/providers/anthropic.js +29 -75
  153. package/dist/providers/anthropic.js.map +1 -1
  154. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  155. package/dist/providers/claude-cli-prompt.js +22 -6
  156. package/dist/providers/claude-cli-prompt.js.map +1 -1
  157. package/dist/providers/claude-cli.d.ts.map +1 -1
  158. package/dist/providers/claude-cli.js +36 -142
  159. package/dist/providers/claude-cli.js.map +1 -1
  160. package/dist/providers/codex-cli.d.ts.map +1 -1
  161. package/dist/providers/codex-cli.js +148 -74
  162. package/dist/providers/codex-cli.js.map +1 -1
  163. package/dist/providers/google.d.ts.map +1 -1
  164. package/dist/providers/google.js +4 -2
  165. package/dist/providers/google.js.map +1 -1
  166. package/dist/providers/index.d.ts +13 -0
  167. package/dist/providers/index.d.ts.map +1 -1
  168. package/dist/providers/index.js.map +1 -1
  169. package/dist/providers/openai.d.ts.map +1 -1
  170. package/dist/providers/openai.js +27 -2
  171. package/dist/providers/openai.js.map +1 -1
  172. package/dist/runtime-context.d.ts +10 -0
  173. package/dist/runtime-context.d.ts.map +1 -0
  174. package/dist/runtime-context.js +30 -0
  175. package/dist/runtime-context.js.map +1 -0
  176. package/dist/storage-mode.d.ts +5 -0
  177. package/dist/storage-mode.d.ts.map +1 -0
  178. package/dist/storage-mode.js +21 -0
  179. package/dist/storage-mode.js.map +1 -0
  180. package/dist/subagent/queue.d.ts.map +1 -1
  181. package/dist/subagent/queue.js +1 -0
  182. package/dist/subagent/queue.js.map +1 -1
  183. package/dist/summarization-pipeline.d.ts +10 -0
  184. package/dist/summarization-pipeline.d.ts.map +1 -1
  185. package/dist/summarization-pipeline.js +147 -34
  186. package/dist/summarization-pipeline.js.map +1 -1
  187. package/dist/tool-permissions.d.ts +2 -0
  188. package/dist/tool-permissions.d.ts.map +1 -0
  189. package/dist/tool-permissions.js +25 -0
  190. package/dist/tool-permissions.js.map +1 -0
  191. package/dist/tools/analyze-image.js +2 -2
  192. package/dist/tools/analyze-image.js.map +1 -1
  193. package/dist/tools/edit-file.js +3 -3
  194. package/dist/tools/edit-file.js.map +1 -1
  195. package/dist/tools/index.d.ts +7 -8
  196. package/dist/tools/index.d.ts.map +1 -1
  197. package/dist/tools/index.js +106 -60
  198. package/dist/tools/index.js.map +1 -1
  199. package/dist/tools/list-directory.js +7 -4
  200. package/dist/tools/list-directory.js.map +1 -1
  201. package/dist/tools/read-file.js +3 -3
  202. package/dist/tools/read-file.js.map +1 -1
  203. package/dist/tools/run-command.js +3 -3
  204. package/dist/tools/run-command.js.map +1 -1
  205. package/dist/tools/sandbox.d.ts +10 -5
  206. package/dist/tools/sandbox.d.ts.map +1 -1
  207. package/dist/tools/sandbox.js +41 -13
  208. package/dist/tools/sandbox.js.map +1 -1
  209. package/dist/tools/search-codebase.js +2 -2
  210. package/dist/tools/search-codebase.js.map +1 -1
  211. package/dist/tools/search-local-memory.d.ts.map +1 -1
  212. package/dist/tools/search-local-memory.js +19 -8
  213. package/dist/tools/search-local-memory.js.map +1 -1
  214. package/dist/tools/search-memory.d.ts.map +1 -1
  215. package/dist/tools/search-memory.js +9 -3
  216. package/dist/tools/search-memory.js.map +1 -1
  217. package/dist/tools/spawn-subagent.d.ts.map +1 -1
  218. package/dist/tools/spawn-subagent.js +1 -0
  219. package/dist/tools/spawn-subagent.js.map +1 -1
  220. package/dist/tools/write-file.js +3 -3
  221. package/dist/tools/write-file.js.map +1 -1
  222. package/dist/types.d.ts +5 -0
  223. package/dist/types.d.ts.map +1 -1
  224. package/dist/types.js +0 -3
  225. package/dist/types.js.map +1 -1
  226. package/dist/verification/index.js +2 -2
  227. package/dist/verification/index.js.map +1 -1
  228. package/dist/wizard-state.d.ts.map +1 -1
  229. package/dist/wizard-state.js +16 -2
  230. package/dist/wizard-state.js.map +1 -1
  231. package/dist/wizard-support.d.ts +2 -2
  232. package/dist/wizard-support.d.ts.map +1 -1
  233. package/dist/wizard-support.js +88 -99
  234. package/dist/wizard-support.js.map +1 -1
  235. package/dist/workflow-engine.d.ts +9 -3
  236. package/dist/workflow-engine.d.ts.map +1 -1
  237. package/dist/workflow-engine.js +378 -82
  238. package/dist/workflow-engine.js.map +1 -1
  239. package/package.json +2 -1
@@ -47,17 +47,12 @@ const approval_1 = require("./approval");
47
47
  const local_db_1 = require("./local-db");
48
48
  const config_1 = require("./config");
49
49
  const auto_detect_1 = require("./auth/auto-detect");
50
+ const anthropic_subscription_1 = require("./auth/anthropic-subscription");
50
51
  const clerk_model_1 = require("./clerk-model");
51
52
  const local_funnel_1 = require("./local-funnel");
52
53
  const auto_organizer_1 = require("./auto-organizer");
53
- const orchestrator_profile_1 = require("./orchestrator-profile");
54
- const response_guard_1 = require("./response-guard");
55
- const context_compressor_1 = require("./context-compressor");
56
- const crypto = __importStar(require("crypto"));
54
+ const tool_permissions_1 = require("./tool-permissions");
57
55
  const data = __importStar(require("./local-data"));
58
- const agent_config_1 = require("./agent-config");
59
- const subscription_runtime_1 = require("./auth/subscription-runtime");
60
- const prompt_template_1 = require("./prompt-template");
61
56
  /** Determine priority from an AgentCommand */
62
57
  function getCommandPriority(command) {
63
58
  if (command.priority)
@@ -72,6 +67,56 @@ function getCommandPriority(command) {
72
67
  return 'medium';
73
68
  }
74
69
  const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
70
+ function classifyToolName(toolName) {
71
+ switch (toolName) {
72
+ case 'read_file':
73
+ return 'file_read';
74
+ case 'list_directory':
75
+ return 'directory_scan';
76
+ case 'write_file':
77
+ case 'edit_file':
78
+ return 'file_write';
79
+ case 'run_command':
80
+ case 'git_status':
81
+ case 'git_diff':
82
+ case 'git_commit':
83
+ return 'command_run';
84
+ case 'web_fetch':
85
+ return 'web_fetch';
86
+ default:
87
+ return 'workspace_hint';
88
+ }
89
+ }
90
+ function extractToolLocation(toolName, args, projectDir) {
91
+ const path = typeof args.path === 'string'
92
+ ? args.path
93
+ : typeof args.filePath === 'string'
94
+ ? args.filePath
95
+ : typeof args.directory === 'string'
96
+ ? args.directory
97
+ : undefined;
98
+ const cwd = typeof args.cwd === 'string'
99
+ ? args.cwd
100
+ : toolName === 'run_command' || toolName.startsWith('git_')
101
+ ? projectDir
102
+ : undefined;
103
+ const url = typeof args.url === 'string' ? args.url : undefined;
104
+ return { path, cwd, url };
105
+ }
106
+ function summarizeToolResult(output, isError) {
107
+ const prefix = isError ? 'Tool failed: ' : 'Tool completed: ';
108
+ const compact = output.replace(/\s+/g, ' ').trim();
109
+ return `${prefix}${compact}`.slice(0, 240);
110
+ }
111
+ function normalizeMcpToolNames(toolNames) {
112
+ if (!Array.isArray(toolNames))
113
+ return undefined;
114
+ const trimmed = toolNames
115
+ .filter((toolName) => typeof toolName === 'string')
116
+ .map((toolName) => toolName.trim())
117
+ .filter(Boolean);
118
+ return [...new Set(trimmed)];
119
+ }
75
120
  class MessageLoop {
76
121
  options;
77
122
  llmProvider;
@@ -86,7 +131,6 @@ class MessageLoop {
86
131
  resolvedAuth = null;
87
132
  idleTimer = null;
88
133
  approvalManager;
89
- _activeAbortController = null;
90
134
  /** Rate limiting / cost guardrails */
91
135
  maxToolCallsPerMessage;
92
136
  maxTokensPerMessage;
@@ -97,7 +141,6 @@ class MessageLoop {
97
141
  static SCHEDULED_TASK_TIMEOUT_MS = 5 * 60 * 1000;
98
142
  scheduledTaskTimer = null;
99
143
  static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
100
- static SERVER_SYNC_RETRY_MAX = 1;
101
144
  constructor(options) {
102
145
  this.options = options;
103
146
  // DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
@@ -114,29 +157,28 @@ class MessageLoop {
114
157
  if (isOAuthToken && effectiveProvider === 'anthropic') {
115
158
  this.resolveAndUpgradeAuth(effectiveKey, options.model);
116
159
  }
117
- // Resolve Funolio API credentials for bot status reporting
118
- const cfg = (0, config_1.loadConfig)();
119
- const funoliApiKey = cfg.auth?.token || process.env.FUNOLIO_API_KEY || '';
120
- const funoliApiBaseUrl = config_1.FUNOLIO_API_URL;
121
160
  this.toolContext = (0, index_2.createToolContext)(options.projectDir, {
122
161
  actorType: 'llm',
123
162
  actorId: options.agentName || options.userId,
124
- llmProvider: effectiveProvider,
125
- llmModel: options.model,
126
- llmApiKey: effectiveKey,
127
- llmAuthMode: isOAuthToken ? 'oauth-bearer' : 'api-key',
128
- apiKey: funoliApiKey,
129
- apiBaseUrl: funoliApiBaseUrl,
130
- botName: options.agentName || 'funolio-agent',
131
163
  });
164
+ this.options.enabledTools = (0, tool_permissions_1.normalizeConfiguredToolNames)(options.enabledTools);
165
+ this.options.enabledMcpTools = normalizeMcpToolNames(options.enabledMcpTools);
132
166
  let allTools = (0, index_2.getAllToolDefinitions)(options.mcpManager);
133
167
  let builtinTools = (0, index_2.getToolDefinitions)();
134
- // Filter tools by enabledTools whitelist if specified
135
- if (options.enabledTools && options.enabledTools.length > 0) {
136
- const allowed = new Set(options.enabledTools);
137
- allTools = allTools.filter((t) => allowed.has(t.name));
138
- builtinTools = builtinTools.filter((t) => allowed.has(t.name));
139
- console.log(chalk_1.default.gray(` [${options.agentName || 'agent'}] Tool filter: ${allTools.length} tools enabled (${options.enabledTools.length} configured)`));
168
+ // Filter builtin and MCP tools independently.
169
+ // An explicit empty array means "deny all" for that category.
170
+ if (this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined) {
171
+ const builtinNames = new Set(builtinTools.map((t) => t.name));
172
+ const allowedBuiltin = this.options.enabledTools !== undefined ? new Set(this.options.enabledTools) : null;
173
+ const allowedMcp = this.options.enabledMcpTools !== undefined ? new Set(this.options.enabledMcpTools) : null;
174
+ allTools = allTools.filter((t) => {
175
+ if (builtinNames.has(t.name)) {
176
+ return allowedBuiltin ? allowedBuiltin.has(t.name) : true;
177
+ }
178
+ return allowedMcp ? allowedMcp.has(t.name) : true;
179
+ });
180
+ builtinTools = builtinTools.filter((t) => (allowedBuiltin ? allowedBuiltin.has(t.name) : true));
181
+ console.log(chalk_1.default.gray(` [${options.agentName || 'agent'}] Tool filter: ${allTools.length} enabled (builtin=${this.options.enabledTools?.length ?? 'all'}, mcp=${this.options.enabledMcpTools?.length ?? 'all'})`));
140
182
  }
141
183
  this.toolDefinitions = allTools;
142
184
  this.builtinToolDefinitions = builtinTools;
@@ -155,9 +197,7 @@ class MessageLoop {
155
197
  });
156
198
  // Rate limiting defaults (can be overridden via options from agent config)
157
199
  this.maxToolCallsPerMessage = options.maxToolCallsPerMessage ?? 50;
158
- // maxTokensPerMessage retained for backwards compat but no longer enforced.
159
- // LLM providers handle their own context limits with proper error responses.
160
- this.maxTokensPerMessage = options.maxTokensPerMessage ?? 1_000_000;
200
+ this.maxTokensPerMessage = options.maxTokensPerMessage ?? 200_000;
161
201
  // Session idle detection timer (checks every 60s)
162
202
  this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
163
203
  }
@@ -169,37 +209,34 @@ class MessageLoop {
169
209
  */
170
210
  async resolveAndUpgradeAuth(oauthToken, model) {
171
211
  try {
172
- const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
173
- preferredModel: model,
174
- inputCredential: {
175
- provider: 'anthropic',
176
- accessToken: oauthToken,
177
- refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
178
- expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
212
+ const result = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
213
+ accessToken: oauthToken,
214
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
215
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
216
+ onRefresh: (credential) => {
217
+ // Propagate refreshed credentials to BOTH auth stores
218
+ if (this.options.resolvedAuth) {
219
+ this.options.resolvedAuth.credential = credential;
220
+ }
221
+ if (this.resolvedAuth) {
222
+ this.resolvedAuth.credential = credential;
223
+ // Also update the apiKey field so ensureFreshToken() sees the new token
224
+ this.resolvedAuth.apiKey = credential.accessToken;
225
+ }
226
+ console.log('[message-loop] Anthropic OAuth token refreshed and propagated to runtime state');
179
227
  },
180
- inputSource: 'request:anthropic',
181
- persistInputCredential: true,
182
228
  });
183
- if (!runtime)
184
- return;
185
- if (this.options.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
186
- this.options.resolvedAuth.credential.accessToken = runtime.apiKey;
187
- }
188
- if (this.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
189
- this.resolvedAuth.credential.accessToken = runtime.apiKey;
190
- this.resolvedAuth.apiKey = runtime.apiKey;
191
- }
192
- if (runtime.authMode !== 'oauth-bearer') {
229
+ if (result.source === 'api-key-exchange') {
193
230
  // Successfully exchanged OAuth token for a temporary API key
194
231
  console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
195
232
  this.llmProvider = (0, index_1.createProvider)('anthropic', {
196
- apiKey: runtime.apiKey,
233
+ apiKey: result.token,
197
234
  model,
198
235
  });
199
236
  }
200
237
  else {
201
238
  // Bearer fallback — recreate provider with the (possibly refreshed) token
202
- const currentToken = runtime.apiKey || this.options.resolvedAuth?.credential?.accessToken || oauthToken;
239
+ const currentToken = this.options.resolvedAuth?.credential?.accessToken || oauthToken;
203
240
  console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
204
241
  (currentToken !== oauthToken ? ' (with refreshed token)' : ''));
205
242
  this.llmProvider = (0, index_1.createProvider)('anthropic', {
@@ -213,95 +250,6 @@ class MessageLoop {
213
250
  console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
214
251
  }
215
252
  }
216
- async syncOAuthFromServer(reason) {
217
- const providerId = this.options.provider;
218
- if (!providerId || providerId.endsWith('-cli'))
219
- return false;
220
- const cfg = (0, config_1.loadConfig)();
221
- const authToken = cfg.auth?.token;
222
- if (!authToken)
223
- return false;
224
- try {
225
- const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
226
- headers: { Authorization: `Bearer ${authToken}` },
227
- });
228
- if (!res.ok)
229
- return false;
230
- const body = await res.json();
231
- const provider = (body.providers || []).find((p) => p.id === providerId && p.connectionType === 'oauth');
232
- if (!provider?.access_token)
233
- return false;
234
- const currentAccess = this.resolvedAuth?.credential?.accessToken || this.options.oauthToken || '';
235
- const currentRefresh = this.resolvedAuth?.credential?.refreshToken || this.options.resolvedAuth?.credential?.refreshToken || '';
236
- const currentExpiry = this.resolvedAuth?.credential?.expiresAt || this.options.resolvedAuth?.credential?.expiresAt || 0;
237
- const nextExpiry = provider.expires_at || 0;
238
- const changed = provider.access_token !== currentAccess
239
- || (provider.refresh_token || '') !== currentRefresh
240
- || nextExpiry > currentExpiry;
241
- if (!changed)
242
- return false;
243
- if (this.resolvedAuth) {
244
- this.resolvedAuth.apiKey = provider.access_token;
245
- this.resolvedAuth.expired = false;
246
- this.resolvedAuth.error = undefined;
247
- if (this.resolvedAuth.credential) {
248
- this.resolvedAuth.credential.accessToken = provider.access_token;
249
- if (provider.refresh_token)
250
- this.resolvedAuth.credential.refreshToken = provider.refresh_token;
251
- if (provider.expires_at)
252
- this.resolvedAuth.credential.expiresAt = provider.expires_at;
253
- }
254
- }
255
- if (this.options.resolvedAuth) {
256
- this.options.resolvedAuth.apiKey = provider.access_token;
257
- this.options.resolvedAuth.expired = false;
258
- this.options.resolvedAuth.error = undefined;
259
- if (this.options.resolvedAuth.credential) {
260
- this.options.resolvedAuth.credential.accessToken = provider.access_token;
261
- if (provider.refresh_token)
262
- this.options.resolvedAuth.credential.refreshToken = provider.refresh_token;
263
- if (provider.expires_at)
264
- this.options.resolvedAuth.credential.expiresAt = provider.expires_at;
265
- }
266
- }
267
- this.options.oauthToken = provider.access_token;
268
- const local = (0, config_1.loadConfig)();
269
- if (!local.providers)
270
- local.providers = [];
271
- const idx = local.providers.findIndex((p) => p.id === providerId);
272
- if (idx >= 0) {
273
- local.providers[idx].authType = 'oauth';
274
- local.providers[idx].oauthToken = provider.access_token;
275
- if (provider.refresh_token)
276
- local.providers[idx].oauthRefreshToken = provider.refresh_token;
277
- if (provider.expires_at)
278
- local.providers[idx].oauthExpiresAt = provider.expires_at;
279
- if (provider.defaultModel)
280
- local.providers[idx].defaultModel = provider.defaultModel;
281
- }
282
- (0, config_1.saveConfig)(local);
283
- if (this.options.agentName) {
284
- const agentLocal = (0, agent_config_1.loadAgentConfig)(this.options.agentName);
285
- if (agentLocal && agentLocal.provider === providerId) {
286
- agentLocal.oauthToken = provider.access_token;
287
- if (provider.refresh_token)
288
- agentLocal.oauthRefreshToken = provider.refresh_token;
289
- if (provider.expires_at)
290
- agentLocal.oauthExpiresAt = provider.expires_at;
291
- if (provider.defaultModel)
292
- agentLocal.model = provider.defaultModel;
293
- (0, agent_config_1.saveAgentConfig)(this.options.agentName, agentLocal);
294
- }
295
- }
296
- this.updateToken(provider.access_token);
297
- console.log(chalk_1.default.green(` [auth] Synced updated ${providerId} OAuth credentials from server (${reason})`));
298
- return true;
299
- }
300
- catch (err) {
301
- console.log(chalk_1.default.gray(` [auth] Server OAuth sync skipped (${reason}): ${err?.message || err}`));
302
- return false;
303
- }
304
- }
305
253
  /** Check if a session has gone idle and trigger auto-organize */
306
254
  async checkIdleSession() {
307
255
  if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
@@ -402,11 +350,6 @@ class MessageLoop {
402
350
  console.log(chalk_1.default.yellow(`Cancelling command ${command.id}`));
403
351
  this.activeCommandId = null;
404
352
  this.approvalManager.cancelAll();
405
- // Abort any running tool execution
406
- if (this._activeAbortController) {
407
- this._activeAbortController.abort();
408
- this._activeAbortController = null;
409
- }
410
353
  }
411
354
  return;
412
355
  }
@@ -428,30 +371,25 @@ class MessageLoop {
428
371
  // For overrides with OAuth tokens, try subscription auth resolution
429
372
  if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
430
373
  try {
431
- const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
432
- preferredModel: model,
433
- inputCredential: {
434
- provider: 'anthropic',
435
- accessToken: apiKey,
436
- refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
437
- expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
374
+ const authResult = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
375
+ accessToken: apiKey,
376
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
377
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
378
+ onRefresh: (credential) => {
379
+ if (this.options.resolvedAuth) {
380
+ this.options.resolvedAuth.credential = credential;
381
+ }
382
+ if (this.resolvedAuth) {
383
+ this.resolvedAuth.credential = credential;
384
+ this.resolvedAuth.apiKey = credential.accessToken;
385
+ }
386
+ console.log('[message-loop] Anthropic OAuth token refreshed (override path)');
438
387
  },
439
- inputSource: 'request:anthropic',
440
- persistInputCredential: true,
441
388
  });
442
- const resolvedKey = runtime?.apiKey || apiKey;
443
- const authMode = runtime?.authMode;
444
- if (this.options.resolvedAuth?.credential && authMode === 'oauth-bearer') {
445
- this.options.resolvedAuth.credential.accessToken = resolvedKey;
446
- }
447
- if (this.resolvedAuth?.credential && authMode === 'oauth-bearer') {
448
- this.resolvedAuth.credential.accessToken = resolvedKey;
449
- this.resolvedAuth.apiKey = resolvedKey;
450
- }
451
389
  llm = (0, index_1.createProvider)(provider, {
452
- apiKey: resolvedKey,
390
+ apiKey: authResult.token,
453
391
  model,
454
- ...(authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
392
+ ...(authResult.authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
455
393
  });
456
394
  }
457
395
  catch {
@@ -467,15 +405,6 @@ class MessageLoop {
467
405
  }
468
406
  // Refresh tool definitions each command so newly installed MCP tools are visible
469
407
  this.toolDefinitions = (0, index_2.getAllToolDefinitions)(this._mcpManager);
470
- // Fix 3: Check for dead MCP servers and note unavailable tools
471
- let deadServerNote = '';
472
- if (this._mcpManager) {
473
- const dead = this._mcpManager.getDeadServers();
474
- if (dead.length > 0) {
475
- deadServerNote = `\n\n⚠️ UNAVAILABLE TOOL SERVERS: The following MCP servers have crashed and are no longer available: ${dead.join(', ')}. Do NOT attempt to use tools from these servers — they will fail. Inform the user if they ask for functionality from these servers.`;
476
- console.log(chalk_1.default.yellow(` [MCP] Dead servers detected: ${dead.join(', ')}`));
477
- }
478
- }
479
408
  this.activeCommandId = command.id;
480
409
  this.activeCommandSource = command.source || 'user';
481
410
  this.processing = true;
@@ -538,19 +467,15 @@ class MessageLoop {
538
467
  : undefined;
539
468
  const effectiveProjectId = command.context?.projectId || activeConversation?.project_id || undefined;
540
469
  const effectiveProject = effectiveProjectId ? localData?.getProject(effectiveProjectId) : undefined;
541
- // Create abort controller for this command execution
542
- const commandAbortController = new AbortController();
543
- this._activeAbortController = commandAbortController;
544
470
  const commandToolContext = {
545
471
  ...this.toolContext,
546
472
  projectId: effectiveProjectId ?? null,
547
- abortSignal: commandAbortController.signal,
548
473
  };
549
474
  // ─── Orchestrator Mode Branch ─────────────────────────
550
475
  // Resolve the selected bot profile — from command.bot, conversation bot_id, or default
551
476
  const selectedBotId = command.bot?.id || activeConversation?.bot_id || localAgentId;
552
477
  const activeProfile = selectedBotId ? localData?.getAgentProfile(selectedBotId) : null;
553
- if ((0, orchestrator_profile_1.isOrchestratorProfile)(activeProfile) && localFirst) {
478
+ if (activeProfile?.role_class === 'orchestrator' && localFirst) {
554
479
  const clerk = (0, clerk_model_1.getClerk)();
555
480
  if (!clerk) {
556
481
  // Do not silently fall through to direct chat — report error
@@ -565,15 +490,6 @@ class MessageLoop {
565
490
  const { getWorkflowEngine } = await Promise.resolve().then(() => __importStar(require('./workflow-engine')));
566
491
  const workflowEngine = getWorkflowEngine(this.options.projectDir);
567
492
  const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
568
- // Orchestrator work is conversation-scoped and can continue in parallel.
569
- // Release the global message-loop lock here so another conversation can start.
570
- this.activeCommandId = null;
571
- this.processing = false;
572
- if (this.scheduledTaskTimer) {
573
- clearTimeout(this.scheduledTaskTimer);
574
- this.scheduledTaskTimer = null;
575
- }
576
- void this.drainQueue();
577
493
  try {
578
494
  const response = await orchestrator.handleUserMessage(command.prompt, localConvId || '', {
579
495
  projectDir: this.options.projectDir,
@@ -590,7 +506,7 @@ class MessageLoop {
590
506
  mqttPublish: (result) => this.options.mqttClient.publishResult(result),
591
507
  });
592
508
  // Save O's response to local DB
593
- if (localConvId && localData && activeProfile) {
509
+ if (localConvId && localData) {
594
510
  localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
595
511
  }
596
512
  // Publish final response and complete
@@ -617,7 +533,7 @@ class MessageLoop {
617
533
  clearTimeout(this.scheduledTaskTimer);
618
534
  this.scheduledTaskTimer = null;
619
535
  }
620
- void this.drainQueue();
536
+ this.drainQueue();
621
537
  }
622
538
  return;
623
539
  }
@@ -662,10 +578,6 @@ class MessageLoop {
662
578
  systemPrompt = this.buildFallbackSystemPrompt(command);
663
579
  }
664
580
  // --- Context priority rules & tool fallback (AFTER context) ---
665
- // Fix 3: Inject dead server warnings into system prompt
666
- if (deadServerNote) {
667
- systemPrompt += deadServerNote;
668
- }
669
581
  systemPrompt += `\n\nCONTEXT PRIORITY RULES:
670
582
  1. ALWAYS use the context provided above (decisions, memory facts, conversations) to answer questions about past work, decisions, or projects. This is your PRIMARY knowledge source — it comes from the user's stored conversation history.
671
583
  2. Do NOT browse local files or run shell commands unless:
@@ -682,101 +594,14 @@ TOOL EFFICIENCY RULES:
682
594
  // Count context sections injected into system prompt
683
595
  const contextSectionCount = ['[Key Decisions]', '[Memory Facts]', '[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]']
684
596
  .filter(header => systemPrompt.includes(header)).length;
685
- // Add the current prompt (with image attachments if present)
686
- if (command.attachments && command.attachments.length > 0) {
687
- const contentParts = [];
688
- for (const att of command.attachments) {
689
- if (att.fileType === 'image' && att.dataUrl) {
690
- // Anthropic native format: source block with base64
691
- const commaIdx = att.dataUrl.indexOf(',');
692
- if (commaIdx >= 0) {
693
- const mimeMatch = att.dataUrl.match(/^data:([^;]+);/);
694
- const mediaType = mimeMatch?.[1] || att.mimeType || 'image/png';
695
- const base64Data = att.dataUrl.slice(commaIdx + 1);
696
- contentParts.push({ type: 'image', source: { type: 'base64', media_type: mediaType, data: base64Data } });
697
- }
698
- else {
699
- // Fallback: treat as URL
700
- contentParts.push({ type: 'image_url', image_url: { url: att.dataUrl, detail: 'auto' } });
701
- }
702
- }
703
- if (att.extractedText) {
704
- contentParts.push({ type: 'text', text: `[File: ${att.filename}]\n${att.extractedText}` });
705
- }
706
- }
707
- contentParts.push({ type: 'text', text: command.prompt });
708
- messages.push({ role: 'user', content: contentParts });
709
- }
710
- else {
711
- messages.push({ role: 'user', content: command.prompt });
712
- }
713
- // Retry budget tracker (Optimization 2)
714
- const toolFailuresByKey = new Map(); // toolName:argsHash → count
715
- const toolFailuresByName = new Map(); // toolName → count
716
- function getArgsHash(args) {
717
- const str = JSON.stringify(args || {}).slice(0, 200);
718
- return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
719
- }
720
- function trackToolFailure(name, args) {
721
- const key = `${name}:${getArgsHash(args)}`;
722
- toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
723
- toolFailuresByName.set(name, (toolFailuresByName.get(name) || 0) + 1);
724
- }
725
- function checkRetryBudget(name, args) {
726
- const key = `${name}:${getArgsHash(args)}`;
727
- const keyCount = toolFailuresByKey.get(key) || 0;
728
- const nameCount = toolFailuresByName.get(name) || 0;
729
- if (keyCount >= 2) {
730
- return `This exact operation has failed ${keyCount} times. Stop retrying and inform the user what went wrong. Suggest an alternative approach or ask for help.`;
731
- }
732
- if (nameCount >= 3) {
733
- return `The ${name} tool has failed ${nameCount} times in this conversation. Consider a completely different approach.`;
734
- }
735
- return null;
736
- }
597
+ // Add the current prompt
598
+ messages.push({ role: 'user', content: command.prompt });
737
599
  // Agentic loop - keep calling LLM until no more tool calls
738
600
  let iteration = 0;
739
- const MAX_ITERATIONS = 100;
740
- const GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes wall-clock timeout
601
+ const MAX_ITERATIONS = Infinity;
741
602
  let totalTokensUsed = 0;
742
- let totalInputTokens = 0;
743
- let totalOutputTokens = 0;
744
- let totalCacheReadTokens = 0;
745
- let totalCacheCreationTokens = 0;
746
- let totalToolCallDurationMs = 0;
747
- let serverSyncRetries = 0;
748
- const deniedTools = new Set(); // Track tools denied by user — don't re-ask
749
- const commandStartMs = Date.now();
750
- // Publish prompt context for "View Prompt" panel
751
- try {
752
- const promptSnapshot = [
753
- { role: 'system', content: systemPrompt.slice(0, 4000) + (systemPrompt.length > 4000 ? '\n...(truncated)' : '') },
754
- ...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 2000) : '' })),
755
- ];
756
- await this.options.mqttClient.publishResult({
757
- commandId: command.id,
758
- type: 'prompt_messages',
759
- messages: promptSnapshot,
760
- contextSections: contextSectionCount,
761
- model,
762
- timestamp: Date.now(),
763
- });
764
- }
765
- catch { }
766
603
  while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
767
604
  iteration++;
768
- // Global wall-clock timeout check
769
- if (Date.now() - commandStartMs > GLOBAL_TIMEOUT_MS) {
770
- console.log(chalk_1.default.red(` ⏱ Global timeout exceeded (${GLOBAL_TIMEOUT_MS / 1000}s) — breaking out of loop`));
771
- await this.options.mqttClient.publishResult({
772
- commandId: command.id,
773
- type: 'error',
774
- error: `Command execution exceeded the ${GLOBAL_TIMEOUT_MS / 60000} minute global timeout limit.`,
775
- timestamp: Date.now(),
776
- });
777
- commandAbortController.abort();
778
- break;
779
- }
780
605
  // Send a separator between agentic loop iterations so streamed text
781
606
  // from different LLM calls doesn't run together in the UI.
782
607
  if (iteration > 1) {
@@ -791,13 +616,6 @@ TOOL EFFICIENCY RULES:
791
616
  if (this.resolvedAuth) {
792
617
  const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
793
618
  if (refreshed.expired) {
794
- if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
795
- const synced = await this.syncOAuthFromServer('refresh-failed');
796
- if (synced) {
797
- serverSyncRetries++;
798
- continue;
799
- }
800
- }
801
619
  console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
802
620
  await this.options.mqttClient.publishResult({
803
621
  commandId: command.id,
@@ -820,15 +638,10 @@ TOOL EFFICIENCY RULES:
820
638
  if (filteredTools.length < this.toolDefinitions.length) {
821
639
  console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
822
640
  }
823
- // Compress context before sending to LLM (Optimizations 3+4)
824
- const compressedMessages = (0, context_compressor_1.compressContext)(messages, 40, 3, 200);
825
- if (compressedMessages.length < messages.length) {
826
- console.log(chalk_1.default.gray(` [context] Compressed ${messages.length} → ${compressedMessages.length} messages`));
827
- }
828
641
  let response;
829
642
  try {
830
643
  response = await llm.chat({
831
- messages: compressedMessages,
644
+ messages,
832
645
  system: systemPrompt,
833
646
  stream: true,
834
647
  tools: filteredTools,
@@ -848,13 +661,6 @@ TOOL EFFICIENCY RULES:
848
661
  const msg = llmError?.message || String(llmError);
849
662
  const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
850
663
  if (isAuthError) {
851
- if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
852
- const synced = await this.syncOAuthFromServer('llm-auth-error');
853
- if (synced) {
854
- serverSyncRetries++;
855
- continue;
856
- }
857
- }
858
664
  console.error(chalk_1.default.red(` [auth] LLM API authentication failed: ${msg}`));
859
665
  await this.options.mqttClient.publishResult({
860
666
  commandId: command.id,
@@ -866,15 +672,9 @@ TOOL EFFICIENCY RULES:
866
672
  }
867
673
  throw llmError;
868
674
  }
869
- // Any successful provider response clears one-shot sync retry budget
870
- serverSyncRetries = 0;
871
675
  // Track token usage across iterations
872
676
  if (response.usage) {
873
677
  totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
874
- totalInputTokens += response.usage.inputTokens;
875
- totalOutputTokens += response.usage.outputTokens;
876
- totalCacheReadTokens += response.usage.cacheReadTokens || 0;
877
- totalCacheCreationTokens += response.usage.cacheCreationTokens || 0;
878
678
  }
879
679
  // If there are tool calls, publish them and continue the loop
880
680
  if (response.toolCalls && response.toolCalls.length > 0) {
@@ -887,30 +687,40 @@ TOOL EFFICIENCY RULES:
887
687
  for (const toolCall of response.toolCalls) {
888
688
  totalToolCalls++;
889
689
  console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
690
+ const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
691
+ const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
890
692
  // Check permission before execution
891
693
  const permMode = this.options.permissionMode || 'autopilot';
892
- let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, this.options.enabledTools);
893
- // If tool was already denied once in this command, auto-deny without re-asking
894
- if (!approval.approved && deniedTools.has(toolCall.name)) {
895
- approval = { approved: false, reason: `Tool "${toolCall.name}" was already denied in this session.` };
896
- }
694
+ const allowedToolNames = this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined
695
+ ? [...(this.options.enabledTools || []), ...(this.options.enabledMcpTools || [])]
696
+ : undefined;
697
+ let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, allowedToolNames);
897
698
  // If not auto-approved, request interactive approval via MQTT
898
- if (!approval.approved && permMode !== 'autopilot' && !deniedTools.has(toolCall.name)) {
699
+ if (!approval.approved && permMode !== 'autopilot') {
899
700
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
900
- // Generate requestId up front so the MQTT result and pending map use the same ID
901
- const approvalRequestId = require('crypto').randomUUID();
902
- await this.options.mqttClient.publishResult({
903
- commandId: command.id,
904
- type: 'approval_request',
905
- requestId: approvalRequestId,
906
- toolCall: {
907
- id: toolCall.id,
908
- name: toolCall.name,
909
- arguments: toolCall.arguments,
701
+ approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
702
+ onRequest: async (request) => {
703
+ await this.options.mqttClient.publishResult({
704
+ commandId: command.id,
705
+ type: 'approval_request',
706
+ requestId: request.requestId,
707
+ riskLevel: request.riskLevel,
708
+ summary: request.summary,
709
+ toolCall: {
710
+ id: toolCall.id,
711
+ name: toolCall.name,
712
+ arguments: toolCall.arguments,
713
+ classification: classifyToolName(toolCall.name),
714
+ summary: toolSummary,
715
+ path: toolLocation.path,
716
+ cwd: toolLocation.cwd,
717
+ url: toolLocation.url,
718
+ importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
719
+ },
720
+ timestamp: Date.now(),
721
+ });
910
722
  },
911
- timestamp: Date.now(),
912
723
  });
913
- approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
914
724
  // If user chose "always allow", persist it
915
725
  if (approval.approved && approval.remember) {
916
726
  (0, approval_1.rememberTool)(toolCall.name);
@@ -918,21 +728,27 @@ TOOL EFFICIENCY RULES:
918
728
  }
919
729
  }
920
730
  if (!approval.approved) {
921
- deniedTools.add(toolCall.name);
922
731
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
732
+ const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
923
733
  await this.options.mqttClient.publishResult({
924
734
  commandId: command.id,
925
735
  type: 'tool_result',
926
736
  toolResult: {
927
737
  callId: toolCall.id,
928
- output: `PERMISSION_DENIED: ${approval.reason}`,
738
+ output: deniedOutput,
929
739
  isError: true,
740
+ success: false,
741
+ summary: summarizeToolResult(deniedOutput, true),
742
+ path: toolLocation.path,
743
+ cwd: toolLocation.cwd,
744
+ url: toolLocation.url,
745
+ pathsTouched: toolLocation.path ? [toolLocation.path] : [],
930
746
  },
931
747
  timestamp: Date.now(),
932
748
  });
933
749
  messages.push({
934
750
  role: 'tool',
935
- content: `PERMISSION_DENIED: ${approval.reason}`,
751
+ content: deniedOutput,
936
752
  toolCallId: toolCall.id,
937
753
  toolName: toolCall.name,
938
754
  });
@@ -945,57 +761,36 @@ TOOL EFFICIENCY RULES:
945
761
  id: toolCall.id,
946
762
  name: toolCall.name,
947
763
  arguments: toolCall.arguments,
764
+ classification: classifyToolName(toolCall.name),
765
+ summary: toolSummary,
766
+ path: toolLocation.path,
767
+ cwd: toolLocation.cwd,
768
+ url: toolLocation.url,
769
+ importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
948
770
  },
949
771
  timestamp: Date.now(),
950
772
  });
951
- // Check retry budget before execution (Optimization 2)
952
- const retryBlock = checkRetryBudget(toolCall.name, toolCall.arguments);
953
- if (retryBlock) {
954
- console.log(chalk_1.default.yellow(` ⚠ Retry budget exceeded for ${toolCall.name}`));
955
- const retryOutput = `ERROR: ${retryBlock}`;
956
- await this.options.mqttClient.publishResult({
957
- commandId: command.id,
958
- type: 'tool_result',
959
- toolResult: {
960
- callId: toolCall.id,
961
- name: toolCall.name,
962
- output: retryOutput,
963
- isError: true,
964
- durationMs: 0,
965
- },
966
- timestamp: Date.now(),
967
- });
968
- messages.push({
969
- role: 'tool',
970
- content: retryOutput,
971
- toolCallId: toolCall.id,
972
- toolName: toolCall.name,
973
- });
974
- continue;
975
- }
976
773
  // Execute the tool (with MCP fallback)
977
- const toolStartMs = Date.now();
978
774
  const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
979
775
  const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
980
776
  const resultOutput = toolResult.success
981
777
  ? toolResult.output
982
778
  : `ERROR: ${toolResult.error || 'Unknown error'}`;
983
- // Track failures for retry budget (Optimization 2)
984
- if (!toolResult.success) {
985
- trackToolFailure(toolCall.name, toolCall.arguments);
986
- }
987
779
  console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
988
- const toolDurationMs = Date.now() - toolStartMs;
989
- totalToolCallDurationMs += toolDurationMs;
990
780
  await this.options.mqttClient.publishResult({
991
781
  commandId: command.id,
992
782
  type: 'tool_result',
993
783
  toolResult: {
994
784
  callId: toolCall.id,
995
- name: toolCall.name,
996
785
  output: resultOutput,
997
786
  isError: !toolResult.success,
998
- durationMs: toolDurationMs,
787
+ success: toolResult.success,
788
+ summary: summarizeToolResult(resultOutput, !toolResult.success),
789
+ path: toolLocation.path,
790
+ cwd: toolLocation.cwd,
791
+ url: toolLocation.url,
792
+ pathsTouched: toolLocation.path ? [toolLocation.path] : [],
793
+ exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
999
794
  },
1000
795
  timestamp: Date.now(),
1001
796
  });
@@ -1012,10 +807,9 @@ TOOL EFFICIENCY RULES:
1012
807
  if (totalToolCalls >= this.maxToolCallsPerMessage * 0.8 && totalToolCalls < this.maxToolCallsPerMessage) {
1013
808
  console.log(chalk_1.default.yellow(` ⚠ Approaching tool call limit: ${totalToolCalls}/${this.maxToolCallsPerMessage}`));
1014
809
  }
1015
- // Log context window usage for monitoring (no artificial cap — let the LLM
1016
- // provider return its own error if context exceeds the model's real limit)
1017
- if (response.usage?.inputTokens) {
1018
- console.log(chalk_1.default.gray(` [tokens] iteration ${iteration}: ${response.usage.inputTokens} input, ${response.usage.outputTokens} output`));
810
+ // Warn at 80% of token limit
811
+ if (totalTokensUsed >= this.maxTokensPerMessage * 0.8 && totalTokensUsed < this.maxTokensPerMessage) {
812
+ console.log(chalk_1.default.yellow(` ⚠ Approaching token limit: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
1019
813
  }
1020
814
  // Check tool call limit
1021
815
  if (totalToolCalls >= this.maxToolCallsPerMessage) {
@@ -1042,20 +836,35 @@ TOOL EFFICIENCY RULES:
1042
836
  }
1043
837
  await this.options.mqttClient.publishResult({
1044
838
  commandId: command.id,
1045
- type: 'stats',
1046
- llmCalls: iteration,
1047
- toolsUsed: totalToolCalls,
1048
- inputTokens: totalInputTokens,
1049
- outputTokens: totalOutputTokens,
1050
- cacheReadTokens: totalCacheReadTokens || undefined,
1051
- cacheCreationTokens: totalCacheCreationTokens || undefined,
1052
- totalTokens: totalTokensUsed,
1053
- latencyMs: Date.now() - commandStartMs,
1054
- toolCallDurationMs: totalToolCallDurationMs || undefined,
1055
- contextSections: contextSectionCount || undefined,
1056
- model,
839
+ type: 'complete',
840
+ content: '',
1057
841
  timestamp: Date.now(),
1058
842
  });
843
+ console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
844
+ break;
845
+ }
846
+ // Check token limit
847
+ if (totalTokensUsed >= this.maxTokensPerMessage) {
848
+ console.log(chalk_1.default.red(` 🛑 Token limit reached: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
849
+ messages.push({ role: 'user', content: 'Token limit reached. Please summarize what you\'ve done so far.' });
850
+ const summaryResponse = await llm.chat({
851
+ messages,
852
+ system: systemPrompt,
853
+ stream: true,
854
+ onChunk: async (chunk) => {
855
+ if (this.activeCommandId !== command.id)
856
+ return;
857
+ await this.options.mqttClient.publishResult({
858
+ commandId: command.id,
859
+ type: 'chunk',
860
+ content: chunk,
861
+ timestamp: Date.now(),
862
+ });
863
+ },
864
+ });
865
+ if (summaryResponse.usage) {
866
+ totalTokensUsed += summaryResponse.usage.inputTokens + summaryResponse.usage.outputTokens;
867
+ }
1059
868
  await this.options.mqttClient.publishResult({
1060
869
  commandId: command.id,
1061
870
  type: 'complete',
@@ -1065,9 +874,6 @@ TOOL EFFICIENCY RULES:
1065
874
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
1066
875
  break;
1067
876
  }
1068
- // No artificial token limit — LLM providers return proper errors (400/413)
1069
- // when context exceeds the model's real limit, and those are caught by the
1070
- // error handler above which surfaces them to the user.
1071
877
  // Continue the loop for the next LLM call
1072
878
  continue;
1073
879
  }
@@ -1075,36 +881,6 @@ TOOL EFFICIENCY RULES:
1075
881
  // DEDUP FIX: Content was already streamed via onChunk callbacks above.
1076
882
  // The 'complete' message signals end-of-response; we send empty content
1077
883
  // to avoid the receiver concatenating streamed chunks + full content.
1078
- // Fix 1: Detect phantom tool use — LLM claims it did something but made 0 tool calls
1079
- const phantom = (0, response_guard_1.detectPhantomToolUse)(response.content || '', totalToolCalls);
1080
- if (phantom.detected) {
1081
- console.log(chalk_1.default.yellow(` ⚠ Phantom tool use detected — injecting correction`));
1082
- // Stream the correction to the user
1083
- await this.options.mqttClient.publishResult({
1084
- commandId: command.id,
1085
- type: 'chunk',
1086
- content: '\n\n---\n⚠️ ' + phantom.correction,
1087
- timestamp: Date.now(),
1088
- });
1089
- }
1090
- // Publish stats before complete
1091
- const latencyMs = Date.now() - commandStartMs;
1092
- await this.options.mqttClient.publishResult({
1093
- commandId: command.id,
1094
- type: 'stats',
1095
- llmCalls: iteration,
1096
- toolsUsed: totalToolCalls,
1097
- inputTokens: totalInputTokens,
1098
- outputTokens: totalOutputTokens,
1099
- cacheReadTokens: totalCacheReadTokens || undefined,
1100
- cacheCreationTokens: totalCacheCreationTokens || undefined,
1101
- totalTokens: totalTokensUsed,
1102
- latencyMs,
1103
- toolCallDurationMs: totalToolCallDurationMs || undefined,
1104
- contextSections: contextSectionCount || undefined,
1105
- model,
1106
- timestamp: Date.now(),
1107
- });
1108
884
  await this.options.mqttClient.publishResult({
1109
885
  commandId: command.id,
1110
886
  type: 'complete',
@@ -1128,6 +904,7 @@ TOOL EFFICIENCY RULES:
1128
904
  }
1129
905
  }
1130
906
  console.log(chalk_1.default.white(` Response: ${response.content?.slice(0, 200)}${(response.content?.length || 0) > 200 ? '...' : ''}`));
907
+ const latencyMs = Date.now() - commandStartMs;
1131
908
  console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs}`);
1132
909
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
1133
910
  break;
@@ -1159,31 +936,11 @@ TOOL EFFICIENCY RULES:
1159
936
  }
1160
937
  /** Build system prompt manually (without clerk) — original logic */
1161
938
  buildFallbackSystemPrompt(command) {
1162
- let systemPrompt = (0, prompt_template_1.buildDefaultSystemPrompt)({
1163
- botName: this.options.agentName,
1164
- model: this.options.model,
1165
- soulMd: command.context?.soulMd || this.options.systemPrompt,
1166
- });
1167
- // Auto-enumerate available tools so the bot always knows its capabilities
1168
- const toolDefs = (0, index_2.getToolDefinitions)();
1169
- const toolList = toolDefs.map(t => `- **${t.name}**: ${t.description.slice(0, 120)}`).join('\n');
1170
- systemPrompt += `\n\n## Your Available Tools\nYou have ${toolDefs.length} tools available. Use them proactively:\n${toolList}`;
1171
- systemPrompt += `\n\n## Working Style
1172
- - You are autonomous — take action, don't just describe what you could do.
1173
- - For complex tasks, use spawn_subagent to parallelize work.
1174
- - Read project files to understand context before making changes.
1175
- - Commit your work with git_commit when you've made meaningful progress.
1176
- - Use web_search and web_fetch to research unfamiliar topics.
1177
- - Write important context to files so you remember it across sessions.
1178
- - If you're unsure about something destructive, ask first. Otherwise, just do it.`;
1179
- // Platform awareness — always present regardless of soulMd content
1180
- systemPrompt += `\n\n## Extending Your Capabilities
1181
- - You have built-in tools (file ops, shell, git, web search, web fetch, tasks, sub-agents, memory search).
1182
- - If the user asks you to do something you don't have a tool for (e.g. Google Drive, GitHub issues, Slack, email, databases), search the Funolio marketplace with \`search_marketplace\`.
1183
- - If a matching MCP tool server exists, use \`request_mcp_install\` to request permission to install it.
1184
- - You can discover and gain new capabilities on-the-fly through the marketplace.
1185
- - **NEVER tell the user to "do it manually" or "upload it yourself"** — always check the marketplace first and offer to install the right tool.`;
939
+ let systemPrompt = command.context?.soulMd
940
+ || this.options.systemPrompt
941
+ || 'You are a Funolio AI agent running locally on the user\'s machine. You have access to their project files and can execute code.';
1186
942
  systemPrompt += '\n\nIMPORTANT: When the user references a project, topic, or past work, use the relevant memory/facts provided below. If no relevant facts are available, say so honestly rather than guessing. Use your tools (file browsing, commands) to find project files on the local machine.';
943
+ systemPrompt += '\n\nIMPORTANT: If a Workspace Manifest or Recent Operational State section is present, use it before repeating discovery work. Check known local files and directories before fetching from external sources. Do not re-fetch from Drive, GitHub, or the web if the state context shows the artifact is already local unless the user explicitly asks for a fresh external copy.';
1187
944
  systemPrompt += '\n\nDo not end with a deferred promise (for example: "Let me check..."). Return a final answer in this turn, or state exactly what is unavailable.';
1188
945
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
1189
946
  // Inject model self-switch info
@@ -1228,6 +985,32 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1228
985
  if (command.context?.files?.length) {
1229
986
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
1230
987
  }
988
+ if (command.context?.stateContext?.summaryText) {
989
+ systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
990
+ }
991
+ if (command.context?.workspaceManifest) {
992
+ const manifestLines = [];
993
+ const manifest = command.context.workspaceManifest;
994
+ if (manifest.projectRoot)
995
+ manifestLines.push(`- Project root: ${manifest.projectRoot}`);
996
+ if (manifest.likelyWorkingDirectory)
997
+ manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
998
+ if (manifest.recentlyReadFiles?.length)
999
+ manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
1000
+ if (manifest.recentlyWrittenFiles?.length)
1001
+ manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
1002
+ if (manifest.recentlyScannedDirectories?.length)
1003
+ manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
1004
+ if (manifest.recentDownloads?.length)
1005
+ manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
1006
+ if (manifest.recentFailures?.length)
1007
+ manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
1008
+ if (manifest.startHereHints?.length)
1009
+ manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
1010
+ if (manifestLines.length) {
1011
+ systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
1012
+ }
1013
+ }
1231
1014
  return systemPrompt;
1232
1015
  }
1233
1016
  async publishError(commandId, error) {