diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-22 00:46:05 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-22 00:46:05 +0200 |
| commit | 70b9a2f8cc2b249362ca4893fdbb6703c0ea7b8a (patch) | |
| tree | b87027e9790b2bb0945f4192a5d08419fa58b478 /modules/core | |
| parent | 14c3cb36a3b037111a9fbdf8ff6ea32ee62b3be1 (diff) | |
feat(newos4): world-model-ask answers with the LLM, grounded in the model (v4.6.50)
`world-model-ask` was a 10-keyword lookup: any real question returned
"Unknown query. Try: modules, skills, ...". The documented example
("what changed since yesterday") compared nothing — it restated the current
snapshot.
- _modelDigest(): whitelisted snapshot (measured 942-965 chars on fixtures,
~3.3 KB live) + recent error sample + changesSinceLastSnapshot
- _buildAskPrompt(): "answer only from this JSON, never invent numbers, name the
command that would produce missing data"
- ask(): connects Ollama on demand, answers via chat(), falls back to the facts
path with a visible warning; --facts keeps the instant lookup
- _diffSincePrev(): keeps one previous snapshot in /system/world-model.prev.json
so "what changed" is a real diff (files 136 -> 140)
- world_model_query tool returns the snapshot (digest or detail:full) instead of
ten canned strings; no nested LLM call inside a tool call
- help text now says what it does; node test modules/test/test-world-model-ask.js
(11 assertions, runs the real class)
Verified live v4.6.50: facts "Loaded 46 modules" | LLM "The snapshot does not
contain information regarding the cause of memory growth ... use
world-model-refresh" | diff "files 136 -> 140" | tool snapshot 3,334 bytes.
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 131 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 17 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 114 |
4 files changed, 237 insertions, 27 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index f467f84..2264ef0 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -8260,7 +8260,7 @@ buildSystemPrompt() { + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n' + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n' - + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.'; + + 'predict_task: estimate duration before complex tasks. world_model_query: read the system snapshot (modules, files, agents, telemetry, recent errors) and answer from it.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -8811,11 +8811,12 @@ buildSystemPrompt() { type: 'function', function: { name: 'world_model_query', - description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".', + description: 'Read the synthesized system world model (modules, files, skills, agents, memory graph, telemetry, recent changes). Returns a JSON snapshot you can reason over; `facts` additionally carries the instant keyword answer. Ask detail:"full" for the complete model (~3 KB).', parameters: { type: 'object', properties: { - question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' } + question: { type: 'string', description: 'What you want to know (used for the fast facts line)' }, + detail: { type: 'string', description: '"digest" (default) or "full"' } }, required: ['question'] } @@ -11197,8 +11198,14 @@ buildSystemPrompt() { var wm = this.terminal.moduleManager.modules['WorldModelModule']; if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' }); if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' }); - var wmAnswer = wm._answerQuery(a.question || ''); - return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized }); + var wmOut = { + question: a.question, + facts: wm._answerQuery(a.question || ''), + version: wm._model.version, + synthesized: wm._model.synthesized, + snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + }; + return JSON.stringify(wmOut); } case 'health_check': case 'diff_module': @@ -17912,7 +17919,7 @@ if (typeof window !== 'undefined') { /* === module-world-model.js === */ // module-world-model.js — Global World Model: system self-awareness synthesis engine // Aggregates from memory graph, neural VFS, skills index, telemetry -// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config +// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config class WorldModelModule { constructor() { this.commands = {}; @@ -17933,7 +17940,7 @@ class WorldModelModule { return { 'world-model': 'world-model — Show full world model as structured output', 'world-model-summary': 'world-model-summary — One-line system self-awareness statement', - 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")', +'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)', 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems', 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects', }; @@ -18285,13 +18292,30 @@ class WorldModelModule { return summary; } - ask(question) { - if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error'); + async ask(question) { + if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error'); + const factsOnly = /(^|\s)--facts\b/.test(question); + if (factsOnly) question = question.replace(/--facts\b/, '').trim(); if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning'); - const answer = this._answerQuery(question); this.terminal.print(`❓ ${question}`, 'info'); - this.terminal.print(`💡 ${answer}`, 'success'); + + if (!factsOnly) { + const om = this.terminal.ollamaManager; + try { + if (om && !om.connected) { try { await om.connect(); } catch (e) {} } + if (om && om.connected) { + const answer = await om.chat(this._buildAskPrompt(question)); + if (answer && String(answer).trim()) { + this.terminal.print(`💡 ${String(answer).trim()}`, 'success'); + return; + } + } + } catch (e) { /* fall through to the facts path */ } + this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning'); + } + + this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success'); } // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool) @@ -18313,8 +18337,12 @@ class WorldModelModule { } else if (q.includes('memory') || q.includes('graph')) { return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`; } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) { - const recent = m.recent; - return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected'; + const diff = this._diffSincePrev(); + const recent = m.recent || []; + const parts = []; + if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; ')); + if (recent.length) parts.push('Current: ' + recent.join('; ')); + return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice'; } else if (q.includes('project') || q.includes('work')) { const projects = m.projects.activeProjects; return projects.length > 0 @@ -18329,6 +18357,80 @@ class WorldModelModule { } } + // 2026-10-21: ask() used to be a keyword table only, so any real question got + // "Unknown query". Whitelist the fields we are willing to send to a model + // (never the raw model object: it is persisted alongside other VFS data). + _modelDigest(maxChars) { + const m = this._model; + if (!m) return ''; + const cap = maxChars || 4000; + const pick = { + version: m.version, + synthesized: m.synthesized, + system: m.system, + modules: m.modules, + files: m.files, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + memory: m.memory, + agents: m.agents, + telemetry: m.telemetry, + projects: m.projects, + recent: (m.recent || []).slice(0, 20), + // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, + // so neither the user nor the LLM could say what failed. Carry the last few. + recentErrorSample: this._errorSample ? this._errorSample() : [], + changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] + }; + let json = JSON.stringify(pick); + if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; + return json; + } + + // Last few tool/agent errors with their tool name, for the digest (bounded). + _errorSample() { + try { + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + const db = ds && ds._errorDB && ds._errorDB.errors; + if (!db) return []; + return Object.keys(db).slice(-5).map(k => { + const e = db[k] || {}; + return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80); + }); + } catch (e) { return []; } + } + + _buildAskPrompt(question) { + return [ + 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.', + 'If the snapshot does not contain the answer, say what is missing and name the command that', + 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.', + 'Answer in 1-3 short sentences, plain text, no markdown headers.', + '', + 'SNAPSHOT: ' + this._modelDigest(), + '', + 'QUESTION: ' + String(question || '').trim() + ].join('\n'); + } + + _diffSincePrev() { + try { + const fs = this.terminal.fs; + const prev = fs && fs.fsReadJSON('/system/world-model.prev.json'); + const cur = this._model; + if (!prev || !cur) return []; + const out = []; + if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`); + const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']]; + fields.forEach(function (f) { + const a = prev[f[0]] && prev[f[0]][f[1]]; + const b = cur[f[0]] && cur[f[0]][f[1]]; + if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`); + }); + if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized); + return out; + } catch (e) { return []; } + } + refresh() { this._synthesize(); this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success'); @@ -18392,6 +18494,9 @@ class WorldModelModule { _saveToVFS(fs) { try { if (!fs.readDir('/system')) fs.mkdir('system'); + // 2026-10-21: keep one previous model so "what changed" can be a real diff. + const prevModel = fs.fsReadJSON('/system/world-model.json'); + if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} } // v4.5.89b fix: Clean stale temp from previous crash (P1 from review) try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {} fs.fsWriteJSON('/system/world-model.tmp.json', this._model); diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index f69494e..a8c0ae1 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -98,7 +98,7 @@ buildSystemPrompt() { + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n' + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n' - + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.'; + + 'predict_task: estimate duration before complex tasks. world_model_query: read the system snapshot (modules, files, agents, telemetry, recent errors) and answer from it.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -649,11 +649,12 @@ buildSystemPrompt() { type: 'function', function: { name: 'world_model_query', - description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".', + description: 'Read the synthesized system world model (modules, files, skills, agents, memory graph, telemetry, recent changes). Returns a JSON snapshot you can reason over; `facts` additionally carries the instant keyword answer. Ask detail:"full" for the complete model (~3 KB).', parameters: { type: 'object', properties: { - question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' } + question: { type: 'string', description: 'What you want to know (used for the fast facts line)' }, + detail: { type: 'string', description: '"digest" (default) or "full"' } }, required: ['question'] } @@ -3035,8 +3036,14 @@ buildSystemPrompt() { var wm = this.terminal.moduleManager.modules['WorldModelModule']; if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' }); if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' }); - var wmAnswer = wm._answerQuery(a.question || ''); - return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized }); + var wmOut = { + question: a.question, + facts: wm._answerQuery(a.question || ''), + version: wm._model.version, + synthesized: wm._model.synthesized, + snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + }; + return JSON.stringify(wmOut); } case 'health_check': case 'diff_module': diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 8330eaf..64ece7c 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 = 'v4.6.49'; + const LATEST_VER = 'v4.6.50'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js index 61eecfc..cbfd1fb 100644 --- a/modules/core/module-world-model.js +++ b/modules/core/module-world-model.js @@ -1,6 +1,6 @@ // module-world-model.js — Global World Model: system self-awareness synthesis engine // Aggregates from memory graph, neural VFS, skills index, telemetry -// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config +// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config class WorldModelModule { constructor() { this.commands = {}; @@ -21,7 +21,7 @@ class WorldModelModule { return { 'world-model': 'world-model — Show full world model as structured output', 'world-model-summary': 'world-model-summary — One-line system self-awareness statement', - 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")', +'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)', 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems', 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects', }; @@ -373,13 +373,30 @@ class WorldModelModule { return summary; } - ask(question) { - if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error'); + async ask(question) { + if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error'); + const factsOnly = /(^|\s)--facts\b/.test(question); + if (factsOnly) question = question.replace(/--facts\b/, '').trim(); if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning'); - const answer = this._answerQuery(question); this.terminal.print(`❓ ${question}`, 'info'); - this.terminal.print(`💡 ${answer}`, 'success'); + + if (!factsOnly) { + const om = this.terminal.ollamaManager; + try { + if (om && !om.connected) { try { await om.connect(); } catch (e) {} } + if (om && om.connected) { + const answer = await om.chat(this._buildAskPrompt(question)); + if (answer && String(answer).trim()) { + this.terminal.print(`💡 ${String(answer).trim()}`, 'success'); + return; + } + } + } catch (e) { /* fall through to the facts path */ } + this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning'); + } + + this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success'); } // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool) @@ -401,8 +418,12 @@ class WorldModelModule { } else if (q.includes('memory') || q.includes('graph')) { return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`; } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) { - const recent = m.recent; - return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected'; + const diff = this._diffSincePrev(); + const recent = m.recent || []; + const parts = []; + if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; ')); + if (recent.length) parts.push('Current: ' + recent.join('; ')); + return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice'; } else if (q.includes('project') || q.includes('work')) { const projects = m.projects.activeProjects; return projects.length > 0 @@ -417,6 +438,80 @@ class WorldModelModule { } } + // 2026-10-21: ask() used to be a keyword table only, so any real question got + // "Unknown query". Whitelist the fields we are willing to send to a model + // (never the raw model object: it is persisted alongside other VFS data). + _modelDigest(maxChars) { + const m = this._model; + if (!m) return ''; + const cap = maxChars || 4000; + const pick = { + version: m.version, + synthesized: m.synthesized, + system: m.system, + modules: m.modules, + files: m.files, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + memory: m.memory, + agents: m.agents, + telemetry: m.telemetry, + projects: m.projects, + recent: (m.recent || []).slice(0, 20), + // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, + // so neither the user nor the LLM could say what failed. Carry the last few. + recentErrorSample: this._errorSample ? this._errorSample() : [], + changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] + }; + let json = JSON.stringify(pick); + if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; + return json; + } + + // Last few tool/agent errors with their tool name, for the digest (bounded). + _errorSample() { + try { + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + const db = ds && ds._errorDB && ds._errorDB.errors; + if (!db) return []; + return Object.keys(db).slice(-5).map(k => { + const e = db[k] || {}; + return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80); + }); + } catch (e) { return []; } + } + + _buildAskPrompt(question) { + return [ + 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.', + 'If the snapshot does not contain the answer, say what is missing and name the command that', + 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.', + 'Answer in 1-3 short sentences, plain text, no markdown headers.', + '', + 'SNAPSHOT: ' + this._modelDigest(), + '', + 'QUESTION: ' + String(question || '').trim() + ].join('\n'); + } + + _diffSincePrev() { + try { + const fs = this.terminal.fs; + const prev = fs && fs.fsReadJSON('/system/world-model.prev.json'); + const cur = this._model; + if (!prev || !cur) return []; + const out = []; + if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`); + const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']]; + fields.forEach(function (f) { + const a = prev[f[0]] && prev[f[0]][f[1]]; + const b = cur[f[0]] && cur[f[0]][f[1]]; + if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`); + }); + if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized); + return out; + } catch (e) { return []; } + } + refresh() { this._synthesize(); this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success'); @@ -480,6 +575,9 @@ class WorldModelModule { _saveToVFS(fs) { try { if (!fs.readDir('/system')) fs.mkdir('system'); + // 2026-10-21: keep one previous model so "what changed" can be a real diff. + const prevModel = fs.fsReadJSON('/system/world-model.json'); + if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} } // v4.5.89b fix: Clean stale temp from previous crash (P1 from review) try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {} fs.fsWriteJSON('/system/world-model.tmp.json', this._model); |