/** * Voice/coding turn budgets. No Bare imports. * * Coding agents may chain many shells. A spoken GNOME assistant should run one * command, then answer — otherwise a 4B model loops hostnamectl/uname/free. */ function num(value, fallback) { const n = Number(value); return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback; } function fromPayload(payload, origin) { payload = payload || {}; const voice = payload.voice === true || origin === 'jarvis-qvac'; const unlimitedShell = payload.maxShellCalls === 0 || payload.maxShellCalls === false; return { voice, maxTurns: num(payload.maxTurns, voice ? 6 : 24), maxShellCalls: unlimitedShell ? 0 : num(payload.maxShellCalls, voice ? 1 : 0), maxToolRounds: num(payload.maxToolRounds, voice ? 6 : 0), completeTimeoutMs: voice ? 45000 : 0, completeIdleMs: voice ? 10000 : 0, shellCalls: 0, toolRounds: 0, answerOnly: false, }; } function capped(limit) { return limit > 0; } function shouldSkipShell(budget) { return !!(budget && capped(budget.maxShellCalls) && budget.shellCalls >= budget.maxShellCalls); } function markShell(budget) { if (!budget) return; budget.shellCalls += 1; if (shouldSkipShell(budget)) budget.answerOnly = true; } function markToolRound(budget) { if (!budget) return; budget.toolRounds += 1; if (capped(budget.maxToolRounds) && budget.toolRounds >= budget.maxToolRounds) budget.answerOnly = true; } function forceAnswer(budget) { if (budget) budget.answerOnly = true; } function skipShellMessage() { return 'A terminal command already ran this turn. Answer the user from that output. Do not run another command.'; } function answerNowMessage() { return 'You have tool results. Reply to the user in one to three sentences. Do not call more tools.'; } const UNFINISHED_TOOL = /\b(let me|i(?:'m| am) going to|i(?:'ll| will)|i should|need to)\b[\s\S]{0,160}\b(fetch|search|look(?:ing)? up|check|open|read|run|call|use)\b/i; function shouldNudgeToolCall(text, budget) { if (!budget || !budget.voice || budget.answerOnly) return false; const blob = String(text || ''); if (UNFINISHED_TOOL.test(blob)) return true; return /\b(search(?:ing)? again|fetch (?:a |the |that )|look(?:ing)? that up)\b/i.test(blob); } function continueToolMessage() { return 'Thinking is not an answer. Call the tool now with a concrete query or URL, then speak the result. Do not describe the next step.'; } function lastToolText(history, maxChars) { const list = Array.isArray(history) ? history : []; for (let i = list.length - 1; i >= 0; i--) { if (list[i] && list[i].role === 'tool' && list[i].content) { const text = String(list[i].content).replace(/\s+/g, ' ').trim(); if (!text) continue; const max = maxChars > 0 ? maxChars : 400; return text.length > max ? text.slice(0, max) + '…' : text; } } return ''; } module.exports = { fromPayload, shouldSkipShell, markShell, markToolRound, forceAnswer, skipShellMessage, answerNowMessage, shouldNudgeToolCall, continueToolMessage, lastToolText, };