P0: Rust engine, wire daemon and audio-reactive descent surface

This commit is contained in:
Emil
2026-11-01 00:36:04 +03:00
parent cd967cced4
commit db4368efae
68 changed files with 19507 additions and 3 deletions
+470
View File
@@ -0,0 +1,470 @@
/* ZHILA · agent
the choir: a model on the local server, acting through the same verbs as the hand.
without a server or a key it falls back to a local conductor — still using the tools. */
(function () {
'use strict';
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.prototype.slice.call(r.querySelectorAll(s));
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const API = location.protocol === 'file:' ? 'http://127.0.0.1:4173' : '';
const E = window.ZHILA_ENGINE;
const Z = window.ZHILA;
const Q = window.ZHILA_QUARRY;
const logEl = $('#log');
const stateEl = $('#agent-state');
const stateText = $('#agent-state-text');
const netText = $('#net-text');
const TOOL_NAMES = ['get_state', 'list_cores', 'extract_core', 'fracture_core', 'strike_face',
'place_stone', 'move_stone', 'delete_stone', 'set_weather', 'set_rings',
'set_tempo', 'transport', 'open_core', 'set_macros'];
let live = false;
let busy = false;
let history = [];
let welcomed = false;
function timeNow() {
const d = new Date();
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
function scrollLog() {
if (logEl) logEl.scrollTop = logEl.scrollHeight;
}
function setStatus(text, thinking) {
if (!stateText) return;
stateText.textContent = text;
if (stateEl) stateEl.classList.toggle('is-thinking', !!thinking);
}
function addSay(text) {
if (!logEl || !text) return;
const li = document.createElement('li');
li.className = 'call call--say';
li.innerHTML = '<span class="call-t"></span><p></p>';
li.querySelector('.call-t').textContent = timeNow();
li.querySelector('p').textContent = text;
logEl.appendChild(li);
scrollLog();
}
function addYou(text) {
if (!logEl) return;
const li = document.createElement('li');
li.className = 'call call--you';
li.innerHTML = '<span class="call-t"></span><p></p>';
li.querySelector('.call-t').textContent = timeNow();
li.querySelector('p').textContent = '\u201C' + text + '\u201D';
logEl.appendChild(li);
scrollLog();
}
function argsHTML(args) {
const keys = Object.keys(args || {});
if (!keys.length) return '<span class="p">{ }</span>';
return '<span class="p">{ </span>' + keys.map(function (k) {
let v = args[k];
let val;
if (typeof v === 'string') val = '"' + esc(v) + '"';
else if (typeof v === 'object' && v !== null) val = esc(JSON.stringify(v));
else val = esc(String(v));
return '<span class="k">"' + esc(k) + '"</span><span class="p">: </span><span class="v">' + val + '</span>';
}).join('<span class="p">, </span>') + '<span class="p"> }</span>';
}
function argLine(name, a) {
const pad = n => String(n).padStart(2, '0');
const pitch = n => n ? (n > 0 ? '+' : '') + n + 'st' : '';
switch (name) {
case 'place_stone':
return [a.vein, a.core_id + ' f' + pad(a.face), a.start_bar + '–' + (+a.start_bar + +a.length_bars),
a.ring_bars ? 'ring ' + a.ring_bars : 'once', pitch(a.pitch), a.gain ? 'gain ' + a.gain : ''].filter(Boolean).join(' · ');
case 'strike_face':
return a.core_id + ' f' + pad(a.face);
case 'extract_core':
return a.name || 'new core';
case 'fracture_core':
case 'open_core':
return a.core_id;
case 'move_stone':
return 'stone ' + a.stone_id + ' → bar ' + a.start_bar;
case 'delete_stone':
return 'stone ' + a.stone_id;
case 'set_weather':
return ['age', 'pressure', 'air', 'heat'].filter(function (k) { return a[k] != null; })
.map(function (k) { return k + ' ' + a[k]; }).join(' · ');
case 'set_rings':
return a.count + ' rings';
case 'set_tempo':
return a.bpm + ' bpm';
case 'transport':
return a.action + (a.bar ? ' · bar ' + a.bar : '');
case 'set_macros':
return ['warmth', 'dust', 'threshold'].filter(function (k) { return a[k] != null; })
.map(function (k) { return k + ' ' + a[k]; }).join(' · ');
default:
return '';
}
}
function addTool(name, args, source) {
const li = document.createElement('li');
li.className = 'call call--tool';
li.innerHTML =
'<div class="call-head">' +
'<span class="call-t">' + timeNow() + '</span>' +
'<span class="call-name"></span>' +
'<span class="call-src">' + (source || 'api') + '</span>' +
'<span class="call-ms">…</span>' +
'</div>' +
'<div class="call-args call-args--line">' + esc(argLine(name, args)) + '</div>';
li.querySelector('.call-name').textContent = name;
li.title = JSON.stringify(args);
logEl.appendChild(li);
scrollLog();
return li;
}
function summarize(name, res) {
if (!res) return '';
if (res.error) return 'error: ' + res.error;
switch (name) {
case 'get_state':
return (res.stones ? res.stones.length : 0) + ' stones · bar ' + res.transport.bar + ' · ' + res.cores.length + ' cores · ' + res.view;
case 'list_cores':
return res.cores.length + ' cores: ' + res.cores.map(function (c) { return c.id; }).join(', ');
case 'extract_core':
return res.name + ' · ' + res.dur + 's · ' + res.seal;
case 'fracture_core':
return res.id + ' · re-cut · ' + res.loud.slice(0, 3).join(', ');
case 'strike_face':
return 'struck ' + res.core + ' f' + String(res.face + 1).padStart(2, '0');
case 'place_stone':
return res.vein + ' · bars ' + res.bars[0] + '–' + res.bars[1] + (res.ring ? ' · ring ' + res.ring : ' · once');
case 'move_stone':
return 'bars ' + res.bars[0] + '–' + res.bars[1];
case 'delete_stone':
return 'buried';
case 'set_weather':
return Object.keys(res.weather).map(function (k) { return k + ' ' + (+res.weather[k]).toFixed(2); }).join(' · ');
case 'set_rings':
return 'rings ' + res.rings;
case 'set_tempo':
return res.bpm + ' bpm';
case 'transport':
return (res.playing ? 'playing' : 'paused') + ' · bar ' + res.bar;
case 'open_core':
return 'quarry · ' + res.core;
case 'set_macros':
return Object.keys(res).map(function (k) { return k + ' ' + (+res[k]).toFixed(2); }).join(' · ');
default:
return JSON.stringify(res).slice(0, 140);
}
}
function finishTool(li, ms, res) {
if (!li) return;
const t = li.querySelector('.call-ms');
if (t) t.textContent = ms;
const name = li.querySelector('.call-name').textContent;
const read = name === 'get_state' || name === 'list_cores';
const line = document.createElement('div');
line.className = 'call-result';
line.textContent = (res && res.error ? 'error · ' : (read ? '' : 'ok · ')) + summarize(name, res);
if (res && res.error) line.style.color = 'rgba(232,64,42,.8)';
li.appendChild(line);
scrollLog();
}
/* ————— the verbs ————— */
async function exec(name, a) {
a = a || {};
if (!E) throw new Error('no engine');
switch (name) {
case 'get_state': {
return {
transport: { playing: E.isPlaying(), bar: +E.getPos().toFixed(2), bpm: E.getBpm() },
weather: Q ? Q.weather() : E.weather,
rings: Q ? Q.rings() : E.rings,
cores: E.cores.map(function (c) {
return { id: c.id, name: c.name, dur: +c.dur.toFixed(1), seal: c.seal, loud: c.ranked.slice(0, 4) };
}),
stones: Z.stones(),
shape: Z.shape(),
view: document.body.dataset.view,
selected_core: Q && Q.selectedCore() ? Q.selectedCore().id : null
};
}
case 'list_cores': {
return {
cores: E.cores.map(function (c) {
return {
id: c.id, name: c.name, dur: +c.dur.toFixed(1), seal: c.seal, faces: 16,
loud: c.ranked.slice(0, 5),
at: c.ranked.slice(0, 5).map(function (i) { return +c.faces[i].start.toFixed(2); })
};
})
};
}
case 'extract_core': {
const c = E.newCore({ name: (a.name || 'a core from the quarry').slice(0, 40), seal: a.seal || 'found' });
E.addLog('cores.extract', c.name, c.seal, 'api');
return { id: c.id, name: c.name, dur: +c.dur.toFixed(1), seal: c.seal, loud: c.ranked.slice(0, 4) };
}
case 'fracture_core': {
const c = E.refracture(a.core_id);
if (!c) throw new Error('no such core: ' + a.core_id);
if (Q) Q.refresh();
return { id: c.id, loud: c.ranked.slice(0, 4), faces: c.faces.length };
}
case 'strike_face': {
const core = E.byId(a.core_id);
if (!core) throw new Error('no such core: ' + a.core_id);
const face = clamp(Math.round(+a.face || 0), 0, 15);
E.strike(core.id, face, { pitch: 0, gain: 0.55 + 0.4 * (a.velocity == null ? 1 : a.velocity), ghost: true });
return { core: core.id, face: face, struck: true };
}
case 'place_stone': {
const r = Z.addStoneAt({
vein: a.vein, core: a.core_id, face: a.face,
start: a.start_bar, length: a.length_bars, ring: a.ring_bars,
pitch: a.pitch, gain: a.gain, name: a.name, actor: 'api'
});
if (r.error) throw new Error(r.error);
return r;
}
case 'move_stone': {
const r = Z.moveStoneAt(a.stone_id, a.start_bar);
if (r.error) throw new Error(r.error);
return r;
}
case 'delete_stone': {
const r = Z.deleteStoneAt(a.stone_id);
if (r.error) throw new Error(r.error);
return r;
}
case 'set_weather': {
const w = Q ? Q.setWeatherUI({ age: a.age, pressure: a.pressure, air: a.air, heat: a.heat }) : E.weather;
return { weather: w };
}
case 'set_rings': {
return { rings: Q ? Q.setRingsUI(a.count) : E.rings };
}
case 'set_tempo':
return { bpm: Z.setTempo(a.bpm) };
case 'transport':
return Z.transport(a.action, a.bar);
case 'open_core': {
if (Q) { Q.setView('quarry'); Q.focusCore(a.core_id); }
return { core: a.core_id, view: 'quarry' };
}
case 'set_macros': {
const out = {};
['warmth', 'dust', 'threshold'].forEach(function (k) {
if (a[k] != null) out[k] = Z.setMacroUI(k, a[k]);
});
return out;
}
default:
throw new Error('unknown tool: ' + name);
}
}
async function invokeTool(name, args, source) {
const li = addTool(name, args, source);
const t0 = performance.now();
let res;
try { res = await exec(name, args); }
catch (e) { res = { error: String(e && e.message || e) }; }
finishTool(li, ((performance.now() - t0) / 1000).toFixed(1) + 's', res);
return res;
}
/* ————— the model loop ————— */
function trimHistory() {
const h = history.slice();
while (h.length > 28) {
const i = h.findIndex(function (m) { return m.role === 'user'; });
if (i < 0) break;
h.splice(0, i + 1);
}
return h;
}
async function modelSay(text) {
history.push({ role: 'user', content: text });
setStatus('thinking', true);
let ran = 0;
let lastError = null;
let wireDown = false;
for (let step = 0; step < 6; step++) {
let r, j;
try {
r = await fetch(API + '/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: trimHistory() }),
signal: AbortSignal.timeout(95000)
});
j = await r.json().catch(function () { return null; });
} catch (e) {
lastError = e;
wireDown = true;
break;
}
if (!r.ok) {
lastError = new Error((j && j.error) || ('agent ' + r.status));
if (j && j.mode === 'local') { wireDown = true; live = false; }
break;
}
const m = j && j.message;
if (!m) { lastError = new Error('empty reply'); break; }
history.push(m);
if (m.content && m.content.trim()) addSay(m.content.trim());
if (!m.tool_calls || !m.tool_calls.length) { lastError = null; break; }
for (let k = 0; k < m.tool_calls.length; k++) {
const tc = m.tool_calls[k];
const name = tc.function && tc.function.name;
let args = {};
try { args = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (e) {}
setStatus('calling ' + name, true);
const res = await invokeTool(name, args, 'api');
ran++;
history.push({ role: 'tool', tool_call_id: tc.id, name: name, content: JSON.stringify(res) });
}
}
if (lastError) {
if (wireDown) {
live = false;
document.body.classList.add('agent-offline');
if (netText) netText.textContent = 'local';
}
addSay('the choir lost the wire: ' + lastError.message + '.');
if (!ran) localSay(text, true);
}
}
/* ————— local conductor (no model) ————— */
async function localSay(text, afterError) {
const t = text.toLowerCase();
const core = (Q && Q.selectedCore()) || E.cores[0];
if (!core) {
addSay('there is no material in the archive yet.');
return;
}
const done = [];
const wantsStop = /(stop|стоп|пауза|pause)/.test(t);
const wantsPlay = /(play|играй|пуск|запусти|start)/.test(t);
if (wantsStop) { await invokeTool('transport', { action: 'pause' }, 'local'); done.push('paused'); }
else if (wantsPlay) { await invokeTool('transport', { action: 'play' }, 'local'); done.push('the wheels are turning'); }
if (/(fracture|cut|реж|трещин|разрез)/.test(t)) {
await invokeTool('fracture_core', { core_id: core.id }, 'local');
done.push('re-cut ' + core.id);
} else if (/(take|bring|extract|найд|принес|достань|новый|room|поле|запись)/.test(t)) {
const c = await invokeTool('extract_core', { name: text.slice(0, 42), seal: 'found' }, 'local');
done.push('the choir brought ' + c.name);
} else if (/(dark|темн|стар|old|fossil|истощ)/.test(t)) {
const w = await invokeTool('set_weather', { age: 0.72, air: 0.3 }, 'local');
done.push('weather: age 0.72, air 0.30');
} else if (/(wet|мокр|room|воздух|air|простор)/.test(t)) {
await invokeTool('set_weather', { air: 0.74, pressure: 0.3 }, 'local');
done.push('weather: air 0.74, pressure 0.30');
}
const wantsPlace = /(place|add|layer|слой|добавь|постав|loop|петл|пульс|bass|низ|голос|шум|звук|дыш)/.test(t);
if (wantsPlace || !done.length) {
let vein = 'silt';
if (/(bass|низ|бас|пол)/.test(t)) vein = 'bass';
else if (/(voice|голос|хор|петь)/.test(t)) vein = 'voice';
else if (/(noise|шум|воздух|ветер)/.test(t)) vein = 'noise';
else if (/(crack|трещ|грит)/.test(t)) vein = 'crack';
const face = E.rankFace(core.id, vein === 'bass' ? 0 : 1);
const start = Math.min(30, Math.ceil(E.getPos()) + 1);
const ring = /(once|один|раз|one)/.test(t) ? 0 : 2;
const pitch = vein === 'bass' ? -5 : 0;
const res = await invokeTool('place_stone', {
vein: vein, core_id: core.id, face: face,
start_bar: start, length_bars: 8, ring_bars: ring, pitch: pitch, gain: 0.8
}, 'local');
if (!res.error) done.push('laid ' + res.name + ' in ' + res.vein + ', bars ' + res.bars[0] + '–' + res.bars[1]);
}
addSay((afterError ? 'the wire is down, so i worked by hand. ' : '') + (done.length ? done.join('. ') + '.' : 'i listened, but found nothing to change.'));
}
async function say(text) {
text = String(text || '').trim();
if (!text) return;
if (busy) {
addSay('one at a time — the choir is still working.');
return;
}
busy = true;
addYou(text);
try {
if (live) {
await modelSay(text);
} else {
setStatus('local', false);
await localSay(text, false);
}
} catch (e) {
addSay('the choir failed: ' + (e && e.message || e));
} finally {
busy = false;
setStatus(live ? 'listening' : 'local', false);
}
}
/* ————— boot ————— */
async function init() {
const meta = $('.state-meta');
if (meta) meta.textContent = TOOL_NAMES.length + ' tools';
try {
const r = await fetch(API + '/api/agent', { signal: AbortSignal.timeout(3000) });
const j = await r.json();
live = !!j.live;
} catch (e) {
live = false;
}
document.body.classList.toggle('agent-offline', !live);
if (netText) netText.textContent = live ? 'live' : 'local';
const chip = $('.chip');
if (chip) {
chip.classList.toggle('is-live', live);
chip.title = live ? 'Embedded agent: live model on the local server' : 'Embedded agent: local conductor (no model on the wire)';
}
setStatus(live ? 'listening' : 'local', false);
if (!welcomed) {
welcomed = true;
addSay(live
? 'the choir is listening. say what should be heard — i can take cores, cut faces, lay stones, or change the weather.'
: 'the choir is local: no model on the wire. i can still take, cut, and lay by rule.');
}
}
window.ZHILA_AGENT = {
say: say,
isLive: function () { return live; },
isBusy: function () { return busy; },
tools: TOOL_NAMES.length
};
if (document.readyState === 'complete' || document.readyState === 'interactive') {
window.setTimeout(init, 250);
} else {
document.addEventListener('DOMContentLoaded', function () { window.setTimeout(init, 250); });
}
})();