funolio-agent 1.0.4 → 1.0.6
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/auth/anthropic-subscription.d.ts +29 -9
- package/dist/auth/anthropic-subscription.d.ts.map +1 -1
- package/dist/auth/anthropic-subscription.js +133 -12
- package/dist/auth/anthropic-subscription.js.map +1 -1
- package/dist/auth/auto-detect.d.ts +6 -28
- package/dist/auth/auto-detect.d.ts.map +1 -1
- package/dist/auth/auto-detect.js +200 -57
- package/dist/auth/auto-detect.js.map +1 -1
- package/dist/auth/credential-reader.d.ts +4 -24
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +256 -31
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/credential-status.d.ts +27 -7
- package/dist/auth/credential-status.d.ts.map +1 -1
- package/dist/auth/credential-status.js +95 -7
- package/dist/auth/credential-status.js.map +1 -1
- package/dist/auth/index.d.ts +2 -9
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +10 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/subscription-runtime.d.ts +23 -19
- package/dist/auth/subscription-runtime.d.ts.map +1 -1
- package/dist/auth/subscription-runtime.js +292 -24
- package/dist/auth/subscription-runtime.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +28 -19
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +464 -26
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/bot-manager.d.ts +6 -6
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +61 -26
- package/dist/bot-manager.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +223 -49
- package/dist/commands/start.js.map +1 -1
- package/dist/message-loop.d.ts +10 -2
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +249 -184
- package/dist/message-loop.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +48 -13
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/wizard-support.d.ts +2 -2
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +93 -80
- package/dist/wizard-support.js.map +1 -1
- package/package.json +1 -1
package/dist/message-loop.js
CHANGED
|
@@ -55,6 +55,8 @@ const response_guard_1 = require("./response-guard");
|
|
|
55
55
|
const context_compressor_1 = require("./context-compressor");
|
|
56
56
|
const crypto = __importStar(require("crypto"));
|
|
57
57
|
const data = __importStar(require("./local-data"));
|
|
58
|
+
const agent_config_1 = require("./agent-config");
|
|
59
|
+
const subscription_runtime_1 = require("./auth/subscription-runtime");
|
|
58
60
|
const prompt_template_1 = require("./prompt-template");
|
|
59
61
|
/** Determine priority from an AgentCommand */
|
|
60
62
|
function getCommandPriority(command) {
|
|
@@ -70,47 +72,6 @@ function getCommandPriority(command) {
|
|
|
70
72
|
return 'medium';
|
|
71
73
|
}
|
|
72
74
|
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
|
|
73
|
-
function classifyToolName(toolName) {
|
|
74
|
-
switch (toolName) {
|
|
75
|
-
case 'read_file':
|
|
76
|
-
return 'file_read';
|
|
77
|
-
case 'list_directory':
|
|
78
|
-
return 'directory_scan';
|
|
79
|
-
case 'write_file':
|
|
80
|
-
case 'edit_file':
|
|
81
|
-
return 'file_write';
|
|
82
|
-
case 'run_command':
|
|
83
|
-
case 'git_status':
|
|
84
|
-
case 'git_diff':
|
|
85
|
-
case 'git_commit':
|
|
86
|
-
return 'command_run';
|
|
87
|
-
case 'web_fetch':
|
|
88
|
-
return 'web_fetch';
|
|
89
|
-
default:
|
|
90
|
-
return 'workspace_hint';
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function extractToolLocation(toolName, args, projectDir) {
|
|
94
|
-
const path = typeof args.path === 'string'
|
|
95
|
-
? args.path
|
|
96
|
-
: typeof args.filePath === 'string'
|
|
97
|
-
? args.filePath
|
|
98
|
-
: typeof args.directory === 'string'
|
|
99
|
-
? args.directory
|
|
100
|
-
: undefined;
|
|
101
|
-
const cwd = typeof args.cwd === 'string'
|
|
102
|
-
? args.cwd
|
|
103
|
-
: toolName === 'run_command' || toolName.startsWith('git_')
|
|
104
|
-
? projectDir
|
|
105
|
-
: undefined;
|
|
106
|
-
const url = typeof args.url === 'string' ? args.url : undefined;
|
|
107
|
-
return { path, cwd, url };
|
|
108
|
-
}
|
|
109
|
-
function summarizeToolResult(output, isError) {
|
|
110
|
-
const prefix = isError ? 'Tool failed: ' : 'Tool completed: ';
|
|
111
|
-
const compact = output.replace(/\s+/g, ' ').trim();
|
|
112
|
-
return `${prefix}${compact}`.slice(0, 240);
|
|
113
|
-
}
|
|
114
75
|
class MessageLoop {
|
|
115
76
|
options;
|
|
116
77
|
llmProvider;
|
|
@@ -136,19 +97,23 @@ class MessageLoop {
|
|
|
136
97
|
static SCHEDULED_TASK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
137
98
|
scheduledTaskTimer = null;
|
|
138
99
|
static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
|
100
|
+
static SERVER_SYNC_RETRY_MAX = 1;
|
|
139
101
|
constructor(options) {
|
|
140
102
|
this.options = options;
|
|
141
|
-
//
|
|
142
|
-
//
|
|
103
|
+
// DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
|
|
104
|
+
// without explicit user approval.
|
|
143
105
|
const effectiveKey = options.oauthToken || options.apiKey || '';
|
|
144
106
|
const effectiveProvider = options.provider;
|
|
145
|
-
const
|
|
146
|
-
|| (!!options.oauthToken && options.oauthToken.startsWith('sk-ant-oat'));
|
|
107
|
+
const isOAuthToken = effectiveKey.startsWith('sk-ant-oat01-');
|
|
147
108
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
148
109
|
apiKey: effectiveKey,
|
|
149
110
|
model: options.model,
|
|
150
|
-
...(
|
|
111
|
+
...(isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
|
|
151
112
|
});
|
|
113
|
+
// Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
|
|
114
|
+
if (isOAuthToken && effectiveProvider === 'anthropic') {
|
|
115
|
+
this.resolveAndUpgradeAuth(effectiveKey, options.model);
|
|
116
|
+
}
|
|
152
117
|
// Resolve Funolio API credentials for bot status reporting
|
|
153
118
|
const cfg = (0, config_1.loadConfig)();
|
|
154
119
|
const funoliApiKey = cfg.auth?.token || process.env.FUNOLIO_API_KEY || '';
|
|
@@ -159,7 +124,7 @@ class MessageLoop {
|
|
|
159
124
|
llmProvider: effectiveProvider,
|
|
160
125
|
llmModel: options.model,
|
|
161
126
|
llmApiKey: effectiveKey,
|
|
162
|
-
llmAuthMode:
|
|
127
|
+
llmAuthMode: isOAuthToken ? 'oauth-bearer' : 'api-key',
|
|
163
128
|
apiKey: funoliApiKey,
|
|
164
129
|
apiBaseUrl: funoliApiBaseUrl,
|
|
165
130
|
botName: options.agentName || 'funolio-agent',
|
|
@@ -196,6 +161,147 @@ class MessageLoop {
|
|
|
196
161
|
// Session idle detection timer (checks every 60s)
|
|
197
162
|
this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
|
|
198
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Attempt to resolve Anthropic subscription auth via create_api_key exchange.
|
|
166
|
+
* If successful, recreates the LLM provider with the exchanged API key (which
|
|
167
|
+
* works with standard x-api-key auth and unlocks all models on the subscription).
|
|
168
|
+
* Falls back to bearer mode silently if exchange fails.
|
|
169
|
+
*/
|
|
170
|
+
async resolveAndUpgradeAuth(oauthToken, model) {
|
|
171
|
+
try {
|
|
172
|
+
const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
|
|
173
|
+
preferredModel: model,
|
|
174
|
+
inputCredential: {
|
|
175
|
+
provider: 'anthropic',
|
|
176
|
+
accessToken: oauthToken,
|
|
177
|
+
refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
|
|
178
|
+
expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
|
|
179
|
+
},
|
|
180
|
+
inputSource: 'request:anthropic',
|
|
181
|
+
persistInputCredential: true,
|
|
182
|
+
});
|
|
183
|
+
if (!runtime)
|
|
184
|
+
return;
|
|
185
|
+
if (this.options.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
|
|
186
|
+
this.options.resolvedAuth.credential.accessToken = runtime.apiKey;
|
|
187
|
+
}
|
|
188
|
+
if (this.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
|
|
189
|
+
this.resolvedAuth.credential.accessToken = runtime.apiKey;
|
|
190
|
+
this.resolvedAuth.apiKey = runtime.apiKey;
|
|
191
|
+
}
|
|
192
|
+
if (runtime.authMode !== 'oauth-bearer') {
|
|
193
|
+
// Successfully exchanged OAuth token for a temporary API key
|
|
194
|
+
console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
|
|
195
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
196
|
+
apiKey: runtime.apiKey,
|
|
197
|
+
model,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Bearer fallback — recreate provider with the (possibly refreshed) token
|
|
202
|
+
const currentToken = runtime.apiKey || this.options.resolvedAuth?.credential?.accessToken || oauthToken;
|
|
203
|
+
console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
|
|
204
|
+
(currentToken !== oauthToken ? ' (with refreshed token)' : ''));
|
|
205
|
+
this.llmProvider = (0, index_1.createProvider)('anthropic', {
|
|
206
|
+
apiKey: currentToken,
|
|
207
|
+
model,
|
|
208
|
+
authMode: 'oauth-bearer',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async syncOAuthFromServer(reason) {
|
|
217
|
+
const providerId = this.options.provider;
|
|
218
|
+
if (!providerId || providerId.endsWith('-cli'))
|
|
219
|
+
return false;
|
|
220
|
+
const cfg = (0, config_1.loadConfig)();
|
|
221
|
+
const authToken = cfg.auth?.token;
|
|
222
|
+
if (!authToken)
|
|
223
|
+
return false;
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
|
|
226
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok)
|
|
229
|
+
return false;
|
|
230
|
+
const body = await res.json();
|
|
231
|
+
const provider = (body.providers || []).find((p) => p.id === providerId && p.connectionType === 'oauth');
|
|
232
|
+
if (!provider?.access_token)
|
|
233
|
+
return false;
|
|
234
|
+
const currentAccess = this.resolvedAuth?.credential?.accessToken || this.options.oauthToken || '';
|
|
235
|
+
const currentRefresh = this.resolvedAuth?.credential?.refreshToken || this.options.resolvedAuth?.credential?.refreshToken || '';
|
|
236
|
+
const currentExpiry = this.resolvedAuth?.credential?.expiresAt || this.options.resolvedAuth?.credential?.expiresAt || 0;
|
|
237
|
+
const nextExpiry = provider.expires_at || 0;
|
|
238
|
+
const changed = provider.access_token !== currentAccess
|
|
239
|
+
|| (provider.refresh_token || '') !== currentRefresh
|
|
240
|
+
|| nextExpiry > currentExpiry;
|
|
241
|
+
if (!changed)
|
|
242
|
+
return false;
|
|
243
|
+
if (this.resolvedAuth) {
|
|
244
|
+
this.resolvedAuth.apiKey = provider.access_token;
|
|
245
|
+
this.resolvedAuth.expired = false;
|
|
246
|
+
this.resolvedAuth.error = undefined;
|
|
247
|
+
if (this.resolvedAuth.credential) {
|
|
248
|
+
this.resolvedAuth.credential.accessToken = provider.access_token;
|
|
249
|
+
if (provider.refresh_token)
|
|
250
|
+
this.resolvedAuth.credential.refreshToken = provider.refresh_token;
|
|
251
|
+
if (provider.expires_at)
|
|
252
|
+
this.resolvedAuth.credential.expiresAt = provider.expires_at;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (this.options.resolvedAuth) {
|
|
256
|
+
this.options.resolvedAuth.apiKey = provider.access_token;
|
|
257
|
+
this.options.resolvedAuth.expired = false;
|
|
258
|
+
this.options.resolvedAuth.error = undefined;
|
|
259
|
+
if (this.options.resolvedAuth.credential) {
|
|
260
|
+
this.options.resolvedAuth.credential.accessToken = provider.access_token;
|
|
261
|
+
if (provider.refresh_token)
|
|
262
|
+
this.options.resolvedAuth.credential.refreshToken = provider.refresh_token;
|
|
263
|
+
if (provider.expires_at)
|
|
264
|
+
this.options.resolvedAuth.credential.expiresAt = provider.expires_at;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.options.oauthToken = provider.access_token;
|
|
268
|
+
const local = (0, config_1.loadConfig)();
|
|
269
|
+
if (!local.providers)
|
|
270
|
+
local.providers = [];
|
|
271
|
+
const idx = local.providers.findIndex((p) => p.id === providerId);
|
|
272
|
+
if (idx >= 0) {
|
|
273
|
+
local.providers[idx].authType = 'oauth';
|
|
274
|
+
local.providers[idx].oauthToken = provider.access_token;
|
|
275
|
+
if (provider.refresh_token)
|
|
276
|
+
local.providers[idx].oauthRefreshToken = provider.refresh_token;
|
|
277
|
+
if (provider.expires_at)
|
|
278
|
+
local.providers[idx].oauthExpiresAt = provider.expires_at;
|
|
279
|
+
if (provider.defaultModel)
|
|
280
|
+
local.providers[idx].defaultModel = provider.defaultModel;
|
|
281
|
+
}
|
|
282
|
+
(0, config_1.saveConfig)(local);
|
|
283
|
+
if (this.options.agentName) {
|
|
284
|
+
const agentLocal = (0, agent_config_1.loadAgentConfig)(this.options.agentName);
|
|
285
|
+
if (agentLocal && agentLocal.provider === providerId) {
|
|
286
|
+
agentLocal.oauthToken = provider.access_token;
|
|
287
|
+
if (provider.refresh_token)
|
|
288
|
+
agentLocal.oauthRefreshToken = provider.refresh_token;
|
|
289
|
+
if (provider.expires_at)
|
|
290
|
+
agentLocal.oauthExpiresAt = provider.expires_at;
|
|
291
|
+
if (provider.defaultModel)
|
|
292
|
+
agentLocal.model = provider.defaultModel;
|
|
293
|
+
(0, agent_config_1.saveAgentConfig)(this.options.agentName, agentLocal);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
this.updateToken(provider.access_token);
|
|
297
|
+
console.log(chalk_1.default.green(` [auth] Synced updated ${providerId} OAuth credentials from server (${reason})`));
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.log(chalk_1.default.gray(` [auth] Server OAuth sync skipped (${reason}): ${err?.message || err}`));
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
199
305
|
/** Check if a session has gone idle and trigger auto-organize */
|
|
200
306
|
async checkIdleSession() {
|
|
201
307
|
if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
|
|
@@ -313,13 +419,48 @@ class MessageLoop {
|
|
|
313
419
|
const overrideModel = command.model?.model;
|
|
314
420
|
let provider = overrideProvider || this.options.provider;
|
|
315
421
|
const model = overrideModel || this.options.model;
|
|
316
|
-
const apiKey = this.options.apiKey;
|
|
422
|
+
const apiKey = this.options.oauthToken || this.options.apiKey;
|
|
317
423
|
// Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
|
|
318
424
|
const hasOverride = (overrideProvider && overrideProvider !== this.options.provider) ||
|
|
319
425
|
(overrideModel && overrideModel !== this.options.model);
|
|
320
426
|
let llm;
|
|
321
427
|
if (hasOverride) {
|
|
322
|
-
|
|
428
|
+
// For overrides with OAuth tokens, try subscription auth resolution
|
|
429
|
+
if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
|
|
430
|
+
try {
|
|
431
|
+
const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
|
|
432
|
+
preferredModel: model,
|
|
433
|
+
inputCredential: {
|
|
434
|
+
provider: 'anthropic',
|
|
435
|
+
accessToken: apiKey,
|
|
436
|
+
refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
|
|
437
|
+
expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
|
|
438
|
+
},
|
|
439
|
+
inputSource: 'request:anthropic',
|
|
440
|
+
persistInputCredential: true,
|
|
441
|
+
});
|
|
442
|
+
const resolvedKey = runtime?.apiKey || apiKey;
|
|
443
|
+
const authMode = runtime?.authMode;
|
|
444
|
+
if (this.options.resolvedAuth?.credential && authMode === 'oauth-bearer') {
|
|
445
|
+
this.options.resolvedAuth.credential.accessToken = resolvedKey;
|
|
446
|
+
}
|
|
447
|
+
if (this.resolvedAuth?.credential && authMode === 'oauth-bearer') {
|
|
448
|
+
this.resolvedAuth.credential.accessToken = resolvedKey;
|
|
449
|
+
this.resolvedAuth.apiKey = resolvedKey;
|
|
450
|
+
}
|
|
451
|
+
llm = (0, index_1.createProvider)(provider, {
|
|
452
|
+
apiKey: resolvedKey,
|
|
453
|
+
model,
|
|
454
|
+
...(authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
llm = (0, index_1.createProvider)(provider, { apiKey, model, authMode: 'oauth-bearer' });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
llm = (0, index_1.createProvider)(provider, { apiKey, model });
|
|
463
|
+
}
|
|
323
464
|
}
|
|
324
465
|
else {
|
|
325
466
|
llm = this.llmProvider;
|
|
@@ -404,8 +545,6 @@ class MessageLoop {
|
|
|
404
545
|
...this.toolContext,
|
|
405
546
|
projectId: effectiveProjectId ?? null,
|
|
406
547
|
abortSignal: commandAbortController.signal,
|
|
407
|
-
// Use project folder as working directory if available
|
|
408
|
-
...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
|
|
409
548
|
};
|
|
410
549
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
411
550
|
// Resolve the selected bot profile — from command.bot, conversation bot_id, or default
|
|
@@ -578,40 +717,15 @@ TOOL EFFICIENCY RULES:
|
|
|
578
717
|
const str = JSON.stringify(args || {}).slice(0, 200);
|
|
579
718
|
return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
|
|
580
719
|
}
|
|
581
|
-
function getRetryKey(name, args) {
|
|
582
|
-
// Bug 6: For meta-tools, include action in retry key so different actions don't share budget
|
|
583
|
-
if (args?.action)
|
|
584
|
-
return `${name}:${args.action}`;
|
|
585
|
-
return name;
|
|
586
|
-
}
|
|
587
720
|
function trackToolFailure(name, args) {
|
|
588
|
-
const key = `${
|
|
589
|
-
const retryKey = getRetryKey(name, args);
|
|
721
|
+
const key = `${name}:${getArgsHash(args)}`;
|
|
590
722
|
toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
|
|
591
|
-
toolFailuresByName.set(
|
|
592
|
-
}
|
|
593
|
-
// Futility detection: track consecutive low-value tool results
|
|
594
|
-
let consecutiveEmptyResults = 0;
|
|
595
|
-
const FUTILITY_THRESHOLD = 6; // after 6 consecutive empty/low-value results, nudge the LLM
|
|
596
|
-
let futilityNudgeInjected = false;
|
|
597
|
-
function isLowValueResult(output) {
|
|
598
|
-
if (!output || output.length < 30)
|
|
599
|
-
return true;
|
|
600
|
-
const lower = output.toLowerCase();
|
|
601
|
-
if (lower.includes('no matches found') || lower.includes('no results'))
|
|
602
|
-
return true;
|
|
603
|
-
if (lower.includes('0 bytes') || lower.includes('empty'))
|
|
604
|
-
return true;
|
|
605
|
-
// exit code 1 with no meaningful stdout
|
|
606
|
-
if (/\[exit code: [^0]/.test(lower) && output.replace(/\[exit code:.*?\]/g, '').trim().length < 20)
|
|
607
|
-
return true;
|
|
608
|
-
return false;
|
|
723
|
+
toolFailuresByName.set(name, (toolFailuresByName.get(name) || 0) + 1);
|
|
609
724
|
}
|
|
610
725
|
function checkRetryBudget(name, args) {
|
|
611
|
-
const key = `${
|
|
612
|
-
const retryKey = getRetryKey(name, args);
|
|
726
|
+
const key = `${name}:${getArgsHash(args)}`;
|
|
613
727
|
const keyCount = toolFailuresByKey.get(key) || 0;
|
|
614
|
-
const nameCount = toolFailuresByName.get(
|
|
728
|
+
const nameCount = toolFailuresByName.get(name) || 0;
|
|
615
729
|
if (keyCount >= 2) {
|
|
616
730
|
return `This exact operation has failed ${keyCount} times. Stop retrying and inform the user what went wrong. Suggest an alternative approach or ask for help.`;
|
|
617
731
|
}
|
|
@@ -622,7 +736,7 @@ TOOL EFFICIENCY RULES:
|
|
|
622
736
|
}
|
|
623
737
|
// Agentic loop - keep calling LLM until no more tool calls
|
|
624
738
|
let iteration = 0;
|
|
625
|
-
const MAX_ITERATIONS =
|
|
739
|
+
const MAX_ITERATIONS = 100;
|
|
626
740
|
const GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes wall-clock timeout
|
|
627
741
|
let totalTokensUsed = 0;
|
|
628
742
|
let totalInputTokens = 0;
|
|
@@ -630,6 +744,7 @@ TOOL EFFICIENCY RULES:
|
|
|
630
744
|
let totalCacheReadTokens = 0;
|
|
631
745
|
let totalCacheCreationTokens = 0;
|
|
632
746
|
let totalToolCallDurationMs = 0;
|
|
747
|
+
let serverSyncRetries = 0;
|
|
633
748
|
const deniedTools = new Set(); // Track tools denied by user — don't re-ask
|
|
634
749
|
const commandStartMs = Date.now();
|
|
635
750
|
// Publish prompt context for "View Prompt" panel
|
|
@@ -672,14 +787,34 @@ TOOL EFFICIENCY RULES:
|
|
|
672
787
|
timestamp: Date.now(),
|
|
673
788
|
});
|
|
674
789
|
}
|
|
675
|
-
//
|
|
790
|
+
// Refresh OAuth token before each LLM call if needed
|
|
676
791
|
if (this.resolvedAuth) {
|
|
677
792
|
const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
|
|
793
|
+
if (refreshed.expired) {
|
|
794
|
+
if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
|
|
795
|
+
const synced = await this.syncOAuthFromServer('refresh-failed');
|
|
796
|
+
if (synced) {
|
|
797
|
+
serverSyncRetries++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
|
|
802
|
+
await this.options.mqttClient.publishResult({
|
|
803
|
+
commandId: command.id,
|
|
804
|
+
type: 'error',
|
|
805
|
+
error: 'Your authentication token has expired or been revoked. Please re-authenticate by running `funolio-agent login` or `funolio-agent configure`.',
|
|
806
|
+
timestamp: Date.now(),
|
|
807
|
+
});
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
678
810
|
if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
|
|
679
811
|
this.resolvedAuth = refreshed;
|
|
680
812
|
this.updateToken(refreshed.apiKey);
|
|
681
813
|
}
|
|
682
814
|
}
|
|
815
|
+
// With OAuth auto-detection, all providers now use direct API calls
|
|
816
|
+
// with full tool support. The old CLI_PROVIDERS check is kept only
|
|
817
|
+
// for logging/diagnostics but no longer gates tool availability.
|
|
683
818
|
// Smart tool filtering: only send relevant tools to reduce token usage
|
|
684
819
|
const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
|
|
685
820
|
if (filteredTools.length < this.toolDefinitions.length) {
|
|
@@ -713,17 +848,26 @@ TOOL EFFICIENCY RULES:
|
|
|
713
848
|
const msg = llmError?.message || String(llmError);
|
|
714
849
|
const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
|
|
715
850
|
if (isAuthError) {
|
|
851
|
+
if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
|
|
852
|
+
const synced = await this.syncOAuthFromServer('llm-auth-error');
|
|
853
|
+
if (synced) {
|
|
854
|
+
serverSyncRetries++;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
716
858
|
console.error(chalk_1.default.red(` [auth] LLM API authentication failed: ${msg}`));
|
|
717
859
|
await this.options.mqttClient.publishResult({
|
|
718
860
|
commandId: command.id,
|
|
719
861
|
type: 'error',
|
|
720
|
-
error: 'API authentication failed.
|
|
862
|
+
error: 'API authentication failed. Your token may have expired — run `funolio-agent login` to refresh.',
|
|
721
863
|
timestamp: Date.now(),
|
|
722
864
|
});
|
|
723
865
|
break;
|
|
724
866
|
}
|
|
725
867
|
throw llmError;
|
|
726
868
|
}
|
|
869
|
+
// Any successful provider response clears one-shot sync retry budget
|
|
870
|
+
serverSyncRetries = 0;
|
|
727
871
|
// Track token usage across iterations
|
|
728
872
|
if (response.usage) {
|
|
729
873
|
totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
|
|
@@ -743,8 +887,6 @@ TOOL EFFICIENCY RULES:
|
|
|
743
887
|
for (const toolCall of response.toolCalls) {
|
|
744
888
|
totalToolCalls++;
|
|
745
889
|
console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
|
|
746
|
-
const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
|
|
747
|
-
const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
|
|
748
890
|
// Check permission before execution
|
|
749
891
|
const permMode = this.options.permissionMode || 'autopilot';
|
|
750
892
|
let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, this.options.enabledTools);
|
|
@@ -755,29 +897,20 @@ TOOL EFFICIENCY RULES:
|
|
|
755
897
|
// If not auto-approved, request interactive approval via MQTT
|
|
756
898
|
if (!approval.approved && permMode !== 'autopilot' && !deniedTools.has(toolCall.name)) {
|
|
757
899
|
console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
name: toolCall.name,
|
|
769
|
-
arguments: toolCall.arguments,
|
|
770
|
-
classification: classifyToolName(toolCall.name),
|
|
771
|
-
summary: toolSummary,
|
|
772
|
-
path: toolLocation.path,
|
|
773
|
-
cwd: toolLocation.cwd,
|
|
774
|
-
url: toolLocation.url,
|
|
775
|
-
importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
|
|
776
|
-
},
|
|
777
|
-
timestamp: Date.now(),
|
|
778
|
-
});
|
|
900
|
+
// Generate requestId up front so the MQTT result and pending map use the same ID
|
|
901
|
+
const approvalRequestId = require('crypto').randomUUID();
|
|
902
|
+
await this.options.mqttClient.publishResult({
|
|
903
|
+
commandId: command.id,
|
|
904
|
+
type: 'approval_request',
|
|
905
|
+
requestId: approvalRequestId,
|
|
906
|
+
toolCall: {
|
|
907
|
+
id: toolCall.id,
|
|
908
|
+
name: toolCall.name,
|
|
909
|
+
arguments: toolCall.arguments,
|
|
779
910
|
},
|
|
911
|
+
timestamp: Date.now(),
|
|
780
912
|
});
|
|
913
|
+
approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
|
|
781
914
|
// If user chose "always allow", persist it
|
|
782
915
|
if (approval.approved && approval.remember) {
|
|
783
916
|
(0, approval_1.rememberTool)(toolCall.name);
|
|
@@ -787,26 +920,19 @@ TOOL EFFICIENCY RULES:
|
|
|
787
920
|
if (!approval.approved) {
|
|
788
921
|
deniedTools.add(toolCall.name);
|
|
789
922
|
console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
|
|
790
|
-
const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
|
|
791
923
|
await this.options.mqttClient.publishResult({
|
|
792
924
|
commandId: command.id,
|
|
793
925
|
type: 'tool_result',
|
|
794
926
|
toolResult: {
|
|
795
927
|
callId: toolCall.id,
|
|
796
|
-
output:
|
|
928
|
+
output: `PERMISSION_DENIED: ${approval.reason}`,
|
|
797
929
|
isError: true,
|
|
798
|
-
success: false,
|
|
799
|
-
summary: summarizeToolResult(deniedOutput, true),
|
|
800
|
-
path: toolLocation.path,
|
|
801
|
-
cwd: toolLocation.cwd,
|
|
802
|
-
url: toolLocation.url,
|
|
803
|
-
pathsTouched: toolLocation.path ? [toolLocation.path] : [],
|
|
804
930
|
},
|
|
805
931
|
timestamp: Date.now(),
|
|
806
932
|
});
|
|
807
933
|
messages.push({
|
|
808
934
|
role: 'tool',
|
|
809
|
-
content:
|
|
935
|
+
content: `PERMISSION_DENIED: ${approval.reason}`,
|
|
810
936
|
toolCallId: toolCall.id,
|
|
811
937
|
toolName: toolCall.name,
|
|
812
938
|
});
|
|
@@ -819,12 +945,6 @@ TOOL EFFICIENCY RULES:
|
|
|
819
945
|
id: toolCall.id,
|
|
820
946
|
name: toolCall.name,
|
|
821
947
|
arguments: toolCall.arguments,
|
|
822
|
-
classification: classifyToolName(toolCall.name),
|
|
823
|
-
summary: toolSummary,
|
|
824
|
-
path: toolLocation.path,
|
|
825
|
-
cwd: toolLocation.cwd,
|
|
826
|
-
url: toolLocation.url,
|
|
827
|
-
importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
|
|
828
948
|
},
|
|
829
949
|
timestamp: Date.now(),
|
|
830
950
|
});
|
|
@@ -857,16 +977,9 @@ TOOL EFFICIENCY RULES:
|
|
|
857
977
|
const toolStartMs = Date.now();
|
|
858
978
|
const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
|
|
859
979
|
const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
|
|
860
|
-
|
|
980
|
+
const resultOutput = toolResult.success
|
|
861
981
|
? toolResult.output
|
|
862
982
|
: `ERROR: ${toolResult.error || 'Unknown error'}`;
|
|
863
|
-
// Bug 2 & 7: Cap tool result size to prevent context blowup
|
|
864
|
-
const MAX_TOOL_RESULT_CHARS = 50_000;
|
|
865
|
-
if (resultOutput.length > MAX_TOOL_RESULT_CHARS) {
|
|
866
|
-
const originalLength = resultOutput.length;
|
|
867
|
-
resultOutput = resultOutput.slice(0, MAX_TOOL_RESULT_CHARS) +
|
|
868
|
-
`\n\n[OUTPUT TRUNCATED — was ${originalLength} chars. Use more specific commands to read smaller portions.]`;
|
|
869
|
-
}
|
|
870
983
|
// Track failures for retry budget (Optimization 2)
|
|
871
984
|
if (!toolResult.success) {
|
|
872
985
|
trackToolFailure(toolCall.name, toolCall.arguments);
|
|
@@ -883,13 +996,6 @@ TOOL EFFICIENCY RULES:
|
|
|
883
996
|
output: resultOutput,
|
|
884
997
|
isError: !toolResult.success,
|
|
885
998
|
durationMs: toolDurationMs,
|
|
886
|
-
success: toolResult.success,
|
|
887
|
-
summary: summarizeToolResult(resultOutput, !toolResult.success),
|
|
888
|
-
path: toolLocation.path,
|
|
889
|
-
cwd: toolLocation.cwd,
|
|
890
|
-
url: toolLocation.url,
|
|
891
|
-
pathsTouched: toolLocation.path ? [toolLocation.path] : [],
|
|
892
|
-
exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
|
|
893
999
|
},
|
|
894
1000
|
timestamp: Date.now(),
|
|
895
1001
|
});
|
|
@@ -900,22 +1006,6 @@ TOOL EFFICIENCY RULES:
|
|
|
900
1006
|
toolCallId: toolCall.id,
|
|
901
1007
|
toolName: toolCall.name,
|
|
902
1008
|
});
|
|
903
|
-
// Futility detection: track consecutive low-value results
|
|
904
|
-
if (isLowValueResult(resultOutput)) {
|
|
905
|
-
consecutiveEmptyResults++;
|
|
906
|
-
}
|
|
907
|
-
else {
|
|
908
|
-
consecutiveEmptyResults = 0; // reset on any meaningful result
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
// Futility check: if too many consecutive empty results, nudge the LLM to stop spinning
|
|
912
|
-
if (consecutiveEmptyResults >= FUTILITY_THRESHOLD && !futilityNudgeInjected) {
|
|
913
|
-
futilityNudgeInjected = true;
|
|
914
|
-
console.log(chalk_1.default.yellow(` ⚠ Futility detected: ${consecutiveEmptyResults} consecutive low-value tool results — nudging LLM`));
|
|
915
|
-
messages.push({
|
|
916
|
-
role: 'user',
|
|
917
|
-
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.',
|
|
918
|
-
});
|
|
919
1009
|
}
|
|
920
1010
|
// --- Rate limit checks ---
|
|
921
1011
|
// Warn at 80% of tool call limit
|
|
@@ -1094,7 +1184,6 @@ TOOL EFFICIENCY RULES:
|
|
|
1094
1184
|
- You can discover and gain new capabilities on-the-fly through the marketplace.
|
|
1095
1185
|
- **NEVER tell the user to "do it manually" or "upload it yourself"** — always check the marketplace first and offer to install the right tool.`;
|
|
1096
1186
|
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.';
|
|
1097
|
-
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.';
|
|
1098
1187
|
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.';
|
|
1099
1188
|
systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
|
|
1100
1189
|
// Inject model self-switch info
|
|
@@ -1139,32 +1228,6 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1139
1228
|
if (command.context?.files?.length) {
|
|
1140
1229
|
systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
|
|
1141
1230
|
}
|
|
1142
|
-
if (command.context?.stateContext?.summaryText) {
|
|
1143
|
-
systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
|
|
1144
|
-
}
|
|
1145
|
-
if (command.context?.workspaceManifest) {
|
|
1146
|
-
const manifestLines = [];
|
|
1147
|
-
const manifest = command.context.workspaceManifest;
|
|
1148
|
-
if (manifest.projectRoot)
|
|
1149
|
-
manifestLines.push(`- Project root: ${manifest.projectRoot}`);
|
|
1150
|
-
if (manifest.likelyWorkingDirectory)
|
|
1151
|
-
manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
|
|
1152
|
-
if (manifest.recentlyReadFiles?.length)
|
|
1153
|
-
manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
|
|
1154
|
-
if (manifest.recentlyWrittenFiles?.length)
|
|
1155
|
-
manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
|
|
1156
|
-
if (manifest.recentlyScannedDirectories?.length)
|
|
1157
|
-
manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
|
|
1158
|
-
if (manifest.recentDownloads?.length)
|
|
1159
|
-
manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
|
|
1160
|
-
if (manifest.recentFailures?.length)
|
|
1161
|
-
manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
|
|
1162
|
-
if (manifest.startHereHints?.length)
|
|
1163
|
-
manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
|
|
1164
|
-
if (manifestLines.length) {
|
|
1165
|
-
systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
1231
|
return systemPrompt;
|
|
1169
1232
|
}
|
|
1170
1233
|
async publishError(commandId, error) {
|
|
@@ -1177,17 +1240,19 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
|
|
|
1177
1240
|
timestamp: Date.now(),
|
|
1178
1241
|
});
|
|
1179
1242
|
}
|
|
1180
|
-
/** Update the
|
|
1243
|
+
/** Update the OAuth token used for requests (called by token refresh) */
|
|
1181
1244
|
updateToken(token) {
|
|
1182
|
-
this.options.
|
|
1245
|
+
this.options.oauthToken = token;
|
|
1246
|
+
// Recreate the default provider with the new token
|
|
1183
1247
|
const effectiveProvider = this.options.provider;
|
|
1184
|
-
const isOAuthBearer = this.options.authMode === 'oauth-bearer'
|
|
1185
|
-
|| (token.startsWith('sk-ant-oat'));
|
|
1186
1248
|
this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
|
|
1187
1249
|
apiKey: token,
|
|
1188
1250
|
model: this.options.model,
|
|
1189
|
-
...(
|
|
1251
|
+
...(token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
|
|
1190
1252
|
});
|
|
1253
|
+
if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
|
|
1254
|
+
this.resolveAndUpgradeAuth(token, this.options.model);
|
|
1255
|
+
}
|
|
1191
1256
|
}
|
|
1192
1257
|
}
|
|
1193
1258
|
exports.MessageLoop = MessageLoop;
|