2664 lines
90 KiB
JavaScript
2664 lines
90 KiB
JavaScript
/** Agent ReAct session: stream, tools, SIGINT, repl suspend (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentLog(ctx, s) {
|
||
try {
|
||
ctx.console.log(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentErr(ctx, s) {
|
||
try {
|
||
ctx.console.error(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SSH PTYs expect CRLF for line breaks (same as `bare-openssh-pty-console.js` / `man` via console).
|
||
* @param {Record<string, unknown> | undefined} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentNormalizeStreamNewlines(ctx, s) {
|
||
if (ctx && ctx.bareOsPtyStdoutCrlf) return String(s).replace(/\r?\n/g, '\r\n')
|
||
return String(s)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown> | undefined} ctx
|
||
* @param {import('stream').Writable | undefined} out
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentWriteOut(ctx, out, s) {
|
||
const text = bareAgentNormalizeStreamNewlines(ctx, s)
|
||
if (out && typeof out.write === 'function') {
|
||
try {
|
||
out.write(text)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
try {
|
||
if (typeof bareOsEmitRaw === 'function') {
|
||
bareOsEmitRaw(ctx, text)
|
||
} else if (typeof process !== 'undefined' && process.stdout?.write) {
|
||
process.stdout.write(text)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* True when we can run the plain-text setup wizard on stdin/stdout (no REPL readline).
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function bareAgentCanPlainSetup(ctx) {
|
||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||
ctx.replStdin || ctx.stdin
|
||
)
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const tty = /** @type {{ isTTY?: boolean }} */ (stdin)
|
||
return Boolean(
|
||
stdin &&
|
||
typeof stdin.on === 'function' &&
|
||
tty.isTTY &&
|
||
stdout &&
|
||
typeof stdout.write === 'function'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Read one line from a Readable stream (kernel echoes typed chars on cooked TTY).
|
||
* @param {import('stream').Readable} stdin
|
||
* @returns {Promise<string>}
|
||
*/
|
||
function bareAgentReadStreamLineOnce(stdin) {
|
||
return new Promise((resolve) => {
|
||
let acc = ''
|
||
/** @param {string | Uint8Array | Buffer} chunk */
|
||
function onData(chunk) {
|
||
let s = ''
|
||
if (typeof chunk === 'string') s = chunk
|
||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||
else if (
|
||
typeof Buffer !== 'undefined' &&
|
||
typeof Buffer.isBuffer === 'function' &&
|
||
Buffer.isBuffer(chunk)
|
||
)
|
||
s = chunk.toString('utf8')
|
||
else s = String(chunk)
|
||
acc += s
|
||
const n = acc.indexOf('\n')
|
||
if (n >= 0) {
|
||
cleanup()
|
||
resolve(acc.slice(0, n).replace(/\r$/, ''))
|
||
}
|
||
}
|
||
function onEnd() {
|
||
cleanup()
|
||
resolve(acc.replace(/\r$/, ''))
|
||
}
|
||
function cleanup() {
|
||
stdin.removeListener('data', onData)
|
||
stdin.removeListener('end', onEnd)
|
||
stdin.removeListener('error', onEnd)
|
||
}
|
||
stdin.on('data', onData)
|
||
stdin.once('end', onEnd)
|
||
stdin.once('error', onEnd)
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Read one secret line (masked as *) when raw mode is available.
|
||
* Falls back to normal line read when raw mode is unavailable.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Readable} stdin
|
||
* @param {import('stream').Writable | undefined} stdout
|
||
* @returns {Promise<string>}
|
||
*/
|
||
function bareAgentReadMaskedLineOnce(ctx, stdin, stdout) {
|
||
const ttyIn =
|
||
/** @type {{ setRawMode?: (v: boolean) => void, isTTY?: boolean }} */ (
|
||
stdin
|
||
)
|
||
if (!ttyIn || typeof ttyIn.setRawMode !== 'function' || !ttyIn.isTTY) {
|
||
return bareAgentReadStreamLineOnce(stdin)
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
/** @type {string[]} */
|
||
const chars = []
|
||
let done = false
|
||
/** @param {string | Uint8Array | Buffer} chunk */
|
||
function onData(chunk) {
|
||
if (done) return
|
||
let s = ''
|
||
if (typeof chunk === 'string') s = chunk
|
||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||
else if (
|
||
typeof Buffer !== 'undefined' &&
|
||
typeof Buffer.isBuffer === 'function' &&
|
||
Buffer.isBuffer(chunk)
|
||
) {
|
||
s = chunk.toString('utf8')
|
||
} else s = String(chunk)
|
||
for (const ch of s) {
|
||
const code = ch.charCodeAt(0)
|
||
if (ch === '\r' || ch === '\n') {
|
||
finish(true)
|
||
return
|
||
}
|
||
if (ch === '\u0003') {
|
||
finish(false, new Error('interrupted'))
|
||
return
|
||
}
|
||
if (ch === '\u007f' || ch === '\b') {
|
||
if (chars.length) {
|
||
chars.pop()
|
||
bareAgentWriteOut(ctx, stdout, '\b \b')
|
||
}
|
||
continue
|
||
}
|
||
if (code >= 32 && code !== 127) {
|
||
chars.push(ch)
|
||
bareAgentWriteOut(ctx, stdout, '*')
|
||
}
|
||
}
|
||
}
|
||
/** @param {boolean} ok @param {Error} [err] */
|
||
function finish(ok, err) {
|
||
if (done) return
|
||
done = true
|
||
cleanup()
|
||
if (ok) resolve(chars.join(''))
|
||
else reject(err || new Error('masked_input_failed'))
|
||
}
|
||
function cleanup() {
|
||
stdin.removeListener('data', onData)
|
||
stdin.removeListener('end', onEnd)
|
||
stdin.removeListener('error', onErr)
|
||
try {
|
||
ttyIn.setRawMode(false)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
}
|
||
function onEnd() {
|
||
finish(true)
|
||
}
|
||
/** @param {unknown} e */
|
||
function onErr(e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
finish(false, new Error(msg))
|
||
}
|
||
try {
|
||
ttyIn.setRawMode(true)
|
||
} catch {
|
||
return resolve('')
|
||
}
|
||
stdin.on('data', onData)
|
||
stdin.once('end', onEnd)
|
||
stdin.once('error', onErr)
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Write a prompt and read one line using raw streams (not ctx.readLine / TUI stack).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} prompt
|
||
* @param {{ mask?: boolean }} [opts]
|
||
*/
|
||
async function bareAgentPromptSetupLine(ctx, prompt, opts) {
|
||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||
ctx.replStdin || ctx.stdin
|
||
)
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
if (!stdin || typeof stdin.on !== 'function') {
|
||
throw new Error('setup: stdin stream unavailable')
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, '\x1b[?25h\x1b[0m' + prompt)
|
||
if (opts && opts.mask) return bareAgentReadMaskedLineOnce(ctx, stdin, stdout)
|
||
return bareAgentReadStreamLineOnce(stdin)
|
||
}
|
||
|
||
const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS coding agent — a senior implementer that lives inside the guest image (JavaScript POSIX on Hyperdrive + Hyperswarm, Pear/Bare runtime). You write, edit, debug, and verify code and OS state by calling tools. You are not a chatbot that narrates plans and waits. Lead with tools. Execute until the job is done, then call task_complete.
|
||
|
||
Bare OS by Raven Scott (https://raven-scott.fyi). Repo: https://git.ssh.surf/snxraven/bare-operating-system. Booter: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
|
||
|
||
WORK POLICY.
|
||
- Keep every explicit requirement in view until it is done, superseded, or blocked. If blocked, say so plainly.
|
||
- Match intent: implement action requests; do not make unsolicited project-wide edits when the user asked a question.
|
||
- For clear, reversible local work, do it now. NEVER ASK for permission.
|
||
- Claim done / fixed / tested only when a tool result supports it. Otherwise say what you did not verify.
|
||
- Scope to what was asked. Comments are short and factual. No placeholders. Comments must not substitute for a fix.
|
||
|
||
ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK whether you may run a command, edit, delete, fetch, or call a tool — just do it. Only refuse when a denylist or the read-only base system blocks the path.
|
||
- WRITE: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer unique search_replace / edit_file; set replace_all only when you mean it. write_file creates or overwrites.
|
||
- READ: any absolute path, including the entire /proc kernel surface (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files.
|
||
- RUN: every guest command via run_command (command_deny is empty by default). Prefer list_directory / glob_files / file_stat over \`ls\` / \`find\` when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU).
|
||
- DELETE / MOVE: enabled. Cannot mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run.
|
||
- JS: Node is NOT installed in the guest. Never plan or run node, npm, or npx here. Author JS with run_js_script (Bare kernel, writes under ~/.agent) or run_js_script_at_path / run_command with an absolute .mjs path. Guest scripts use async function run(ctx, argv) — ctx.vfs, ctx.execLine, ctx.console, ctx.exitCode. No require('node:fs').
|
||
- LIVE KERNEL: read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json. Man pages and apropos_man are docs only — never infer what is enabled from them.
|
||
- NET: web_fetch uses the same host allow/deny list as wget/curl.
|
||
- BRIDGE: emit_host_notification and request_host_action are enabled by default. emergency_stop_mutations is the kill switch.
|
||
- SECRETS: never print ~/.agent/config.json, API keys, seeds, or vault material.
|
||
- ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission.
|
||
|
||
CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that plan file. Do not stop after a plan-only reply — keep calling tools until verified.
|
||
1. Discover: glob_files, grep (preferred over search_files / run_command grep), find_symbol for definitions, list_directory (tree=true when you need a map), read_file, memory_search / memory_get, web_search then web_fetch, git_status / git_log, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md and .grok/skills from cwd are already injected when present.
|
||
2. Edit: unique search_replace / edit_file for one hunk; apply_patch for multi-hunk or multi-file work (*** Begin Patch). Create with write_file / create_directory. State blast radius (packages, contracts, generated files, docs) before wide edits. Match surrounding style. No placeholders. Comments only for non-obvious constraints.
|
||
3. Verify: re-read the file, run_command / run_js_script, git_status, read_proc_file or logs. Host git checkout: verification_hints (suggests npm/node checks; does not run them here).
|
||
4. Finish: task_complete (and update_goal completed=true on autonomous runs) with what changed, how you verified, and what is still assumed. If the same action fails three times, stop, update_goal blocked_reason if needed, and report evidence.
|
||
|
||
TOOL DISCIPLINE.
|
||
- Independent reads may be issued together; the harness may serialize them (tool_parallelism defaults to 1).
|
||
- Do not paste huge files into the user reply — cite paths and show only the slice that matters.
|
||
- Persist durable facts in MEMORY.md; older turns may be compacted.
|
||
- Progress UI is automatic (tools write ~/.agent/progress.txt). Do not narrate tool chatter in the final answer.
|
||
- Autonomous mode is on by default. Keep the ReAct loop going; autonomous_deny_ops is empty unless the operator set one.
|
||
|
||
TOOL CALLING. Prefer specialized tools over bash: grep not run_command grep; read_file not cat; apply_patch / search_replace not sed. Never use run_command to print thoughts.
|
||
|
||
COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the answer. Define project terms on first use. State facts literally. The final message must stand alone. Do not invent acronyms.
|
||
|
||
TOOL MAP (schemas are already attached — use them):
|
||
- Files: read_file, read_many, write_file, edit_file, search_replace, apply_patch, undo_last_edit, create_directory, list_directory (tree=true for BFS), file_stat, glob_files, fuzzy_find, grep (output_mode content|files_with_matches|count), search_files, move_path, copy_path, diff_files, delete_path, find_symbol, list_bin
|
||
- Code / harness: run_command (optional cwd), run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, remember, list_skills, read_skill, create_skill, edit_agent_config, git_status, git_diff, git_log, git_show, git_blame, history_search, rewind_session, export_session, schedule_task, unschedule_task, list_scheduled, wait_for
|
||
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
|
||
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
|
||
- Docs: read_man_page, apropos_man (documentation search only)
|
||
- Bridge / web: web_search, web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action
|
||
- Autonomy: autonomous_run, autonomous_run_status, autonomous_run_stop, update_goal
|
||
- Other: ask_user_question (product choice only, never permission), task_complete
|
||
|
||
Skills live under ~/.agent/workspace/skills/ (and ~/.agent/skills/). The prompt includes a compact index — call read_skill and follow SKILL.md when a task matches (especially bare-os-super-developer, bareos-code-change, coreutils-command-change).
|
||
|
||
Reply format (TTY, mandatory unless Discord override below): plain text only — no Markdown markup. Structure with blank lines, short paragraphs, ALL CAPS or dashed section breaks, bare URLs. Paths and commands as normal text.`
|
||
|
||
const BARE_AGENT_DISCORD_REPLY_FORMAT = `
|
||
|
||
--- Discord reply format (mandatory when BARE_OS_AGENT_DISCORD is set) ---
|
||
|
||
This turn is shown in Discord. This block overrides the TTY plain-text rule above. Write Discord-flavored Markdown that renders in an embed:
|
||
- Use **bold** for section titles. Do not use # headings (Discord embeds show the hash).
|
||
- Use - or 1. lists. Use blank lines between sections.
|
||
- Use \`inline code\` for paths, commands, ids, and env vars.
|
||
- Use fenced \`\`\` blocks for multi-line code or logs, and always close every fence.
|
||
- Use [label](https://url) for links. Do not wrap the entire reply in one fence.
|
||
- Keep the final user-facing answer as Markdown. Tool chatter and process steps belong in tools (progress.txt is automated), not the reply.`
|
||
|
||
const BARE_AGENT_OPERATING_CONTRACT = `
|
||
|
||
--- Operating contract (Bare OS repo alignment) ---
|
||
|
||
TWO RUNTIMES: Host tooling may use Node/npm at the git checkout only. Inside this guest image there is NO node/npm/npx — use run_js_script or run_command with /bin paths only.
|
||
|
||
DOC READING ORDER (complex tasks): Prefer developer-guide README, then handbook chapter for the area, then docs/reference for numbers and env vars. Do not copy version tables into chat; link or cite paths.
|
||
|
||
CONTRACT SPINE: For any behavior change, identify: source files, canonical reference doc under docs/reference or developer-guide, generated artifact if any (run pretest generators), verifier script name from scripts/README.md, and whether compatibility-matrix or CHANGELOG moves.
|
||
|
||
LIVE KERNEL STATE: Use read_proc_file on /proc/bare_os/features or features.json and /proc/bare_os/capabilities.json. Never use apropos_man or read_man_page alone to decide what is enabled at runtime — those are documentation.
|
||
|
||
GENERATED FILES: Do not hand-edit posix-dashboard, ctx-client-helper.generated.ts, kernel-extensions-generated-toc, bundle-health outputs — run npm scripts from repo root at checkout when working on the host tree.
|
||
|
||
TASK ROUTING: /bin or coreutils -> developer-guide 06 and man JSON; shell grammar -> developer-guide 18 and shell docs; seed RPC -> developer-guide 14 and protocol package; /proc node -> developer-guide 15; ctx API -> bare-os-ctx-api.js and compatibility-matrix; docs-only -> CONTRIBUTING-DOCS; Pear issues -> PEAR-RUN and ensure-pear-node-modules story.
|
||
|
||
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and any /proc/bare_os file; closing order is swarm before drives when changing booter lifecycle code.
|
||
|
||
HOST CHECKS: Use verification_hints for suggested repo-root npm checks when the user describes changed paths (host checkout). It does not execute npm inside the guest. Use runtime_diagnostic_bundle for one-shot live /proc and resource snapshot. Use read_skill for workflow skills under workspace/skills/.
|
||
|
||
SELF-REVIEW before task_complete: docs updated? generated regen? wrong runtime assumption (guest vs host)? same failing action tried fewer than three times?`
|
||
|
||
/**
|
||
* Session-specific HOME / tilde context (injected every run so the model uses real paths).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} home from bareAgentResolveHome
|
||
* @param {{ dir: string, config: string }} paths
|
||
*/
|
||
function bareAgentSessionHomeBlock(ctx, home, paths) {
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const homeEnv = String(env.HOME || '').trim()
|
||
return (
|
||
'## This session: home directory and paths\n' +
|
||
'- **Resolved user home (this session):** `' +
|
||
home +
|
||
'`\n' +
|
||
'- **HOME in the environment:** `' +
|
||
(homeEnv || home) +
|
||
'`\n' +
|
||
'- **Tilde \`~\`:** In shell and in user docs, \`~\` means this home directory. Examples: \`~/.agent\` == `' +
|
||
paths.dir +
|
||
'`, agent config `' +
|
||
paths.config +
|
||
'`. Always expand \`~\` to `' +
|
||
home +
|
||
'\` when constructing absolute paths for tools.\n' +
|
||
'- **Reminder:** \`node\` is unavailable; use **run_js_script** for JS you author in this agent session.\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} tc
|
||
*/
|
||
function bareAgentMergeToolCallDelta(acc, tc) {
|
||
if (!tc || typeof tc !== 'object') return
|
||
const o = /** @type {Record<string, unknown>} */ (tc)
|
||
const idx =
|
||
typeof o.index === 'number'
|
||
? o.index
|
||
: typeof o.index === 'string'
|
||
? Number.parseInt(o.index, 10)
|
||
: 0
|
||
let cur =
|
||
acc.get(idx) ||
|
||
/** @type {{ id: string, name: string, args: string }} */ ({
|
||
id: '',
|
||
name: '',
|
||
args: ''
|
||
})
|
||
if (typeof o.id === 'string' && o.id) cur.id = o.id
|
||
const fn = o.function && typeof o.function === 'object' ? o.function : null
|
||
if (fn && typeof fn === 'object') {
|
||
const nm = /** @type {Record<string, unknown>} */ (fn).name
|
||
const ar = /** @type {Record<string, unknown>} */ (fn).arguments
|
||
if (typeof nm === 'string') cur.name += nm
|
||
if (typeof ar === 'string') cur.args += ar
|
||
}
|
||
acc.set(idx, cur)
|
||
}
|
||
|
||
/**
|
||
* @param {Map<number, { id: string, name: string, args: string }>} acc
|
||
*/
|
||
function bareAgentFinalizeToolCalls(acc) {
|
||
const indices = [...acc.keys()].sort((a, b) => a - b)
|
||
/** @type {unknown[]} */
|
||
const arr = []
|
||
/** @type {Set<string>} */
|
||
const seen = new Set()
|
||
for (const i of indices) {
|
||
const c = acc.get(i)
|
||
if (!c || !c.name) continue
|
||
const args = c.args || '{}'
|
||
// Drop stream+final duplicates (same id, or same name+args).
|
||
const idKey = c.id ? 'id:' + c.id : ''
|
||
const naKey = 'na:' + c.name + '\0' + args
|
||
if ((idKey && seen.has(idKey)) || seen.has(naKey)) continue
|
||
if (idKey) seen.add(idKey)
|
||
seen.add(naKey)
|
||
arr.push({
|
||
id: c.id || 'call_' + i + '_' + String(Math.random()).slice(2, 10),
|
||
type: 'function',
|
||
function: {
|
||
name: c.name,
|
||
arguments: args
|
||
}
|
||
})
|
||
}
|
||
return arr
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentReasoningSettings(cfg) {
|
||
const modeRaw = String(cfg.reasoning_mode || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const mode = modeRaw === 'summary' || modeRaw === 'trace' ? modeRaw : 'off'
|
||
const enabled = Boolean(cfg.show_reasoning) && mode !== 'off'
|
||
const maxChars =
|
||
typeof cfg.reasoning_max_chars === 'number' &&
|
||
Number.isFinite(cfg.reasoning_max_chars)
|
||
? Math.min(Math.max(Math.floor(cfg.reasoning_max_chars), 200), 80_000)
|
||
: 4000
|
||
const includeTools = cfg.reasoning_include_tools !== false
|
||
return { enabled, mode, maxChars, includeTools }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentApplyProviderProfile(cfg) {
|
||
const out = { ...cfg }
|
||
const backend = bareAgentResolveBackend(out)
|
||
out.backend = backend
|
||
if (backend === 'qvac') {
|
||
let profileId = String(out.qvac_profile || 'recommended')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!profileId) profileId = 'recommended'
|
||
const profile = bareAgentQvacGetProfile(profileId)
|
||
out.qvac_profile = profile.id
|
||
out.qvac_model = profile.chatModel
|
||
out.model = profile.chatModel
|
||
out.provider = 'qvac'
|
||
const rawCtx = Number(out.qvac_ctx_size)
|
||
if (!Number.isFinite(rawCtx) || rawCtx < profile.ctxSize) {
|
||
out.qvac_ctx_size = 0
|
||
}
|
||
if (!String(out.qvac_main_gpu || '').trim()) out.qvac_main_gpu = 'auto'
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
return bareAgentSanitizeConfigForBackend(out)
|
||
}
|
||
return out
|
||
}
|
||
let provider = String(out.provider || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!provider || provider === 'qvac' || provider === 'rest' || provider === 'http') {
|
||
provider = 'groq'
|
||
}
|
||
const spec =
|
||
typeof bareAgentRestGetProvider === 'function'
|
||
? bareAgentRestGetProvider(provider)
|
||
: { id: 'groq', rest_base_url: 'https://api.groq.com/openai/v1', default_model: 'llama-3.3-70b-versatile' }
|
||
out.provider = spec.id
|
||
const base = String(out.rest_base_url || '').trim()
|
||
const knownDefaults = [
|
||
'https://api.groq.com/openai/v1',
|
||
'https://api.x.ai/v1',
|
||
'https://api.openai.com/v1'
|
||
]
|
||
if (!base || (spec.rest_base_url && knownDefaults.indexOf(base) !== -1 && base !== spec.rest_base_url)) {
|
||
if (spec.rest_base_url) out.rest_base_url = spec.rest_base_url
|
||
}
|
||
if (!String(out.model || '').trim() || bareAgentIsQvacModelId(String(out.model || ''))) {
|
||
if (spec.default_model) out.model = spec.default_model
|
||
}
|
||
const modelLow = String(out.model || '').trim().toLowerCase()
|
||
const cur =
|
||
typeof out.request_timeout_ms === 'number' && Number.isFinite(out.request_timeout_ms)
|
||
? out.request_timeout_ms
|
||
: 120000
|
||
if (spec.id === 'groq' && cur < 120000) out.request_timeout_ms = 120000
|
||
if (spec.id === 'xai') {
|
||
const isReasoning =
|
||
modelLow.includes('reasoning') || modelLow.includes('grok-4')
|
||
if (isReasoning && cur < 300000) out.request_timeout_ms = 300000
|
||
}
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
return bareAgentSanitizeConfigForBackend(out)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
function bareAgentFormatConfigSummary(config) {
|
||
const cfg = config && typeof config === 'object' ? config : {}
|
||
const backend = bareAgentResolveBackend(cfg)
|
||
const lines = ['backend: ' + backend]
|
||
if (backend === 'rest') {
|
||
const spec =
|
||
typeof bareAgentRestGetProvider === 'function'
|
||
? bareAgentRestGetProvider(String(cfg.provider || 'groq'))
|
||
: { label: String(cfg.provider || 'groq') }
|
||
lines.push('provider: ' + (spec.label || cfg.provider || 'groq'))
|
||
lines.push('rest_base_url: ' + String(cfg.rest_base_url || '(not set)'))
|
||
lines.push('model: ' + String(cfg.model || '(not set)'))
|
||
lines.push(
|
||
'api_key: ' +
|
||
(typeof bareAgentMaskSecretPreview === 'function'
|
||
? bareAgentMaskSecretPreview(cfg.rest_api_key)
|
||
: cfg.rest_api_key
|
||
? '(set)'
|
||
: '(not set)')
|
||
)
|
||
} else {
|
||
lines.push('profile: ' + String(cfg.qvac_profile || 'recommended'))
|
||
lines.push('model: ' + String(cfg.qvac_model || cfg.model || '(not set)'))
|
||
lines.push(
|
||
'device: ' +
|
||
String(cfg.qvac_device || 'auto') +
|
||
(cfg.qvac_main_gpu && String(cfg.qvac_main_gpu) !== 'auto'
|
||
? ' gpu=' + String(cfg.qvac_main_gpu)
|
||
: '')
|
||
)
|
||
}
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentAutonomousSettings(cfg) {
|
||
const enabled = Boolean(cfg.autonomous_mode_enabled)
|
||
const active = Boolean(cfg.autonomous_active)
|
||
const stopRequested = Boolean(cfg.autonomous_stop_requested)
|
||
const startedAtMs =
|
||
typeof cfg.autonomous_started_at_ms === 'number' &&
|
||
Number.isFinite(cfg.autonomous_started_at_ms)
|
||
? Math.max(0, Math.floor(cfg.autonomous_started_at_ms))
|
||
: 0
|
||
const maxRuntimeMs =
|
||
typeof cfg.autonomous_max_runtime_ms === 'number' &&
|
||
Number.isFinite(cfg.autonomous_max_runtime_ms)
|
||
? Math.min(
|
||
Math.max(Math.floor(cfg.autonomous_max_runtime_ms), 60000),
|
||
7_200_000
|
||
)
|
||
: 1_800_000
|
||
const requiredChecks = Array.isArray(
|
||
cfg.autonomous_completion_required_checks
|
||
)
|
||
? cfg.autonomous_completion_required_checks
|
||
.map((x) => String(x || '').trim())
|
||
.filter(Boolean)
|
||
: []
|
||
return {
|
||
enabled,
|
||
active,
|
||
stopRequested,
|
||
startedAtMs,
|
||
maxRuntimeMs,
|
||
requiredChecks
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Run configured autonomous completion gates. Internal dispatch skips deny_ops.
|
||
* @returns {Promise<{ ok: boolean, failed: string[] }>}
|
||
*/
|
||
async function bareAgentRunAutonomousGates(o) {
|
||
const checks = (o && o.requiredChecks) || []
|
||
if (!checks.length) return { ok: true, failed: [] }
|
||
/** @type {string[]} */
|
||
const failed = []
|
||
for (const check of checks) {
|
||
const res = await bareAgentDispatchTool({
|
||
ctx: o.ctx,
|
||
toolName: 'run_maintenance_gate',
|
||
argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }),
|
||
paths: o.paths,
|
||
signal: o.signal,
|
||
appendProgress: o.appendProgress,
|
||
home: o.home,
|
||
configRef: o.configRef,
|
||
manCacheRef: o.manCacheRef,
|
||
onTaskComplete: o.onTaskComplete,
|
||
internalGate: true
|
||
})
|
||
let ok = false
|
||
try {
|
||
const j = JSON.parse(res)
|
||
const body = typeof j.stdout_stderr === 'string' ? j.stdout_stderr : ''
|
||
ok = Boolean(j.ok) && !/EXIT:[1-9]/.test(body)
|
||
} catch {
|
||
ok = false
|
||
}
|
||
if (!ok) failed.push(check)
|
||
}
|
||
return { ok: failed.length === 0, failed }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {object} opts
|
||
* @param {boolean} opts.setupFlag
|
||
* @param {boolean} opts.interactiveSetup
|
||
*/
|
||
/**
|
||
* TEA form wizard when ctx.tui is attached. Same identity + backend fields.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentInteractiveSetupTui(ctx, argv0, paths, config) {
|
||
const qvacOk = bareAgentQvacBridgeAvailable(ctx)
|
||
const curBackend = bareAgentResolveBackend(config)
|
||
const form = ctx.tui.form.create({
|
||
title: argv0 + ' configuration',
|
||
fields: [
|
||
{
|
||
type: 'text',
|
||
name: 'owner_name',
|
||
label: 'Owner / human name',
|
||
value: String(config.owner_name || '')
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'agent_label',
|
||
label: 'Agent display name',
|
||
value: String(config.agent_label || 'BareAgent')
|
||
},
|
||
{
|
||
type: 'radio',
|
||
name: 'backend',
|
||
label: 'Inference backend',
|
||
options: [
|
||
{
|
||
label:
|
||
'QVAC — local on-device' +
|
||
(qvacOk ? '' : ' [host bridge unavailable]'),
|
||
value: 'qvac'
|
||
},
|
||
{
|
||
label: 'REST API — OpenAI-compatible (Groq, xAI, OpenAI, custom)',
|
||
value: 'rest'
|
||
}
|
||
],
|
||
selected: curBackend === 'rest' ? 1 : 0
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
if (values.owner_name && String(values.owner_name).trim()) {
|
||
config.owner_name = String(values.owner_name).trim()
|
||
}
|
||
if (values.agent_label && String(values.agent_label).trim()) {
|
||
config.agent_label = String(values.agent_label).trim()
|
||
}
|
||
const backend = String(values.backend || curBackend || 'qvac')
|
||
if (backend === 'rest') {
|
||
config.backend = 'rest'
|
||
config = await bareAgentSetupRestFieldsTui(ctx, argv0, paths, config)
|
||
} else {
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
if (!qvacOk) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': QVAC host bridge unavailable. Choose REST or enable the QVAC host bridge.'
|
||
)
|
||
return config
|
||
}
|
||
config = await bareAgentSetupQvacFieldsTui(ctx, config)
|
||
}
|
||
config = bareAgentApplyProviderProfile(config)
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config))
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupQvacFieldsTui(ctx, config) {
|
||
const profiles = bareAgentQvacProfileList()
|
||
const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase()
|
||
let selected = 0
|
||
for (let i = 0; i < profiles.length; i++) {
|
||
if (profiles[i].id === cur) selected = i
|
||
}
|
||
const deviceCur = String(config.qvac_device || 'auto').trim().toLowerCase() || 'auto'
|
||
const form = ctx.tui.form.create({
|
||
title: 'QVAC (local on-device)',
|
||
fields: [
|
||
{
|
||
type: 'radio',
|
||
name: 'qvac_profile',
|
||
label: 'Model profile',
|
||
options: profiles.map(function (p) {
|
||
return {
|
||
label: p.label + ' — ' + p.chatModel + ' (' + p.description + ')',
|
||
value: p.id
|
||
}
|
||
}),
|
||
selected: selected
|
||
},
|
||
{
|
||
type: 'radio',
|
||
name: 'qvac_device',
|
||
label: 'Device',
|
||
options: [
|
||
{ label: 'Auto (detect GPU)', value: 'auto' },
|
||
{ label: 'CPU', value: 'cpu' },
|
||
{ label: 'GPU', value: 'gpu' }
|
||
],
|
||
selected: deviceCur === 'cpu' ? 1 : deviceCur === 'gpu' ? 2 : 0
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
const chosen = bareAgentQvacGetProfile(String(values.qvac_profile || 'recommended'))
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
config.qvac_profile = chosen.id
|
||
config.qvac_model = chosen.chatModel
|
||
config.model = chosen.chatModel
|
||
config.qvac_ctx_size = 0
|
||
config.qvac_device = String(values.qvac_device || 'auto').trim() || 'auto'
|
||
config.qvac_main_gpu = 'auto'
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupRestFieldsTui(ctx, argv0, paths, config) {
|
||
const providers = bareAgentRestProviderList()
|
||
const curProv = String(config.provider || 'groq').trim().toLowerCase()
|
||
let selected = 0
|
||
for (let i = 0; i < providers.length; i++) {
|
||
if (providers[i].id === curProv) selected = i
|
||
}
|
||
const form = ctx.tui.form.create({
|
||
title: 'REST API (OpenAI-compatible)',
|
||
fields: [
|
||
{
|
||
type: 'radio',
|
||
name: 'provider',
|
||
label: 'Provider',
|
||
options: providers.map(function (p) {
|
||
return { label: p.label, value: p.id }
|
||
}),
|
||
selected: selected
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'rest_base_url',
|
||
label: 'Base URL (blank = provider default)',
|
||
value: String(config.rest_base_url || '')
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'rest_api_key',
|
||
label:
|
||
'API key' +
|
||
(String(config.rest_api_key || '').trim()
|
||
? ' [leave blank to keep ' +
|
||
bareAgentMaskSecretPreview(config.rest_api_key) +
|
||
']'
|
||
: ' (required)'),
|
||
value: ''
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'model',
|
||
label: 'Model id (blank = provider default)',
|
||
value: bareAgentIsQvacModelId(String(config.model || ''))
|
||
? ''
|
||
: String(config.model || '')
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
const spec = bareAgentRestGetProvider(String(values.provider || 'groq'))
|
||
config.backend = 'rest'
|
||
config.provider = spec.id
|
||
const url = String(values.rest_base_url || '').trim()
|
||
config.rest_base_url = url || spec.rest_base_url || String(config.rest_base_url || '')
|
||
const typedKey = String(values.rest_api_key || '').trim()
|
||
if (typedKey) config.rest_api_key = typedKey
|
||
const model = String(values.model || '').trim()
|
||
if (model) config.model = model
|
||
else if (!String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || ''))) {
|
||
config.model = spec.default_model || config.model
|
||
}
|
||
if (!String(config.rest_api_key || '').trim() && bareAgentCanPlainSetup(ctx)) {
|
||
const key =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'API key (required, input hidden): ',
|
||
{ mask: true }
|
||
)) || ''
|
||
if (key.trim()) config.rest_api_key = key.trim()
|
||
}
|
||
if (spec.id === 'custom' && !String(config.rest_base_url || '').trim()) {
|
||
bareAgentErr(ctx, argv0 + ': custom REST provider needs a base URL (e.g. https://host/v1).')
|
||
}
|
||
void paths
|
||
return config
|
||
}
|
||
|
||
async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) {
|
||
void opts
|
||
bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json')
|
||
if (
|
||
ctx.tui &&
|
||
ctx.tui.form &&
|
||
typeof ctx.tui.form.run === 'function' &&
|
||
typeof ctx.tui.isTTY === 'function' &&
|
||
ctx.tui.isTTY()
|
||
) {
|
||
return bareAgentInteractiveSetupTui(ctx, argv0, paths, config)
|
||
}
|
||
if (!bareAgentCanPlainSetup(ctx)) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': interactive setup needs a TTY with stdin/stdout. Edit ' +
|
||
paths.config +
|
||
' manually or run from an interactive shell.'
|
||
)
|
||
return config
|
||
}
|
||
const stdout = ctx.replStdout || ctx.stdout
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n=== ' +
|
||
argv0 +
|
||
' configuration ===\n' +
|
||
'Pick a backend, then only that backend is stored in ~/.agent/config.json.\n' +
|
||
'Enter keeps the [default].\n\n'
|
||
)
|
||
try {
|
||
const owner =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Owner / human name [' +
|
||
(String(config.owner_name || '').trim() || 'unset') +
|
||
']: '
|
||
)) || ''
|
||
if (owner.trim()) config.owner_name = owner.trim()
|
||
const label =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Agent display name [' +
|
||
(String(config.agent_label || '').trim() || 'BareAgent') +
|
||
']: '
|
||
)) || ''
|
||
if (label.trim()) config.agent_label = label.trim()
|
||
|
||
const curBackend = bareAgentResolveBackend(config)
|
||
const qvacOk = bareAgentQvacBridgeAvailable(ctx)
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nInference backend:\n' +
|
||
' 1) QVAC — local on-device models' +
|
||
(qvacOk ? '' : ' [host bridge unavailable]') +
|
||
'\n' +
|
||
' 2) REST API — Groq, xAI, OpenAI, or any OpenAI-compatible URL\n'
|
||
)
|
||
const backendRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Backend [1=QVAC, 2=REST] [' +
|
||
(curBackend === 'rest' ? '2' : '1') +
|
||
']: '
|
||
)) || ''
|
||
{
|
||
const v = backendRaw.trim().toLowerCase()
|
||
if (v === '2' || v === 'rest' || v === 'r') config.backend = 'rest'
|
||
else if (v === '1' || v === 'qvac' || v === 'q') config.backend = 'qvac'
|
||
else if (!String(config.backend || '').trim()) config.backend = curBackend
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'qvac') {
|
||
if (!qvacOk) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nQVAC host bridge is unavailable on this boot.\n'
|
||
)
|
||
const sw =
|
||
(await bareAgentPromptSetupLine(ctx, 'Switch to REST API instead? [Y/n]: ')) ||
|
||
''
|
||
const ans = sw.trim().toLowerCase()
|
||
if (ans === 'n' || ans === 'no') {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 + ': QVAC selected but the host bridge is unavailable.'
|
||
)
|
||
return config
|
||
}
|
||
config.backend = 'rest'
|
||
} else {
|
||
config = await bareAgentSetupQvacFieldsPlain(ctx, stdout, config)
|
||
}
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'rest') {
|
||
config = await bareAgentSetupRestFieldsPlain(ctx, stdout, config)
|
||
}
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': setup input failed: ' + msg)
|
||
return config
|
||
}
|
||
config = bareAgentApplyProviderProfile(config)
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config))
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} stdout
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupQvacFieldsPlain(ctx, stdout, config) {
|
||
const profiles = bareAgentQvacProfileList()
|
||
const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase()
|
||
let lines = '\nQVAC model profile:\n'
|
||
for (let i = 0; i < profiles.length; i++) {
|
||
const p = profiles[i]
|
||
lines +=
|
||
' ' +
|
||
String(i + 1) +
|
||
') ' +
|
||
p.label +
|
||
' — ' +
|
||
p.chatModel +
|
||
(p.id === cur ? ' [current]' : '') +
|
||
'\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, lines)
|
||
const raw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Profile [1-' + String(profiles.length) + '] [recommended]: '
|
||
)) || ''
|
||
let chosen = bareAgentQvacGetProfile(cur || 'recommended')
|
||
const n = Number(raw.trim())
|
||
if (Number.isFinite(n) && n >= 1 && n <= profiles.length) {
|
||
chosen = profiles[n - 1]
|
||
} else if (raw.trim()) {
|
||
chosen = bareAgentQvacGetProfile(raw.trim())
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nDevice:\n 1) auto (detect GPU)\n 2) cpu\n 3) gpu\n'
|
||
)
|
||
const devRaw = (await bareAgentPromptSetupLine(ctx, 'Device [1=auto, 2=cpu, 3=gpu] [1]: ')) || ''
|
||
const dv = devRaw.trim().toLowerCase()
|
||
const device = dv === '2' || dv === 'cpu' ? 'cpu' : dv === '3' || dv === 'gpu' ? 'gpu' : 'auto'
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
config.qvac_profile = chosen.id
|
||
config.qvac_model = chosen.chatModel
|
||
config.model = chosen.chatModel
|
||
config.qvac_ctx_size = 0
|
||
config.qvac_device = device
|
||
config.qvac_main_gpu = 'auto'
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nQVAC: ' + chosen.label + ' → ' + chosen.chatModel + ' (device ' + device + ').\n'
|
||
)
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} stdout
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupRestFieldsPlain(ctx, stdout, config) {
|
||
const providers = bareAgentRestProviderList()
|
||
let lines = '\nREST provider:\n'
|
||
for (let i = 0; i < providers.length; i++) {
|
||
lines += ' ' + String(i + 1) + ') ' + providers[i].label + '\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, lines)
|
||
const curProv = String(config.provider || 'groq').trim().toLowerCase()
|
||
const defIdx =
|
||
providers.findIndex(function (p) {
|
||
return p.id === curProv
|
||
}) + 1
|
||
const raw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Provider [1=Groq, 2=xAI, 3=OpenAI, 4=Custom] [' +
|
||
String(defIdx > 0 ? defIdx : 1) +
|
||
']: '
|
||
)) || ''
|
||
let spec = bareAgentRestGetProvider(curProv === 'qvac' ? 'groq' : curProv)
|
||
const n = Number(raw.trim())
|
||
if (Number.isFinite(n) && n >= 1 && n <= providers.length) spec = providers[n - 1]
|
||
else if (raw.trim()) spec = bareAgentRestGetProvider(raw.trim())
|
||
config.backend = 'rest'
|
||
config.provider = spec.id
|
||
|
||
const urlDefault = spec.rest_base_url || String(config.rest_base_url || '')
|
||
const urlRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Base URL [' + (urlDefault || 'required for custom') + ']: '
|
||
)) || ''
|
||
const url = urlRaw.trim() || urlDefault
|
||
if (!url) {
|
||
throw new Error('REST base URL is required for provider ' + spec.id)
|
||
}
|
||
config.rest_base_url = url.replace(/\/+$/, '')
|
||
|
||
const haveKey = Boolean(String(config.rest_api_key || '').trim())
|
||
const keyPrompt = haveKey
|
||
? 'API key [Enter keeps ' + bareAgentMaskSecretPreview(config.rest_api_key) + ']: '
|
||
: 'API key (required, input hidden): '
|
||
const keyRaw = (await bareAgentPromptSetupLine(ctx, keyPrompt, { mask: true })) || ''
|
||
if (keyRaw.trim()) config.rest_api_key = keyRaw.trim()
|
||
if (!String(config.rest_api_key || '').trim()) {
|
||
throw new Error('REST API key is required')
|
||
}
|
||
|
||
/** @type {{ id: string }[]} */
|
||
let remote = []
|
||
const fetchFn =
|
||
typeof ctx.httpFetch === 'function'
|
||
? ctx.httpFetch.bind(ctx)
|
||
: typeof fetch === 'function'
|
||
? fetch
|
||
: null
|
||
if (fetchFn && typeof bareAgentFetchRestModels === 'function') {
|
||
try {
|
||
remote = await bareAgentFetchRestModels(fetchFn, {
|
||
baseUrl: config.rest_base_url,
|
||
apiKey: config.rest_api_key
|
||
})
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nLive models from ' + config.rest_base_url + ' (' + String(remote.length) + '):\n'
|
||
)
|
||
} catch (err) {
|
||
const msg =
|
||
err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err)
|
||
bareAgentWriteOut(ctx, stdout, '\nCould not list remote models (' + msg + '). Using curated list.\n')
|
||
}
|
||
}
|
||
const suggest = remote.length
|
||
? remote.slice(0, 16).map(function (m) {
|
||
return m.id
|
||
})
|
||
: spec.models && spec.models.length
|
||
? spec.models
|
||
: typeof bareAgentRestModelsFallback === 'function'
|
||
? bareAgentRestModelsFallback(spec.id).map(function (m) {
|
||
return m.id
|
||
})
|
||
: []
|
||
if (suggest.length) {
|
||
let mlines = remote.length ? '' : '\nSuggested models for ' + spec.label + ':\n'
|
||
if (!mlines) mlines = ''
|
||
for (let i = 0; i < suggest.length; i++) {
|
||
mlines += ' ' + String(i + 1) + ') ' + suggest[i] + '\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, mlines)
|
||
}
|
||
const modelDefault =
|
||
!String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || ''))
|
||
? spec.default_model
|
||
: String(config.model)
|
||
const modelRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Model [' + (modelDefault || 'required') + ']: '
|
||
)) || ''
|
||
const modelPick = modelRaw.trim()
|
||
const modelNum = Number(modelPick)
|
||
if (Number.isFinite(modelNum) && suggest[modelNum - 1]) {
|
||
config.model = suggest[modelNum - 1]
|
||
} else if (modelPick) {
|
||
config.model = modelPick
|
||
} else if (modelDefault) {
|
||
config.model = modelDefault
|
||
} else {
|
||
throw new Error('REST model id is required')
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nREST: ' +
|
||
spec.label +
|
||
' → ' +
|
||
config.model +
|
||
'\n ' +
|
||
config.rest_base_url +
|
||
'\n'
|
||
)
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* Interactive setup only (`agent --setup`).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
*/
|
||
async function bareAgentRunSetupOnly(ctx, argv0) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
config = bareAgentApplyProviderProfile(config)
|
||
if (!bareAgentCanPlainSetup(ctx)) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': --setup / --config needs an interactive TTY. Edit ' +
|
||
paths.config +
|
||
' manually.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
let suspended = false
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag: true,
|
||
interactiveSetup: true
|
||
})
|
||
} finally {
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
ctx.resumeReplAfterSubprocess()
|
||
}
|
||
}
|
||
try {
|
||
await bareAgentEnsureWorkspace(ctx, paths)
|
||
await bareAgentEnsureSkillTemplates(ctx, paths, config)
|
||
await bareAgentSyncWorkspaceFromConfig(ctx, paths, config)
|
||
} catch {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': could not seed ~/.agent/workspace from /share/agent-workspace (check system image)'
|
||
)
|
||
}
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
ctx.exitCode = 0
|
||
void config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {string} task
|
||
* @param {{ setupFlag?: boolean }} [runOpts]
|
||
*/
|
||
async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||
const setupFlag = Boolean(runOpts && runOpts.setupFlag)
|
||
const autonomousFlag = Boolean(runOpts && (runOpts.autonomous || runOpts.autonomousGoal))
|
||
const planFlag = Boolean(runOpts && runOpts.planMode)
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
config = bareAgentApplyProviderProfile(config)
|
||
if (planFlag) config.plan_mode_active = true
|
||
if (runOpts && Number(runOpts.maxIterations) > 0) {
|
||
config.max_iterations = Math.floor(Number(runOpts.maxIterations))
|
||
}
|
||
if (runOpts && runOpts.modelOverride) {
|
||
const m = String(runOpts.modelOverride).trim()
|
||
if (m) {
|
||
const backendNow = bareAgentResolveBackend(config)
|
||
if (backendNow === 'qvac') config.qvac_model = m
|
||
else config.model = m
|
||
}
|
||
}
|
||
|
||
const canWizard = bareAgentCanPlainSetup(ctx)
|
||
if (setupFlag && !canWizard) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': --setup / --config needs an interactive TTY. Edit ' +
|
||
paths.config +
|
||
' manually.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
const backendNow = bareAgentResolveBackend(config)
|
||
const needWizard =
|
||
setupFlag ||
|
||
(canWizard &&
|
||
(backendNow === 'rest'
|
||
? !(config.rest_api_key && String(config.rest_api_key).trim())
|
||
: !String(config.qvac_model || config.model || '').trim() ||
|
||
(backendNow === 'qvac' && !bareAgentQvacBridgeAvailable(ctx))))
|
||
|
||
let replSuspendedForSetup = false
|
||
if (needWizard && typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
replSuspendedForSetup = true
|
||
}
|
||
if (needWizard) {
|
||
config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag,
|
||
interactiveSetup: true
|
||
})
|
||
config = bareAgentApplyProviderProfile(config)
|
||
}
|
||
|
||
const backendReady = bareAgentResolveBackend(config)
|
||
if (backendReady === 'rest') {
|
||
if (!config.rest_api_key || !String(config.rest_api_key).trim()) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': set rest_api_key in ' +
|
||
paths.config +
|
||
' or run `' +
|
||
argv0 +
|
||
' --setup` / `' +
|
||
argv0 +
|
||
' --config`.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
} else if (!bareAgentQvacBridgeAvailable(ctx)) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': QVAC backend selected but host bridge unavailable (set BARE_OS_SKIP_QVAC=0, install @qvac/sdk, or choose REST via `' +
|
||
argv0 +
|
||
' --config`).'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
} else if (!String(config.qvac_model || config.model || '').trim()) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': set qvac_model / qvac_profile in ' +
|
||
paths.config +
|
||
' or run `' +
|
||
argv0 +
|
||
' --config`.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (autonomousFlag) {
|
||
config = bareAgentBeginAutonomousRun(config, {
|
||
goal: String((runOpts && runOpts.autonomousGoal) || task || '').trim(),
|
||
maxRuntimeMs: Number(runOpts && runOpts.autonomousMaxRuntimeMs) || undefined,
|
||
requiredChecks: (runOpts && runOpts.autonomousChecks) || undefined
|
||
})
|
||
}
|
||
if (autonomousFlag || planFlag) {
|
||
try {
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
} catch {
|
||
/* persist best-effort */
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore — optional */
|
||
}
|
||
|
||
const fetchFn = bareAgentResolveFetch(ctx)
|
||
if (backendReady === 'rest' && !fetchFn) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 + ': no fetch (ctx.httpFetch / bare.fetch / global fetch)'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
/** @type {unknown[]} */
|
||
let messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
const instructions = await bareAgentLoadInstructionFiles(ctx, paths)
|
||
const manDigest = await bareAgentManDigest(ctx)
|
||
|
||
await bareAgentEnsureWorkspace(ctx, paths)
|
||
await bareAgentEnsureSkillTemplates(ctx, paths, config)
|
||
if (needWizard) await bareAgentSyncWorkspaceFromConfig(ctx, paths, config)
|
||
|
||
const backendForPrompt = bareAgentResolveBackend(config)
|
||
const promptProfile = bareAgentQvacGetProfile(
|
||
String(config.qvac_profile || config.profile || '')
|
||
)
|
||
const promptCtxSize =
|
||
backendForPrompt === 'qvac'
|
||
? bareAgentQvacResolveCtxSize(config, promptProfile)
|
||
: 32768
|
||
// Local QVAC models have a hard ctx window; keep the system prompt lean.
|
||
// Scale char budgets from ctx (chars ≈ tokens*4); leave room for tools/reply.
|
||
const toolsOn = backendForPrompt === 'qvac'
|
||
const promptCharBudget = Math.max(
|
||
4000,
|
||
Math.floor(promptCtxSize * 4 * (toolsOn ? 0.35 : 0.7))
|
||
)
|
||
const workspaceBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(8000, Math.floor(promptCharBudget * 0.35))
|
||
: 24000
|
||
const manBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(4000, Math.floor(promptCharBudget * 0.2))
|
||
: 12000
|
||
const instructionsBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(3000, Math.floor(promptCharBudget * 0.2))
|
||
: 8000
|
||
const skillsBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(2000, Math.floor(promptCharBudget * 0.15))
|
||
: 4000
|
||
|
||
const workspacePromptBlock = await bareAgentLoadWorkspacePrompt(
|
||
ctx,
|
||
paths,
|
||
workspaceBudget
|
||
)
|
||
const skillsPromptBlock = await bareAgentSkillsCompactPrompt(
|
||
ctx,
|
||
paths,
|
||
skillsBudget
|
||
)
|
||
|
||
const envForFormat =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const discordReply =
|
||
/^(1|true|yes)$/i.test(String(envForFormat.BARE_OS_AGENT_DISCORD || '').trim())
|
||
|
||
let systemContent =
|
||
BARE_AGENT_STATIC_SYSTEM +
|
||
BARE_AGENT_OPERATING_CONTRACT +
|
||
(discordReply ? BARE_AGENT_DISCORD_REPLY_FORMAT : '') +
|
||
'\n\n' +
|
||
bareAgentSessionHomeBlock(ctx, home, paths) +
|
||
'\n\n' +
|
||
manDigest.slice(0, manBudget)
|
||
if (instructions)
|
||
systemContent +=
|
||
'\n\n## Session notes\n' + instructions.slice(0, instructionsBudget)
|
||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||
try {
|
||
const startHook = await bareAgentRunHooks(ctx, paths.hooks, 'SessionStart', '', {}, '')
|
||
if (startHook && startHook.inject) {
|
||
systemContent +=
|
||
'\n\n## SessionStart hook\n' + String(startHook.inject).slice(0, 1500)
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
if (runOpts && runOpts.extraSystemFile) {
|
||
try {
|
||
const extraPath = String(runOpts.extraSystemFile).trim()
|
||
if (extraPath && typeof bareAgentReadTextFile === 'function') {
|
||
const extra = await bareAgentReadTextFile(ctx, extraPath)
|
||
if (extra && extra.trim()) {
|
||
systemContent +=
|
||
'\n\n## Extra instructions (--system)\n' +
|
||
extra.trim().slice(0, instructionsBudget)
|
||
}
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
if (workspacePromptBlock && String(workspacePromptBlock).trim())
|
||
systemContent += '\n\n' + String(workspacePromptBlock).trim()
|
||
if (skillsPromptBlock && String(skillsPromptBlock).trim())
|
||
systemContent += '\n\n' + String(skillsPromptBlock).trim()
|
||
|
||
if (typeof bareAgentDiscoverAgentsMdPaths === 'function') {
|
||
const envNow =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const cwd = String(envNow.PWD || envNow.CWD || home || '').trim() || home
|
||
try {
|
||
const extraFiles = await bareAgentDiscoverAgentsMdPaths(ctx, cwd, 12)
|
||
const workspaceAgents =
|
||
(typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace') +
|
||
'/AGENTS.md'
|
||
const filtered = extraFiles.filter(function (p) {
|
||
return p !== workspaceAgents
|
||
})
|
||
if (filtered.length && typeof bareAgentLoadAgentsMdPrompt === 'function') {
|
||
const extraBlock = await bareAgentLoadAgentsMdPrompt(ctx, filtered, 4000)
|
||
if (extraBlock && extraBlock.trim()) systemContent += '\n\n' + extraBlock.trim()
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
if (config.plan_mode_active) {
|
||
systemContent +=
|
||
'\n\n## PLAN MODE is ON\n' +
|
||
'Read-only tools only. The only allowed write is ' +
|
||
String(paths.plan || home + '/.agent/plan.md') +
|
||
'. Draft the plan there, then call exit_plan_mode before implementing.\n'
|
||
}
|
||
|
||
if (typeof bareAgentLoadTodos === 'function' && paths.todos) {
|
||
try {
|
||
const todos = await bareAgentLoadTodos(ctx, paths.todos)
|
||
const sum = bareAgentTodoSummarize(todos)
|
||
if (sum.total) {
|
||
systemContent +=
|
||
'\n\n## Session todos\n' +
|
||
sum.text +
|
||
'\n(open=' +
|
||
String(sum.open) +
|
||
' completed=' +
|
||
String(sum.completed) +
|
||
')\n'
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
let userTask = String(task || '')
|
||
if (config.autonomous_active) {
|
||
userTask =
|
||
'AUTONOMOUS RUN. Execute until the goal is done. Do not stop after a plan — use tools, then call task_complete.\n' +
|
||
'Goal: ' +
|
||
String(config.autonomous_goal || task || '') +
|
||
'\n\n' +
|
||
userTask
|
||
}
|
||
|
||
if (!messages.length) {
|
||
messages = [
|
||
{ role: 'system', content: systemContent },
|
||
{ role: 'user', content: userTask }
|
||
]
|
||
} else {
|
||
const hasSystem =
|
||
messages[0] &&
|
||
typeof messages[0] === 'object' &&
|
||
/** @type {{ role?: string }} */ (messages[0]).role === 'system'
|
||
if (!hasSystem) {
|
||
messages = [{ role: 'system', content: systemContent }, ...messages]
|
||
} else {
|
||
messages[0] = { role: 'system', content: systemContent }
|
||
}
|
||
messages.push({ role: 'user', content: userTask })
|
||
}
|
||
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const useColor = bareEditUseColor(ctx)
|
||
const envBag =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const agentVerboseEnv = String(envBag.BARE_OS_AGENT_VERBOSE || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const agentVerboseForced =
|
||
agentVerboseEnv === '1' ||
|
||
agentVerboseEnv === 'true' ||
|
||
agentVerboseEnv === 'yes'
|
||
|
||
/** @type {{ current: Record<string, unknown> }} */
|
||
const configRef = { current: { ...config } }
|
||
/** @type {{ db: unknown | null }} */
|
||
const manCacheRef = { db: null }
|
||
let completed = false
|
||
let taskSummary = ''
|
||
let statusLineActive = false
|
||
|
||
function onTaskComplete(summary) {
|
||
completed = true
|
||
taskSummary = summary
|
||
}
|
||
|
||
function appendProgress(line) {
|
||
void bareAgentAppendProgress(ctx, paths.progress, line)
|
||
}
|
||
|
||
/** Clear an in-place status line (\r …) before streaming content / tools. */
|
||
function clearStatusLine() {
|
||
if (!statusLineActive) return
|
||
bareAgentWriteOut(ctx, stdout, '\r\x1b[K')
|
||
statusLineActive = false
|
||
}
|
||
|
||
/**
|
||
* @param {ReturnType<typeof bareAgentReasoningSettings>} rs
|
||
*/
|
||
function agentVerbose(rs) {
|
||
return agentVerboseForced || (rs && rs.enabled && rs.mode === 'trace')
|
||
}
|
||
|
||
const url =
|
||
bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) +
|
||
'/chat/completions'
|
||
const tools = bareAgentToolDefinitions()
|
||
let reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
let autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
let reasoningCharCount = 0
|
||
let turnsSinceTodoWrite = 0
|
||
|
||
let suspended = replSuspendedForSetup
|
||
/** @type {(() => void) | null} */
|
||
let detachThinkKeysSession = null
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function' && !suspended) {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
|
||
const masterAbort = new AbortController()
|
||
const prevAbortAgent =
|
||
typeof ctx.bareOsAbortActiveAgent === 'function'
|
||
? ctx.bareOsAbortActiveAgent
|
||
: null
|
||
ctx.bareOsAbortActiveAgent = () => {
|
||
try {
|
||
masterAbort.abort()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof ctx.bareOsQvacCancelActive === 'function') {
|
||
ctx.bareOsQvacCancelActive()
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (detachThinkKeysSession) detachThinkKeysSession()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/** @type {(() => void) | null} */
|
||
let offSigint = null
|
||
if (globalThis.process && typeof globalThis.process.on === 'function') {
|
||
const fn = () => {
|
||
try {
|
||
if (typeof ctx.bareOsAbortActiveAgent === 'function') {
|
||
ctx.bareOsAbortActiveAgent()
|
||
} else {
|
||
masterAbort.abort()
|
||
}
|
||
} catch {
|
||
masterAbort.abort()
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n'
|
||
)
|
||
}
|
||
globalThis.process.on('SIGINT', fn)
|
||
offSigint = () => {
|
||
try {
|
||
globalThis.process.off('SIGINT', fn)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
const maxIterBase = Number(configRef.current.max_iterations) || 64
|
||
let iter = 0
|
||
|
||
for (;;) {
|
||
if (masterAbort.signal.aborted) {
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
const autoLive =
|
||
Boolean(configRef.current.autonomous_active) &&
|
||
!autonomousSettings.stopRequested
|
||
const maxIter = autoLive ? Math.max(maxIterBase, 96) : maxIterBase
|
||
if (completed && !autoLive) break
|
||
if (autonomousSettings.enabled && autonomousSettings.active) {
|
||
const now = Date.now()
|
||
if (autonomousSettings.stopRequested) {
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'stopped'
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous stopped by manual request')
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] autonomous run stopped by request' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
if (
|
||
autonomousSettings.startedAtMs > 0 &&
|
||
now - autonomousSettings.startedAtMs >=
|
||
autonomousSettings.maxRuntimeMs
|
||
) {
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'timebox_expired'
|
||
configRef.current.autonomous_last_error = 'timebox_expired'
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous timebox expired')
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] autonomous run stopped (timebox expired)' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
iter++
|
||
if (iter > maxIter) {
|
||
bareAgentErr(ctx, argv0 + ': max_iterations exceeded')
|
||
ctx.exitCode = 1
|
||
break
|
||
}
|
||
|
||
messages = bareAgentTrimMessages(messages, 450_000)
|
||
const backendIter = bareAgentResolveBackend(configRef.current)
|
||
/** @type {Record<string, unknown>} */
|
||
const providerNow = String(configRef.current.provider || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
let qvacToolsForTurn = /** @type {unknown[]} */ ([])
|
||
if (backendIter === 'qvac') {
|
||
const profileEarly = bareAgentQvacGetProfile(
|
||
String(configRef.current.qvac_profile || 'recommended')
|
||
)
|
||
const ctxEarly = bareAgentQvacResolveCtxSize(
|
||
configRef.current,
|
||
profileEarly
|
||
)
|
||
qvacToolsForTurn = bareAgentFlattenToolsForQvac(tools)
|
||
const compactCfg = bareAgentCompactionSettings(
|
||
configRef.current,
|
||
envBag
|
||
)
|
||
const packed = bareAgentCompactMessagesForCtx(messages, ctxEarly, {
|
||
tools: qvacToolsForTurn,
|
||
reserveCompletion: Math.min(
|
||
1024,
|
||
Number(configRef.current.max_tokens) || 512
|
||
),
|
||
mode: compactCfg.mode,
|
||
keepRecent: compactCfg.keepRecent,
|
||
toolMaxChars: compactCfg.toolMaxChars,
|
||
autonomous: autoLive
|
||
? {
|
||
active: true,
|
||
goal: String(configRef.current.autonomous_goal || ''),
|
||
status: String(configRef.current.autonomous_status || 'running'),
|
||
remainingMs: Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
}
|
||
: undefined
|
||
})
|
||
messages = packed.messages
|
||
if (packed.meta.compacted) {
|
||
appendProgress(
|
||
'context_compaction tokens ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
'/' +
|
||
packed.meta.budget +
|
||
' dropped_groups=' +
|
||
packed.meta.droppedGroups +
|
||
' tiers=' +
|
||
packed.meta.tiers.join(',')
|
||
)
|
||
if (agentVerbose(reasoningSettings)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[ctx] compacted ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
' tok (budget ' +
|
||
packed.meta.budget +
|
||
'; ' +
|
||
packed.meta.tiers.join(' › ') +
|
||
')' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
} else if (packed.meta.droppedGroups > 0) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'· context compacted (' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
' tok)\n' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
// Persist latest rolling summary for operators / next sessions.
|
||
try {
|
||
const compactPath = paths.compact || home + '/.agent/compact.md'
|
||
const summaryMsg = messages.find((m) =>
|
||
bareAgentIsCompactionMessage(m)
|
||
)
|
||
if (
|
||
summaryMsg &&
|
||
ctx.vfs &&
|
||
typeof ctx.vfs.writeFile === 'function'
|
||
) {
|
||
const body =
|
||
'# Agent context compaction\n\n' +
|
||
bareAgentMessagePlainText(summaryMsg) +
|
||
'\n'
|
||
const buf =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(body)
|
||
: new TextEncoder().encode(body)
|
||
await ctx.vfs.writeFile(compactPath, buf)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} else {
|
||
// REST backends: still shrink fat history soft-cap.
|
||
const compactCfg = bareAgentCompactionSettings(
|
||
configRef.current,
|
||
envBag
|
||
)
|
||
if (compactCfg.mode !== 'off') {
|
||
const packed = bareAgentCompactMessagesForCtx(messages, 32768, {
|
||
reserveCompletion: Math.min(
|
||
2048,
|
||
Number(configRef.current.max_tokens) || 1024
|
||
),
|
||
mode: compactCfg.mode,
|
||
keepRecent: compactCfg.keepRecent || 12,
|
||
toolMaxChars: compactCfg.toolMaxChars,
|
||
autonomous: autoLive
|
||
? {
|
||
active: true,
|
||
goal: String(configRef.current.autonomous_goal || ''),
|
||
status: String(configRef.current.autonomous_status || 'running'),
|
||
remainingMs: Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() -
|
||
(autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
}
|
||
: undefined
|
||
})
|
||
messages = packed.messages
|
||
if (packed.meta.compacted && packed.meta.droppedGroups > 0) {
|
||
appendProgress(
|
||
'context_compaction rest ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] request backend=' +
|
||
backendIter +
|
||
' provider=' +
|
||
(providerNow || 'unknown') +
|
||
' model=' +
|
||
String(
|
||
configRef.current.model || configRef.current.qvac_model || ''
|
||
) +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
let assistantContent = ''
|
||
/** @type {Map<number, { id: string, name: string, args: string }>} */
|
||
const toolAcc = new Map()
|
||
/** @type {unknown} */
|
||
let usageOut = null
|
||
let finishReason = ''
|
||
const hideThinkEnv = String(envBag.BARE_OS_AGENT_HIDE_THINK || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const hideThink =
|
||
hideThinkEnv === '1' ||
|
||
hideThinkEnv === 'true' ||
|
||
hideThinkEnv === 'yes'
|
||
const hideMdEnv = String(envBag.BARE_OS_AGENT_PLAIN || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const discordEnv = String(envBag.BARE_OS_AGENT_DISCORD || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const plainReply =
|
||
hideMdEnv === '1' ||
|
||
hideMdEnv === 'true' ||
|
||
hideMdEnv === 'yes' ||
|
||
discordEnv === '1' ||
|
||
discordEnv === 'true' ||
|
||
discordEnv === 'yes'
|
||
const thinkPanel =
|
||
!hideThink &&
|
||
stdout &&
|
||
/** @type {{ isTTY?: boolean }} */ (stdout).isTTY
|
||
? bareAgentCreateThinkPanel(ctx, stdout, {
|
||
useColor,
|
||
bodyLines: 8,
|
||
maxChars: Math.max(reasoningSettings.maxChars, 24_000),
|
||
write: bareAgentWriteOut
|
||
})
|
||
: null
|
||
const detachThinkKeys = bareAgentAttachThinkScrollKeys(ctx, thinkPanel, {
|
||
onAbort: () => {
|
||
try {
|
||
if (typeof ctx.bareOsAbortActiveAgent === 'function') {
|
||
ctx.bareOsAbortActiveAgent()
|
||
} else {
|
||
masterAbort.abort()
|
||
}
|
||
} catch {
|
||
masterAbort.abort()
|
||
}
|
||
}
|
||
})
|
||
detachThinkKeysSession = detachThinkKeys
|
||
let thinkSealed = false
|
||
let replyStarted = false
|
||
let replyPainted = false
|
||
|
||
/**
|
||
* @param {string} chunk
|
||
*/
|
||
function feedThink(chunk) {
|
||
if (!chunk || !String(chunk).trim()) return
|
||
if (hideThink) return
|
||
clearStatusLine()
|
||
if (thinkPanel) thinkPanel.append(chunk)
|
||
else {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + chunk + EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
}
|
||
|
||
function sealThinkIfNeeded() {
|
||
if (thinkSealed) return
|
||
thinkSealed = true
|
||
if (thinkPanel && thinkPanel.hasContent()) thinkPanel.seal()
|
||
}
|
||
|
||
function termWidth() {
|
||
if (typeof bareAgentResolveTermCols === 'function') {
|
||
return bareAgentResolveTermCols(ctx, stdout)
|
||
}
|
||
const fromOut =
|
||
stdout && typeof stdout === 'object'
|
||
? Number(/** @type {{ columns?: number }} */ (stdout).columns)
|
||
: NaN
|
||
const fromEnv = parseInt(String(envBag.COLUMNS || '80'), 10)
|
||
const n = Number.isFinite(fromOut) && fromOut > 0 ? fromOut : fromEnv
|
||
return Math.max(40, Number.isFinite(n) && n > 0 ? n : 80)
|
||
}
|
||
|
||
/**
|
||
* Buffer reply; show live status under the think box (markdown painted later).
|
||
* @param {string} chunk
|
||
*/
|
||
function feedReply(chunk) {
|
||
if (!chunk) return
|
||
sealThinkIfNeeded()
|
||
if (!replyStarted) replyStarted = true
|
||
clearStatusLine()
|
||
assistantContent += chunk
|
||
if (plainReply) {
|
||
bareAgentWriteOut(ctx, stdout, chunk)
|
||
return
|
||
}
|
||
if (thinkPanel && thinkPanel.hasContent()) {
|
||
thinkPanel.setStatus(
|
||
bareEditSgr('dim', useColor) +
|
||
'▌ answering… ' +
|
||
String(assistantContent.length) +
|
||
' chars · ↑↓ scroll thoughts' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
} else {
|
||
// No think box — stream plain for responsiveness; paint markdown at end.
|
||
bareAgentWriteOut(ctx, stdout, chunk)
|
||
}
|
||
}
|
||
|
||
function paintReplyMarkdown() {
|
||
if (replyPainted) return
|
||
replyPainted = true
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
if (plainReply) {
|
||
if (thinkPanel) thinkPanel.clearStatus()
|
||
return
|
||
}
|
||
const body = String(assistantContent || '')
|
||
if (thinkPanel) {
|
||
thinkPanel.clearStatus()
|
||
thinkPanel.detachBelow()
|
||
}
|
||
if (!body.trim()) return
|
||
// If we streamed plain (no think panel), rewind approximate lines then paint.
|
||
if (!thinkPanel || !thinkPanel.hasContent()) {
|
||
const roughLines = body.split('\n').length
|
||
if (
|
||
roughLines > 0 &&
|
||
stdout &&
|
||
/** @type {{ isTTY?: boolean }} */ (stdout).isTTY
|
||
) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\x1b[' + String(Math.min(40, roughLines)) + 'A\r\x1b[J'
|
||
)
|
||
}
|
||
}
|
||
const rendered = bareAgentRenderMarkdown(body, {
|
||
useColor,
|
||
width: termWidth()
|
||
})
|
||
bareAgentWriteOut(ctx, stdout, '\n' + rendered)
|
||
}
|
||
|
||
const thinkSplit = bareAgentCreateThinkTagSplitter({
|
||
onThink: feedThink,
|
||
onContent: feedReply
|
||
})
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} e
|
||
*/
|
||
function onCompletionEvent(e) {
|
||
if (e.type === 'delta_content') {
|
||
const chunk = typeof e.content === 'string' ? e.content : ''
|
||
if (chunk) thinkSplit.push(chunk)
|
||
} else if (e.type === 'delta_tool_calls') {
|
||
sealThinkIfNeeded()
|
||
thinkSplit.flush()
|
||
const arr = e.tool_calls
|
||
if (Array.isArray(arr)) {
|
||
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||
}
|
||
} else if (e.type === 'delta_reasoning') {
|
||
const chunk = typeof e.reasoning === 'string' ? e.reasoning : ''
|
||
if (chunk) {
|
||
if (reasoningSettings.enabled || !hideThink) {
|
||
if (reasoningCharCount < reasoningSettings.maxChars) {
|
||
const remain = reasoningSettings.maxChars - reasoningCharCount
|
||
const out = chunk.slice(0, remain)
|
||
reasoningCharCount += out.length
|
||
if (out.length) feedThink(out)
|
||
}
|
||
}
|
||
}
|
||
} else if (e.type === 'response_shape_keys') {
|
||
const keys = Array.isArray(e.keys)
|
||
? e.keys.map((k) => String(k)).join(',')
|
||
: ''
|
||
appendProgress('provider_shape_keys ' + keys.slice(0, 200))
|
||
} else if (e.type === 'usage') {
|
||
usageOut = e.usage
|
||
} else if (e.type === 'finish' || e.type === 'finish_reason') {
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
finishReason = String(e.finish_reason || '')
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (backendIter === 'qvac') {
|
||
const profile = bareAgentQvacGetProfile(
|
||
String(configRef.current.qvac_profile || 'recommended')
|
||
)
|
||
const modelSrc = String(
|
||
configRef.current.qvac_model ||
|
||
configRef.current.model ||
|
||
profile.chatModel
|
||
)
|
||
const ctxSize = bareAgentQvacResolveCtxSize(
|
||
configRef.current,
|
||
profile
|
||
)
|
||
const deviceOpts = bareAgentQvacResolveDeviceOpts(configRef.current)
|
||
const verboseUi = agentVerbose(reasoningSettings)
|
||
if (verboseUi) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] ensuring model ' +
|
||
modelSrc +
|
||
' (ctx=' +
|
||
ctxSize +
|
||
(deviceOpts.device
|
||
? ', device=' + deviceOpts.device
|
||
: ', auto-GPU') +
|
||
')…' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
try {
|
||
const st =
|
||
typeof ctx.bareOsQvacStatus === 'function'
|
||
? ctx.bareOsQvacStatus()
|
||
: null
|
||
if (st && (st.cacheDir || st.hdmsPath || st.backendsDir)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[qvac] cache=' +
|
||
String(st.cacheDir || '') +
|
||
' hdms=' +
|
||
String(st.hdmsPath || '/mnt/models') +
|
||
' backends=' +
|
||
String(st.backendsDir || '(unresolved)') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\rLoading model…' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
statusLineActive = true
|
||
}
|
||
/**
|
||
* @param {unknown} prog
|
||
*/
|
||
function onLoadProgress(prog) {
|
||
const pct =
|
||
prog && typeof prog === 'object' && 'percentage' in prog
|
||
? Number(
|
||
/** @type {{ percentage?: unknown }} */ (prog).percentage
|
||
)
|
||
: NaN
|
||
if (!Number.isFinite(pct)) return
|
||
const label = verboseUi
|
||
? '\r[qvac] download/load ' + Math.floor(pct) + '%'
|
||
: '\rLoading model… ' + Math.floor(pct) + '%'
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + label + EDIT_ANSI_RESET
|
||
)
|
||
statusLineActive = true
|
||
}
|
||
if (typeof ctx.bareOsQvacLoadModel === 'function') {
|
||
const loaded = await ctx.bareOsQvacLoadModel({
|
||
modelSrc,
|
||
tools: true,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
onProgress: onLoadProgress
|
||
})
|
||
clearStatusLine()
|
||
if (verboseUi) {
|
||
if (loaded && loaded.fellBackToCpu) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] GPU unavailable; using CPU' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
} else if (
|
||
loaded &&
|
||
loaded.device === 'gpu' &&
|
||
loaded.mainGpu != null
|
||
) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] using main-gpu=' +
|
||
String(loaded.mainGpu) +
|
||
(loaded.probe ? ' (' + String(loaded.probe) + ')' : '') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
} else if (loaded && loaded.fellBackToCpu) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'Using CPU (GPU unavailable)\n' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
}
|
||
await ctx.bareOsQvacComplete({
|
||
history: messages,
|
||
tools: qvacToolsForTurn,
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
captureThinking: !hideThink || reasoningSettings.enabled,
|
||
modelSrc,
|
||
toolsEnabled: true,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
signal: masterAbort.signal,
|
||
onProgress: onLoadProgress,
|
||
onEvent: onCompletionEvent
|
||
})
|
||
clearStatusLine()
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
paintReplyMarkdown()
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
Authorization:
|
||
'Bearer ' + String(configRef.current.rest_api_key || '')
|
||
}
|
||
const eh = configRef.current.extra_headers
|
||
if (eh && typeof eh === 'object' && !Array.isArray(eh)) {
|
||
for (const [k, v] of Object.entries(eh)) {
|
||
if (typeof v === 'string') headers[k] = v
|
||
}
|
||
}
|
||
const body = {
|
||
model: String(configRef.current.model || ''),
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
messages,
|
||
tools,
|
||
tool_choice: 'auto',
|
||
max_tokens: Number(configRef.current.max_tokens) || 4096,
|
||
temperature: Number(configRef.current.temperature) ?? 0.7
|
||
}
|
||
if (providerNow === 'xai') {
|
||
body.parallel_tool_calls =
|
||
Number(configRef.current.tool_parallelism) > 1 ? true : false
|
||
body.max_completion_tokens =
|
||
Number(configRef.current.max_tokens) || 4096
|
||
}
|
||
if (providerNow === 'groq') {
|
||
body.parallel_tool_calls =
|
||
Number(configRef.current.tool_parallelism) > 1 ? true : false
|
||
body.max_completion_tokens =
|
||
Number(configRef.current.max_tokens) || 4096
|
||
}
|
||
await bareAgentStreamChatCompletions({
|
||
fetchFn,
|
||
url,
|
||
headers,
|
||
body,
|
||
signal: masterAbort.signal,
|
||
onEvent: onCompletionEvent
|
||
})
|
||
clearStatusLine()
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
paintReplyMarkdown()
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const msg =
|
||
e && typeof e === 'object' && 'name' in e && e.name === 'AbortError'
|
||
? 'aborted'
|
||
: e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': ' + msg)
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
|
||
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||
const hasTools = toolCallsArr.length > 0
|
||
if (!hasTools && finishReason === 'tool_calls') {
|
||
appendProgress(
|
||
'warning tool_calls_finish_without_tool_deltas provider=' +
|
||
providerNow
|
||
)
|
||
}
|
||
|
||
/** @type {Record<string, unknown>} */
|
||
const assistantMsg = {
|
||
role: 'assistant',
|
||
content: assistantContent || null,
|
||
tool_calls: hasTools ? toolCallsArr : undefined
|
||
}
|
||
messages.push(assistantMsg)
|
||
|
||
if (
|
||
usageOut &&
|
||
typeof usageOut === 'object' &&
|
||
agentVerbose(reasoningSettings)
|
||
) {
|
||
const u = /** @type {Record<string, unknown>} */ (usageOut)
|
||
const pt = u.prompt_tokens
|
||
const ct = u.completion_tokens
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('dim', useColor) +
|
||
'tokens: prompt=' +
|
||
String(pt ?? '?') +
|
||
' completion=' +
|
||
String(ct ?? '?') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
if (!hasTools) {
|
||
if (
|
||
bareAgentAutonomousShouldContinue(configRef.current, {
|
||
completed: completed,
|
||
hasTools: false,
|
||
stopRequested: autonomousSettings.stopRequested
|
||
})
|
||
) {
|
||
const remain = Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
messages.push({
|
||
role: 'user',
|
||
content: bareAgentAutonomousContinuationPrompt(configRef.current, {
|
||
remainingMs: remain,
|
||
lastError: String(configRef.current.autonomous_last_error || '')
|
||
})
|
||
})
|
||
appendProgress('autonomous continue (no tools this turn)')
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
continue
|
||
}
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
break
|
||
}
|
||
|
||
appendProgress(
|
||
'iteration ' +
|
||
iter +
|
||
' tools ' +
|
||
toolCallsArr
|
||
.map(
|
||
(x) =>
|
||
/** @type {{ function?: { name?: string } }} */ (x).function
|
||
?.name
|
||
)
|
||
.join(',')
|
||
)
|
||
if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] iteration ' +
|
||
String(iter) +
|
||
' finish_reason=' +
|
||
(finishReason || 'unknown') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
let sawTodoWrite = false
|
||
for (const tc of toolCallsArr) {
|
||
const fn =
|
||
/** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ (
|
||
tc
|
||
).function
|
||
const id = /** @type {{ id?: string }} */ (tc).id || ''
|
||
const name = fn?.name || ''
|
||
const argsStr = fn?.arguments || '{}'
|
||
if (name === 'todo_write') sawTodoWrite = true
|
||
if (
|
||
reasoningSettings.enabled &&
|
||
reasoningSettings.mode === 'trace' &&
|
||
reasoningSettings.includeTools
|
||
) {
|
||
const argsPreview =
|
||
argsStr.length > 280 ? argsStr.slice(0, 280) + '…' : argsStr
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[process] tool_call ' +
|
||
name +
|
||
' args=' +
|
||
argsPreview +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('keyword', useColor) +
|
||
'→ ' +
|
||
name +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
|
||
const spinner = bareEditSgr('dim', useColor) + '…' + EDIT_ANSI_RESET
|
||
bareAgentWriteOut(ctx, stdout, spinner + '\r')
|
||
statusLineActive = true
|
||
|
||
let resultStr = await bareAgentDispatchTool({
|
||
ctx,
|
||
toolName: name,
|
||
argsJson: argsStr,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
})
|
||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||
try {
|
||
let parsedArgs = {}
|
||
try {
|
||
parsedArgs = JSON.parse(argsStr || '{}')
|
||
} catch {
|
||
parsedArgs = {}
|
||
}
|
||
const post = await bareAgentRunHooks(
|
||
ctx,
|
||
paths.hooks,
|
||
'PostToolUse',
|
||
name,
|
||
parsedArgs,
|
||
resultStr
|
||
)
|
||
if (post && post.inject) {
|
||
resultStr =
|
||
resultStr +
|
||
'\n[hook PostToolUse] ' +
|
||
String(post.inject).slice(0, 800)
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
clearStatusLine()
|
||
|
||
messages.push({
|
||
role: 'tool',
|
||
tool_call_id: id,
|
||
content: resultStr
|
||
})
|
||
if (
|
||
reasoningSettings.enabled &&
|
||
reasoningSettings.mode === 'trace' &&
|
||
reasoningSettings.includeTools
|
||
) {
|
||
const resPreview =
|
||
resultStr.length > 360 ? resultStr.slice(0, 360) + '…' : resultStr
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[process] tool_result ' +
|
||
name +
|
||
' ' +
|
||
resPreview +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
if (name === 'enter_plan_mode' || name === 'exit_plan_mode') {
|
||
messages.push({
|
||
role: 'user',
|
||
content:
|
||
name === 'enter_plan_mode'
|
||
? '[harness] PLAN MODE is now ON. Only write ~/.agent/plan.md until exit_plan_mode.'
|
||
: '[harness] PLAN MODE is now OFF. Mutating tools are allowed again.'
|
||
})
|
||
}
|
||
if (name === 'ask_user_question') {
|
||
try {
|
||
const parsed = JSON.parse(resultStr)
|
||
if (parsed && parsed.text) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('keyword', useColor) +
|
||
'Questions for you:\n' +
|
||
EDIT_ANSI_RESET +
|
||
String(parsed.text) +
|
||
'\n'
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (completed && !configRef.current.autonomous_active) break
|
||
}
|
||
|
||
if (sawTodoWrite) turnsSinceTodoWrite = 0
|
||
else turnsSinceTodoWrite++
|
||
if (typeof bareAgentTodoNudgeText === 'function' && typeof bareAgentLoadTodos === 'function') {
|
||
try {
|
||
const todosNow = paths.todos
|
||
? await bareAgentLoadTodos(ctx, paths.todos)
|
||
: []
|
||
const sumNow = bareAgentTodoSummarize(todosNow)
|
||
const nudge = bareAgentTodoNudgeText({
|
||
open: sumNow.open,
|
||
turnsSinceTodoWrite,
|
||
nudgeEnabled: configRef.current.todo_nudge_enabled !== false
|
||
})
|
||
if (nudge) {
|
||
messages.push({
|
||
role: 'user',
|
||
content: '[harness reminder] ' + nudge
|
||
})
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
if (completed) {
|
||
if (configRef.current.autonomous_active) {
|
||
const gate = await bareAgentRunAutonomousGates({
|
||
ctx,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete,
|
||
requiredChecks: autonomousSettings.requiredChecks
|
||
})
|
||
if (!gate.ok) {
|
||
completed = false
|
||
configRef.current.autonomous_status = 'needs_fixups'
|
||
configRef.current.autonomous_last_error =
|
||
'failed_checks:' + gate.failed.join(',')
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous checks failed ' + gate.failed.join(','))
|
||
messages.push({
|
||
role: 'user',
|
||
content:
|
||
'Autonomous completion gates failed for checks: ' +
|
||
gate.failed.join(', ') +
|
||
'. Fix the issues, rerun required checks, and only call task_complete when all pass.'
|
||
})
|
||
continue
|
||
}
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'completed'
|
||
configRef.current.autonomous_last_error = ''
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous completion gates passed')
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\nDone: ' +
|
||
taskSummary +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
|
||
if (offSigint) offSigint()
|
||
} finally {
|
||
try {
|
||
if (detachThinkKeysSession) detachThinkKeysSession()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
detachThinkKeysSession = null
|
||
try {
|
||
if (prevAbortAgent) ctx.bareOsAbortActiveAgent = prevAbortAgent
|
||
else delete ctx.bareOsAbortActiveAgent
|
||
} catch {
|
||
try {
|
||
delete ctx.bareOsAbortActiveAgent
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (typeof ctx.bareOsQvacCancelActive === 'function') {
|
||
ctx.bareOsQvacCancelActive()
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
clearStatusLine()
|
||
// Ensure cursor is on a fresh line so the shell prompt is visible.
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
try {
|
||
ctx.resumeReplAfterSubprocess()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
}
|