funolio-agent 1.0.47 → 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.
- package/dist/agent-config.d.ts +9 -1
- package/dist/agent-config.d.ts.map +1 -1
- package/dist/agent-config.js +4 -1
- package/dist/agent-config.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +4 -3
- package/dist/auto-organizer.js.map +1 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +3 -2
- package/dist/backfill.js.map +1 -1
- package/dist/bot-manager.d.ts +7 -23
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +52 -388
- package/dist/bot-manager.js.map +1 -1
- package/dist/clerk-model.d.ts +5 -1
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +40 -28
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-session-epoch.d.ts +10 -0
- package/dist/cli-session-epoch.d.ts.map +1 -0
- package/dist/cli-session-epoch.js +61 -0
- package/dist/cli-session-epoch.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pool.js +1 -1
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/setup.d.ts +37 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +146 -43
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +194 -164
- package/dist/commands/start.js.map +1 -1
- package/dist/config-cleanup.d.ts.map +1 -1
- package/dist/config-cleanup.js +2 -1
- package/dist/config-cleanup.js.map +1 -1
- package/dist/config.d.ts +6 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -30
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +33 -5
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +121 -20
- package/dist/context-window.js.map +1 -1
- package/dist/eval/orchestrator-front-door-replay.js +1 -1
- package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
- package/dist/eval/policy-detection-replay.js +1 -1
- package/dist/eval/policy-detection-replay.js.map +1 -1
- package/dist/integration-tokens.d.ts +1 -6
- package/dist/integration-tokens.d.ts.map +1 -1
- package/dist/integration-tokens.js +38 -40
- package/dist/integration-tokens.js.map +1 -1
- package/dist/local-cli-pty-manager.d.ts +50 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -0
- package/dist/local-cli-pty-manager.js +645 -0
- package/dist/local-cli-pty-manager.js.map +1 -0
- package/dist/local-data.d.ts +30 -0
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +56 -1
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +54 -1
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +3 -2
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-memory-search.d.ts +1 -0
- package/dist/local-memory-search.d.ts.map +1 -1
- package/dist/local-memory-search.js +101 -18
- package/dist/local-memory-search.js.map +1 -1
- package/dist/local-server.d.ts +0 -16
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +339 -287
- package/dist/local-server.js.map +1 -1
- package/dist/mcp/bridge-server.d.ts.map +1 -1
- package/dist/mcp/bridge-server.js +2 -1
- package/dist/mcp/bridge-server.js.map +1 -1
- package/dist/mcp/local-memory-server.d.ts +5 -0
- package/dist/mcp/local-memory-server.d.ts.map +1 -1
- package/dist/mcp/local-memory-server.js +15 -2
- package/dist/mcp/local-memory-server.js.map +1 -1
- package/dist/mcp/manager.d.ts +3 -22
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +66 -388
- package/dist/mcp/manager.js.map +1 -1
- package/dist/memory-extraction.d.ts +2 -0
- package/dist/memory-extraction.d.ts.map +1 -1
- package/dist/memory-extraction.js +3 -1
- package/dist/memory-extraction.js.map +1 -1
- package/dist/message-loop.d.ts +9 -6
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +217 -538
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +2 -31
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +2 -2
- package/dist/mqtt-client.js.map +1 -1
- package/dist/oauth.d.ts +6 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +91 -0
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/front-door-policy.d.ts +5 -2
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +25 -28
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-blocked-prompt.js +1 -1
- package/dist/orchestration/orchestrator-final-response-prompt.js +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +3 -3
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +5 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +141 -81
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompt-template.js +3 -3
- package/dist/prompt-template.js.map +1 -1
- package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +22 -6
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +20 -2
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +71 -16
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/index.d.ts +11 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/runtime-context.d.ts +10 -0
- package/dist/runtime-context.d.ts.map +1 -0
- package/dist/runtime-context.js +30 -0
- package/dist/runtime-context.js.map +1 -0
- package/dist/subagent/queue.d.ts.map +1 -1
- package/dist/subagent/queue.js +1 -0
- package/dist/subagent/queue.js.map +1 -1
- package/dist/summarization-pipeline.d.ts +1 -0
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +94 -25
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/tool-permissions.d.ts +2 -0
- package/dist/tool-permissions.d.ts.map +1 -0
- package/dist/tool-permissions.js +25 -0
- package/dist/tool-permissions.js.map +1 -0
- package/dist/tools/index.d.ts +7 -8
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +70 -60
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/search-memory.d.ts.map +1 -1
- package/dist/tools/search-memory.js +9 -3
- package/dist/tools/search-memory.js.map +1 -1
- package/dist/tools/spawn-subagent.d.ts.map +1 -1
- package/dist/tools/spawn-subagent.js +1 -0
- package/dist/tools/spawn-subagent.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +8 -6
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +6 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +254 -77
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -1
package/dist/message-loop.js
CHANGED
|
@@ -47,15 +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");
|
|
52
|
+
const local_funnel_1 = require("./local-funnel");
|
|
51
53
|
const auto_organizer_1 = require("./auto-organizer");
|
|
52
|
-
const
|
|
53
|
-
const response_guard_1 = require("./response-guard");
|
|
54
|
-
const context_compressor_1 = require("./context-compressor");
|
|
55
|
-
const crypto = __importStar(require("crypto"));
|
|
54
|
+
const tool_permissions_1 = require("./tool-permissions");
|
|
56
55
|
const data = __importStar(require("./local-data"));
|
|
57
|
-
const prompt_template_1 = require("./prompt-template");
|
|
58
|
-
const local_funnel_1 = require("./local-funnel");
|
|
59
56
|
/** Determine priority from an AgentCommand */
|
|
60
57
|
function getCommandPriority(command) {
|
|
61
58
|
if (command.priority)
|
|
@@ -111,12 +108,20 @@ function summarizeToolResult(output, isError) {
|
|
|
111
108
|
const compact = output.replace(/\s+/g, ' ').trim();
|
|
112
109
|
return `${prefix}${compact}`.slice(0, 240);
|
|
113
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
|
+
}
|
|
114
120
|
class MessageLoop {
|
|
115
121
|
options;
|
|
116
122
|
llmProvider;
|
|
117
123
|
activeCommandId = null;
|
|
118
124
|
activeCommandSource = null;
|
|
119
|
-
chunkSeq = 0;
|
|
120
125
|
toolContext;
|
|
121
126
|
toolDefinitions;
|
|
122
127
|
builtinToolDefinitions;
|
|
@@ -126,7 +131,6 @@ class MessageLoop {
|
|
|
126
131
|
resolvedAuth = null;
|
|
127
132
|
idleTimer = null;
|
|
128
133
|
approvalManager;
|
|
129
|
-
_activeAbortController = null;
|
|
130
134
|
/** Rate limiting / cost guardrails */
|
|
131
135
|
maxToolCallsPerMessage;
|
|
132
136
|
maxTokensPerMessage;
|
|
@@ -139,41 +143,42 @@ class MessageLoop {
|
|
|
139
143
|
static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
|
140
144
|
constructor(options) {
|
|
141
145
|
this.options = options;
|
|
142
|
-
|
|
146
|
+
// DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
|
|
147
|
+
// without explicit user approval.
|
|
148
|
+
const effectiveKey = options.oauthToken || options.apiKey || '';
|
|
143
149
|
const effectiveProvider = options.provider;
|
|
144
|
-
const
|
|
150
|
+
const isOAuthToken = effectiveKey.startsWith('sk-ant-oat01-');
|
|
145
151
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
146
152
|
apiKey: effectiveKey,
|
|
147
153
|
model: options.model,
|
|
148
|
-
authMode:
|
|
149
|
-
...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
|
|
150
|
-
...(options.apiStyle ? { apiStyle: options.apiStyle } : {}),
|
|
154
|
+
...(isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
|
|
151
155
|
});
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const unrestrictedCliProvider = index_1.CLI_PROVIDERS.has(effectiveProvider);
|
|
156
|
+
// Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
|
|
157
|
+
if (isOAuthToken && effectiveProvider === 'anthropic') {
|
|
158
|
+
this.resolveAndUpgradeAuth(effectiveKey, options.model);
|
|
159
|
+
}
|
|
157
160
|
this.toolContext = (0, index_2.createToolContext)(options.projectDir, {
|
|
158
161
|
actorType: 'llm',
|
|
159
162
|
actorId: options.agentName || options.userId,
|
|
160
|
-
llmProvider: effectiveProvider,
|
|
161
|
-
llmModel: options.model,
|
|
162
|
-
llmApiKey: effectiveKey,
|
|
163
|
-
llmAuthMode,
|
|
164
|
-
restrictFileAccessToProject: unrestrictedCliProvider ? false : undefined,
|
|
165
|
-
apiKey: funoliApiKey,
|
|
166
|
-
apiBaseUrl: funoliApiBaseUrl,
|
|
167
|
-
botName: options.agentName || 'funolio-agent',
|
|
168
163
|
});
|
|
164
|
+
this.options.enabledTools = (0, tool_permissions_1.normalizeConfiguredToolNames)(options.enabledTools);
|
|
165
|
+
this.options.enabledMcpTools = normalizeMcpToolNames(options.enabledMcpTools);
|
|
169
166
|
let allTools = (0, index_2.getAllToolDefinitions)(options.mcpManager);
|
|
170
167
|
let builtinTools = (0, index_2.getToolDefinitions)();
|
|
171
|
-
// Filter
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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'})`));
|
|
177
182
|
}
|
|
178
183
|
this.toolDefinitions = allTools;
|
|
179
184
|
this.builtinToolDefinitions = builtinTools;
|
|
@@ -192,12 +197,59 @@ class MessageLoop {
|
|
|
192
197
|
});
|
|
193
198
|
// Rate limiting defaults (can be overridden via options from agent config)
|
|
194
199
|
this.maxToolCallsPerMessage = options.maxToolCallsPerMessage ?? 50;
|
|
195
|
-
|
|
196
|
-
// LLM providers handle their own context limits with proper error responses.
|
|
197
|
-
this.maxTokensPerMessage = options.maxTokensPerMessage ?? 1_000_000;
|
|
200
|
+
this.maxTokensPerMessage = options.maxTokensPerMessage ?? 200_000;
|
|
198
201
|
// Session idle detection timer (checks every 60s)
|
|
199
202
|
this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
|
|
200
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Attempt to resolve Anthropic subscription auth via create_api_key exchange.
|
|
206
|
+
* If successful, recreates the LLM provider with the exchanged API key (which
|
|
207
|
+
* works with standard x-api-key auth and unlocks all models on the subscription).
|
|
208
|
+
* Falls back to bearer mode silently if exchange fails.
|
|
209
|
+
*/
|
|
210
|
+
async resolveAndUpgradeAuth(oauthToken, model) {
|
|
211
|
+
try {
|
|
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');
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
if (result.source === 'api-key-exchange') {
|
|
230
|
+
// Successfully exchanged OAuth token for a temporary API key
|
|
231
|
+
console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
|
|
232
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
233
|
+
apiKey: result.token,
|
|
234
|
+
model,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Bearer fallback — recreate provider with the (possibly refreshed) token
|
|
239
|
+
const currentToken = this.options.resolvedAuth?.credential?.accessToken || oauthToken;
|
|
240
|
+
console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
|
|
241
|
+
(currentToken !== oauthToken ? ' (with refreshed token)' : ''));
|
|
242
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
243
|
+
apiKey: currentToken,
|
|
244
|
+
model,
|
|
245
|
+
authMode: 'oauth-bearer',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
201
253
|
/** Check if a session has gone idle and trigger auto-organize */
|
|
202
254
|
async checkIdleSession() {
|
|
203
255
|
if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
|
|
@@ -298,11 +350,6 @@ class MessageLoop {
|
|
|
298
350
|
console.log(chalk_1.default.yellow(`Cancelling command ${command.id}`));
|
|
299
351
|
this.activeCommandId = null;
|
|
300
352
|
this.approvalManager.cancelAll();
|
|
301
|
-
// Abort any running tool execution
|
|
302
|
-
if (this._activeAbortController) {
|
|
303
|
-
this._activeAbortController.abort();
|
|
304
|
-
this._activeAbortController = null;
|
|
305
|
-
}
|
|
306
353
|
}
|
|
307
354
|
return;
|
|
308
355
|
}
|
|
@@ -315,37 +362,51 @@ class MessageLoop {
|
|
|
315
362
|
const overrideModel = command.model?.model;
|
|
316
363
|
let provider = overrideProvider || this.options.provider;
|
|
317
364
|
const model = overrideModel || this.options.model;
|
|
318
|
-
const apiKey = this.options.apiKey;
|
|
365
|
+
const apiKey = this.options.oauthToken || this.options.apiKey;
|
|
319
366
|
// Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
|
|
320
367
|
const hasOverride = (overrideProvider && overrideProvider !== this.options.provider) ||
|
|
321
368
|
(overrideModel && overrideModel !== this.options.model);
|
|
322
369
|
let llm;
|
|
323
370
|
if (hasOverride) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
371
|
+
// For overrides with OAuth tokens, try subscription auth resolution
|
|
372
|
+
if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
|
|
373
|
+
try {
|
|
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)');
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
llm = (0, index_1.createProvider)(provider, {
|
|
390
|
+
apiKey: authResult.token,
|
|
391
|
+
model,
|
|
392
|
+
...(authResult.authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
llm = (0, index_1.createProvider)(provider, { apiKey, model, authMode: 'oauth-bearer' });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
llm = (0, index_1.createProvider)(provider, { apiKey, model });
|
|
401
|
+
}
|
|
331
402
|
}
|
|
332
403
|
else {
|
|
333
404
|
llm = this.llmProvider;
|
|
334
405
|
}
|
|
335
406
|
// Refresh tool definitions each command so newly installed MCP tools are visible
|
|
336
407
|
this.toolDefinitions = (0, index_2.getAllToolDefinitions)(this._mcpManager);
|
|
337
|
-
// Fix 3: Check for dead MCP servers and note unavailable tools
|
|
338
|
-
let deadServerNote = '';
|
|
339
|
-
if (this._mcpManager) {
|
|
340
|
-
const dead = this._mcpManager.getDeadServers();
|
|
341
|
-
if (dead.length > 0) {
|
|
342
|
-
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.`;
|
|
343
|
-
console.log(chalk_1.default.yellow(` [MCP] Dead servers detected: ${dead.join(', ')}`));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
408
|
this.activeCommandId = command.id;
|
|
347
409
|
this.activeCommandSource = command.source || 'user';
|
|
348
|
-
this.chunkSeq = 0;
|
|
349
410
|
this.processing = true;
|
|
350
411
|
this.lastMessageAt = Date.now();
|
|
351
412
|
const commandStartMs = Date.now();
|
|
@@ -406,21 +467,15 @@ class MessageLoop {
|
|
|
406
467
|
: undefined;
|
|
407
468
|
const effectiveProjectId = command.context?.projectId || activeConversation?.project_id || undefined;
|
|
408
469
|
const effectiveProject = effectiveProjectId ? localData?.getProject(effectiveProjectId) : undefined;
|
|
409
|
-
// Create abort controller for this command execution
|
|
410
|
-
const commandAbortController = new AbortController();
|
|
411
|
-
this._activeAbortController = commandAbortController;
|
|
412
470
|
const commandToolContext = {
|
|
413
471
|
...this.toolContext,
|
|
414
472
|
projectId: effectiveProjectId ?? null,
|
|
415
|
-
abortSignal: commandAbortController.signal,
|
|
416
|
-
// Use project folder as working directory if available
|
|
417
|
-
...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
|
|
418
473
|
};
|
|
419
474
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
420
475
|
// Resolve the selected bot profile — from command.bot, conversation bot_id, or default
|
|
421
476
|
const selectedBotId = command.bot?.id || activeConversation?.bot_id || localAgentId;
|
|
422
477
|
const activeProfile = selectedBotId ? localData?.getAgentProfile(selectedBotId) : null;
|
|
423
|
-
if (
|
|
478
|
+
if (activeProfile?.role_class === 'orchestrator' && localFirst) {
|
|
424
479
|
const clerk = (0, clerk_model_1.getClerk)();
|
|
425
480
|
if (!clerk) {
|
|
426
481
|
// Do not silently fall through to direct chat — report error
|
|
@@ -435,15 +490,6 @@ class MessageLoop {
|
|
|
435
490
|
const { getWorkflowEngine } = await Promise.resolve().then(() => __importStar(require('./workflow-engine')));
|
|
436
491
|
const workflowEngine = getWorkflowEngine(this.options.projectDir);
|
|
437
492
|
const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
|
|
438
|
-
// Orchestrator work is conversation-scoped and can continue in parallel.
|
|
439
|
-
// Release the global message-loop lock here so another conversation can start.
|
|
440
|
-
this.activeCommandId = null;
|
|
441
|
-
this.processing = false;
|
|
442
|
-
if (this.scheduledTaskTimer) {
|
|
443
|
-
clearTimeout(this.scheduledTaskTimer);
|
|
444
|
-
this.scheduledTaskTimer = null;
|
|
445
|
-
}
|
|
446
|
-
void this.drainQueue();
|
|
447
493
|
try {
|
|
448
494
|
const response = await orchestrator.handleUserMessage(command.prompt, localConvId || '', {
|
|
449
495
|
projectDir: this.options.projectDir,
|
|
@@ -453,33 +499,27 @@ class MessageLoop {
|
|
|
453
499
|
await this.options.mqttClient.publishResult({
|
|
454
500
|
commandId: command.id,
|
|
455
501
|
type: 'chunk',
|
|
456
|
-
seq: this.chunkSeq++,
|
|
457
502
|
content: status + '\n',
|
|
458
503
|
timestamp: Date.now(),
|
|
459
504
|
});
|
|
460
505
|
},
|
|
461
506
|
mqttPublish: (result) => this.options.mqttClient.publishResult(result),
|
|
462
507
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const finalModelLabel = responseMeta?.modelLabel || undefined;
|
|
467
|
-
// Save final response to local DB
|
|
468
|
-
if (localConvId && localData && activeProfile) {
|
|
469
|
-
localData.addMessage(localConvId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
|
|
508
|
+
// Save O's response to local DB
|
|
509
|
+
if (localConvId && localData) {
|
|
510
|
+
localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
|
|
470
511
|
}
|
|
471
512
|
// Publish final response and complete
|
|
472
513
|
await this.options.mqttClient.publishResult({
|
|
473
514
|
commandId: command.id,
|
|
474
515
|
type: 'chunk',
|
|
475
|
-
seq: this.chunkSeq++,
|
|
476
516
|
content: response,
|
|
477
517
|
timestamp: Date.now(),
|
|
478
518
|
});
|
|
479
519
|
await this.options.mqttClient.publishResult({
|
|
480
520
|
commandId: command.id,
|
|
481
521
|
type: 'complete',
|
|
482
|
-
content:
|
|
522
|
+
content: '',
|
|
483
523
|
timestamp: Date.now(),
|
|
484
524
|
});
|
|
485
525
|
}
|
|
@@ -493,7 +533,7 @@ class MessageLoop {
|
|
|
493
533
|
clearTimeout(this.scheduledTaskTimer);
|
|
494
534
|
this.scheduledTaskTimer = null;
|
|
495
535
|
}
|
|
496
|
-
|
|
536
|
+
this.drainQueue();
|
|
497
537
|
}
|
|
498
538
|
return;
|
|
499
539
|
}
|
|
@@ -524,7 +564,7 @@ class MessageLoop {
|
|
|
524
564
|
includeKeyDecisions: false,
|
|
525
565
|
});
|
|
526
566
|
systemPrompt = built.systemPrompt;
|
|
527
|
-
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
567
|
+
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
528
568
|
}
|
|
529
569
|
else {
|
|
530
570
|
systemPrompt = this.buildFallbackSystemPrompt(command);
|
|
@@ -538,17 +578,13 @@ class MessageLoop {
|
|
|
538
578
|
systemPrompt = this.buildFallbackSystemPrompt(command);
|
|
539
579
|
}
|
|
540
580
|
// --- Context priority rules & tool fallback (AFTER context) ---
|
|
541
|
-
// Fix 3: Inject dead server warnings into system prompt
|
|
542
|
-
if (deadServerNote) {
|
|
543
|
-
systemPrompt += deadServerNote;
|
|
544
|
-
}
|
|
545
581
|
systemPrompt += `\n\nCONTEXT PRIORITY RULES:
|
|
546
|
-
1. ALWAYS use the context provided above (
|
|
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.
|
|
547
583
|
2. Do NOT browse local files or run shell commands unless:
|
|
548
584
|
a. The provided context does not contain the answer, OR
|
|
549
585
|
b. The user explicitly asks you to look at local files or run a command, OR
|
|
550
586
|
c. The user asks about current runtime state (logs, running processes, file contents right now)
|
|
551
|
-
3. When you use provided context, briefly cite the source (e.g. "Based on your stored
|
|
587
|
+
3. When you use provided context, briefly cite the source (e.g. "Based on your stored decisions..." or "From your conversation history...").
|
|
552
588
|
4. If no relevant context was provided AND the question is about past work/decisions, say "I don't have stored context about that topic yet" — don't guess or hallucinate.
|
|
553
589
|
|
|
554
590
|
TOOL EFFICIENCY RULES:
|
|
@@ -556,245 +592,56 @@ TOOL EFFICIENCY RULES:
|
|
|
556
592
|
- PREFER one broad command over many narrow ones (e.g. "find . -name '*.json' -exec grep 'error' {} +" instead of checking files one by one)
|
|
557
593
|
- PLAN your approach before starting — outline what you need to find, then execute with minimal tool calls`;
|
|
558
594
|
// Count context sections injected into system prompt
|
|
559
|
-
const contextSectionCount = ['[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]'
|
|
595
|
+
const contextSectionCount = ['[Key Decisions]', '[Memory Facts]', '[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]']
|
|
560
596
|
.filter(header => systemPrompt.includes(header)).length;
|
|
561
|
-
// Add the current prompt
|
|
562
|
-
|
|
563
|
-
const contentParts = [];
|
|
564
|
-
for (const att of command.attachments) {
|
|
565
|
-
if (att.fileType === 'image' && att.dataUrl) {
|
|
566
|
-
// Anthropic native format: source block with base64
|
|
567
|
-
const commaIdx = att.dataUrl.indexOf(',');
|
|
568
|
-
if (commaIdx >= 0) {
|
|
569
|
-
const mimeMatch = att.dataUrl.match(/^data:([^;]+);/);
|
|
570
|
-
const mediaType = mimeMatch?.[1] || att.mimeType || 'image/png';
|
|
571
|
-
const base64Data = att.dataUrl.slice(commaIdx + 1);
|
|
572
|
-
contentParts.push({ type: 'image', source: { type: 'base64', media_type: mediaType, data: base64Data } });
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
// Fallback: treat as URL
|
|
576
|
-
contentParts.push({ type: 'image_url', image_url: { url: att.dataUrl, detail: 'auto' } });
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if (att.extractedText) {
|
|
580
|
-
contentParts.push({ type: 'text', text: `[File: ${att.filename}]\n${att.extractedText}` });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
contentParts.push({ type: 'text', text: command.prompt });
|
|
584
|
-
messages.push({ role: 'user', content: contentParts });
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
messages.push({ role: 'user', content: command.prompt });
|
|
588
|
-
}
|
|
589
|
-
// Retry budget tracker (Optimization 2)
|
|
590
|
-
const toolFailuresByKey = new Map(); // toolName:argsHash → count
|
|
591
|
-
const toolFailuresByName = new Map(); // toolName → count
|
|
592
|
-
function getArgsHash(args) {
|
|
593
|
-
const str = JSON.stringify(args || {}).slice(0, 200);
|
|
594
|
-
return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
|
|
595
|
-
}
|
|
596
|
-
function getRetryKey(name, args) {
|
|
597
|
-
// Bug 6: For meta-tools, include action in retry key so different actions don't share budget
|
|
598
|
-
if (args?.action)
|
|
599
|
-
return `${name}:${args.action}`;
|
|
600
|
-
return name;
|
|
601
|
-
}
|
|
602
|
-
function trackToolFailure(name, args) {
|
|
603
|
-
const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
|
|
604
|
-
const retryKey = getRetryKey(name, args);
|
|
605
|
-
toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
|
|
606
|
-
toolFailuresByName.set(retryKey, (toolFailuresByName.get(retryKey) || 0) + 1);
|
|
607
|
-
}
|
|
608
|
-
// Futility detection: track consecutive low-value tool results
|
|
609
|
-
let consecutiveEmptyResults = 0;
|
|
610
|
-
const FUTILITY_THRESHOLD = 6; // after 6 consecutive empty/low-value results, nudge the LLM
|
|
611
|
-
let futilityNudgeInjected = false;
|
|
612
|
-
function isLowValueResult(output) {
|
|
613
|
-
if (!output || output.length < 30)
|
|
614
|
-
return true;
|
|
615
|
-
const lower = output.toLowerCase();
|
|
616
|
-
if (lower.includes('no matches found') || lower.includes('no results'))
|
|
617
|
-
return true;
|
|
618
|
-
if (lower.includes('0 bytes') || lower.includes('empty'))
|
|
619
|
-
return true;
|
|
620
|
-
// exit code 1 with no meaningful stdout
|
|
621
|
-
if (/\[exit code: [^0]/.test(lower) && output.replace(/\[exit code:.*?\]/g, '').trim().length < 20)
|
|
622
|
-
return true;
|
|
623
|
-
return false;
|
|
624
|
-
}
|
|
625
|
-
function checkRetryBudget(name, args) {
|
|
626
|
-
const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
|
|
627
|
-
const retryKey = getRetryKey(name, args);
|
|
628
|
-
const keyCount = toolFailuresByKey.get(key) || 0;
|
|
629
|
-
const nameCount = toolFailuresByName.get(retryKey) || 0;
|
|
630
|
-
if (keyCount >= 2) {
|
|
631
|
-
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.`;
|
|
632
|
-
}
|
|
633
|
-
if (nameCount >= 3) {
|
|
634
|
-
return `The ${name} tool has failed ${nameCount} times in this conversation. Consider a completely different approach.`;
|
|
635
|
-
}
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
597
|
+
// Add the current prompt
|
|
598
|
+
messages.push({ role: 'user', content: command.prompt });
|
|
638
599
|
// Agentic loop - keep calling LLM until no more tool calls
|
|
639
600
|
let iteration = 0;
|
|
640
|
-
|
|
641
|
-
// Web/MQTT commands (source undefined or 'user') get tighter cap; scheduled/system tasks get more room
|
|
642
|
-
const MAX_ITERATIONS = (command.source === 'scheduled' || command.source === 'system') ? 10 : 5;
|
|
643
|
-
const MAX_INPUT_TOKENS = 150_000; // 150K cumulative input token budget
|
|
644
|
-
const MAX_TOOL_CALLS = 20; // hard cap on tool calls per turn
|
|
645
|
-
const INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes of no LLM/tool activity
|
|
646
|
-
let lastActivityMs = Date.now();
|
|
647
|
-
let lastProgressMs = Date.now();
|
|
601
|
+
const MAX_ITERATIONS = Infinity;
|
|
648
602
|
let totalTokensUsed = 0;
|
|
649
|
-
let totalInputTokens = 0;
|
|
650
|
-
let totalOutputTokens = 0;
|
|
651
|
-
let totalCacheReadTokens = 0;
|
|
652
|
-
let totalCacheCreationTokens = 0;
|
|
653
|
-
let totalToolCallDurationMs = 0;
|
|
654
|
-
const deniedTools = new Set(); // Track tools denied by user — don't re-ask
|
|
655
|
-
const commandStartMs = Date.now();
|
|
656
|
-
// Publish prompt context for "View Prompt" panel
|
|
657
|
-
try {
|
|
658
|
-
const promptSnapshot = [
|
|
659
|
-
{ role: 'system', content: systemPrompt.slice(0, 4000) + (systemPrompt.length > 4000 ? '\n...(truncated)' : '') },
|
|
660
|
-
...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 2000) : '' })),
|
|
661
|
-
];
|
|
662
|
-
await this.options.mqttClient.publishResult({
|
|
663
|
-
commandId: command.id,
|
|
664
|
-
type: 'prompt_messages',
|
|
665
|
-
messages: promptSnapshot,
|
|
666
|
-
contextSections: contextSectionCount,
|
|
667
|
-
model,
|
|
668
|
-
timestamp: Date.now(),
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
catch { }
|
|
672
603
|
while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
|
|
673
604
|
iteration++;
|
|
674
|
-
// Emit progress every 60 seconds
|
|
675
|
-
if (Date.now() - lastProgressMs >= 60_000) {
|
|
676
|
-
lastProgressMs = Date.now();
|
|
677
|
-
await this.options.mqttClient.publishResult({
|
|
678
|
-
commandId: command.id,
|
|
679
|
-
type: 'progress',
|
|
680
|
-
elapsed: Math.round((Date.now() - commandStartMs) / 1000),
|
|
681
|
-
iteration,
|
|
682
|
-
toolsUsed: totalToolCalls,
|
|
683
|
-
timestamp: Date.now(),
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
// Inactivity timeout check (5 min of no LLM response or tool completion)
|
|
687
|
-
if (Date.now() - lastActivityMs > INACTIVITY_TIMEOUT_MS) {
|
|
688
|
-
console.log(chalk_1.default.red(` ⏱ Inactivity timeout exceeded (${INACTIVITY_TIMEOUT_MS / 1000}s) — attempting graceful wind-down`));
|
|
689
|
-
// Inject wind-down system message and do one final LLM call
|
|
690
|
-
messages.push({
|
|
691
|
-
role: 'user',
|
|
692
|
-
content: '[System] You are running out of time. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
|
|
693
|
-
});
|
|
694
|
-
let windDownSucceeded = false;
|
|
695
|
-
let windDownResponse = null;
|
|
696
|
-
try {
|
|
697
|
-
windDownResponse = await llm.chat({
|
|
698
|
-
messages,
|
|
699
|
-
system: systemPrompt,
|
|
700
|
-
stream: true,
|
|
701
|
-
onChunk: async (chunk) => {
|
|
702
|
-
if (this.activeCommandId !== command.id)
|
|
703
|
-
return;
|
|
704
|
-
await this.options.mqttClient.publishResult({
|
|
705
|
-
commandId: command.id,
|
|
706
|
-
type: 'chunk',
|
|
707
|
-
seq: this.chunkSeq++,
|
|
708
|
-
content: chunk,
|
|
709
|
-
timestamp: Date.now(),
|
|
710
|
-
});
|
|
711
|
-
},
|
|
712
|
-
});
|
|
713
|
-
if (windDownResponse.usage) {
|
|
714
|
-
totalTokensUsed += windDownResponse.usage.inputTokens + windDownResponse.usage.outputTokens;
|
|
715
|
-
totalInputTokens += windDownResponse.usage.inputTokens;
|
|
716
|
-
totalOutputTokens += windDownResponse.usage.outputTokens;
|
|
717
|
-
}
|
|
718
|
-
windDownSucceeded = true;
|
|
719
|
-
}
|
|
720
|
-
catch (windDownErr) {
|
|
721
|
-
console.error(chalk_1.default.red(` Wind-down LLM call failed: ${windDownErr.message}`));
|
|
722
|
-
commandAbortController.abort();
|
|
723
|
-
}
|
|
724
|
-
// Emit stats
|
|
725
|
-
await this.options.mqttClient.publishResult({
|
|
726
|
-
commandId: command.id,
|
|
727
|
-
type: 'stats',
|
|
728
|
-
llmCalls: iteration,
|
|
729
|
-
toolsUsed: totalToolCalls,
|
|
730
|
-
inputTokens: totalInputTokens,
|
|
731
|
-
outputTokens: totalOutputTokens,
|
|
732
|
-
cacheReadTokens: totalCacheReadTokens || undefined,
|
|
733
|
-
cacheCreationTokens: totalCacheCreationTokens || undefined,
|
|
734
|
-
totalTokens: totalTokensUsed,
|
|
735
|
-
latencyMs: Date.now() - commandStartMs,
|
|
736
|
-
toolCallDurationMs: totalToolCallDurationMs || undefined,
|
|
737
|
-
contextSections: contextSectionCount || undefined,
|
|
738
|
-
model,
|
|
739
|
-
timestamp: Date.now(),
|
|
740
|
-
});
|
|
741
|
-
if (windDownSucceeded) {
|
|
742
|
-
// Wind-down produced a summary — treat as completed
|
|
743
|
-
await this.options.mqttClient.publishResult({
|
|
744
|
-
commandId: command.id,
|
|
745
|
-
type: 'complete',
|
|
746
|
-
content: windDownResponse?.content || '',
|
|
747
|
-
timestamp: Date.now(),
|
|
748
|
-
});
|
|
749
|
-
console.log(chalk_1.default.green(` Command completed (inactivity wind-down): ${iteration} iterations, ${totalToolCalls} tool calls`));
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
// Wind-down failed — emit error so the job is marked failed, not completed
|
|
753
|
-
await this.options.mqttClient.publishResult({
|
|
754
|
-
commandId: command.id,
|
|
755
|
-
type: 'error',
|
|
756
|
-
error: `Command timed out after ${Math.round((Date.now() - commandStartMs) / 60000)} minutes of inactivity and wind-down failed.`,
|
|
757
|
-
timestamp: Date.now(),
|
|
758
|
-
});
|
|
759
|
-
console.log(chalk_1.default.red(` Command failed (wind-down failed): ${iteration} iterations, ${totalToolCalls} tool calls`));
|
|
760
|
-
}
|
|
761
|
-
break;
|
|
762
|
-
}
|
|
763
605
|
// Send a separator between agentic loop iterations so streamed text
|
|
764
606
|
// from different LLM calls doesn't run together in the UI.
|
|
765
607
|
if (iteration > 1) {
|
|
766
608
|
await this.options.mqttClient.publishResult({
|
|
767
609
|
commandId: command.id,
|
|
768
610
|
type: 'chunk',
|
|
769
|
-
seq: this.chunkSeq++,
|
|
770
611
|
content: '\n\n',
|
|
771
612
|
timestamp: Date.now(),
|
|
772
613
|
});
|
|
773
614
|
}
|
|
774
|
-
//
|
|
615
|
+
// Refresh OAuth token before each LLM call if needed
|
|
775
616
|
if (this.resolvedAuth) {
|
|
776
617
|
const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
|
|
618
|
+
if (refreshed.expired) {
|
|
619
|
+
console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
|
|
620
|
+
await this.options.mqttClient.publishResult({
|
|
621
|
+
commandId: command.id,
|
|
622
|
+
type: 'error',
|
|
623
|
+
error: 'Your authentication token has expired or been revoked. Please re-authenticate by running `funolio-agent login` or `funolio-agent configure`.',
|
|
624
|
+
timestamp: Date.now(),
|
|
625
|
+
});
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
777
628
|
if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
|
|
778
629
|
this.resolvedAuth = refreshed;
|
|
779
630
|
this.updateToken(refreshed.apiKey);
|
|
780
631
|
}
|
|
781
632
|
}
|
|
633
|
+
// With OAuth auto-detection, all providers now use direct API calls
|
|
634
|
+
// with full tool support. The old CLI_PROVIDERS check is kept only
|
|
635
|
+
// for logging/diagnostics but no longer gates tool availability.
|
|
782
636
|
// Smart tool filtering: only send relevant tools to reduce token usage
|
|
783
|
-
const filteredTools =
|
|
784
|
-
|
|
785
|
-
: (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
|
|
786
|
-
if (!index_1.CLI_PROVIDERS.has(this.options.provider) && filteredTools.length < this.toolDefinitions.length) {
|
|
637
|
+
const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
|
|
638
|
+
if (filteredTools.length < this.toolDefinitions.length) {
|
|
787
639
|
console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
|
|
788
640
|
}
|
|
789
|
-
// Compress context before sending to LLM (Optimizations 3+4)
|
|
790
|
-
const compressedMessages = (0, context_compressor_1.compressContext)(messages, 40, 3, 200);
|
|
791
|
-
if (compressedMessages.length < messages.length) {
|
|
792
|
-
console.log(chalk_1.default.gray(` [context] Compressed ${messages.length} → ${compressedMessages.length} messages`));
|
|
793
|
-
}
|
|
794
641
|
let response;
|
|
795
642
|
try {
|
|
796
643
|
response = await llm.chat({
|
|
797
|
-
messages
|
|
644
|
+
messages,
|
|
798
645
|
system: systemPrompt,
|
|
799
646
|
stream: true,
|
|
800
647
|
tools: filteredTools,
|
|
@@ -804,7 +651,6 @@ TOOL EFFICIENCY RULES:
|
|
|
804
651
|
await this.options.mqttClient.publishResult({
|
|
805
652
|
commandId: command.id,
|
|
806
653
|
type: 'chunk',
|
|
807
|
-
seq: this.chunkSeq++,
|
|
808
654
|
content: chunk,
|
|
809
655
|
timestamp: Date.now(),
|
|
810
656
|
});
|
|
@@ -819,76 +665,16 @@ TOOL EFFICIENCY RULES:
|
|
|
819
665
|
await this.options.mqttClient.publishResult({
|
|
820
666
|
commandId: command.id,
|
|
821
667
|
type: 'error',
|
|
822
|
-
error: 'API authentication failed.
|
|
668
|
+
error: 'API authentication failed. Your token may have expired — run `funolio-agent login` to refresh.',
|
|
823
669
|
timestamp: Date.now(),
|
|
824
670
|
});
|
|
825
671
|
break;
|
|
826
672
|
}
|
|
827
673
|
throw llmError;
|
|
828
674
|
}
|
|
829
|
-
// Reset inactivity timer after LLM response
|
|
830
|
-
lastActivityMs = Date.now();
|
|
831
675
|
// Track token usage across iterations
|
|
832
676
|
if (response.usage) {
|
|
833
677
|
totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
|
|
834
|
-
totalInputTokens += response.usage.inputTokens;
|
|
835
|
-
totalOutputTokens += response.usage.outputTokens;
|
|
836
|
-
totalCacheReadTokens += response.usage.cacheReadTokens || 0;
|
|
837
|
-
totalCacheCreationTokens += response.usage.cacheCreationTokens || 0;
|
|
838
|
-
}
|
|
839
|
-
// Phase 1d: Check cumulative input token budget
|
|
840
|
-
if (totalInputTokens >= MAX_INPUT_TOKENS) {
|
|
841
|
-
console.log(`[token-cap] Input token budget exceeded: ${totalInputTokens}/${MAX_INPUT_TOKENS} — triggering wind-down`);
|
|
842
|
-
messages.push({
|
|
843
|
-
role: 'user',
|
|
844
|
-
content: '[System] Token budget exceeded. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
|
|
845
|
-
});
|
|
846
|
-
try {
|
|
847
|
-
const capResponse = await llm.chat({
|
|
848
|
-
messages, system: systemPrompt, stream: true,
|
|
849
|
-
onChunk: async (chunk) => {
|
|
850
|
-
if (this.activeCommandId !== command.id)
|
|
851
|
-
return;
|
|
852
|
-
await this.options.mqttClient.publishResult({ commandId: command.id, type: 'chunk', seq: this.chunkSeq++, content: chunk, timestamp: Date.now() });
|
|
853
|
-
},
|
|
854
|
-
});
|
|
855
|
-
if (capResponse.usage) {
|
|
856
|
-
totalTokensUsed += capResponse.usage.inputTokens + capResponse.usage.outputTokens;
|
|
857
|
-
totalInputTokens += capResponse.usage.inputTokens;
|
|
858
|
-
totalOutputTokens += capResponse.usage.outputTokens;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
catch (e) {
|
|
862
|
-
console.error(`[token-cap] Wind-down LLM call failed: ${e.message}`);
|
|
863
|
-
}
|
|
864
|
-
break;
|
|
865
|
-
}
|
|
866
|
-
// Phase 1d: Check tool call hard cap
|
|
867
|
-
if (totalToolCalls >= MAX_TOOL_CALLS) {
|
|
868
|
-
console.log(`[token-cap] Tool call cap exceeded: ${totalToolCalls}/${MAX_TOOL_CALLS} — triggering wind-down`);
|
|
869
|
-
messages.push({
|
|
870
|
-
role: 'user',
|
|
871
|
-
content: '[System] Tool call limit reached. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
|
|
872
|
-
});
|
|
873
|
-
try {
|
|
874
|
-
const capResponse = await llm.chat({
|
|
875
|
-
messages, system: systemPrompt, stream: true,
|
|
876
|
-
onChunk: async (chunk) => {
|
|
877
|
-
if (this.activeCommandId !== command.id)
|
|
878
|
-
return;
|
|
879
|
-
await this.options.mqttClient.publishResult({ commandId: command.id, type: 'chunk', seq: this.chunkSeq++, content: chunk, timestamp: Date.now() });
|
|
880
|
-
},
|
|
881
|
-
});
|
|
882
|
-
if (capResponse.usage) {
|
|
883
|
-
totalTokensUsed += capResponse.usage.inputTokens + capResponse.usage.outputTokens;
|
|
884
|
-
totalInputTokens += capResponse.usage.inputTokens;
|
|
885
|
-
totalOutputTokens += capResponse.usage.outputTokens;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
catch (e) {
|
|
889
|
-
console.error(`[token-cap] Wind-down LLM call failed: ${e.message}`);
|
|
890
|
-
}
|
|
891
|
-
break;
|
|
892
678
|
}
|
|
893
679
|
// If there are tool calls, publish them and continue the loop
|
|
894
680
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
@@ -904,16 +690,13 @@ TOOL EFFICIENCY RULES:
|
|
|
904
690
|
const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
|
|
905
691
|
const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
|
|
906
692
|
// Check permission before execution
|
|
907
|
-
const permMode =
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
if (!approval.approved && deniedTools.has(toolCall.name)) {
|
|
913
|
-
approval = { approved: false, reason: `Tool "${toolCall.name}" was already denied in this session.` };
|
|
914
|
-
}
|
|
693
|
+
const permMode = this.options.permissionMode || 'autopilot';
|
|
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);
|
|
915
698
|
// If not auto-approved, request interactive approval via MQTT
|
|
916
|
-
if (!approval.approved && permMode !== 'autopilot'
|
|
699
|
+
if (!approval.approved && permMode !== 'autopilot') {
|
|
917
700
|
console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
|
|
918
701
|
approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
|
|
919
702
|
onRequest: async (request) => {
|
|
@@ -945,7 +728,6 @@ TOOL EFFICIENCY RULES:
|
|
|
945
728
|
}
|
|
946
729
|
}
|
|
947
730
|
if (!approval.approved) {
|
|
948
|
-
deniedTools.add(toolCall.name);
|
|
949
731
|
console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
|
|
950
732
|
const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
|
|
951
733
|
await this.options.mqttClient.publishResult({
|
|
@@ -988,62 +770,20 @@ TOOL EFFICIENCY RULES:
|
|
|
988
770
|
},
|
|
989
771
|
timestamp: Date.now(),
|
|
990
772
|
});
|
|
991
|
-
// Check retry budget before execution (Optimization 2)
|
|
992
|
-
const retryBlock = checkRetryBudget(toolCall.name, toolCall.arguments);
|
|
993
|
-
if (retryBlock) {
|
|
994
|
-
console.log(chalk_1.default.yellow(` ⚠ Retry budget exceeded for ${toolCall.name}`));
|
|
995
|
-
const retryOutput = `ERROR: ${retryBlock}`;
|
|
996
|
-
await this.options.mqttClient.publishResult({
|
|
997
|
-
commandId: command.id,
|
|
998
|
-
type: 'tool_result',
|
|
999
|
-
toolResult: {
|
|
1000
|
-
callId: toolCall.id,
|
|
1001
|
-
name: toolCall.name,
|
|
1002
|
-
output: retryOutput,
|
|
1003
|
-
isError: true,
|
|
1004
|
-
durationMs: 0,
|
|
1005
|
-
},
|
|
1006
|
-
timestamp: Date.now(),
|
|
1007
|
-
});
|
|
1008
|
-
messages.push({
|
|
1009
|
-
role: 'tool',
|
|
1010
|
-
content: retryOutput,
|
|
1011
|
-
toolCallId: toolCall.id,
|
|
1012
|
-
toolName: toolCall.name,
|
|
1013
|
-
});
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
773
|
// Execute the tool (with MCP fallback)
|
|
1017
|
-
const toolStartMs = Date.now();
|
|
1018
774
|
const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
|
|
1019
775
|
const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
|
|
1020
|
-
|
|
776
|
+
const resultOutput = toolResult.success
|
|
1021
777
|
? toolResult.output
|
|
1022
778
|
: `ERROR: ${toolResult.error || 'Unknown error'}`;
|
|
1023
|
-
// Bug 2 & 7: Cap tool result size to prevent context blowup
|
|
1024
|
-
const MAX_TOOL_RESULT_CHARS = 50_000;
|
|
1025
|
-
if (resultOutput.length > MAX_TOOL_RESULT_CHARS) {
|
|
1026
|
-
const originalLength = resultOutput.length;
|
|
1027
|
-
resultOutput = resultOutput.slice(0, MAX_TOOL_RESULT_CHARS) +
|
|
1028
|
-
`\n\n[OUTPUT TRUNCATED — was ${originalLength} chars. Use more specific commands to read smaller portions.]`;
|
|
1029
|
-
}
|
|
1030
|
-
// Track failures for retry budget (Optimization 2)
|
|
1031
|
-
if (!toolResult.success) {
|
|
1032
|
-
trackToolFailure(toolCall.name, toolCall.arguments);
|
|
1033
|
-
}
|
|
1034
779
|
console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
|
|
1035
|
-
const toolDurationMs = Date.now() - toolStartMs;
|
|
1036
|
-
totalToolCallDurationMs += toolDurationMs;
|
|
1037
|
-
lastActivityMs = Date.now(); // Reset inactivity timer after tool completion
|
|
1038
780
|
await this.options.mqttClient.publishResult({
|
|
1039
781
|
commandId: command.id,
|
|
1040
782
|
type: 'tool_result',
|
|
1041
783
|
toolResult: {
|
|
1042
784
|
callId: toolCall.id,
|
|
1043
|
-
name: toolCall.name,
|
|
1044
785
|
output: resultOutput,
|
|
1045
786
|
isError: !toolResult.success,
|
|
1046
|
-
durationMs: toolDurationMs,
|
|
1047
787
|
success: toolResult.success,
|
|
1048
788
|
summary: summarizeToolResult(resultOutput, !toolResult.success),
|
|
1049
789
|
path: toolLocation.path,
|
|
@@ -1061,32 +801,15 @@ TOOL EFFICIENCY RULES:
|
|
|
1061
801
|
toolCallId: toolCall.id,
|
|
1062
802
|
toolName: toolCall.name,
|
|
1063
803
|
});
|
|
1064
|
-
// Futility detection: track consecutive low-value results
|
|
1065
|
-
if (isLowValueResult(resultOutput)) {
|
|
1066
|
-
consecutiveEmptyResults++;
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
consecutiveEmptyResults = 0; // reset on any meaningful result
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
// Futility check: if too many consecutive empty results, nudge the LLM to stop spinning
|
|
1073
|
-
if (consecutiveEmptyResults >= FUTILITY_THRESHOLD && !futilityNudgeInjected) {
|
|
1074
|
-
futilityNudgeInjected = true;
|
|
1075
|
-
console.log(chalk_1.default.yellow(` ⚠ Futility detected: ${consecutiveEmptyResults} consecutive low-value tool results — nudging LLM`));
|
|
1076
|
-
messages.push({
|
|
1077
|
-
role: 'user',
|
|
1078
|
-
content: '[System] Your last several tool calls returned empty or no-match results. Stop searching and work with what you have. Summarize what you found (or could not find) and suggest next steps to the user. Do not make more search attempts.',
|
|
1079
|
-
});
|
|
1080
804
|
}
|
|
1081
805
|
// --- Rate limit checks ---
|
|
1082
806
|
// Warn at 80% of tool call limit
|
|
1083
807
|
if (totalToolCalls >= this.maxToolCallsPerMessage * 0.8 && totalToolCalls < this.maxToolCallsPerMessage) {
|
|
1084
808
|
console.log(chalk_1.default.yellow(` ⚠ Approaching tool call limit: ${totalToolCalls}/${this.maxToolCallsPerMessage}`));
|
|
1085
809
|
}
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
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}`));
|
|
1090
813
|
}
|
|
1091
814
|
// Check tool call limit
|
|
1092
815
|
if (totalToolCalls >= this.maxToolCallsPerMessage) {
|
|
@@ -1103,7 +826,6 @@ TOOL EFFICIENCY RULES:
|
|
|
1103
826
|
await this.options.mqttClient.publishResult({
|
|
1104
827
|
commandId: command.id,
|
|
1105
828
|
type: 'chunk',
|
|
1106
|
-
seq: this.chunkSeq++,
|
|
1107
829
|
content: chunk,
|
|
1108
830
|
timestamp: Date.now(),
|
|
1109
831
|
});
|
|
@@ -1114,74 +836,55 @@ TOOL EFFICIENCY RULES:
|
|
|
1114
836
|
}
|
|
1115
837
|
await this.options.mqttClient.publishResult({
|
|
1116
838
|
commandId: command.id,
|
|
1117
|
-
type: '
|
|
1118
|
-
|
|
1119
|
-
toolsUsed: totalToolCalls,
|
|
1120
|
-
inputTokens: totalInputTokens,
|
|
1121
|
-
outputTokens: totalOutputTokens,
|
|
1122
|
-
cacheReadTokens: totalCacheReadTokens || undefined,
|
|
1123
|
-
cacheCreationTokens: totalCacheCreationTokens || undefined,
|
|
1124
|
-
totalTokens: totalTokensUsed,
|
|
1125
|
-
latencyMs: Date.now() - commandStartMs,
|
|
1126
|
-
toolCallDurationMs: totalToolCallDurationMs || undefined,
|
|
1127
|
-
contextSections: contextSectionCount || undefined,
|
|
1128
|
-
model,
|
|
839
|
+
type: 'complete',
|
|
840
|
+
content: '',
|
|
1129
841
|
timestamp: Date.now(),
|
|
1130
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
|
+
}
|
|
1131
868
|
await this.options.mqttClient.publishResult({
|
|
1132
869
|
commandId: command.id,
|
|
1133
870
|
type: 'complete',
|
|
1134
|
-
content:
|
|
871
|
+
content: '',
|
|
1135
872
|
timestamp: Date.now(),
|
|
1136
873
|
});
|
|
1137
874
|
console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
|
|
1138
875
|
break;
|
|
1139
876
|
}
|
|
1140
|
-
// No artificial token limit — LLM providers return proper errors (400/413)
|
|
1141
|
-
// when context exceeds the model's real limit, and those are caught by the
|
|
1142
|
-
// error handler above which surfaces them to the user.
|
|
1143
877
|
// Continue the loop for the next LLM call
|
|
1144
878
|
continue;
|
|
1145
879
|
}
|
|
1146
880
|
// No tool calls - final response.
|
|
1147
|
-
//
|
|
1148
|
-
// The
|
|
1149
|
-
//
|
|
1150
|
-
// Fix 1: Detect phantom tool use — LLM claims it did something but made 0 tool calls
|
|
1151
|
-
const phantom = (0, response_guard_1.detectPhantomToolUse)(response.content || '', totalToolCalls);
|
|
1152
|
-
if (phantom.detected) {
|
|
1153
|
-
console.log(chalk_1.default.yellow(` ⚠ Phantom tool use detected — injecting correction`));
|
|
1154
|
-
// Stream the correction to the user
|
|
1155
|
-
await this.options.mqttClient.publishResult({
|
|
1156
|
-
commandId: command.id,
|
|
1157
|
-
type: 'chunk',
|
|
1158
|
-
seq: this.chunkSeq++,
|
|
1159
|
-
content: '\n\n---\n⚠️ ' + phantom.correction,
|
|
1160
|
-
timestamp: Date.now(),
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
// Publish stats before complete
|
|
1164
|
-
const latencyMs = Date.now() - commandStartMs;
|
|
1165
|
-
await this.options.mqttClient.publishResult({
|
|
1166
|
-
commandId: command.id,
|
|
1167
|
-
type: 'stats',
|
|
1168
|
-
llmCalls: iteration,
|
|
1169
|
-
toolsUsed: totalToolCalls,
|
|
1170
|
-
inputTokens: totalInputTokens,
|
|
1171
|
-
outputTokens: totalOutputTokens,
|
|
1172
|
-
cacheReadTokens: totalCacheReadTokens || undefined,
|
|
1173
|
-
cacheCreationTokens: totalCacheCreationTokens || undefined,
|
|
1174
|
-
totalTokens: totalTokensUsed,
|
|
1175
|
-
latencyMs,
|
|
1176
|
-
toolCallDurationMs: totalToolCallDurationMs || undefined,
|
|
1177
|
-
contextSections: contextSectionCount || undefined,
|
|
1178
|
-
model,
|
|
1179
|
-
timestamp: Date.now(),
|
|
1180
|
-
});
|
|
881
|
+
// DEDUP FIX: Content was already streamed via onChunk callbacks above.
|
|
882
|
+
// The 'complete' message signals end-of-response; we send empty content
|
|
883
|
+
// to avoid the receiver concatenating streamed chunks + full content.
|
|
1181
884
|
await this.options.mqttClient.publishResult({
|
|
1182
885
|
commandId: command.id,
|
|
1183
886
|
type: 'complete',
|
|
1184
|
-
content:
|
|
887
|
+
content: '',
|
|
1185
888
|
timestamp: Date.now(),
|
|
1186
889
|
});
|
|
1187
890
|
// Local-first: save assistant response to SQLite + trigger funnel
|
|
@@ -1189,6 +892,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1189
892
|
try {
|
|
1190
893
|
const localData = await Promise.resolve().then(() => __importStar(require('./local-data')));
|
|
1191
894
|
localData.addMessage(localConvId, 'assistant', response.content || '', model);
|
|
895
|
+
// Trigger background memory extraction
|
|
1192
896
|
(0, local_funnel_1.scheduleFunnelProcessing)(localConvId);
|
|
1193
897
|
// Track for session idle auto-organize
|
|
1194
898
|
this.lastOrganizedConvId = localConvId;
|
|
@@ -1200,6 +904,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1200
904
|
}
|
|
1201
905
|
}
|
|
1202
906
|
console.log(chalk_1.default.white(` Response: ${response.content?.slice(0, 200)}${(response.content?.length || 0) > 200 ? '...' : ''}`));
|
|
907
|
+
const latencyMs = Date.now() - commandStartMs;
|
|
1203
908
|
console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs}`);
|
|
1204
909
|
console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
|
|
1205
910
|
break;
|
|
@@ -1209,22 +914,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1209
914
|
}
|
|
1210
915
|
}
|
|
1211
916
|
catch (error) {
|
|
1212
|
-
|
|
1213
|
-
const details = [` ✗ Error: ${error.message}`];
|
|
1214
|
-
if (error.status)
|
|
1215
|
-
details.push(` ✗ HTTP status: ${error.status}`);
|
|
1216
|
-
if (error.type)
|
|
1217
|
-
details.push(` ✗ Error type: ${error.type}`);
|
|
1218
|
-
if (error.response?.body)
|
|
1219
|
-
details.push(` ✗ Response body: ${JSON.stringify(error.response.body).slice(0, 500)}`);
|
|
1220
|
-
if (error.error?.type)
|
|
1221
|
-
details.push(` ✗ Provider error type: ${error.error.type}`);
|
|
1222
|
-
if (error.cause)
|
|
1223
|
-
details.push(` ✗ Cause: ${error.cause}`);
|
|
1224
|
-
// For Anthropic "terminated" / stop_reason issues
|
|
1225
|
-
if (error.stop_reason)
|
|
1226
|
-
details.push(` ✗ Stop reason: ${error.stop_reason}`);
|
|
1227
|
-
details.forEach(d => console.error(chalk_1.default.red(d)));
|
|
917
|
+
console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
|
|
1228
918
|
await this.publishError(command.id, error.message);
|
|
1229
919
|
}
|
|
1230
920
|
finally {
|
|
@@ -1246,31 +936,10 @@ TOOL EFFICIENCY RULES:
|
|
|
1246
936
|
}
|
|
1247
937
|
/** Build system prompt manually (without clerk) — original logic */
|
|
1248
938
|
buildFallbackSystemPrompt(command) {
|
|
1249
|
-
let systemPrompt =
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
});
|
|
1254
|
-
// Auto-enumerate available tools so the bot always knows its capabilities
|
|
1255
|
-
const toolDefs = (0, index_2.getToolDefinitions)();
|
|
1256
|
-
const toolList = toolDefs.map(t => `- **${t.name}**: ${t.description.slice(0, 120)}`).join('\n');
|
|
1257
|
-
systemPrompt += `\n\n## Your Available Tools\nYou have ${toolDefs.length} tools available. Use them proactively:\n${toolList}`;
|
|
1258
|
-
systemPrompt += `\n\n## Working Style
|
|
1259
|
-
- You are autonomous — take action, don't just describe what you could do.
|
|
1260
|
-
- For complex tasks, use spawn_subagent to parallelize work.
|
|
1261
|
-
- Read project files to understand context before making changes.
|
|
1262
|
-
- Commit your work with git_commit when you've made meaningful progress.
|
|
1263
|
-
- Use web_search and web_fetch to research unfamiliar topics.
|
|
1264
|
-
- Write important context to files so you remember it across sessions.
|
|
1265
|
-
- If you're unsure about something destructive, ask first. Otherwise, just do it.`;
|
|
1266
|
-
// Platform awareness — always present regardless of soulMd content
|
|
1267
|
-
systemPrompt += `\n\n## Extending Your Capabilities
|
|
1268
|
-
- You have built-in tools (file ops, shell, git, web search, web fetch, tasks, sub-agents, memory search).
|
|
1269
|
-
- 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\`.
|
|
1270
|
-
- If a matching MCP tool server exists, use \`request_mcp_install\` to request permission to install it.
|
|
1271
|
-
- You can discover and gain new capabilities on-the-fly through the marketplace.
|
|
1272
|
-
- **NEVER tell the user to "do it manually" or "upload it yourself"** — always check the marketplace first and offer to install the right tool.`;
|
|
1273
|
-
systemPrompt += '\n\nIMPORTANT: When the user references a project, topic, or past work, use the relevant summaries and conversation history provided below. If no relevant context is available, say so honestly rather than guessing. Use your tools (file browsing, commands) to find project files on the local machine.';
|
|
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.';
|
|
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.';
|
|
1274
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.';
|
|
1275
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.';
|
|
1276
945
|
systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
|
|
@@ -1293,7 +962,13 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1293
962
|
systemPrompt += '\n\n[Bot Memory]\n' + command.context.memoryMd;
|
|
1294
963
|
}
|
|
1295
964
|
const sc = command.context?.structuredContext;
|
|
1296
|
-
if (sc && (sc.codeRefs?.length || sc.conversations?.length)) {
|
|
965
|
+
if (sc && (sc.decisions?.length || sc.memoryFacts?.length || sc.codeRefs?.length || sc.conversations?.length)) {
|
|
966
|
+
if (sc.decisions?.length) {
|
|
967
|
+
systemPrompt += '\n\n[Key Decisions]\n' + sc.decisions.map(d => `- ${d}`).join('\n');
|
|
968
|
+
}
|
|
969
|
+
if (sc.memoryFacts?.length) {
|
|
970
|
+
systemPrompt += '\n\n[Memory Facts]\n' + sc.memoryFacts.map(f => `- ${f}`).join('\n');
|
|
971
|
+
}
|
|
1297
972
|
if (sc.conversations?.length) {
|
|
1298
973
|
systemPrompt += '\n\n[Relevant Conversations]\n' + sc.conversations.map(c => `- ${c}`).join('\n');
|
|
1299
974
|
}
|
|
@@ -1301,8 +976,11 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1301
976
|
systemPrompt += '\n\n[Code References]\n' + sc.codeRefs.map(r => `- ${r}`).join('\n');
|
|
1302
977
|
}
|
|
1303
978
|
}
|
|
979
|
+
else if (command.context?.facts?.length) {
|
|
980
|
+
systemPrompt += '\n\nRelevant memory/facts from the user\'s stored conversations:\n' + command.context.facts.map(f => `- ${f}`).join('\n');
|
|
981
|
+
}
|
|
1304
982
|
else {
|
|
1305
|
-
systemPrompt += '\n\nNo relevant
|
|
983
|
+
systemPrompt += '\n\nNo relevant memories were found for this query. If the user is asking about a specific project or past work, let them know you don\'t have stored context about it and offer to search their local files instead.';
|
|
1306
984
|
}
|
|
1307
985
|
if (command.context?.files?.length) {
|
|
1308
986
|
systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
|
|
@@ -1345,18 +1023,19 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1345
1023
|
timestamp: Date.now(),
|
|
1346
1024
|
});
|
|
1347
1025
|
}
|
|
1348
|
-
/** Update the
|
|
1026
|
+
/** Update the OAuth token used for requests (called by token refresh) */
|
|
1349
1027
|
updateToken(token) {
|
|
1350
|
-
this.options.
|
|
1028
|
+
this.options.oauthToken = token;
|
|
1029
|
+
// Recreate the default provider with the new token
|
|
1351
1030
|
const effectiveProvider = this.options.provider;
|
|
1352
|
-
const authMode = this.options.authMode || (token.startsWith('sk-ant-oat') ? 'oauth-bearer' : 'api-key');
|
|
1353
1031
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
1354
1032
|
apiKey: token,
|
|
1355
1033
|
model: this.options.model,
|
|
1356
|
-
authMode,
|
|
1357
|
-
...(this.options.baseUrl ? { baseUrl: this.options.baseUrl } : {}),
|
|
1358
|
-
...(this.options.apiStyle ? { apiStyle: this.options.apiStyle } : {}),
|
|
1034
|
+
...(token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
|
|
1359
1035
|
});
|
|
1036
|
+
if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
|
|
1037
|
+
this.resolveAndUpgradeAuth(token, this.options.model);
|
|
1038
|
+
}
|
|
1360
1039
|
}
|
|
1361
1040
|
}
|
|
1362
1041
|
exports.MessageLoop = MessageLoop;
|