aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-world-model.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-22 00:46:05 +0200
committerAgando <roduccomm@gmail.com>2026-10-22 00:46:05 +0200
commit70b9a2f8cc2b249362ca4893fdbb6703c0ea7b8a (patch)
treeb87027e9790b2bb0945f4192a5d08419fa58b478 /modules/core/module-world-model.js
parent14c3cb36a3b037111a9fbdf8ff6ea32ee62b3be1 (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/module-world-model.js')
-rw-r--r--modules/core/module-world-model.js114
1 files changed, 106 insertions, 8 deletions
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);