funolio-agent 1.0.47 → 1.0.49
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/auth/auto-detect.d.ts +1 -0
- package/dist/auth/auto-detect.d.ts.map +1 -1
- package/dist/auth/auto-detect.js +16 -13
- package/dist/auth/auto-detect.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 +8 -23
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +61 -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 +154 -43
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +195 -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 +10 -6
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +241 -540
- 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,47 @@ 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-');
|
|
151
|
+
const resolved = options.resolvedAuth;
|
|
152
|
+
const resolvedMatchesProvider = !!resolved && resolved.provider === effectiveProvider;
|
|
145
153
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
146
154
|
apiKey: effectiveKey,
|
|
147
155
|
model: options.model,
|
|
148
|
-
authMode:
|
|
149
|
-
...(
|
|
150
|
-
...(
|
|
156
|
+
...(resolvedMatchesProvider && resolved?.authMode ? { authMode: resolved.authMode } : {}),
|
|
157
|
+
...(resolvedMatchesProvider && resolved?.baseUrl ? { baseUrl: resolved.baseUrl } : {}),
|
|
158
|
+
...(resolvedMatchesProvider && resolved?.apiStyle ? { apiStyle: resolved.apiStyle } : {}),
|
|
159
|
+
...(!resolvedMatchesProvider && isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
|
|
151
160
|
});
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const unrestrictedCliProvider = index_1.CLI_PROVIDERS.has(effectiveProvider);
|
|
161
|
+
// Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
|
|
162
|
+
if (isOAuthToken && effectiveProvider === 'anthropic') {
|
|
163
|
+
this.resolveAndUpgradeAuth(effectiveKey, options.model);
|
|
164
|
+
}
|
|
157
165
|
this.toolContext = (0, index_2.createToolContext)(options.projectDir, {
|
|
158
166
|
actorType: 'llm',
|
|
159
167
|
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
168
|
});
|
|
169
|
+
this.options.enabledTools = (0, tool_permissions_1.normalizeConfiguredToolNames)(options.enabledTools);
|
|
170
|
+
this.options.enabledMcpTools = normalizeMcpToolNames(options.enabledMcpTools);
|
|
169
171
|
let allTools = (0, index_2.getAllToolDefinitions)(options.mcpManager);
|
|
170
172
|
let builtinTools = (0, index_2.getToolDefinitions)();
|
|
171
|
-
// Filter
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
// Filter builtin and MCP tools independently.
|
|
174
|
+
// An explicit empty array means "deny all" for that category.
|
|
175
|
+
if (this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined) {
|
|
176
|
+
const builtinNames = new Set(builtinTools.map((t) => t.name));
|
|
177
|
+
const allowedBuiltin = this.options.enabledTools !== undefined ? new Set(this.options.enabledTools) : null;
|
|
178
|
+
const allowedMcp = this.options.enabledMcpTools !== undefined ? new Set(this.options.enabledMcpTools) : null;
|
|
179
|
+
allTools = allTools.filter((t) => {
|
|
180
|
+
if (builtinNames.has(t.name)) {
|
|
181
|
+
return allowedBuiltin ? allowedBuiltin.has(t.name) : true;
|
|
182
|
+
}
|
|
183
|
+
return allowedMcp ? allowedMcp.has(t.name) : true;
|
|
184
|
+
});
|
|
185
|
+
builtinTools = builtinTools.filter((t) => (allowedBuiltin ? allowedBuiltin.has(t.name) : true));
|
|
186
|
+
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
187
|
}
|
|
178
188
|
this.toolDefinitions = allTools;
|
|
179
189
|
this.builtinToolDefinitions = builtinTools;
|
|
@@ -192,12 +202,59 @@ class MessageLoop {
|
|
|
192
202
|
});
|
|
193
203
|
// Rate limiting defaults (can be overridden via options from agent config)
|
|
194
204
|
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;
|
|
205
|
+
this.maxTokensPerMessage = options.maxTokensPerMessage ?? 200_000;
|
|
198
206
|
// Session idle detection timer (checks every 60s)
|
|
199
207
|
this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
|
|
200
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Attempt to resolve Anthropic subscription auth via create_api_key exchange.
|
|
211
|
+
* If successful, recreates the LLM provider with the exchanged API key (which
|
|
212
|
+
* works with standard x-api-key auth and unlocks all models on the subscription).
|
|
213
|
+
* Falls back to bearer mode silently if exchange fails.
|
|
214
|
+
*/
|
|
215
|
+
async resolveAndUpgradeAuth(oauthToken, model) {
|
|
216
|
+
try {
|
|
217
|
+
const result = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
|
|
218
|
+
accessToken: oauthToken,
|
|
219
|
+
refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
|
|
220
|
+
expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
|
|
221
|
+
onRefresh: (credential) => {
|
|
222
|
+
// Propagate refreshed credentials to BOTH auth stores
|
|
223
|
+
if (this.options.resolvedAuth) {
|
|
224
|
+
this.options.resolvedAuth.credential = credential;
|
|
225
|
+
}
|
|
226
|
+
if (this.resolvedAuth) {
|
|
227
|
+
this.resolvedAuth.credential = credential;
|
|
228
|
+
// Also update the apiKey field so ensureFreshToken() sees the new token
|
|
229
|
+
this.resolvedAuth.apiKey = credential.accessToken;
|
|
230
|
+
}
|
|
231
|
+
console.log('[message-loop] Anthropic OAuth token refreshed and propagated to runtime state');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
if (result.source === 'api-key-exchange') {
|
|
235
|
+
// Successfully exchanged OAuth token for a temporary API key
|
|
236
|
+
console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
|
|
237
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
238
|
+
apiKey: result.token,
|
|
239
|
+
model,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Bearer fallback — recreate provider with the (possibly refreshed) token
|
|
244
|
+
const currentToken = this.options.resolvedAuth?.credential?.accessToken || oauthToken;
|
|
245
|
+
console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
|
|
246
|
+
(currentToken !== oauthToken ? ' (with refreshed token)' : ''));
|
|
247
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
248
|
+
apiKey: currentToken,
|
|
249
|
+
model,
|
|
250
|
+
authMode: 'oauth-bearer',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
201
258
|
/** Check if a session has gone idle and trigger auto-organize */
|
|
202
259
|
async checkIdleSession() {
|
|
203
260
|
if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
|
|
@@ -298,11 +355,6 @@ class MessageLoop {
|
|
|
298
355
|
console.log(chalk_1.default.yellow(`Cancelling command ${command.id}`));
|
|
299
356
|
this.activeCommandId = null;
|
|
300
357
|
this.approvalManager.cancelAll();
|
|
301
|
-
// Abort any running tool execution
|
|
302
|
-
if (this._activeAbortController) {
|
|
303
|
-
this._activeAbortController.abort();
|
|
304
|
-
this._activeAbortController = null;
|
|
305
|
-
}
|
|
306
358
|
}
|
|
307
359
|
return;
|
|
308
360
|
}
|
|
@@ -313,39 +365,65 @@ class MessageLoop {
|
|
|
313
365
|
// Use model override from command if provided (but NOT apiKey — agent uses its own stored credentials)
|
|
314
366
|
const overrideProvider = command.model?.provider;
|
|
315
367
|
const overrideModel = command.model?.model;
|
|
316
|
-
|
|
368
|
+
const isSubscriptionRuntime = /_subscription$/i.test(String(this.options.accessMode || ''))
|
|
369
|
+
|| this.options.provider === 'claude-cli'
|
|
370
|
+
|| this.options.provider === 'codex-cli';
|
|
371
|
+
// For subscription runtimes, never let server command metadata swap us to API-provider paths.
|
|
372
|
+
let provider = isSubscriptionRuntime ? this.options.provider : (overrideProvider || this.options.provider);
|
|
317
373
|
const model = overrideModel || this.options.model;
|
|
318
|
-
const apiKey = this.options.apiKey;
|
|
374
|
+
const apiKey = this.options.oauthToken || this.options.apiKey;
|
|
319
375
|
// Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
|
|
320
|
-
const hasOverride = (
|
|
376
|
+
const hasOverride = (provider !== this.options.provider) ||
|
|
321
377
|
(overrideModel && overrideModel !== this.options.model);
|
|
322
378
|
let llm;
|
|
323
379
|
if (hasOverride) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
380
|
+
// For overrides with OAuth tokens, try subscription auth resolution
|
|
381
|
+
if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
|
|
382
|
+
try {
|
|
383
|
+
const authResult = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
|
|
384
|
+
accessToken: apiKey,
|
|
385
|
+
refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
|
|
386
|
+
expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
|
|
387
|
+
onRefresh: (credential) => {
|
|
388
|
+
if (this.options.resolvedAuth) {
|
|
389
|
+
this.options.resolvedAuth.credential = credential;
|
|
390
|
+
}
|
|
391
|
+
if (this.resolvedAuth) {
|
|
392
|
+
this.resolvedAuth.credential = credential;
|
|
393
|
+
this.resolvedAuth.apiKey = credential.accessToken;
|
|
394
|
+
}
|
|
395
|
+
console.log('[message-loop] Anthropic OAuth token refreshed (override path)');
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
llm = (0, index_1.createProvider)(provider, {
|
|
399
|
+
apiKey: authResult.token,
|
|
400
|
+
model,
|
|
401
|
+
...(authResult.authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
llm = (0, index_1.createProvider)(provider, { apiKey, model, authMode: 'oauth-bearer' });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const resolved = this.options.resolvedAuth;
|
|
410
|
+
const resolvedMatchesProvider = !!resolved && resolved.provider === provider;
|
|
411
|
+
llm = (0, index_1.createProvider)(provider, {
|
|
412
|
+
apiKey,
|
|
413
|
+
model,
|
|
414
|
+
...(resolvedMatchesProvider && resolved?.authMode ? { authMode: resolved.authMode } : {}),
|
|
415
|
+
...(resolvedMatchesProvider && resolved?.baseUrl ? { baseUrl: resolved.baseUrl } : {}),
|
|
416
|
+
...(resolvedMatchesProvider && resolved?.apiStyle ? { apiStyle: resolved.apiStyle } : {}),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
331
419
|
}
|
|
332
420
|
else {
|
|
333
421
|
llm = this.llmProvider;
|
|
334
422
|
}
|
|
335
423
|
// Refresh tool definitions each command so newly installed MCP tools are visible
|
|
336
424
|
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
425
|
this.activeCommandId = command.id;
|
|
347
426
|
this.activeCommandSource = command.source || 'user';
|
|
348
|
-
this.chunkSeq = 0;
|
|
349
427
|
this.processing = true;
|
|
350
428
|
this.lastMessageAt = Date.now();
|
|
351
429
|
const commandStartMs = Date.now();
|
|
@@ -406,21 +484,15 @@ class MessageLoop {
|
|
|
406
484
|
: undefined;
|
|
407
485
|
const effectiveProjectId = command.context?.projectId || activeConversation?.project_id || undefined;
|
|
408
486
|
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
487
|
const commandToolContext = {
|
|
413
488
|
...this.toolContext,
|
|
414
489
|
projectId: effectiveProjectId ?? null,
|
|
415
|
-
abortSignal: commandAbortController.signal,
|
|
416
|
-
// Use project folder as working directory if available
|
|
417
|
-
...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
|
|
418
490
|
};
|
|
419
491
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
420
492
|
// Resolve the selected bot profile — from command.bot, conversation bot_id, or default
|
|
421
493
|
const selectedBotId = command.bot?.id || activeConversation?.bot_id || localAgentId;
|
|
422
494
|
const activeProfile = selectedBotId ? localData?.getAgentProfile(selectedBotId) : null;
|
|
423
|
-
if (
|
|
495
|
+
if (activeProfile?.role_class === 'orchestrator' && localFirst) {
|
|
424
496
|
const clerk = (0, clerk_model_1.getClerk)();
|
|
425
497
|
if (!clerk) {
|
|
426
498
|
// Do not silently fall through to direct chat — report error
|
|
@@ -435,15 +507,6 @@ class MessageLoop {
|
|
|
435
507
|
const { getWorkflowEngine } = await Promise.resolve().then(() => __importStar(require('./workflow-engine')));
|
|
436
508
|
const workflowEngine = getWorkflowEngine(this.options.projectDir);
|
|
437
509
|
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
510
|
try {
|
|
448
511
|
const response = await orchestrator.handleUserMessage(command.prompt, localConvId || '', {
|
|
449
512
|
projectDir: this.options.projectDir,
|
|
@@ -453,33 +516,27 @@ class MessageLoop {
|
|
|
453
516
|
await this.options.mqttClient.publishResult({
|
|
454
517
|
commandId: command.id,
|
|
455
518
|
type: 'chunk',
|
|
456
|
-
seq: this.chunkSeq++,
|
|
457
519
|
content: status + '\n',
|
|
458
520
|
timestamp: Date.now(),
|
|
459
521
|
});
|
|
460
522
|
},
|
|
461
523
|
mqttPublish: (result) => this.options.mqttClient.publishResult(result),
|
|
462
524
|
});
|
|
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);
|
|
525
|
+
// Save O's response to local DB
|
|
526
|
+
if (localConvId && localData) {
|
|
527
|
+
localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
|
|
470
528
|
}
|
|
471
529
|
// Publish final response and complete
|
|
472
530
|
await this.options.mqttClient.publishResult({
|
|
473
531
|
commandId: command.id,
|
|
474
532
|
type: 'chunk',
|
|
475
|
-
seq: this.chunkSeq++,
|
|
476
533
|
content: response,
|
|
477
534
|
timestamp: Date.now(),
|
|
478
535
|
});
|
|
479
536
|
await this.options.mqttClient.publishResult({
|
|
480
537
|
commandId: command.id,
|
|
481
538
|
type: 'complete',
|
|
482
|
-
content:
|
|
539
|
+
content: '',
|
|
483
540
|
timestamp: Date.now(),
|
|
484
541
|
});
|
|
485
542
|
}
|
|
@@ -493,7 +550,7 @@ class MessageLoop {
|
|
|
493
550
|
clearTimeout(this.scheduledTaskTimer);
|
|
494
551
|
this.scheduledTaskTimer = null;
|
|
495
552
|
}
|
|
496
|
-
|
|
553
|
+
this.drainQueue();
|
|
497
554
|
}
|
|
498
555
|
return;
|
|
499
556
|
}
|
|
@@ -524,7 +581,7 @@ class MessageLoop {
|
|
|
524
581
|
includeKeyDecisions: false,
|
|
525
582
|
});
|
|
526
583
|
systemPrompt = built.systemPrompt;
|
|
527
|
-
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
584
|
+
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
528
585
|
}
|
|
529
586
|
else {
|
|
530
587
|
systemPrompt = this.buildFallbackSystemPrompt(command);
|
|
@@ -538,17 +595,13 @@ class MessageLoop {
|
|
|
538
595
|
systemPrompt = this.buildFallbackSystemPrompt(command);
|
|
539
596
|
}
|
|
540
597
|
// --- 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
598
|
systemPrompt += `\n\nCONTEXT PRIORITY RULES:
|
|
546
|
-
1. ALWAYS use the context provided above (
|
|
599
|
+
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
600
|
2. Do NOT browse local files or run shell commands unless:
|
|
548
601
|
a. The provided context does not contain the answer, OR
|
|
549
602
|
b. The user explicitly asks you to look at local files or run a command, OR
|
|
550
603
|
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
|
|
604
|
+
3. When you use provided context, briefly cite the source (e.g. "Based on your stored decisions..." or "From your conversation history...").
|
|
552
605
|
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
606
|
|
|
554
607
|
TOOL EFFICIENCY RULES:
|
|
@@ -556,245 +609,56 @@ TOOL EFFICIENCY RULES:
|
|
|
556
609
|
- PREFER one broad command over many narrow ones (e.g. "find . -name '*.json' -exec grep 'error' {} +" instead of checking files one by one)
|
|
557
610
|
- PLAN your approach before starting — outline what you need to find, then execute with minimal tool calls`;
|
|
558
611
|
// Count context sections injected into system prompt
|
|
559
|
-
const contextSectionCount = ['[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]'
|
|
612
|
+
const contextSectionCount = ['[Key Decisions]', '[Memory Facts]', '[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]']
|
|
560
613
|
.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
|
-
}
|
|
614
|
+
// Add the current prompt
|
|
615
|
+
messages.push({ role: 'user', content: command.prompt });
|
|
638
616
|
// Agentic loop - keep calling LLM until no more tool calls
|
|
639
617
|
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();
|
|
618
|
+
const MAX_ITERATIONS = Infinity;
|
|
648
619
|
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
620
|
while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
|
|
673
621
|
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
622
|
// Send a separator between agentic loop iterations so streamed text
|
|
764
623
|
// from different LLM calls doesn't run together in the UI.
|
|
765
624
|
if (iteration > 1) {
|
|
766
625
|
await this.options.mqttClient.publishResult({
|
|
767
626
|
commandId: command.id,
|
|
768
627
|
type: 'chunk',
|
|
769
|
-
seq: this.chunkSeq++,
|
|
770
628
|
content: '\n\n',
|
|
771
629
|
timestamp: Date.now(),
|
|
772
630
|
});
|
|
773
631
|
}
|
|
774
|
-
//
|
|
632
|
+
// Refresh OAuth token before each LLM call if needed
|
|
775
633
|
if (this.resolvedAuth) {
|
|
776
634
|
const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
|
|
635
|
+
if (refreshed.expired) {
|
|
636
|
+
console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
|
|
637
|
+
await this.options.mqttClient.publishResult({
|
|
638
|
+
commandId: command.id,
|
|
639
|
+
type: 'error',
|
|
640
|
+
error: 'Your authentication token has expired or been revoked. Please re-authenticate by running `funolio-agent login` or `funolio-agent configure`.',
|
|
641
|
+
timestamp: Date.now(),
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
777
645
|
if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
|
|
778
646
|
this.resolvedAuth = refreshed;
|
|
779
647
|
this.updateToken(refreshed.apiKey);
|
|
780
648
|
}
|
|
781
649
|
}
|
|
650
|
+
// With OAuth auto-detection, all providers now use direct API calls
|
|
651
|
+
// with full tool support. The old CLI_PROVIDERS check is kept only
|
|
652
|
+
// for logging/diagnostics but no longer gates tool availability.
|
|
782
653
|
// 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) {
|
|
654
|
+
const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
|
|
655
|
+
if (filteredTools.length < this.toolDefinitions.length) {
|
|
787
656
|
console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
|
|
788
657
|
}
|
|
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
658
|
let response;
|
|
795
659
|
try {
|
|
796
660
|
response = await llm.chat({
|
|
797
|
-
messages
|
|
661
|
+
messages,
|
|
798
662
|
system: systemPrompt,
|
|
799
663
|
stream: true,
|
|
800
664
|
tools: filteredTools,
|
|
@@ -804,7 +668,6 @@ TOOL EFFICIENCY RULES:
|
|
|
804
668
|
await this.options.mqttClient.publishResult({
|
|
805
669
|
commandId: command.id,
|
|
806
670
|
type: 'chunk',
|
|
807
|
-
seq: this.chunkSeq++,
|
|
808
671
|
content: chunk,
|
|
809
672
|
timestamp: Date.now(),
|
|
810
673
|
});
|
|
@@ -819,76 +682,16 @@ TOOL EFFICIENCY RULES:
|
|
|
819
682
|
await this.options.mqttClient.publishResult({
|
|
820
683
|
commandId: command.id,
|
|
821
684
|
type: 'error',
|
|
822
|
-
error: 'API authentication failed.
|
|
685
|
+
error: 'API authentication failed. Your token may have expired — run `funolio-agent login` to refresh.',
|
|
823
686
|
timestamp: Date.now(),
|
|
824
687
|
});
|
|
825
688
|
break;
|
|
826
689
|
}
|
|
827
690
|
throw llmError;
|
|
828
691
|
}
|
|
829
|
-
// Reset inactivity timer after LLM response
|
|
830
|
-
lastActivityMs = Date.now();
|
|
831
692
|
// Track token usage across iterations
|
|
832
693
|
if (response.usage) {
|
|
833
694
|
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
695
|
}
|
|
893
696
|
// If there are tool calls, publish them and continue the loop
|
|
894
697
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
@@ -904,16 +707,13 @@ TOOL EFFICIENCY RULES:
|
|
|
904
707
|
const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
|
|
905
708
|
const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
|
|
906
709
|
// 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
|
-
}
|
|
710
|
+
const permMode = this.options.permissionMode || 'autopilot';
|
|
711
|
+
const allowedToolNames = this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined
|
|
712
|
+
? [...(this.options.enabledTools || []), ...(this.options.enabledMcpTools || [])]
|
|
713
|
+
: undefined;
|
|
714
|
+
let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, allowedToolNames);
|
|
915
715
|
// If not auto-approved, request interactive approval via MQTT
|
|
916
|
-
if (!approval.approved && permMode !== 'autopilot'
|
|
716
|
+
if (!approval.approved && permMode !== 'autopilot') {
|
|
917
717
|
console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
|
|
918
718
|
approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
|
|
919
719
|
onRequest: async (request) => {
|
|
@@ -945,7 +745,6 @@ TOOL EFFICIENCY RULES:
|
|
|
945
745
|
}
|
|
946
746
|
}
|
|
947
747
|
if (!approval.approved) {
|
|
948
|
-
deniedTools.add(toolCall.name);
|
|
949
748
|
console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
|
|
950
749
|
const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
|
|
951
750
|
await this.options.mqttClient.publishResult({
|
|
@@ -988,62 +787,20 @@ TOOL EFFICIENCY RULES:
|
|
|
988
787
|
},
|
|
989
788
|
timestamp: Date.now(),
|
|
990
789
|
});
|
|
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
790
|
// Execute the tool (with MCP fallback)
|
|
1017
|
-
const toolStartMs = Date.now();
|
|
1018
791
|
const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
|
|
1019
792
|
const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
|
|
1020
|
-
|
|
793
|
+
const resultOutput = toolResult.success
|
|
1021
794
|
? toolResult.output
|
|
1022
795
|
: `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
796
|
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
797
|
await this.options.mqttClient.publishResult({
|
|
1039
798
|
commandId: command.id,
|
|
1040
799
|
type: 'tool_result',
|
|
1041
800
|
toolResult: {
|
|
1042
801
|
callId: toolCall.id,
|
|
1043
|
-
name: toolCall.name,
|
|
1044
802
|
output: resultOutput,
|
|
1045
803
|
isError: !toolResult.success,
|
|
1046
|
-
durationMs: toolDurationMs,
|
|
1047
804
|
success: toolResult.success,
|
|
1048
805
|
summary: summarizeToolResult(resultOutput, !toolResult.success),
|
|
1049
806
|
path: toolLocation.path,
|
|
@@ -1061,32 +818,15 @@ TOOL EFFICIENCY RULES:
|
|
|
1061
818
|
toolCallId: toolCall.id,
|
|
1062
819
|
toolName: toolCall.name,
|
|
1063
820
|
});
|
|
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
821
|
}
|
|
1081
822
|
// --- Rate limit checks ---
|
|
1082
823
|
// Warn at 80% of tool call limit
|
|
1083
824
|
if (totalToolCalls >= this.maxToolCallsPerMessage * 0.8 && totalToolCalls < this.maxToolCallsPerMessage) {
|
|
1084
825
|
console.log(chalk_1.default.yellow(` ⚠ Approaching tool call limit: ${totalToolCalls}/${this.maxToolCallsPerMessage}`));
|
|
1085
826
|
}
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
console.log(chalk_1.default.gray(` [tokens] iteration ${iteration}: ${response.usage.inputTokens} input, ${response.usage.outputTokens} output`));
|
|
827
|
+
// Warn at 80% of token limit
|
|
828
|
+
if (totalTokensUsed >= this.maxTokensPerMessage * 0.8 && totalTokensUsed < this.maxTokensPerMessage) {
|
|
829
|
+
console.log(chalk_1.default.yellow(` ⚠ Approaching token limit: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
|
|
1090
830
|
}
|
|
1091
831
|
// Check tool call limit
|
|
1092
832
|
if (totalToolCalls >= this.maxToolCallsPerMessage) {
|
|
@@ -1103,7 +843,6 @@ TOOL EFFICIENCY RULES:
|
|
|
1103
843
|
await this.options.mqttClient.publishResult({
|
|
1104
844
|
commandId: command.id,
|
|
1105
845
|
type: 'chunk',
|
|
1106
|
-
seq: this.chunkSeq++,
|
|
1107
846
|
content: chunk,
|
|
1108
847
|
timestamp: Date.now(),
|
|
1109
848
|
});
|
|
@@ -1114,74 +853,55 @@ TOOL EFFICIENCY RULES:
|
|
|
1114
853
|
}
|
|
1115
854
|
await this.options.mqttClient.publishResult({
|
|
1116
855
|
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,
|
|
856
|
+
type: 'complete',
|
|
857
|
+
content: '',
|
|
1129
858
|
timestamp: Date.now(),
|
|
1130
859
|
});
|
|
860
|
+
console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
// Check token limit
|
|
864
|
+
if (totalTokensUsed >= this.maxTokensPerMessage) {
|
|
865
|
+
console.log(chalk_1.default.red(` 🛑 Token limit reached: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
|
|
866
|
+
messages.push({ role: 'user', content: 'Token limit reached. Please summarize what you\'ve done so far.' });
|
|
867
|
+
const summaryResponse = await llm.chat({
|
|
868
|
+
messages,
|
|
869
|
+
system: systemPrompt,
|
|
870
|
+
stream: true,
|
|
871
|
+
onChunk: async (chunk) => {
|
|
872
|
+
if (this.activeCommandId !== command.id)
|
|
873
|
+
return;
|
|
874
|
+
await this.options.mqttClient.publishResult({
|
|
875
|
+
commandId: command.id,
|
|
876
|
+
type: 'chunk',
|
|
877
|
+
content: chunk,
|
|
878
|
+
timestamp: Date.now(),
|
|
879
|
+
});
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
if (summaryResponse.usage) {
|
|
883
|
+
totalTokensUsed += summaryResponse.usage.inputTokens + summaryResponse.usage.outputTokens;
|
|
884
|
+
}
|
|
1131
885
|
await this.options.mqttClient.publishResult({
|
|
1132
886
|
commandId: command.id,
|
|
1133
887
|
type: 'complete',
|
|
1134
|
-
content:
|
|
888
|
+
content: '',
|
|
1135
889
|
timestamp: Date.now(),
|
|
1136
890
|
});
|
|
1137
891
|
console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
|
|
1138
892
|
break;
|
|
1139
893
|
}
|
|
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
894
|
// Continue the loop for the next LLM call
|
|
1144
895
|
continue;
|
|
1145
896
|
}
|
|
1146
897
|
// 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
|
-
});
|
|
898
|
+
// DEDUP FIX: Content was already streamed via onChunk callbacks above.
|
|
899
|
+
// The 'complete' message signals end-of-response; we send empty content
|
|
900
|
+
// to avoid the receiver concatenating streamed chunks + full content.
|
|
1181
901
|
await this.options.mqttClient.publishResult({
|
|
1182
902
|
commandId: command.id,
|
|
1183
903
|
type: 'complete',
|
|
1184
|
-
content:
|
|
904
|
+
content: '',
|
|
1185
905
|
timestamp: Date.now(),
|
|
1186
906
|
});
|
|
1187
907
|
// Local-first: save assistant response to SQLite + trigger funnel
|
|
@@ -1189,6 +909,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1189
909
|
try {
|
|
1190
910
|
const localData = await Promise.resolve().then(() => __importStar(require('./local-data')));
|
|
1191
911
|
localData.addMessage(localConvId, 'assistant', response.content || '', model);
|
|
912
|
+
// Trigger background memory extraction
|
|
1192
913
|
(0, local_funnel_1.scheduleFunnelProcessing)(localConvId);
|
|
1193
914
|
// Track for session idle auto-organize
|
|
1194
915
|
this.lastOrganizedConvId = localConvId;
|
|
@@ -1200,6 +921,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1200
921
|
}
|
|
1201
922
|
}
|
|
1202
923
|
console.log(chalk_1.default.white(` Response: ${response.content?.slice(0, 200)}${(response.content?.length || 0) > 200 ? '...' : ''}`));
|
|
924
|
+
const latencyMs = Date.now() - commandStartMs;
|
|
1203
925
|
console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs}`);
|
|
1204
926
|
console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
|
|
1205
927
|
break;
|
|
@@ -1209,22 +931,7 @@ TOOL EFFICIENCY RULES:
|
|
|
1209
931
|
}
|
|
1210
932
|
}
|
|
1211
933
|
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)));
|
|
934
|
+
console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
|
|
1228
935
|
await this.publishError(command.id, error.message);
|
|
1229
936
|
}
|
|
1230
937
|
finally {
|
|
@@ -1246,31 +953,10 @@ TOOL EFFICIENCY RULES:
|
|
|
1246
953
|
}
|
|
1247
954
|
/** Build system prompt manually (without clerk) — original logic */
|
|
1248
955
|
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.';
|
|
956
|
+
let systemPrompt = command.context?.soulMd
|
|
957
|
+
|| this.options.systemPrompt
|
|
958
|
+
|| 'You are a Funolio AI agent running locally on the user\'s machine. You have access to their project files and can execute code.';
|
|
959
|
+
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
960
|
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
961
|
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
962
|
systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
|
|
@@ -1293,7 +979,13 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1293
979
|
systemPrompt += '\n\n[Bot Memory]\n' + command.context.memoryMd;
|
|
1294
980
|
}
|
|
1295
981
|
const sc = command.context?.structuredContext;
|
|
1296
|
-
if (sc && (sc.codeRefs?.length || sc.conversations?.length)) {
|
|
982
|
+
if (sc && (sc.decisions?.length || sc.memoryFacts?.length || sc.codeRefs?.length || sc.conversations?.length)) {
|
|
983
|
+
if (sc.decisions?.length) {
|
|
984
|
+
systemPrompt += '\n\n[Key Decisions]\n' + sc.decisions.map(d => `- ${d}`).join('\n');
|
|
985
|
+
}
|
|
986
|
+
if (sc.memoryFacts?.length) {
|
|
987
|
+
systemPrompt += '\n\n[Memory Facts]\n' + sc.memoryFacts.map(f => `- ${f}`).join('\n');
|
|
988
|
+
}
|
|
1297
989
|
if (sc.conversations?.length) {
|
|
1298
990
|
systemPrompt += '\n\n[Relevant Conversations]\n' + sc.conversations.map(c => `- ${c}`).join('\n');
|
|
1299
991
|
}
|
|
@@ -1301,8 +993,11 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1301
993
|
systemPrompt += '\n\n[Code References]\n' + sc.codeRefs.map(r => `- ${r}`).join('\n');
|
|
1302
994
|
}
|
|
1303
995
|
}
|
|
996
|
+
else if (command.context?.facts?.length) {
|
|
997
|
+
systemPrompt += '\n\nRelevant memory/facts from the user\'s stored conversations:\n' + command.context.facts.map(f => `- ${f}`).join('\n');
|
|
998
|
+
}
|
|
1304
999
|
else {
|
|
1305
|
-
systemPrompt += '\n\nNo relevant
|
|
1000
|
+
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
1001
|
}
|
|
1307
1002
|
if (command.context?.files?.length) {
|
|
1308
1003
|
systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
|
|
@@ -1345,18 +1040,24 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1345
1040
|
timestamp: Date.now(),
|
|
1346
1041
|
});
|
|
1347
1042
|
}
|
|
1348
|
-
/** Update the
|
|
1043
|
+
/** Update the OAuth token used for requests (called by token refresh) */
|
|
1349
1044
|
updateToken(token) {
|
|
1350
|
-
this.options.
|
|
1045
|
+
this.options.oauthToken = token;
|
|
1046
|
+
// Recreate the default provider with the new token
|
|
1351
1047
|
const effectiveProvider = this.options.provider;
|
|
1352
|
-
const
|
|
1048
|
+
const resolved = this.options.resolvedAuth;
|
|
1049
|
+
const resolvedMatchesProvider = !!resolved && resolved.provider === effectiveProvider;
|
|
1353
1050
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
1354
1051
|
apiKey: token,
|
|
1355
1052
|
model: this.options.model,
|
|
1356
|
-
authMode,
|
|
1357
|
-
...(
|
|
1358
|
-
...(
|
|
1053
|
+
...(resolvedMatchesProvider && resolved?.authMode ? { authMode: resolved.authMode } : {}),
|
|
1054
|
+
...(resolvedMatchesProvider && resolved?.baseUrl ? { baseUrl: resolved.baseUrl } : {}),
|
|
1055
|
+
...(resolvedMatchesProvider && resolved?.apiStyle ? { apiStyle: resolved.apiStyle } : {}),
|
|
1056
|
+
...(!resolvedMatchesProvider && token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
|
|
1359
1057
|
});
|
|
1058
|
+
if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
|
|
1059
|
+
this.resolveAndUpgradeAuth(token, this.options.model);
|
|
1060
|
+
}
|
|
1360
1061
|
}
|
|
1361
1062
|
}
|
|
1362
1063
|
exports.MessageLoop = MessageLoop;
|