diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-25 06:00:45 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-25 06:00:45 +0200 |
| commit | 8b3e3e4527772bf060984f98777a2cff8e88aa09 (patch) | |
| tree | 0ff8cf6db6563be2cf53062f73aa7e06fea76ce4 /modules/core/module-aiassist.js | |
| parent | b08aa5b479395441462effed59ff1ee02d9d8c7f (diff) | |
auto: 2026-06-25_06:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 524 |
1 files changed, 502 insertions, 22 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 31f8665..6359f17 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -30,6 +30,10 @@ this._skillDebounceMs = 1800000; this._chatLockTimeoutMs = 10000; this._loadGuardrailPolicy(); + // ── Agent Loop Profile System ── + this._activeLoopProfile = null; + this._loopProfiles = {}; + this._initLoopProfiles(); // Restore AI config from localStorage (maxContext, MAX_TOOL_TURNS, etc.) try { const cfg = localStorage.getItem('webx11_ai_config'); if (cfg) { const p = JSON.parse(cfg); if (p.maxTurns) this.MAX_TOOL_TURNS = p.maxTurns; if (p.maxContext) this.maxContext = p.maxContext; if (p.maxContextTokens) this.maxContextTokens = p.maxContextTokens; if (p.confirmTimeoutMs) this._confirmTimeoutMs = p.confirmTimeoutMs; if (p.skillDebounceMs) this._skillDebounceMs = p.skillDebounceMs; if (p.chatLockTimeoutMs) this._chatLockTimeoutMs = p.chatLockTimeoutMs; } } catch (e) {} try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {} @@ -87,11 +91,31 @@ buildSystemPrompt() { if (profilePrompt) { base = profilePrompt + '\n\n' + base; } + // Apply agent loop profile prepend/append directives + var lp = this._activeLoopProfile; + if (lp && lp.systemPrompt) { + if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base; + if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append; + } return base; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); - return this._toolDefs; + return this._filterToolsByProfile(this._toolDefs); + } + _filterToolsByProfile(allTools) { + var p = this._activeLoopProfile; + if (!p || !p.tools) return allTools; + if (p.tools.mode === 'all') { + if (p.tools.disabled && p.tools.disabled.length > 0) { + return allTools.filter(function(t) { return p.tools.disabled.indexOf(t.function.name) === -1; }); + } + return allTools; + } + if (p.tools.mode === 'custom' && p.tools.enabled && p.tools.enabled.length > 0) { + return allTools.filter(function(t) { return p.tools.enabled.indexOf(t.function.name) !== -1; }); + } + return allTools; } _buildToolDefs() { return [{ @@ -507,8 +531,64 @@ buildSystemPrompt() { }, required: [] } + }, + { + type: 'function', + function: { + name: 'agent_loop_list', + description: 'List all available agent loop profiles with their settings and benchmark results.', + parameters: { type: 'object', properties: {}, required: [] } + } + }, + { + type: 'function', + function: { + name: 'agent_loop_switch', + description: 'Switch to a named agent loop profile. Changes tools, max turns, system prompt directives, and loop behavior.', + parameters: { + type: 'object', + properties: { name: { type: 'string', description: 'Profile name to switch to' } }, + required: ['name'] + } + } + }, + { + type: 'function', + function: { + name: 'agent_loop_create', + description: 'Create a new agent loop profile with custom settings (tools, max turns, temperature, system prompt directives).', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Profile name (lowercase, hyphens)' }, + description: { type: 'string', description: 'Profile description' }, + maxTurns: { type: 'number', description: 'Max tool turns (default: 8)' }, + toolsMode: { type: 'string', description: 'all or custom', enum: ['all', 'custom'] }, + toolsEnabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to enable (if mode=custom)' }, + toolsDisabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to disable (if mode=all)' }, + prependPrompt: { type: 'string', description: 'Directive to prepend to system prompt' }, + appendPrompt: { type: 'string', description: 'Directive to append to system prompt' } + }, + required: ['name'] + } + } + }, + { + type: 'function', + function: { + name: 'agent_loop_benchmark', + description: 'Run the benchmark suite on a specified agent loop profile. Returns task results with latency, tokens, and pass/fail.', + parameters: { + type: 'object', + properties: { + profile: { type: 'string', description: 'Profile name to benchmark (default: active)' }, + tasks: { type: 'array', items: { type: 'string' }, description: 'Specific task IDs to run (default: all)' } + }, + required: [] + } + } } - }]; + ]; } getHelpInfo() { return { @@ -523,6 +603,9 @@ buildSystemPrompt() { 'ai-stop': 'ai-stop — Cancel current AI operation', 'ai-model': 'ai-model [model] — Show or set AI model', 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))', + 'agent-loops': 'agent-loops [name|new|edit|delete|clone|export|reset] — Manage agent loop profiles', + 'bench': 'bench [profile] [--tasks=ids] [--repeat=N] — Run benchmark suite on a profile', + 'bench-compare': 'bench-compare <p1> <p2> — Compare benchmark results of two profiles', 'debug': 'debug — Toggle system debug console (logs, fetch, errors)', 'debug-ai': 'debug-ai — Toggle AI tool-call debug panel', 'mem-debug': 'mem-debug — Show memory breakdown by component', @@ -544,6 +627,9 @@ buildSystemPrompt() { this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); this.commands['ai-config'] = (args) => this.aiConfig(args); + this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args); + this.commands['bench'] = (args) => this.benchCmd(args); + this.commands['bench-compare'] = (args) => this.benchCompareCmd(args); this.commands['debug'] = () => this.toggleDebugPanel(); this.commands['debug-ai'] = () => this.toggleDebug(); this.commands['guardrail'] = (args) => this.guardrailCmd(args); @@ -658,6 +744,14 @@ buildSystemPrompt() { if (loopResult.toolTurns >= 2 && !this._cancelled) { await this.afterActionReflection(userMessage, loopResult.toolTurns); } + // Self-improvement: post-run anomaly detection + if (loopResult && !this._cancelled) { + try { + var om = this.terminal ? this.terminal.ollamaManager : null; + var usageData = om && om._usageLog && om._usageLog.length > 0 ? om._usageLog[om._usageLog.length - 1] : null; + this._postRunReview(loopResult, this._toolCallLog.slice(-(loopResult.toolTurns || 0)), usageData); + } catch (e) { /* self-improvement is best-effort */ } + } // Auto-create skills from repeated errors if (this._errorLog.length >= 3) { await this._autoCreateErrorSkill(); @@ -778,16 +872,22 @@ buildSystemPrompt() { } catch (e) { /* ignore */ } } async agenticLoop(messages) { + var profile = this._activeLoopProfile || {}; + var loopCfg = profile.loop || {}; + var maxTurns = loopCfg.maxTurns || this.MAX_TOOL_TURNS; + var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 }; + var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 }; + var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.'; let toolTurns = 0; - const _seenCalls = new Map(); // v4.5.66: loop detection — track repeated tool calls - const _loopWindow = 30000; // 30s window for loop detection - while (toolTurns < this.MAX_TOOL_TURNS) { + const _seenCalls = new Map(); + const _loopWindow = loopDetect.windowMs || 30000; + while (toolTurns < maxTurns) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; // On last turn, inject wrap-up prompt - if (toolTurns === this.MAX_TOOL_TURNS) { - this.terminal.print(`⏱ Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) — AI must provide final answer`, 'warning'); - messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' }); + if (toolTurns === maxTurns) { + this.terminal.print(`⏱ Final turn (${toolTurns}/${maxTurns}) — AI must provide final answer`, 'warning'); + messages.push({ role: 'system', content: finalPrompt }); } const thinkStart = Date.now(); const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions()); @@ -803,7 +903,7 @@ buildSystemPrompt() { const vm = this.terminal.moduleManager && this.terminal.moduleManager.modules['VoiceModule']; if (vm && vm.speakText) vm.speakText(text); } - this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info'); + this.terminal.print(`💬 AI answered (turn ${toolTurns}/${maxTurns}, ${thinkTime}s)`, 'info'); return { finalAnswer: text, toolTurns }; } if (res.tool_calls && res.tool_calls.length > 0) { @@ -821,7 +921,7 @@ buildSystemPrompt() { entry.count++; entry.ts = now; _seenCalls.set(sig, entry); - if (entry.count >= 6) { + if (entry.count >= (loopDetect.threshold || 6)) { this.terminal.print(`🔄 Loop detected: ${tc.function.name} called ${entry.count}x with same args in 30s — breaking loop`, 'warning'); looping = true; break; @@ -832,7 +932,7 @@ buildSystemPrompt() { messages.push({ role: 'system', content: 'You are repeating the same tool call with identical arguments. Stop calling tools and provide your final answer based on what you already know.' }); continue; } - this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); + this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${maxTurns} (${thinkTime}s)`, 'info'); messages.push(res.message); let pendingText = res.message?.content || null; const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments)); @@ -892,15 +992,15 @@ buildSystemPrompt() { } // Only block on destructive tools if (destructiveTools.length > 0) { - // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= 90% + // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= threshold (from profile) let autoApprove = false; - if (pendingText) { + if (confEsc.enabled !== false && pendingText) { const confidence = this._parseConfidence(pendingText); - if (confidence !== null && confidence >= 90) { + if (confidence !== null && confidence >= (confEsc.threshold || 90)) { autoApprove = true; - this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved`, 'success'); + this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved (threshold: ${confEsc.threshold || 90}%)`, 'info'); } else if (confidence !== null) { - this.terminal.print(`🎯 AI confidence: ${confidence}% — below 90% threshold, requiring manual approval`, 'warning'); + this.terminal.print(`🎯 AI confidence: ${confidence}% — below ${confEsc.threshold || 90}% threshold, requiring manual approval`, 'warning'); } else { this.terminal.print('🎯 No confidence declared for destructive action — requiring manual approval', 'warning'); } @@ -985,7 +1085,7 @@ buildSystemPrompt() { } // Loop exited without final answer — auto-summary const toolNames = Object.keys(this._commandFreq).slice(-3).join(', '); - this.terminal.print(`🤖 AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning'); + this.terminal.print(`🤖 AI reached the ${toolTurns}/${maxTurns} tool limit.`, 'warning'); this.terminal.print('Tools were executed but no final answer was produced — try a more specific query.', 'info'); return { finalAnswer: null, toolTurns }; } @@ -2102,6 +2202,45 @@ buildSystemPrompt() { const stats = om.getUsageStats(filters); return JSON.stringify({ success: true, stats }); } + case 'agent_loop_list': { + var profiles = this._loopProfiles; + var list = Object.keys(profiles).map(function(k) { + var p = profiles[k]; + return { name: k, displayName: p.displayName, description: p.description, active: this._activeLoopProfile === p, maxTurns: p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS, toolsMode: p.tools ? p.tools.mode : 'all', benchmark: p.benchmark || null }; + }.bind(this)); + return JSON.stringify({ profiles: list, count: list.length, active: this._activeLoopProfile ? this._activeLoopProfile.name : 'none' }); + } + case 'agent_loop_switch': { + var name = (a.name || '').trim(); + if (!name) return JSON.stringify({ error: 'Profile name required' }); + if (!this._loopProfiles[name]) return JSON.stringify({ error: "Profile '" + name + "' not found", available: Object.keys(this._loopProfiles) }); + this._switchLoopProfile(name); + return JSON.stringify({ success: true, name: name, message: "Switched to '" + name + "'" }); + } + case 'agent_loop_create': { + var name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase(); + if (!name) return JSON.stringify({ error: 'Name required' }); + var base = this._defaultLoopProfile(name, a.description || 'Custom profile'); + if (a.maxTurns) base.loop.maxTurns = a.maxTurns; + if (a.toolsMode) base.tools.mode = a.toolsMode; + if (a.toolsEnabled) base.tools.enabled = a.toolsEnabled; + if (a.toolsDisabled) base.tools.disabled = a.toolsDisabled; + if (a.prependPrompt) base.systemPrompt.prepend = a.prependPrompt; + if (a.appendPrompt) base.systemPrompt.append = a.appendPrompt; + this._loopProfiles[name] = base; + this._saveLoopProfiles(); + return JSON.stringify({ success: true, name: name, message: "Profile '" + name + "' created" }); + } + case 'agent_loop_benchmark': { + var profileName = a.profile || (this._activeLoopProfile ? this._activeLoopProfile.name : 'default'); + var taskIds = a.tasks; + this._runBenchmark(profileName, taskIds).then(function(results) { + this.terminal.print("📊 Benchmark complete for '" + profileName + "'", 'success'); + }.bind(this)).catch(function(e) { + this.terminal.print('Benchmark error: ' + e.message, 'error'); + }.bind(this)); + return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' }); + } default: return JSON.stringify({ error: `Unknown tool: ${name}` }); } @@ -2316,14 +2455,44 @@ buildSystemPrompt() { setSystemPrompt(args) { if (!args[0]) { this.terminal.print('=== System Prompt ===', 'info'); - this.systemPrompt.split('\n').forEach(line => this.terminal.print(line, 'text-muted')); - this.terminal.print(`=== End (${this.systemPrompt.length} chars) ===`, 'info'); + this.systemPrompt.split('\n').forEach(function(line) { this.terminal.print(line, 'text-muted'); }.bind(this)); + this.terminal.print('=== End (' + this.systemPrompt.length + ' chars) ===', 'info'); + return; + } + var arg = args.join(' '); + if (arg === 'reset') { + localStorage.removeItem('webx11_ai_system_prompt'); + this.systemPrompt = this.buildSystemPrompt(); + this.terminal.print('System prompt reset to default', 'success'); + return; + } + if (arg.charAt(0) === '+') { + var directive = arg.slice(1).trim(); + var lp = this._activeLoopProfile; + if (lp) { + if (!lp.systemPrompt) lp.systemPrompt = { base: null, prepend: '', append: '', useDefault: true }; + lp.systemPrompt.prepend = (lp.systemPrompt.prepend ? lp.systemPrompt.prepend + '\n' : '') + directive; + this._saveLoopProfile(lp); + } + this.systemPrompt = this.buildSystemPrompt(); + this.terminal.print('Directive prepended to system prompt (profile: ' + (lp ? lp.name : 'none') + ')', 'success'); return; } - this.systemPrompt = args.join(' '); - // Persist to localStorage — next page load will use this instead of built-in + if (arg.charAt(0) === '-') { + var directive = arg.slice(1).trim(); + var lp = this._activeLoopProfile; + if (lp) { + if (!lp.systemPrompt) lp.systemPrompt = { base: null, prepend: '', append: '', useDefault: true }; + lp.systemPrompt.append = (lp.systemPrompt.append ? lp.systemPrompt.append + '\n' : '') + directive; + this._saveLoopProfile(lp); + } + this.systemPrompt = this.buildSystemPrompt(); + this.terminal.print('Directive appended to system prompt (profile: ' + (lp ? lp.name : 'none') + ')', 'success'); + return; + } + this.systemPrompt = arg; try { localStorage.setItem('webx11_ai_system_prompt', this.systemPrompt); } catch (e) {} - this.terminal.print('System prompt updated (persisted)', 'success'); + this.terminal.print('System prompt updated (persisted, full replace)', 'success'); } memDebug() { const toolLogSize = this._toolCallLog.length; @@ -2693,4 +2862,315 @@ buildSystemPrompt() { this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning'); } } + // ── Agent Loop Profile System ── + _defaultLoopProfile(name, desc) { + return { + name: name, displayName: desc, description: desc, + version: 1, created: new Date().toISOString(), updated: new Date().toISOString(), + systemPrompt: { base: null, prepend: '', append: '', useDefault: true }, + loop: { + strategy: 'standard', maxTurns: this.MAX_TOOL_TURNS, + loopDetection: { enabled: true, threshold: 6, windowMs: 30000 }, + confidenceEscalation: { enabled: true, threshold: 90 }, + finalTurnPrompt: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' + }, + tools: { mode: 'all', enabled: [], disabled: [] }, + model: { temperature: null, topP: null, maxTokens: null, thinking: null }, + provider: { name: null, model: null }, + benchmark: { lastRun: null, avgLatency: null, avgTokens: null, taskCompletionRate: null, tasks: [] } + }; + } + _initLoopProfiles() { + try { + var saved = localStorage.getItem('webx11_agent_loops'); + if (saved) { this._loopProfiles = JSON.parse(saved); } + } catch (e) {} + if (!this._loopProfiles || Object.keys(this._loopProfiles).length === 0) { + this._loopProfiles = {}; + this._loopProfiles['default'] = this._defaultLoopProfile('default', 'Balanced loop for general tasks'); + var p = this._defaultLoopProfile('minimal', 'Fast, low-token for simple tasks'); + p.loop.maxTurns = 4; p.tools = { mode: 'custom', enabled: ['list_files','read_file','write_file','nvfs_resolve'], disabled: [] }; p.model.temperature = 0.3; + this._loopProfiles['minimal'] = p; + p = this._defaultLoopProfile('research', 'Read-only exploration, web + files'); + p.loop.maxTurns = 6; p.tools = { mode: 'custom', enabled: ['web_search','web_fetch','read_file','list_files','nvfs_resolve','view_module','list_modules'], disabled: [] }; p.model.temperature = 0.5; + this._loopProfiles['research'] = p; + p = this._defaultLoopProfile('safe', 'No destructive tools'); + p.tools = { mode: 'all', enabled: [], disabled: ['delete_file','edit_module','run_command','make_directory'] }; p.model.temperature = 0.5; + this._loopProfiles['safe'] = p; + p = this._defaultLoopProfile('creative', 'More turns, higher temp'); + p.loop.maxTurns = 12; p.model.temperature = 0.9; + this._loopProfiles['creative'] = p; + p = this._defaultLoopProfile('coder', 'Code-focused, low temp'); + p.loop.maxTurns = 10; p.model.temperature = 0.2; p.systemPrompt.append = 'You are a code expert. Always show the exact code changes. Use edit_module for modifications.'; + this._loopProfiles['coder'] = p; + this._saveLoopProfiles(); + } + try { + var active = localStorage.getItem('webx11_active_loop_profile'); + if (active && this._loopProfiles[active]) { this._activeLoopProfile = this._loopProfiles[active]; } + else { this._activeLoopProfile = this._loopProfiles['default']; } + } catch (e) { this._activeLoopProfile = this._loopProfiles['default']; } + } + _saveLoopProfiles() { + try { localStorage.setItem('webx11_agent_loops', JSON.stringify(this._loopProfiles)); } catch (e) {} + } + _saveLoopProfile(p) { + if (p && p.name) { p.updated = new Date().toISOString(); this._loopProfiles[p.name] = p; this._saveLoopProfiles(); } + } + _switchLoopProfile(name) { + if (!this._loopProfiles[name]) return false; + this._activeLoopProfile = this._loopProfiles[name]; + localStorage.setItem('webx11_active_loop_profile', name); + this._toolDefs = null; + this.systemPrompt = this.buildSystemPrompt(); + var mp = this._activeLoopProfile.model; + var om = this.terminal ? this.terminal.ollamaManager : null; + if (om && mp) { + if (mp.temperature !== null) { om.modelParams.temperature = mp.temperature; om._saveModelParams(); } + if (mp.topP !== null) { om.modelParams.top_p = mp.topP; om._saveModelParams(); } + if (mp.maxTokens !== null) { om.modelParams.max_tokens = mp.maxTokens; om._saveModelParams(); } + if (mp.thinking !== null) { om.modelParams.thinking = mp.thinking; om._saveModelParams(); } + } + return true; + } + agentLoopsCmd(args) { + if (!args[0]) { + this.terminal.print('=== Agent Loop Profiles ===', 'info'); + var keys = Object.keys(this._loopProfiles); + for (var ki = 0; ki < keys.length; ki++) { + var k = keys[ki]; + var p = this._loopProfiles[k]; + var active = this._activeLoopProfile === p ? ' ✅' : ''; + var bench = p.benchmark && p.benchmark.taskCompletionRate != null ? ' (bench: ' + Math.round(p.benchmark.taskCompletionRate * 100) + '%)' : ''; + var tools = p.tools && p.tools.mode === 'custom' ? (p.tools.enabled ? p.tools.enabled.length : 0) + ' tools' : 'all tools'; + this.terminal.print(' ' + k + ': ' + p.displayName + active + ' — ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' turns, ' + tools + bench); + } + this.terminal.print('Usage: agent-loops <name> | new <name> <desc> | edit <name> <key> <val> | delete <name> | clone <src> <dst> | export <name> | reset <name>', 'text-muted'); + return; + } + var sub = args[0]; + if (sub === 'new') { + var name = args[1] ? args[1].replace(/\s+/g, '-').toLowerCase() : ''; + if (!name) { this.terminal.print('Usage: agent-loops new <name> <description>', 'warning'); return; } + var desc = args.slice(2).join(' ') || 'Custom profile'; + this._loopProfiles[name] = this._defaultLoopProfile(name, desc); + this._saveLoopProfiles(); + this.terminal.print("Profile '" + name + "' created. Use: agent-loops edit " + name + " <key> <value>", 'success'); + } else if (sub === 'edit') { + var name = args[1]; + if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; } + var key = args[2]; + var val = args.slice(3).join(' '); + if (!key || !val) { this.terminal.print('Usage: agent-loops edit <name> <key> <value>', 'warning'); return; } + this._setNestedKey(this._loopProfiles[name], key, val); + this._saveLoopProfiles(); + if (this._activeLoopProfile === this._loopProfiles[name]) { this._toolDefs = null; this.systemPrompt = this.buildSystemPrompt(); } + this.terminal.print('Edited ' + name + '.' + key + ' = ' + val, 'success'); + } else if (sub === 'delete') { + var name = args[1]; + if (!name) { this.terminal.print('Usage: agent-loops delete <name>', 'warning'); return; } + if (name === 'default') { this.terminal.print('Cannot delete default profile', 'error'); return; } + if (this._activeLoopProfile === this._loopProfiles[name]) { this.terminal.print('Cannot delete active profile — switch first', 'error'); return; } + delete this._loopProfiles[name]; + this._saveLoopProfiles(); + this.terminal.print("Profile '" + name + "' deleted", 'success'); + } else if (sub === 'clone') { + var src = args[1], dst = args[2] ? args[2].replace(/\s+/g, '-').toLowerCase() : ''; + if (!src || !dst || !this._loopProfiles[src]) { this.terminal.print('Usage: agent-loops clone <src> <dst>', 'warning'); return; } + this._loopProfiles[dst] = JSON.parse(JSON.stringify(this._loopProfiles[src])); + this._loopProfiles[dst].name = dst; + this._loopProfiles[dst].created = new Date().toISOString(); + this._saveLoopProfiles(); + this.terminal.print("Cloned '" + src + "' -> '" + dst + "'", 'success'); + } else if (sub === 'export') { + var name = args[1]; + if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; } + this.terminal.printJSON(JSON.stringify(this._loopProfiles[name], null, 2)); + } else if (sub === 'reset') { + var name = args[1]; + if (!name) { this.terminal.print('Usage: agent-loops reset <name>', 'warning'); return; } + var desc = this._loopProfiles[name] ? this._loopProfiles[name].displayName : 'Reset profile'; + this._loopProfiles[name] = this._defaultLoopProfile(name, desc); + this._saveLoopProfiles(); + if (this._activeLoopProfile && this._activeLoopProfile.name === name) { this._switchLoopProfile(name); } + this.terminal.print("Profile '" + name + "' reset to defaults", 'success'); + } else { + if (this._loopProfiles[sub]) { + this._switchLoopProfile(sub); + var p = this._activeLoopProfile; + this.terminal.print("Switched to '" + sub + "': " + p.displayName, 'success'); + this.terminal.print(' Max turns: ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' | Tools: ' + (p.tools ? p.tools.mode : 'all') + ' | Temp: ' + (p.model && p.model.temperature !== null ? p.model.temperature : 'inherit'), 'text-muted'); + } else { + this.terminal.print("Profile '" + sub + "' not found. Available: " + Object.keys(this._loopProfiles).join(', '), 'error'); + } + } + } + _setNestedKey(obj, key, val) { + var parts = key.split('.'); + var o = obj; + for (var i = 0; i < parts.length - 1; i++) { + if (!o[parts[i]]) o[parts[i]] = {}; + o = o[parts[i]]; + } + var last = parts[parts.length - 1]; + try { o[last] = JSON.parse(val); } catch (e) { o[last] = val; } + } + // ── BENCHMARK SUITE ── + _benchmarkTasks() { + return [ + { id: 'fs-navigate', prompt: 'List all files in the root directory and tell me how many .js files exist', maxTurns: 3, successCheck: function(r, log) { return r.length > 10 && log.some(function(t) { return t.name === 'list_files'; }); } }, + { id: 'fs-create-read', prompt: 'Create a file called /tmp/bench-test.txt with "hello bench", then read it back to verify', maxTurns: 4, successCheck: function(r) { return r.toLowerCase().indexOf('hello') !== -1; }, destructive: true }, + { id: 'nvfs-resolve', prompt: 'What terminal command handles file operations? Use nvfs-resolve to find it', maxTurns: 3, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'nvfs_resolve'; }) && r.length > 20; } }, + { id: 'web-search', prompt: 'Search the web for "what is the capital of France" and tell me the answer', maxTurns: 3, successCheck: function(r) { return r.toLowerCase().indexOf('paris') !== -1; } }, + { id: 'multi-tool', prompt: 'Read the file /package.json, then list all modules, then summarize both results', maxTurns: 6, successCheck: function(r, log) { return log.filter(function(t) { return ['read_file','list_files','list_modules'].indexOf(t.name) !== -1; }).length >= 2; } }, + { id: 'code-edit', prompt: 'Create a file /tmp/bench-code.js with a function that adds two numbers, then read it back to verify', maxTurns: 5, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'write_file'; }); }, destructive: true }, + ]; + } + async _runBenchmark(profileName, taskIds) { + var tasks = this._benchmarkTasks(); + if (taskIds) tasks = tasks.filter(function(t) { return taskIds.indexOf(t.id) !== -1; }); + var prevProfile = this._activeLoopProfile ? this._activeLoopProfile.name : null; + if (profileName && this._loopProfiles[profileName]) this._switchLoopProfile(profileName); + this.terminal.print('📊 Benchmark: ' + profileName + ' (' + tasks.length + ' tasks)', 'info'); + var results = []; + for (var ti = 0; ti < tasks.length; ti++) { + var task = tasks[ti]; + this.terminal.print(' ▶ ' + task.id + '...', 'text-muted'); + var t0 = Date.now(); + var toolLogBefore = this._toolCallLog.length; + var messages = [{ role: 'user', content: task.prompt }]; + messages.unshift({ role: 'system', content: this.systemPrompt }); + var savedMax = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : null; + if (this._activeLoopProfile && this._activeLoopProfile.loop) { this._activeLoopProfile.loop.maxTurns = task.maxTurns; } + try { + var result = await this.agenticLoop(messages); + var latency = Date.now() - t0; + var newTools = this._toolCallLog.slice(toolLogBefore); + var passed = task.successCheck(result.finalAnswer || '', newTools); + var tokens = await this._estimateTokensForRun(newTools, result.finalAnswer); + results.push({ id: task.id, turns: result.toolTurns, tokens: tokens, latency: latency, pass: passed }); + this.terminal.print(' ' + (passed ? '✅' : '❌') + ' ' + result.toolTurns + ' turns, ' + tokens + ' tokens, ' + (latency/1000).toFixed(1) + 's', passed ? 'success' : 'warning'); + } catch (e) { + results.push({ id: task.id, turns: 0, tokens: 0, latency: Date.now() - t0, pass: false, error: e.message }); + this.terminal.print(' ❌ Error: ' + e.message, 'error'); + } + if (this._activeLoopProfile && this._activeLoopProfile.loop && savedMax != null) { this._activeLoopProfile.loop.maxTurns = savedMax; } + this.context = []; + } + var profile = this._loopProfiles[profileName]; + if (profile) { + var passed = results.filter(function(r) { return r.pass; }).length; + profile.benchmark = { + lastRun: new Date().toISOString(), + tasks: results, + avgLatency: Math.round(results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length), + avgTokens: Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length), + taskCompletionRate: passed / results.length + }; + this._saveLoopProfile(profile); + } + this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info'); + this.terminal.print('│ Benchmark: ' + profileName, 'info'); + this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info'); + for (var ri = 0; ri < results.length; ri++) { + var r = results[ri]; + var status = r.pass ? '✅' : '❌'; + this.terminal.print('│ ' + r.id + ' ' + r.turns + 't ' + r.tokens + 'tok ' + (r.latency/1000).toFixed(1) + 's ' + status, 'info'); + } + this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info'); + var passed = results.filter(function(r) { return r.pass; }).length; + var avgLat = (results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length / 1000).toFixed(1); + var avgTok = Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length); + this.terminal.print('│ ' + passed + '/' + results.length + ' passed (' + Math.round(passed/results.length*100) + '%) | Avg: ' + avgLat + 's, ' + avgTok + 'tok', 'info'); + this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info'); + if (prevProfile && prevProfile !== profileName) this._switchLoopProfile(prevProfile); + return results; + } + async _estimateTokensForRun(toolLog, finalAnswer) { + var om = this.terminal ? this.terminal.ollamaManager : null; + if (om && om._usageLog && om._usageLog.length > 0) { + var last = om._usageLog[om._usageLog.length - 1]; + return last.total_tokens || 0; + } + var toolChars = toolLog.reduce(function(s, t) { return s + (t.result ? t.result.length : 0); }, 0); + var answerChars = (finalAnswer || '').length; + return Math.round((toolChars + answerChars) / 4); + } + benchCmd(args) { + var profile = null; + var taskIds = null; + for (var ai = 0; ai < args.length; ai++) { + if (args[ai].indexOf('--tasks=') === 0) taskIds = args[ai].split('=')[1].split(','); + else if (args[ai].indexOf('--') !== 0) profile = args[ai]; + } + if (!profile) { + this.terminal.print('Usage: bench [profile] [--tasks=id1,id2] [--repeat=N]', 'warning'); + this.terminal.print('Available tasks: ' + this._benchmarkTasks().map(function(t) { return t.id; }).join(', '), 'text-muted'); + return; + } + this._runBenchmark(profile, taskIds); + } + benchCompareCmd(args) { + var p1 = args[0], p2 = args[1]; + if (!p1 || !p2) { this.terminal.print('Usage: bench-compare <profile1> <profile2>', 'warning'); return; } + var b1 = this._loopProfiles[p1] ? this._loopProfiles[p1].benchmark : null; + var b2 = this._loopProfiles[p2] ? this._loopProfiles[p2].benchmark : null; + if (!b1 || !b1.lastRun) { this.terminal.print("No benchmark for '" + p1 + "'. Run: bench " + p1, 'warning'); return; } + if (!b2 || !b2.lastRun) { this.terminal.print("No benchmark for '" + p2 + "'. Run: bench " + p2, 'warning'); return; } + this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info'); + this.terminal.print('│ ' + p1 + ' vs ' + p2, 'info'); + this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info'); + this.terminal.print('│ Pass rate: ' + (b1.taskCompletionRate*100).toFixed(0) + '% vs ' + (b2.taskCompletionRate*100).toFixed(0) + '%', 'info'); + this.terminal.print('│ Avg tokens: ' + b1.avgTokens + ' vs ' + b2.avgTokens, 'info'); + this.terminal.print('│ Avg latency: ' + (b1.avgLatency/1000).toFixed(1) + 's vs ' + (b2.avgLatency/1000).toFixed(1) + 's', 'info'); + this.terminal.print('│ Last run: ' + b1.lastRun.substring(0,10) + ' vs ' + b2.lastRun.substring(0,10), 'info'); + this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info'); + } + // ── SELF-IMPROVEMENT LOOP ── + _postRunReview(result, toolLog, usageData) { + var anomalies = []; + var maxT = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : this.MAX_TOOL_TURNS; + if (result.toolTurns >= maxT * 0.8) { + anomalies.push({ type: 'max_turns_near_limit', value: result.toolTurns, max: maxT }); + } + if (usageData && usageData.total_tokens > 50000) { + anomalies.push({ type: 'high_token_usage', value: usageData.total_tokens }); + } + var failedTools = toolLog.filter(function(t) { return t.error; }); + if (failedTools.length > 2) { + var uniqueTools = []; + failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); }); + anomalies.push({ type: 'tool_failures', count: failedTools.length, tools: uniqueTools }); + } + var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null; + if (bench && bench.avgTokens && usageData) { + if (usageData.total_tokens > bench.avgTokens * 1.5) { + anomalies.push({ type: 'above_benchmark', value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) }); + } + } + if (anomalies.length === 0) return; + var suggestions = []; + for (var ai = 0; ai < anomalies.length; ai++) { + var a = anomalies[ai]; + if (a.type === 'max_turns_near_limit') suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task'); + else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks'); + else if (a.type === 'tool_failures') suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct'); + else if (a.type === 'above_benchmark') suggestions.push('This run used ' + a.ratio + 'x more tokens than benchmark avg (' + a.benchmark + ') — consider profile optimization'); + } + var suggestion = suggestions.join('\n'); + try { + var fs = this.terminal.fs; + var index = fs.fsReadJSON('/skills/index.json') || {}; + var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; }); + if (autoSkills.length < 10) { + var skillName = 'loop-lesson-' + Date.now(); + var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', '); + fs.mkdirRecursive('/skills'); + fs.writeFile('/skills/' + skillName + '.md', content); + index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString() }; + fs.fsWriteJSON('/skills/index.json', index); + } + } catch (e) { /* VFS not available */ } + this.terminal.print('💡 Self-improvement: ' + suggestion, 'info'); + } } |