diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-02 06:00:40 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-02 06:00:40 +0200 |
| commit | ea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (patch) | |
| tree | 2361e33e52cdb4e0880ed0c776ed75943b37d7cf /modules/core/module-aiassist.js | |
| parent | 8b3e3e4527772bf060984f98777a2cff8e88aa09 (diff) | |
auto: 2026-07-02_06:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 333 |
1 files changed, 318 insertions, 15 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 6359f17..8a0a7c8 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -22,6 +22,8 @@ this._toolDefs = null; this._toolCallLog = []; this._debugPanelEnabled = false; + this._lastRawResponse = null; + this._lastRawMessages = null; // 🔒 Guardrail Layer this._guardrailPolicy = null; this._shadowSnapshot = null; @@ -431,7 +433,7 @@ buildSystemPrompt() { type: 'function', function: { name: 'smart_context_list', - description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.', + description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.', parameters: { type: 'object', properties: {}, @@ -442,11 +444,11 @@ buildSystemPrompt() { type: 'function', function: { name: 'smart_context_set', - description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.", + description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.", parameters: { type: 'object', properties: { - name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' } + name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' } }, required: ['name'] } @@ -632,6 +634,11 @@ buildSystemPrompt() { this.commands['bench-compare'] = (args) => this.benchCompareCmd(args); this.commands['debug'] = () => this.toggleDebugPanel(); this.commands['debug-ai'] = () => this.toggleDebug(); + this.commands['debug-ai-last'] = () => this._showLastRawResponse(); + this.commands['debug-ai-send'] = () => this._showLastRawRequest(); + this.commands['debug-ai-log'] = () => this._showToolCallLog(); + this.commands['archive-list'] = () => this._archiveList(); + this.commands['archive-read'] = (args) => this._archiveRead(args); this.commands['guardrail'] = (args) => this.guardrailCmd(args); this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]); this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]); @@ -773,9 +780,14 @@ buildSystemPrompt() { // Log the AI interaction this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100)); if (loopResult && loopResult.toolTurns) { - this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)'); + const toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', '); + this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames); } } + // Auto-archive every 10 messages + if (this._messageCount > 0 && this._messageCount % 10 === 0) { + this._archiveSession('Auto-archive at message ' + this._messageCount); + } // Register session save on page unload (runs once per session) if (!this._sessionSaved) { this._sessionSaved = true; @@ -807,12 +819,13 @@ buildSystemPrompt() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-ai-panel'); if (!panel) return; + const activeTab = panel.getAttribute('data-tab') || 'tools'; const recent = this._toolCallLog.slice(-20); - let html = ''; + let toolHtml = ''; for (const entry of recent) { const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok'; const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60); - html += '<div class="debug-entry ' + statusClass + '">' + + toolHtml += '<div class="debug-entry ' + statusClass + '">' + '<span class="debug-turn">#' + entry.turn + '</span> ' + '<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' + '<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' + @@ -820,14 +833,41 @@ buildSystemPrompt() { (entry.error ? ' <span class="debug-err">⚠️</span>' : '') + '</div>'; } + // Raw I/O tab content + let rawHtml = ''; + if (this._lastRawResponse) { + const r = this._lastRawResponse; + const content = (r.message && r.message.content) || ''; + const hasTools = r.tool_calls && r.tool_calls.length > 0; + rawHtml += '<div style="padding:6px 8px;font-size:0.75em;line-height:1.3">'; + if (r.error) rawHtml += '<div style="color:var(--error-color)">⚠️ ' + this._escapeHtml(r.error) + '</div>'; + if (content) { + const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content); + rawHtml += '<div><strong>Content:</strong> ' + safe + '</div>'; + } + if (hasTools) { + rawHtml += '<div><strong>Tools:</strong> ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '</div>'; + } + if (this._lastRawMessages) { + rawHtml += '<div style="margin-top:4px;color:var(--text-muted)">Messages: ' + this._lastRawMessages.length + '</div>'; + } + rawHtml += '</div>'; + } else { + rawHtml = '<div style="padding:8px;color:var(--text-muted);font-size:0.75em">No AI response yet. Send a message first.</div>'; + } + const tabBar = '<div style="display:flex;border-bottom:1px solid #333">' + + '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'tools' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'tools\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">🔧 Tools</span>' + + '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'raw' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'raw\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">📡 Raw I/O</span>' + + '</div>'; + const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml; panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' + - '<span>🔧 Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' + + '<span>🔧 AI Debug <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' + '<span style="display:flex;gap:4px;">' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' + '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' + - '</span></div>' + html; + '</span></div>' + tabBar + bodyHtml; } _isToolError(result) { if (!result || typeof result !== 'string') return null; @@ -878,6 +918,28 @@ buildSystemPrompt() { 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.'; + // Memory-augmented planning: inject prior loop lessons as negative constraints + try { + var fs = this.terminal.fs; + if (fs) { + var index = fs.fsReadJSON('/skills/index.json') || {}; + var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); + if (lessonKeys.length > 0) { + var recentLessons = lessonKeys.sort().slice(-3); + var lessonTexts = []; + for (var li = 0; li < recentLessons.length; li++) { + var lc = fs.readFile('/skills/' + recentLessons[li] + '.md'); + if (lc) { + var excerpt = lc.substring(0, 300); + lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt); + } + } + if (lessonTexts.length > 0) { + messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' }); + } + } + } + } catch (e) { /* VFS not available — skip memory-augmented planning */ } let toolTurns = 0; const _seenCalls = new Map(); const _loopWindow = loopDetect.windowMs || 30000; @@ -892,6 +954,9 @@ buildSystemPrompt() { const thinkStart = Date.now(); const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions()); const thinkTime = Math.round((Date.now() - thinkStart) / 1000); + // Store raw response for debug-ai-last + this._lastRawResponse = res; + this._lastRawMessages = messages.slice(); if (this._cancelled) return { finalAnswer: null, toolTurns }; if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; } if (res.message && !res.tool_calls) { @@ -2068,10 +2133,26 @@ buildSystemPrompt() { return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) }); } scm.activeProfile = name; + scm.saveActiveProfile(); const profile = scm.contextProfiles[name]; // Rebuild system prompt to reflect new context profile this.systemPrompt = this.buildSystemPrompt(); - return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature }); + // Also switch agent loop profile to match + const PROFILE_MAP = { + 'coding': 'coder', + 'debug': 'minimal', + 'creative': 'creative', + 'technical': 'research', + 'chat': 'default', + 'system-admin': 'safe', + 'research': 'research', + 'security': 'safe' + }; + const targetLoop = PROFILE_MAP[name]; + if (targetLoop && this._switchLoopProfile) { + this._switchLoopProfile(targetLoop); + } + return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null }); } case 'create_mindmap': { const mindmap = this.terminal.moduleManager.modules['MindmapModule']; @@ -2796,6 +2877,121 @@ buildSystemPrompt() { } return total; } + _showToolCallLog() { + if (!this._toolCallLog || this._toolCallLog.length === 0) { + this.terminal.print('No tool calls recorded this session.', 'warning'); + return; + } + let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n'; + for (var i = 0; i < this._toolCallLog.length; i++) { + var e = this._toolCallLog[i]; + var err = e.error ? ' ⚠️' : ''; + var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80); + out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n'; + } + out += '── End ──'; + this.terminal.print(out, 'info'); + } + _archiveList() { + try { + const fs = this.terminal.fs; + const files = fs.ls('/memory/archive') || []; + const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }); + if (jsonFiles.length === 0) { + this.terminal.print('No archives found. Run ai-compress to create one.', 'warning'); + return; + } + jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; }); + let out = '── Archives (' + jsonFiles.length + ') ──\n'; + for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) { + var f = jsonFiles[i]; + var size = f.size ? '(' + f.size + 'b)' : ''; + out += ' ' + f.name + ' ' + size + '\n'; + } + out += '── End ──\nUse: archive-read <filename>'; + this.terminal.print(out, 'info'); + } catch (e) { + this.terminal.print('Error listing archives: ' + e.message, 'error'); + } + } + _archiveRead(args) { + var name = (args && args[0]) || ''; + if (!name) { + this.terminal.print('Usage: archive-read <filename> (e.g. 2026-06-25-14-30.json)', 'warning'); + return; + } + try { + const fs = this.terminal.fs; + var data = fs.fsReadJSON('/memory/archive/' + name); + if (!data) { + this.terminal.print('Archive not found: ' + name, 'error'); + return; + } + var out = '── Archive: ' + name + ' ──\n'; + out += 'Archived: ' + (data.archivedAt || '?') + '\n'; + out += 'Summary: ' + (data.summary || '(none)') + '\n'; + out += 'User messages: ' + (data.user?.messageCount || 0) + '\n'; + out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n'; + out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n'; + out += '── End ──'; + this.terminal.print(out, 'info'); + } catch (e) { + this.terminal.print('Error reading archive: ' + e.message, 'error'); + } + } + _showLastRawResponse() { + if (!this._lastRawResponse) { + this.terminal.print('No AI response recorded yet. Send a message first.', 'warning'); + return; + } + const r = this._lastRawResponse; + var content = (r.message && r.message.content) || ''; + var hasTools = r.tool_calls && r.tool_calls.length > 0; + var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : ''; + // Render as HTML card for better readability + var html = '<div class="msg-card ai" style="border:1px solid var(--border-color);border-radius:6px;margin:4px 0;overflow:hidden">' + + '<div class="msg-card-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border-color);font-weight:bold;font-size:0.9em">' + + '<span>🤖 Last AI Response</span>' + + '<span style="display:flex;gap:4px">' + + '<button class="card-btn" onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m&&m._lastRawResponse){navigator.clipboard.writeText(JSON.stringify(m._lastRawResponse,null,2)).then(function(){m.terminal.print(\'📋 Raw response copied\',\'success\')})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em">📋 Copy Raw</button>' + + '</span></div>' + + '<div class="msg-card-body" style="padding:8px 10px;font-size:0.85em;line-height:1.4">'; + if (r.error) { + html += '<div style="color:var(--error-color)">⚠️ Error: ' + this._escapeHtml(r.error) + '</div>'; + } + if (content) { + var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content); + html += '<div style="margin-bottom:4px"><strong>Content:</strong><br>' + safe + '</div>'; + } else if (!hasTools) { + html += '<div style="color:var(--warning-color)">⚠️ Empty response (no content, no tool calls)</div>'; + } + if (hasTools) { + html += '<div style="margin-top:4px"><strong>Tool calls:</strong> ' + this._escapeHtml(toolStr) + '</div>'; + } + html += '</div></div>'; + this.terminal.printHTML(html); + } + _showLastRawRequest() { + if (!this._lastRawMessages) { + this.terminal.print('No AI request recorded yet. Send a message first.', 'warning'); + return; + } + const msgs = this._lastRawMessages; + let out = '── Last AI Request (' + msgs.length + ' messages) ──\n'; + for (let i = 0; i < msgs.length; i++) { + const m = msgs[i]; + const role = m.role || '?'; + const content = (m.content || '').substring(0, 200); + const suffix = (m.content || '').length > 200 ? '...' : ''; + out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n'; + if (i > 8 && i < msgs.length - 2) { + out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n'; + i = msgs.length - 3; + } + } + out += '── End ──'; + this.terminal.print(out, 'info'); + } async _compressContext() { if (this.context.length <= 6) return; const totalTokens = this._estimateContextTokens(); @@ -2821,10 +3017,58 @@ buildSystemPrompt() { } catch (e) { /* fallback to truncation */ } this.context = [{ role: 'system', content: compressed }, ...recentMsgs]; this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info'); + // Also archive session snapshot + this._archiveSession(compressed); + } + _archiveSession(summary) { + try { + const fs = this.terminal.fs; + fs.mkdirRecursive('/memory/archive'); + const now = new Date(); + const pad = function(n) { return String(n).padStart(2, '0'); }; + const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes()); + const archive = { + archivedAt: now.toISOString(), + summary: summary || '', + user: { + profile: this._userProfile || {}, + topics: [], + messageCount: this._messageCount + }, + system: { + modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0, + model: this.terminal?.ollamaManager?.currentModel || 'unknown', + provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown' + }, + context: { + messageCount: this.context.length, + estimatedTokens: this._estimateContextTokens() + } + }; + fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive); + // Rotation: keep last 30 + try { + const files = fs.ls('/memory/archive') || []; + const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }); + if (jsonFiles.length > 30) { + jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; }); + const toDelete = jsonFiles.slice(0, jsonFiles.length - 30); + for (var i = 0; i < toDelete.length; i++) { + fs.deleteFile('/memory/archive/' + toDelete[i].name); + } + } + } catch (e) { /* rotation best-effort */ } + } catch (e) { /* archive best-effort */ } } _manualCompress() { this._compressContext().then(() => { - this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info'); + const fs = this.terminal.fs; + let archiveCount = 0; + try { + const files = fs.ls('/memory/archive') || []; + archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length; + } catch (e) {} + this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info'); }).catch(e => { this.terminal.print('Compression error: ' + e.message, 'error'); }); @@ -3150,11 +3394,38 @@ buildSystemPrompt() { } if (anomalies.length === 0) return; var suggestions = []; + var profileAdjusted = false; 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'); + if (a.type === 'max_turns_near_limit') { + suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task'); + // Auto-correct: bump maxTurns by +2 + if (this._activeLoopProfile && this._activeLoopProfile.loop) { + var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2; + this._activeLoopProfile.loop.maxTurns = newMax; + this._saveLoopProfile(this._activeLoopProfile); + suggestions.push(' → Auto-adjusted maxTurns to ' + newMax); + profileAdjusted = true; + } + } 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 === 'tool_failures') { + suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct'); + // Auto-correct: inject negative constraint into profile append + if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { + var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.'; + if (this._activeLoopProfile.systemPrompt.append) { + if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) { + this._activeLoopProfile.systemPrompt.append += '\n' + constraint; + } + } else { + this._activeLoopProfile.systemPrompt.append = constraint; + } + this._saveLoopProfile(this._activeLoopProfile); + suggestions.push(' → Added constraint to profile: avoid ' + a.tools.join(', ')); + profileAdjusted = true; + } + } 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'); @@ -3162,15 +3433,47 @@ buildSystemPrompt() { 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) { + // One-time cleanup: remove all existing loop-lesson files on first run + if (!this._loopLessonCleaned) { + this._loopLessonCleaned = true; + autoSkills.forEach(function(e) { + try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} + delete index[e[0]]; + }); + autoSkills = []; + } + // Age limit: remove lessons older than 24h + var now = Date.now(); + autoSkills = autoSkills.filter(function(e) { + var created = new Date(e[1].created).getTime(); + if (now - created > 86400000) { + try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} + delete index[e[0]]; + return false; + } + return true; + }); + // Dedup: skip if latest lesson has same anomaly types + var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(','); + var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes; + // FIFO rotation: if at cap, delete oldest before creating + if (!isDuplicate && autoSkills.length >= 3) { + var oldest = autoSkills[0]; + try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {} + delete index[oldest[0]]; + autoSkills.shift(); + } + if (!isDuplicate) { 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() }; + 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(), anomalyTypes: anomalyTypes }; fs.fsWriteJSON('/skills/index.json', index); } } catch (e) { /* VFS not available */ } - this.terminal.print('💡 Self-improvement: ' + suggestion, 'info'); + var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)'; + if (profileAdjusted) msg += ' — profile auto-adjusted'; + this.terminal.print(msg, 'info'); } } |