diff --git a/daemon/harness-bridge.js b/daemon/harness-bridge.js index 29cbe6e..c557cb4 100644 --- a/daemon/harness-bridge.js +++ b/daemon/harness-bridge.js @@ -42,7 +42,24 @@ export class HarnessBridge extends EventEmitter { async ask(text) { if (!this.session) await this.start(); - return this.session.prompt(text); + // 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(); } diff --git a/test/daemon.test.js b/test/daemon.test.js index 0454765..3612e5b 100644 --- a/test/daemon.test.js +++ b/test/daemon.test.js @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import { VoiceStateMachine } from '../daemon/voice-state.js'; import { QvacScheduler } from '../daemon/qvac-scheduler.js'; import { JarvisDaemon } from '../daemon/index.js'; +import { HarnessBridge } from '../daemon/harness-bridge.js'; import { spokenReply } from '../skills/voice-prompt.js'; test('voice state machine handles wake, reply, cancel, and idle sleep', () => { @@ -53,6 +54,20 @@ test('ask extracts harness reply text instead of stringifying the object', async } }); +test('harness bridge recovers streamed text when final envelope is empty after a tool call', async () => { + const session = new (await import('node:events')).EventEmitter(); + session.prompt = async () => { + session.emit('agent_message_chunk', { text: 'I will check that. ' }); + session.emit('tool_call', { call: { name: 'runtime_status' } }); + session.emit('agent_message_chunk', { text: 'Your computer is ready.' }); + return { ok: true, text: '', reason: 'stop' }; + }; + const bridge = Object.create(HarnessBridge.prototype); + bridge.session = session; + const reply = await bridge.ask('How is my computer?'); + assert.equal(reply.text, 'Your computer is ready.'); +}); + test('QVAC scheduler prioritizes voice and keeps one active job', async () => { const scheduler = new QvacScheduler(); const order = [];