/** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */ /** * @param {Record} ctx * @returns {string} */ function bareAgentResolveHome(ctx) { const env = ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const vfsHome = ctx && typeof ctx === 'object' && ctx.vfs && typeof ctx.vfs === 'object' && typeof /** @type {{ home?: string }} */ (ctx.vfs).home === 'string' ? String(/** @type {{ home?: string }} */ (ctx.vfs).home) : '' const h = env.HOME || vfsHome || '/home/guest' return h.replace(/\/+$/, '') || '/home/guest' } /** * @param {string} home */ function bareAgentPaths(home) { const base = home + '/.agent' return { dir: base, config: base + '/config.json', history: base + '/history.json', progress: base + '/progress.txt', instructions: base + '/instructions.md', context: base + '/context.md', compact: base + '/compact.md', cmdOut: base + '/last_command_out.txt', workspace: base + '/workspace', workspaceMemory: base + '/workspace/memory', workspaceSkills: base + '/workspace/skills', skillsGlobal: base + '/skills', todos: base + '/todos.json', plan: base + '/plan.md', hooks: base + '/hooks', ask: base + '/ask.json', edits: base + '/edits.json', lastCwd: base + '/last_cwd' } } function bareAgentDefaultConfig() { return { backend: 'qvac', rest_base_url: 'https://api.groq.com/openai/v1', rest_api_key: '', model: 'QWEN3_1_7B_INST_Q4', qvac_model: 'QWEN3_1_7B_INST_Q4', qvac_profile: 'recommended', qvac_ctx_size: 0, qvac_device: '', qvac_main_gpu: 'auto', qvac_gpu_layers: -1, max_tokens: 4096, temperature: 0.7, provider: 'qvac', max_iterations: 64, stream: true, tool_parallelism: 1, request_timeout_ms: 120000, extra_headers: /** @type {Record} */ ({}), access_policy: 'full', allow_delete: true, require_confirm_token: '', owner_name: '', agent_label: '', show_reasoning: false, reasoning_mode: 'off', reasoning_max_chars: 4000, reasoning_include_tools: true, allow_bridge_mutations: true, allow_host_notifications: true, allow_host_actions: true, emergency_stop_mutations: false, autonomous_mode_enabled: true, autonomous_max_runtime_ms: 1800000, autonomous_completion_required_checks: [], autonomous_allow_paths: ['*'], autonomous_deny_ops: [], command_deny: [], mutate_deny_prefixes: [ '/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run' ], autonomous_active: false, autonomous_started_at_ms: 0, autonomous_stop_requested: false, autonomous_goal: '', autonomous_status: 'idle', autonomous_last_error: '', context_compaction: 'auto', compaction_keep_recent: 8, compaction_tool_chars: 1600, plan_mode_active: false, todo_nudge_enabled: true } } /** * @param {unknown} v * @returns {v is Record} */ function bareAgentIsPlainObject(v) { return v != null && typeof v === 'object' && !Array.isArray(v) } /** * Shallow merge known keys from src into defaults. * @param {Record} defaults * @param {Record} src */ function bareAgentMergeConfig(defaults, src) { const out = { ...defaults } const known = new Set([ 'backend', 'rest_base_url', 'rest_api_key', 'model', 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'access_policy', 'allow_delete', 'require_confirm_token', 'command_deny', 'mutate_deny_prefixes', 'owner_name', 'agent_label', 'show_reasoning', 'reasoning_mode', 'reasoning_max_chars', 'reasoning_include_tools', 'allow_bridge_mutations', 'allow_host_notifications', 'allow_host_actions', 'emergency_stop_mutations', 'autonomous_mode_enabled', 'autonomous_max_runtime_ms', 'autonomous_completion_required_checks', 'autonomous_allow_paths', 'autonomous_deny_ops', 'autonomous_active', 'autonomous_started_at_ms', 'autonomous_stop_requested', 'autonomous_goal', 'autonomous_status', 'autonomous_last_error', 'context_compaction', 'compaction_keep_recent', 'compaction_tool_chars', 'plan_mode_active', 'todo_nudge_enabled' ]) for (const k of Object.keys(src)) { if (k.startsWith('x-')) continue if (!known.has(k)) continue const val = src[k] if (k === 'extra_headers' && bareAgentIsPlainObject(val)) { out.extra_headers = /** @type {Record} */ ({ ...val }) continue } if (k === 'backend') { const b = String(val ?? '').trim().toLowerCase() out.backend = b === 'rest' || b === 'openai' || b === 'http' ? 'rest' : 'qvac' continue } if ( k === 'rest_base_url' || k === 'rest_api_key' || k === 'model' || k === 'qvac_model' || k === 'qvac_profile' || k === 'provider' || k === 'owner_name' || k === 'agent_label' || k === 'autonomous_goal' || k === 'autonomous_status' || k === 'autonomous_last_error' || k === 'qvac_device' || k === 'qvac_main_gpu' || k === 'require_confirm_token' ) { out[k] = String(val ?? '') continue } if (k === 'context_compaction') { const mode = String(val ?? '').trim().toLowerCase() out.context_compaction = mode === 'off' || mode === 'aggressive' ? mode : 'auto' continue } if (k === 'reasoning_mode') { const mode = String(val ?? '').trim().toLowerCase() out.reasoning_mode = mode === 'summary' || mode === 'trace' ? mode : 'off' continue } if ( k === 'max_tokens' || k === 'temperature' || k === 'max_iterations' || k === 'tool_parallelism' || k === 'request_timeout_ms' || k === 'reasoning_max_chars' || k === 'qvac_ctx_size' || k === 'qvac_gpu_layers' || k === 'autonomous_max_runtime_ms' || k === 'autonomous_started_at_ms' || k === 'compaction_keep_recent' || k === 'compaction_tool_chars' ) { const n = Number(val) out[k] = Number.isFinite(n) ? n : defaults[k] continue } if ( k === 'autonomous_completion_required_checks' || k === 'autonomous_allow_paths' || k === 'autonomous_deny_ops' || k === 'command_deny' || k === 'mutate_deny_prefixes' ) { out[k] = Array.isArray(val) ? val.map((x) => String(x ?? '')).filter(Boolean) : defaults[k] continue } if ( k === 'stream' || k === 'allow_delete' || k === 'show_reasoning' || k === 'reasoning_include_tools' || k === 'allow_bridge_mutations' || k === 'allow_host_notifications' || k === 'allow_host_actions' || k === 'emergency_stop_mutations' || k === 'autonomous_mode_enabled' || k === 'autonomous_active' || k === 'autonomous_stop_requested' || k === 'plan_mode_active' || k === 'todo_nudge_enabled' ) { out[k] = Boolean(val) continue } if (k === 'require_confirm_token') { out.require_confirm_token = String(val ?? '') continue } if (k === 'access_policy') { const pol = String(val ?? '').trim().toLowerCase() out.access_policy = pol === 'restricted' ? 'restricted' : 'full' continue } } return out } /** * Old configs persisted restrictive defaults. Missing access_policy means * upgrade onto full guest admin (denylist-only) so existing homes match. * @param {Record} raw * @param {Record} merged */ function bareAgentApplyAccessPolicyUpgrade(raw, merged) { const src = bareAgentIsPlainObject(raw) ? raw : {} if (Object.prototype.hasOwnProperty.call(src, 'access_policy')) { return { config: merged, upgraded: false } } const next = { ...merged } next.access_policy = 'full' next.allow_delete = true next.require_confirm_token = '' next.allow_bridge_mutations = true next.allow_host_notifications = true next.allow_host_actions = true next.emergency_stop_mutations = false next.autonomous_deny_ops = [] next.autonomous_allow_paths = ['*'] next.command_deny = Array.isArray(next.command_deny) ? next.command_deny : [] next.autonomous_mode_enabled = true if (!Array.isArray(next.mutate_deny_prefixes) || !next.mutate_deny_prefixes.length) { next.mutate_deny_prefixes = bareAgentDefaultConfig().mutate_deny_prefixes } return { config: next, upgraded: true } } /** * @param {Record} raw */ function bareAgentValidateConfigShape(raw) { if (!bareAgentIsPlainObject(raw)) throw new Error('config must be a JSON object') for (const k of Object.keys(raw)) { if (k.startsWith('x-')) continue const known = [ 'backend', 'rest_base_url', 'rest_api_key', 'model', 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'access_policy', 'allow_delete', 'require_confirm_token', 'command_deny', 'mutate_deny_prefixes', 'owner_name', 'agent_label', 'show_reasoning', 'reasoning_mode', 'reasoning_max_chars', 'reasoning_include_tools', 'allow_bridge_mutations', 'allow_host_notifications', 'allow_host_actions', 'emergency_stop_mutations', 'autonomous_mode_enabled', 'autonomous_max_runtime_ms', 'autonomous_completion_required_checks', 'autonomous_allow_paths', 'autonomous_deny_ops', 'autonomous_active', 'autonomous_started_at_ms', 'autonomous_stop_requested', 'autonomous_goal', 'autonomous_status', 'autonomous_last_error', 'context_compaction', 'compaction_keep_recent', 'compaction_tool_chars', 'plan_mode_active', 'todo_nudge_enabled' ] if (!known.includes(k)) { throw new Error('unknown config key: ' + k) } } } /** * @param {Record} ctx * @param {{ dir: string, config: string }} paths * @returns {Promise<{ config: ReturnType, created: boolean }>} */ async function bareAgentLoadOrCreateConfig(ctx, paths) { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function') throw new Error('agent: vfs.mkdir unavailable') await vfs.mkdir(paths.dir, { recursive: true }) let missingOrEmpty = false /** @type {Record} */ let raw = {} try { if (typeof vfs.readFile === 'function') { const buf = await vfs.readFile(paths.config) if (!buf || !buf.length) missingOrEmpty = true else { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) raw = JSON.parse(t) } } else missingOrEmpty = true } catch { missingOrEmpty = true raw = {} } if (!bareAgentIsPlainObject(raw)) raw = {} if (missingOrEmpty || Object.keys(raw).length === 0) { raw = bareAgentDefaultConfig() if (typeof bareAgentSanitizeConfigForBackend === 'function') { raw = bareAgentSanitizeConfigForBackend(raw) } await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw)) return { config: /** @type {any} */ (raw), created: true } } bareAgentValidateConfigShape(raw) const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw) const applied = bareAgentApplyAccessPolicyUpgrade(raw, merged) let next = applied.config if (typeof bareAgentSanitizeConfigForBackend === 'function') { next = bareAgentSanitizeConfigForBackend(next) } const dirty = applied.upgraded || JSON.stringify(next) !== JSON.stringify(merged) if (dirty) { try { await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (next)) } catch { /* keep upgraded in-memory even if persist fails */ } } return { config: /** @type {any} */ (next), created: false } } /** * @param {Record} ctx * @param {{ config: string }} paths * @param {Record} config */ async function bareAgentSaveConfig(ctx, paths, config) { const vfs = ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') throw new Error('agent: vfs.writeFile unavailable') let next = config if (typeof bareAgentSanitizeConfigForBackend === 'function') { next = bareAgentSanitizeConfigForBackend(config) if (config && typeof config === 'object' && config !== next) { for (const k of Object.keys(config)) { if (!Object.prototype.hasOwnProperty.call(next, k)) delete config[k] } Object.assign(config, next) } } bareAgentValidateConfigShape(next) const json = JSON.stringify(next, null, 2) + '\n' const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(json) : new TextEncoder().encode(json) await vfs.writeFile(paths.config, body) } /** * Drop middle messages until JSON size fits (keep first system + recent tail). * @param {unknown[]} msgs * @param {number} maxBytes */ function bareAgentTrimMessages(msgs, maxBytes) { if (!Array.isArray(msgs) || maxBytes <= 0) return [] /** @type {unknown[]} */ let out = msgs.slice() while (JSON.stringify(out).length > maxBytes && out.length > 3) { out.splice(2, 1) } return out } /** * Rough token estimate (chars/4). Good enough for local ctx budgeting. * @param {unknown} value */ function bareAgentEstimateTokens(value) { try { const n = JSON.stringify(value == null ? '' : value).length return Math.max(0, Math.ceil(n / 4)) } catch { return 0 } } /** * Trim history + shrink system content so prompt fits a QVAC ctx window. * Reserves room for completion and optional tools JSON. * @param {unknown[]} msgs * @param {number} ctxSize * @param {{ tools?: unknown[], reserveCompletion?: number }} [opts] */ function bareAgentTrimMessagesForCtx(msgs, ctxSize, opts) { const ctx = Math.max(2048, Math.floor(Number(ctxSize) || 4096)) const reserve = opts && Number.isFinite(Number(opts.reserveCompletion)) ? Math.max(256, Math.floor(Number(opts.reserveCompletion))) : Math.min(1024, Math.max(256, Math.floor(ctx * 0.15))) const toolsTok = bareAgentEstimateTokens( opts && Array.isArray(opts.tools) && opts.tools.length ? opts.tools : [] ) const budget = Math.max(512, ctx - reserve - toolsTok) /** @type {unknown[]} */ let out = Array.isArray(msgs) ? msgs.slice() : [] // Drop middle turns first (keep system + recent). while (bareAgentEstimateTokens(out) > budget && out.length > 3) { out.splice(2, 1) } // Shrink system blob if still over (workspace/man/skills dominate). if (bareAgentEstimateTokens(out) > budget && out[0] && typeof out[0] === 'object') { const sys = /** @type {Record} */ (out[0]) if (sys.role === 'system' && typeof sys.content === 'string') { let content = sys.content let guard = 0 while ( bareAgentEstimateTokens(out) > budget && content.length > 800 && guard < 24 ) { content = content.slice(0, Math.floor(content.length * 0.82)) + '\n… truncated' out[0] = { ...sys, content } guard++ } } } return out } /** * Best-effort load instruction files. * @param {Record} ctx * @param {{ instructions: string, context: string }} paths */ async function bareAgentLoadInstructionFiles(ctx, paths) { const vfs = ctx.vfs const parts = [] if (!vfs || typeof vfs.readFile !== 'function') return '' try { const b = await vfs.readFile(paths.instructions) if (b && b.length) { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (t.trim()) parts.push('## User instructions (~/.agent/instructions.md)\n' + t.trim()) } } catch { /* ignore */ } try { const b = await vfs.readFile(paths.context) if (b && b.length) { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (t.trim()) parts.push('## Agent context (~/.agent/context.md)\n' + t.trim()) } } catch { /* ignore */ } return parts.join('\n\n') } /** * One-time compact index from man.json for system prompt. * @param {Record} ctx */ async function bareAgentManDigest(ctx) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' try { const buf = await vfs.readFile('/share/man/man.json') if (!buf || !buf.length) return '' const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const j = JSON.parse(t) const pages = Array.isArray(j.pages) ? j.pages : [] /** @type {string[]} */ const lines = [] const max = Math.min(pages.length, 400) for (let i = 0; i < max; i++) { const p = pages[i] if (!p || typeof p !== 'object') continue const name = typeof p.name === 'string' ? p.name : '' const title = typeof p.title === 'string' ? p.title : '' if (name) lines.push('- ' + name + (title ? ': ' + title : '')) } let s = lines.join('\n') if (s.length > 24000) s = s.slice(0, 24000) + '\n…' return ( 'Manual page index (see `man ` for full text). Sample entries:\n' + s ) } catch { return '(man digest unavailable)' } } /** * @param {Record} ctx * @param {string} path */ async function bareAgentAppendProgress(ctx, path, line) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return let prev = '' try { const b = await vfs.readFile(path) if (b && b.length) { prev = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) } } catch { /* ignore */ } const chunk = new Date().toISOString() + ' ' + line.replace(/\r?\n/g, ' ') + '\n' const maxKeep = 120_000 let next = prev + chunk if (next.length > maxKeep) next = next.slice(-maxKeep) const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(next) : new TextEncoder().encode(next) await vfs.writeFile(path, body) } /** * @param {Record} ctx * @param {string} path * @param {unknown[]} messages */ async function bareAgentSaveHistory(ctx, path, messages) { const vfs = ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') return const compacted = typeof bareAgentCompactMessagesForDisk === 'function' ? bareAgentCompactMessagesForDisk(messages, 500_000, { keepRecent: 16 }) : bareAgentTrimMessages(messages, 500_000) const trimmed = typeof bareAgentTrimMessages === 'function' ? bareAgentTrimMessages(compacted, 500_000) : compacted const json = JSON.stringify(trimmed, null, 2) + '\n' const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(json) : new TextEncoder().encode(json) await vfs.writeFile(path, body) } /** * @param {Record} ctx * @param {string} path * @returns {Promise} */ async function bareAgentLoadHistory(ctx, path) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return [] try { const buf = await vfs.readFile(path) if (!buf || !buf.length) return [] const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const j = JSON.parse(t) return Array.isArray(j) ? j : [] } catch { return [] } } /** * Start an autonomous run on a config object (does not persist). * @param {Record} cfg * @param {{ goal: string, maxRuntimeMs?: number, requiredChecks?: string[], scopePath?: string }} opts */ function bareAgentBeginAutonomousRun(cfg, opts) { const goal = String((opts && opts.goal) || '').trim() const maxRuntimeMs = Math.min( Math.max(Math.floor(Number((opts && opts.maxRuntimeMs) || cfg.autonomous_max_runtime_ms) || 0), 60000), 7_200_000 ) const requiredChecks = Array.isArray(opts && opts.requiredChecks) ? opts.requiredChecks.map((x) => String(x || '').trim()).filter(Boolean) : Array.isArray(cfg.autonomous_completion_required_checks) ? cfg.autonomous_completion_required_checks.map((x) => String(x || '').trim()).filter(Boolean) : [] return { ...cfg, autonomous_mode_enabled: true, autonomous_active: true, autonomous_stop_requested: false, autonomous_started_at_ms: Date.now(), autonomous_goal: goal, autonomous_status: 'running', autonomous_last_error: '', autonomous_max_runtime_ms: maxRuntimeMs, autonomous_completion_required_checks: requiredChecks } } /** * @param {Record} cfg * @param {string} [reason] */ function bareAgentStopAutonomousRun(cfg, reason) { return { ...cfg, autonomous_stop_requested: true, autonomous_status: 'stopping', autonomous_last_error: reason || String(cfg.autonomous_last_error || '') } } /** * @param {Record} cfg * @param {{ remainingMs?: number, lastError?: string }} [extra] */ function bareAgentAutonomousContinuationPrompt(cfg, extra) { const goal = String(cfg.autonomous_goal || '').trim() || '(goal unset)' const remain = extra && typeof extra.remainingMs === 'number' ? Math.max(0, Math.round(extra.remainingMs / 1000)) : 0 return ( 'AUTONOMOUS RUN still active. Do not stop with a plan — take the next concrete tool action.\n' + 'Goal: ' + goal + '\n' + (remain > 0 ? 'Time remaining: ' + String(remain) + 's.\n' : '') + (extra && extra.lastError ? 'Last gate error: ' + extra.lastError + '\n' : '') + 'Call task_complete(summary) only when the goal is actually finished and verified.' ) } /** * @param {Record} cfg * @param {{ completed?: boolean, hasTools?: boolean, stopRequested?: boolean }} state */ function bareAgentAutonomousShouldContinue(cfg, state) { if (!cfg || !cfg.autonomous_active) return false if (state && state.stopRequested) return false if (state && state.completed) return false if (state && state.hasTools) return false return true } async function bareAgentResetChatSession(ctx, paths, argv0) { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.writeFile !== 'function') { throw new Error('agent reset: vfs unavailable') } await vfs.mkdir(paths.dir, { recursive: true }) const emptyHist = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from('[]\n') : new TextEncoder().encode('[]\n') await vfs.writeFile(paths.history, emptyHist) const stamp = new Date().toISOString() + ' chat session reset\n' const progBody = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(stamp) : new TextEncoder().encode(stamp) await vfs.writeFile(paths.progress, progBody) try { const z = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from('') : new TextEncoder().encode('') await vfs.writeFile(paths.cmdOut, z) } catch { /* ignore */ } try { ctx.console.log( argv0 + ': chat session cleared (' + paths.history + ', ' + paths.progress + ')' ) } catch { /* ignore */ } }