Files
gnome-jarvis/daemon/harness-bridge.js
T
snxraven af9c490532
Rolling release / release (push) Successful in 9m50s
Updates
2026-09-11 20:55:17 -04:00

79 lines
3.4 KiB
JavaScript

import { EventEmitter } from 'node:events';
import { acquireQvac, closeQvac, releaseQvac, Agent } from './qvac-master.js';
import { createRuntimeTools } from '../skills/runtime-tools.js';
import { createPhase2Tools } from '../skills/phase2-tools.js';
import { createQvacTools } from '../skills/qvac-tools.js';
import { VOICE_SYSTEM_PROMPT, parseHudSidecar } from '../skills/voice-prompt.js';
import { createComputerObserveTools } from '../skills/computer-observe.js';
import { createComputerActTools } from '../skills/computer-act.js';
import { createPhase9GatewayTool } from '../skills/phase9-tools.js';
export class HarnessBridge extends EventEmitter {
constructor({ cwd = process.cwd(), model = 'qwen3.5-4b', tools = [], computer, observer, actuator, permissionMode = 'ask' } = {}) {
super();
this.options = { cwd, model, tools: [...createRuntimeTools({ computer }), ...createPhase2Tools({ cwd, computer }), ...createComputerObserveTools({ computer, observer }), ...createComputerActTools({ actuator }), ...createQvacTools(), ...createPhase9GatewayTool(), ...tools], permissionMode, origin: 'jarvis-qvac', system: VOICE_SYSTEM_PROMPT };
this.session = null;
}
async start() {
if (this.session) return this.session;
if (process.env.JARVIS_QVAC_MODEL && this.options.model !== process.env.JARVIS_QVAC_MODEL) {
throw new Error(`Jarvis uses one QVAC master model (${process.env.JARVIS_QVAC_MODEL}); requested ${this.options.model}`);
}
await acquireQvac();
try {
this.session = await Agent.create(this.options);
} catch (error) {
releaseQvac();
await closeQvac();
throw error;
}
for (const event of ['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_result', 'permission', 'ask_user', 'cap-chunk', 'error']) {
this.session.on(event, (payload) => {
if (event === 'agent_message_chunk' && payload?.text) {
const parsed = parseHudSidecar(payload.text);
if (parsed.hud) this.emit('hud_sidecar', parsed.hud);
}
this.emit(event, payload);
});
}
return this.session;
}
async ask(text) {
if (!this.session) await this.start();
// Some QVAC runs deliver the final assistant text through the stream but
// omit it from the final envelope after a tool call. Keep the current
// post-tool stream as a fallback so the daemon can still speak the reply.
let streamed = '';
const onChunk = (payload) => { streamed += String(payload?.text || payload?.delta || ''); };
const onToolCall = () => { streamed = ''; };
const chunkSubscription = this.session.on('agent_message_chunk', onChunk);
const toolSubscription = this.session.on('tool_call', onToolCall);
const offChunk = typeof chunkSubscription === 'function' ? chunkSubscription : () => this.session.off?.('agent_message_chunk', onChunk);
const offToolCall = typeof toolSubscription === 'function' ? toolSubscription : () => this.session.off?.('tool_call', onToolCall);
try {
const reply = await this.session.prompt(text);
if (reply && !reply.text && streamed.trim()) return { ...reply, text: streamed };
return reply;
} finally {
offChunk?.();
offToolCall?.();
}
}
cancel() { this.session?.cancel(); }
async resetContext() {
this.session?.cancel?.();
await this.session?.dispose?.();
this.session = null;
}
async close() {
await this.session?.dispose();
releaseQvac();
await closeQvac();
}
}