@@ -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(); }
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user