aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js182
-rw-r--r--modules/core/module-aiassist.js524
-rw-r--r--modules/core/module-loader.js2
3 files changed, 674 insertions, 34 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 00e8db3..6f94a44 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2436,7 +2436,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this._registerBuiltIn();
}
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','agent-loops','bench','bench-compare'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -7656,6 +7656,10 @@ class ServerMonitorModule {
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) {}
@@ -7717,7 +7721,21 @@ buildSystemPrompt() {
}
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 [{
@@ -8149,6 +8167,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',
@@ -8170,6 +8191,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);
@@ -8284,6 +8308,13 @@ buildSystemPrompt() {
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
+ 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) {}
+ }
// Auto-create skills from repeated errors
if (this._errorLog.length >= 3) {
await this._autoCreateErrorSkill();
@@ -8404,10 +8435,16 @@ 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: false, 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
@@ -8447,7 +8484,7 @@ buildSystemPrompt() {
entry.count++;
entry.ts = now;
_seenCalls.set(sig, entry);
- if (entry.count >= 6) {
+ if (loopDetect.enabled !== false && 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;
@@ -9946,10 +9983,40 @@ buildSystemPrompt() {
this.terminal.print(`=== End (${this.systemPrompt.length} chars) ===`, 'info');
return;
}
- this.systemPrompt = args.join(' ');
- // Persist to localStorage — next page load will use this instead of built-in
+ 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;
+ }
+ 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;
@@ -10066,6 +10133,8 @@ buildSystemPrompt() {
this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, '');
this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, '');
this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, '');
+ var lp = this._activeLoopProfile;
+ this.terminal.print('loop-detection: ' + (lp && lp.loop && lp.loop.loopDetection ? (lp.loop.loopDetection.enabled ? 'on' : 'off') : 'off (default)') + ' (profile: ' + (lp ? lp.name : 'none') + ')', '');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) {
this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, '');
@@ -10074,7 +10143,7 @@ buildSystemPrompt() {
this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, '');
}
this.terminal.print('Usage: ai-config <key> <value>', 'info');
- this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection', 'info');
return;
}
const key = args[0];
@@ -10127,8 +10196,17 @@ buildSystemPrompt() {
if (!validModes.includes(val)) return this.terminal.print(`thinking must be one of: ${validModes.join(', ')}`, 'warning');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) { om.modelParams.thinking = val; om._saveModelParams(); }
+ } else if (key === 'loop-detection') {
+ var lp = this._activeLoopProfile;
+ if (!lp) { this.terminal.print('No active profile', 'error'); return; }
+ if (!lp.loop) lp.loop = {};
+ if (!lp.loop.loopDetection) lp.loop.loopDetection = { enabled: false, threshold: 6, windowMs: 30000 };
+ lp.loop.loopDetection.enabled = val !== 'off';
+ this._saveLoopProfile(lp);
+ this.terminal.print('Loop detection ' + (lp.loop.loopDetection.enabled ? 'enabled' : 'disabled') + ' (profile: ' + lp.name + ')', 'success');
+ return;
} else {
- return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning');
+ return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection`, 'warning');
}
// Persist
localStorage.setItem('webx11_ai_config', JSON.stringify({
@@ -10311,6 +10389,88 @@ buildSystemPrompt() {
this.terminal.print('Error: ' + e.message, 'error');
}
}
+ // ── 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: false, 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) {}
+ this.terminal.print("💡 Self-improvement: " + suggestion, "info");
+ }
_vaultmindStatusCmd() {
const token = localStorage.getItem('vaultmind_token');
if (token) {
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');
+ }
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 5d1c320..f024d99 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-18-v4.5.79';
+ const LATEST_VER = '2026-06-24-v4.5.80';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {