/** ~/.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', cmdOut: base + '/last_command_out.txt', workspace: base + '/workspace', workspaceMemory: base + '/workspace/memory', workspaceSkills: base + '/workspace/skills', skillsGlobal: base + '/skills' } } 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} */ ({}), allow_delete: false, require_confirm_token: '', owner_name: '', agent_label: '', show_reasoning: false, reasoning_mode: 'off', reasoning_max_chars: 4000, reasoning_include_tools: true, allow_bridge_mutations: false, allow_host_notifications: false, allow_host_actions: false, emergency_stop_mutations: false, autonomous_mode_enabled: false, autonomous_max_runtime_ms: 1800000, autonomous_completion_required_checks: [], autonomous_allow_paths: ['*'], autonomous_deny_ops: [ 'delete_path', 'request_host_action', 'emit_host_notification', 'list_verification_scripts', 'run_maintenance_gate', 'run_contract_checks', 'summarize_build_drift' ], autonomous_active: false, autonomous_started_at_ms: 0, autonomous_stop_requested: false, autonomous_goal: '', autonomous_status: 'idle', autonomous_last_error: '' } } /** * @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', 'allow_delete', 'require_confirm_token', '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' ]) 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' ) { out[k] = String(val ?? '') 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' ) { 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' ) { 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' ) { out[k] = Boolean(val) continue } if (k === 'require_confirm_token') { out.require_confirm_token = String(val ?? '') continue } } return out } /** * @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', 'allow_delete', 'require_confirm_token', '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' ] 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() await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw)) return { config: /** @type {any} */ (raw), created: true } } bareAgentValidateConfigShape(raw) const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw) return { config: /** @type {any} */ (merged), 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') bareAgentValidateConfigShape(config) const json = JSON.stringify(config, 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 trimmed = bareAgentTrimMessages(messages, 500_000) 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 [] } } /** * Clear persisted chat session (history + progress log; keeps config and instructions). * @param {Record} ctx * @param {ReturnType} paths * @param {string} argv0 */ 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 */ } }