@@ -25,6 +25,7 @@ export async function serveOnSessionBus(daemon) {
|
||||
async Say(text) { await daemon.say?.(text); }
|
||||
async Ask(text) { await daemon.ask(text); }
|
||||
async ResetContext() { await daemon.resetContext(); }
|
||||
Confirm(jobId, toolCallId, decision) { daemon.confirmPermission?.(jobId, toolCallId, decision); }
|
||||
Cancel() { daemon.cancel(); }
|
||||
SetMode(mode) { daemon.mode = mode; daemon.emit('ModeChanged', mode); }
|
||||
GetState() { return daemon.state; }
|
||||
@@ -42,6 +43,7 @@ export async function serveOnSessionBus(daemon) {
|
||||
// private $emitter. Returning the payload is sufficient; calling
|
||||
// `this.emit` here crashes because Interface does not expose that method.
|
||||
ConfirmationRequired(tool, args, pattern) { return [String(tool), String(args), String(pattern)]; }
|
||||
ContextReset() { return; }
|
||||
StateChanged(state) { return String(state); }
|
||||
Reply(text) { return String(text); }
|
||||
Token(text) { return String(text); }
|
||||
@@ -68,6 +70,7 @@ export async function serveOnSessionBus(daemon) {
|
||||
Say: { inSignature: 's', outSignature: '', method: 'Say' },
|
||||
Ask: { inSignature: 's', outSignature: '', method: 'Ask' },
|
||||
ResetContext: { inSignature: '', outSignature: '', method: 'ResetContext' },
|
||||
Confirm: { inSignature: 'sss', outSignature: '', method: 'Confirm' },
|
||||
Cancel: { inSignature: '', outSignature: '', method: 'Cancel' },
|
||||
SetMode: { inSignature: 's', outSignature: '', method: 'SetMode' },
|
||||
GetState: { inSignature: '', outSignature: 's', method: 'GetState' },
|
||||
@@ -141,7 +144,7 @@ export async function serveOnSessionBus(daemon) {
|
||||
daemon.on('Reply', (reply) => iface.Reply(reply));
|
||||
daemon.on('Token', (token) => iface.Token(token));
|
||||
daemon.on('Error', (code, message) => iface.Error(code, message));
|
||||
daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify(event?.args || {}), event?.pattern || 'explicit confirmation required'));
|
||||
daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify({ jobId: event?.jobId || '', toolCallId: event?.toolCallId || '', args: event?.args || {} }), event?.pattern || 'explicit confirmation required'));
|
||||
for (const signal of ['ContextReset', 'WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight', 'Thinking', 'ToolCall', 'ToolResult']) {
|
||||
daemon.on(signal, (...args) => iface[signal](...args));
|
||||
}
|
||||
|
||||
@@ -46,15 +46,22 @@ export class HarnessBridge extends EventEmitter {
|
||||
// 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 = ''; };
|
||||
let postTool = '';
|
||||
let sawTool = false;
|
||||
const onChunk = (payload) => {
|
||||
const chunk = String(payload?.text || payload?.delta || '');
|
||||
streamed += chunk;
|
||||
if (sawTool) postTool += chunk;
|
||||
};
|
||||
const onToolCall = () => { sawTool = true; postTool = ''; 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 };
|
||||
const spoken = String(reply?.text || '').trim() || postTool.trim() || streamed.trim();
|
||||
if (reply && spoken && spoken !== String(reply.text || '').trim()) return { ...reply, text: spoken };
|
||||
return reply;
|
||||
} finally {
|
||||
offChunk?.();
|
||||
|
||||
@@ -68,6 +68,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
const reply = await job;
|
||||
this.telemetry.record('llm', startedAt, { success: true });
|
||||
const spoken = spokenReply(reply);
|
||||
if (!spoken) { this.setState('LISTENING'); return ''; }
|
||||
this._beginSpeech(); this.lastReply = spoken; this.emit('Reply', spoken); this._speakReply(spoken); return spoken;
|
||||
} catch (error) {
|
||||
this.telemetry.record('llm', Date.now(), { success: false }); this.emit('Error', 'QVAC', error.message); this.voice.cancel(); this.setState('ARMED'); throw error;
|
||||
@@ -86,6 +87,9 @@ export class JarvisDaemon extends EventEmitter {
|
||||
this.setState('ARMED');
|
||||
this.emit('ContextReset');
|
||||
}
|
||||
confirmPermission(jobId, toolCallId, decision) {
|
||||
this.harness.session?.permit?.(String(jobId || ''), String(toolCallId || ''), String(decision || 'deny'));
|
||||
}
|
||||
_beginSpeech() {
|
||||
try {
|
||||
if (this.voice.state === 'ARMED' || this.voice.state === 'SLEEPING') this.voice.wake();
|
||||
|
||||
@@ -5,9 +5,12 @@ import os from 'node:os';
|
||||
export function voiceSettings() {
|
||||
let config = {};
|
||||
try { config = JSON.parse(fs.readFileSync(path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'jarvis/config.json'), 'utf8')); } catch {}
|
||||
const phrase = config.wakePhrase || config.wake_phrase || 'hey jarvis';
|
||||
const aliases = Array.isArray(config.aliases) ? config.aliases : ['jarvis', 'okay jarvis'];
|
||||
return {
|
||||
command: process.env.JARVIS_WAKE_COMMAND || config.wake_command || '',
|
||||
phrases: [config.wake_phrase || 'hey jarvis', 'jarvis', 'okay jarvis'],
|
||||
ttsEnabled: config.tts_enabled !== false,
|
||||
command: process.env.JARVIS_WAKE_COMMAND || config.wakeCommand || config.wake_command || '',
|
||||
phrases: [phrase, ...aliases].map((item) => String(item || '').trim()).filter(Boolean),
|
||||
ttsEnabled: config.ttsEnabled !== false && config.tts_enabled !== false,
|
||||
modelProfile: config.modelProfile || config.model_profile || 'laptop-16gb',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user