528 lines
15 KiB
JavaScript
528 lines
15 KiB
JavaScript
/** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @returns {string}
|
|
*/
|
|
function bareAgentResolveHome(ctx) {
|
|
const env =
|
|
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
|
? /** @type {Record<string, string>} */ (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 {
|
|
rest_base_url: 'https://api.groq.com/openai/v1',
|
|
rest_api_key: '',
|
|
model: 'llama3-70b-8192',
|
|
max_tokens: 4096,
|
|
temperature: 0.7,
|
|
provider: 'groq',
|
|
max_iterations: 64,
|
|
stream: true,
|
|
tool_parallelism: 1,
|
|
request_timeout_ms: 120000,
|
|
extra_headers: /** @type {Record<string, string>} */ ({}),
|
|
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<string, unknown>}
|
|
*/
|
|
function bareAgentIsPlainObject(v) {
|
|
return v != null && typeof v === 'object' && !Array.isArray(v)
|
|
}
|
|
|
|
/**
|
|
* Shallow merge known keys from src into defaults.
|
|
* @param {Record<string, unknown>} defaults
|
|
* @param {Record<string, unknown>} src
|
|
*/
|
|
function bareAgentMergeConfig(defaults, src) {
|
|
const out = { ...defaults }
|
|
const known = new Set([
|
|
'rest_base_url',
|
|
'rest_api_key',
|
|
'model',
|
|
'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<string, string>} */ ({ ...val })
|
|
continue
|
|
}
|
|
if (
|
|
k === 'rest_base_url' ||
|
|
k === 'rest_api_key' ||
|
|
k === 'model' ||
|
|
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 === '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<string, unknown>} 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 = [
|
|
'rest_base_url',
|
|
'rest_api_key',
|
|
'model',
|
|
'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<string, unknown>} ctx
|
|
* @param {{ dir: string, config: string }} paths
|
|
* @returns {Promise<{ config: ReturnType<typeof bareAgentDefaultConfig>, 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<string, unknown>} */
|
|
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<string, unknown>} ctx
|
|
* @param {{ config: string }} paths
|
|
* @param {Record<string, unknown>} 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
|
|
}
|
|
|
|
/**
|
|
* Best-effort load instruction files.
|
|
* @param {Record<string, unknown>} 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<string, unknown>} 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 <name>` for full text). Sample entries:\n' + s
|
|
)
|
|
} catch {
|
|
return '(man digest unavailable)'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
|
|
* @param {string} path
|
|
* @returns {Promise<unknown[]>}
|
|
*/
|
|
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<string, unknown>} ctx
|
|
* @param {ReturnType<typeof bareAgentPaths>} 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 */
|
|
}
|
|
}
|