aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js106
-rw-r--r--modules/core/ollama-manager.js25
-rw-r--r--modules/core/terminal.js130
3 files changed, 257 insertions, 4 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 70882a9..602ae3e 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -35,7 +35,10 @@
const now = new Date();
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
- return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.\n\n== TOOL REFERENCE ==
+ const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false;
+ const CR = String.fromCharCode(10);
+ const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR;
+ return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE ==
You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE):
FILESYSTEM:
@@ -436,7 +439,8 @@ RULES:
}
// Auto-load skills relevant to the user's query (awaited, not fire-and-forget)
// Skip skill loading for trivial queries (< 10 chars, no actionable keywords)
- const isTrivial = userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage);
+ const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false;
+ const isTrivial = optEnabled && userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage);
if (!isTrivial) {
try {
const skillRes = await this.runTool('load_skills', { query: userMessage });
@@ -539,6 +543,42 @@ RULES:
}
return null;
}
+ _parseToolBlocksFromText(text) {
+ if (!text) return [];
+ const toolCalls = [];
+ // Match ```tool name(args) or ```tool\nname(args) blocks
+ const blockRegex = /```tool(?:\s*\n)?\s*(\w+)\s*({[^}]*})?\s*```/gi;
+ let match;
+ while ((match = blockRegex.exec(text)) !== null) {
+ const name = match[1];
+ let args = {};
+ try {
+ if (match[2]) args = JSON.parse(match[2]);
+ } catch (e) {
+ // If JSON parse fails, treat as raw string
+ args = match[2] ? { path: match[2].replace(/[{}]/g,'').trim() } : {};
+ }
+ toolCalls.push({
+ id: 'text_' + Date.now() + '_' + toolCalls.length,
+ type: 'function',
+ function: { name, arguments: args }
+ });
+ }
+ // Also handle single-line format: ```tool get_status{}
+ const simpleRegex = /```tool\s+(\w+)\s*\{([^}]*)\}\s*```/gi;
+ while ((match = simpleRegex.exec(text)) !== null) {
+ // Avoid duplicates — skip if already caught by blockRegex
+ const name = match[1];
+ if (!toolCalls.find(tc => tc.function.name === name)) {
+ toolCalls.push({
+ id: 'text_' + Date.now() + '_' + toolCalls.length,
+ type: 'function',
+ function: { name, arguments: {} }
+ });
+ }
+ }
+ return toolCalls;
+ }
_trackToolFailure(toolName, error) {
try {
const key = 'webx11_tool_failures';
@@ -571,7 +611,67 @@ RULES:
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) {
- const text = res.message.content || '';
+ let text = res.message.content || '';
+ // Fallback: parse ```tool blocks from text when model writes them instead of native tool_calls
+ let parsedToolCalls = this._parseToolBlocksFromText(text);
+ if (parsedToolCalls.length > 0) {
+ this.terminal.print(`🔧 Parsed ${parsedToolCalls.length} tool call(s) from text block — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
+ // Extract any non-tool text as pending assistant content
+ const cleanText = text.replace(/```tool[\s\S]*?```/g, '').trim();
+ if (cleanText) messages.push({ role: 'assistant', content: cleanText });
+ messages.push({ role: 'assistant', content: '(calling tool via text block)' });
+ let pendingText = cleanText || null;
+ const destructiveTools = parsedToolCalls.filter(tc => this.isDestructive(tc.function.name));
+ const safeTools = parsedToolCalls.filter(tc => !this.isDestructive(tc.function.name));
+ const safeResults = await Promise.all(safeTools.map(async (tc) => {
+ const fn = tc.function;
+ const t0 = Date.now();
+ const toolResult = await this.runTool(fn.name, fn.arguments);
+ const t1 = Math.round((Date.now() - t0) / 1000);
+ return { fn, toolResult, t1 };
+ }));
+ for (const { fn, toolResult, t1 } of safeResults) {
+ this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
+ const obs = this._summarizeObservation(toolResult, 150);
+ const err = this._isToolError(toolResult);
+ if (err) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
+ this._trackToolFailure(fn.name, err);
+ this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
+ } else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
+ this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
+ }
+ messages.push({ role: 'tool', content: toolResult });
+ }
+ if (destructiveTools.length > 0) {
+ const approved = await this.confirmActionMulti(destructiveTools);
+ if (approved) {
+ for (const tc of destructiveTools) {
+ const fn = tc.function;
+ const t0 = Date.now();
+ this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
+ const toolResult = await this.runTool(fn.name, fn.arguments);
+ const t1 = Math.round((Date.now() - t0) / 1000);
+ const dObs = this._summarizeObservation(toolResult, 150);
+ const dErr = this._isToolError(toolResult);
+ if (dErr) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
+ this._trackToolFailure(fn.name, dErr);
+ this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning');
+ } else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
+ this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
+ }
+ messages.push({ role: 'tool', content: toolResult });
+ }
+ } else {
+ messages.push({ role: 'tool', content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied.` });
+ }
+ }
+ if (pendingText) messages.push({ role: 'assistant', content: pendingText });
+ continue;
+ }
this.terminal.printAIResponse(text || '(no response)');
if (text) this.terminal.ollamaManager.fireResponseHooks(text);
this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 3f46b13..34ece29 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -11,6 +11,12 @@ class OllamaManager {
this.currentModel = localStorage.getItem('webx11_ollama_model') || 'gemma4:31b-cloud';
this.responseHooks = [];
this._abortController = null;
+ // --- AI Agent Loop Optimization Toggle ---
+ this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false';
+ // --- Validator Agent Config (future) ---
+ this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || '';
+ this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// Build fallback endpoints
this.fallbackEndpoints = [];
const origin = typeof window !== 'undefined' ? window.location.origin : '';
@@ -900,6 +906,25 @@ class OllamaManager {
saveSettings() {
localStorage.setItem('webx11_ollama_endpoint', this.endpoint);
localStorage.setItem('webx11_ollama_model', this.currentModel);
+ localStorage.setItem('webx11_optimization_enabled', this.optimizationEnabled ? 'true' : 'false');
+ localStorage.setItem('webx11_validator_enabled', this.validatorEnabled ? 'true' : 'false');
+ localStorage.setItem('webx11_validator_model', this.validatorModel);
+ localStorage.setItem('webx11_validator_endpoint', this.validatorEndpoint || '');
+ }
+
+ setOptimizationEnabled(val) {
+ this.optimizationEnabled = !!val;
+ this.terminal?.print(`Agent optimization ${this.optimizationEnabled ? 'enabled' : 'disabled'}. Reload to apply.`, 'info');
+ this.saveSettings();
+ }
+
+ setValidatorConfig(enabled, model) {
+ this.validatorEnabled = !!enabled;
+ if (model !== undefined) {
+ this.validatorModel = model;
+ localStorage.setItem('webx11_validator_model', model);
+ }
+ this.saveSettings();
}
getModels() { return this.models; }
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 1bfc4cd..5e6db6a 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -7,7 +7,7 @@
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- this.destructiveCmds = new Set(['rm','delete','del','edit','write','save','overwrite','format','kill','stop','shutdown','reboot']);
+ this.destructiveCmds = new Set(['rm','delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history']);
@@ -1143,6 +1143,7 @@
document.getElementById('ollama-endpoint').value = this.ollamaManager.endpoint;
this.updateOllamaStatus();
this.renderOllamaModels();
+ this.initOllamaUI();
modal.style.display = 'flex';
}
closeOllamaManager() {
@@ -1392,6 +1393,133 @@
const select = document.getElementById('ollama-current-model');
this.ollamaManager.setModel(select.value);
}
+ toggleOptimization() {
+ const cb = document.getElementById('ollama-optimization-toggle');
+ const label = document.getElementById('ollama-optimization-label');
+ const slider = document.getElementById('ollama-optimization-slider');
+ const enabled = cb.checked;
+ this.ollamaManager.setOptimizationEnabled(enabled);
+ if (label) {
+ label.textContent = enabled ? 'Enabled' : 'Disabled';
+ label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (slider) {
+ slider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ slider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ this.ollamaManager.saveSettings();
+ }
+ toggleValidator() {
+ const cb = document.getElementById('ollama-validator-toggle');
+ const label = document.getElementById('ollama-validator-label');
+ const slider = document.getElementById('ollama-validator-slider');
+ const enabled = cb.checked;
+ this.ollamaManager.validatorEnabled = enabled;
+ if (label) {
+ label.textContent = enabled ? 'Enabled' : 'Disabled';
+ label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (slider) {
+ slider.style.background = enabled ? 'var(--info-color)' : 'var(--bg-tertiary)';
+ slider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)';
+ }
+ this.ollamaManager.saveSettings();
+ }
+ saveValidatorConfig() {
+ const select = document.getElementById('validator-model-select');
+ const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434';
+ const model = select ? select.value : '';
+ localStorage.setItem('webx11_validator_endpoint', endpoint);
+ this.ollamaManager.validatorEndpoint = endpoint;
+ this.ollamaManager.setValidatorConfig(this.ollamaManager.validatorEnabled, model);
+ this.print('Validator config saved. Model: ' + (model || '(none)') + ' at ' + endpoint, 'success');
+ }
+ async connectValidatorEndpoint() {
+ const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434';
+ const statusEl = document.getElementById('validator-connection-status');
+ const select = document.getElementById('validator-model-select');
+ if (statusEl) statusEl.textContent = '⏳ Connecting to ' + endpoint + '...';
+ if (select) { select.disabled = true; select.innerHTML = '<option value="">Connecting...</option>'; }
+ try {
+ const resp = await fetch(endpoint + '/api/tags');
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
+ const data = await resp.json();
+ const models = data.models || [];
+ if (statusEl) statusEl.textContent = '✅ Connected: ' + models.length + ' model(s)';
+ // Populate dropdown
+ if (select) {
+ select.innerHTML = '<option value="">Select a validator model</option>';
+ models.forEach(function(m) {
+ var opt = document.createElement('option');
+ opt.value = m.name;
+ opt.textContent = m.name;
+ select.appendChild(opt);
+ });
+ select.disabled = false;
+ }
+ // Save endpoint
+ localStorage.setItem('webx11_validator_endpoint', endpoint);
+ this.ollamaManager.validatorEndpoint = endpoint;
+ this.ollamaManager.saveSettings();
+ } catch (e) {
+ if (statusEl) {
+ statusEl.innerHTML = '❌ Connection failed: ' + e.message +
+ '<br><span style="font-size:0.85em;color:var(--text-muted);">Make sure Ollama is running and ' +
+ '<code>OLLAMA_ORIGINS</code> includes this page\'s origin.</span>';
+ }
+ if (select) { select.disabled = true; select.innerHTML = '<option value="">Connection failed — try again</option>'; }
+ }
+ }
+ initOllamaUI() {
+ // Initialize optimization toggle from stored state
+ const optCb = document.getElementById('ollama-optimization-toggle');
+ const optLabel = document.getElementById('ollama-optimization-label');
+ const optSlider = document.getElementById('ollama-optimization-slider');
+ if (optCb && this.ollamaManager) {
+ const enabled = this.ollamaManager.optimizationEnabled !== false;
+ optCb.checked = enabled;
+ if (optLabel) {
+ optLabel.textContent = enabled ? 'Enabled' : 'Disabled';
+ optLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (optSlider) {
+ optSlider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ optSlider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ }
+ // Initialize validator toggle from stored state
+ const valCb = document.getElementById('ollama-validator-toggle');
+ const valLabel = document.getElementById('ollama-validator-label');
+ const valSlider = document.getElementById('ollama-validator-slider');
+ if (valCb && this.ollamaManager) {
+ const enabled = this.ollamaManager.validatorEnabled === true;
+ valCb.checked = enabled;
+ if (valLabel) {
+ valLabel.textContent = enabled ? 'Enabled' : 'Disabled';
+ valLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (valSlider) {
+ valSlider.style.background = enabled ? 'var(--info-color)' : 'var(--bg-tertiary)';
+ valSlider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)';
+ }
+ }
+ // Populate validator model dropdown from stored config
+ const valSelect = document.getElementById('validator-model-select');
+ const valEndpoint = document.getElementById('validator-endpoint-input');
+ if (valEndpoint && this.ollamaManager) {
+ valEndpoint.value = this.ollamaManager.validatorEndpoint || 'http://localhost:11434';
+ }
+ if (valSelect && this.ollamaManager) {
+ const storedModel = this.ollamaManager.validatorModel || '';
+ if (storedModel) {
+ valSelect.innerHTML = '<option value="' + storedModel + '">' + storedModel + '</option><option value="">Disable (use main AI model)</option>';
+ valSelect.disabled = false;
+ } else {
+ valSelect.innerHTML = '<option value="">Connect to load models</option>';
+ valSelect.disabled = true;
+ }
+ }
+ }
openStartupModules() {
document.getElementById('startup-modules').style.display = 'flex';
this.renderStartupModules();