Further agent harness updates

This commit is contained in:
2026-08-18 15:19:20 -04:00
parent 187c8c4df8
commit e081b719d0
23 changed files with 1194 additions and 83 deletions
@@ -41,6 +41,8 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
runtime_diagnostic_bundle: 1,
memory_search: 1,
memory_get: 1,
memory_append: 1,
list_skills: 1,
todo_write: 1,
enter_plan_mode: 1,
exit_plan_mode: 1,
@@ -1213,3 +1215,45 @@ function bareAgentParseSearchResults(payload, max) {
walk(related)
return out
}
/**
* Parse Grok-style intervals (5m / 2h / 1d) or a five-field cron line.
* @param {string} raw
* @returns {{ kind: 'everyMs', everyMs: number } | { kind: 'calendar', onCalendar: string } | { error: string }}
*/
function bareAgentParseScheduleInterval(raw) {
const s = String(raw || '').trim()
if (!s) return { error: 'interval_required' }
const compact = s.replace(/\s+/g, '')
const m = /^(\d+)(ms|s|m|h|d)$/i.exec(compact)
if (m) {
const n = Number(m[1])
const unit = m[2].toLowerCase()
let ms = 0
if (unit === 'ms') ms = n
else if (unit === 's') ms = n * 1000
else if (unit === 'm') ms = n * 60 * 1000
else if (unit === 'h') ms = n * 60 * 60 * 1000
else ms = n * 24 * 60 * 60 * 1000
if (ms < 1000) return { error: 'interval_too_short', min_ms: 1000 }
if (ms > 86400000) return { error: 'interval_too_long', max_ms: 86400000 }
return { kind: 'everyMs', everyMs: ms }
}
const fields = s.split(/\s+/)
if (fields.length === 5) return { kind: 'calendar', onCalendar: s }
return { error: 'bad_interval', hint: 'use 5m, 2h, 1d, or five cron fields' }
}
/**
* @param {string} id
*/
function bareAgentScheduleId(id) {
let s = String(id || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/^-+|-+$/g, '')
if (!s) s = 'task'
if (s.indexOf('agent-') !== 0) s = 'agent-' + s
return s.slice(0, 40)
}
@@ -1184,6 +1184,79 @@ function bareAgentToolDefinitions() {
}
}
},
{
type: 'function',
function: {
name: 'memory_append',
description:
'Append a durable FACT / CHECK / RISK / HANDOFF line to MEMORY.md or today\'s daily log. Prefer this over write_file for memory.',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: 'What to remember' },
kind: {
type: 'string',
enum: ['FACT', 'CHECK', 'RISK', 'HANDOFF', 'NOTE'],
description: 'Ledger tag (default NOTE)'
},
daily: {
type: 'boolean',
description: 'Append to memory/YYYY-MM-DD.md instead of MEMORY.md'
}
},
required: ['text']
}
}
},
{
type: 'function',
function: {
name: 'list_skills',
description:
'List discovered SKILL.md ids from workspace/skills and ~/.agent/skills. Use read_skill to load one.',
parameters: {
type: 'object',
properties: {
max: { type: 'integer', description: 'Max entries (default 40)' }
}
}
}
},
{
type: 'function',
function: {
name: 'schedule_task',
description:
'Create or replace a guest timer that re-runs the agent (Grok scheduler, mapped to ~/.config/bare-os/timers). Interval like 5m, 2h, 1d, or five cron fields. Max 8 timers on the guest.',
parameters: {
type: 'object',
properties: {
id: { type: 'string', description: 'Timer id (stored as agent-<id>.timer)' },
interval: { type: 'string', description: '5m, 2h, 1d, or cron (min hour day month dow)' },
prompt: { type: 'string', description: 'Task the agent should run on each fire' },
auto: {
type: 'boolean',
description: 'Pass --auto (default true)'
}
},
required: ['interval', 'prompt']
}
}
},
{
type: 'function',
function: {
name: 'unschedule_task',
description: 'Delete a guest agent timer by id (agent-<id>.timer).',
parameters: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
}
}
},
{
type: 'function',
function: {
@@ -3421,6 +3494,159 @@ async function bareAgentDispatchTool(o) {
)
}
if (toolName === 'memory_append') {
const text = typeof args.text === 'string' ? args.text.trim() : ''
if (!text) return bareAgentJsonResult({ ok: false, error: 'text_required' })
const kindRaw = String(args.kind || 'NOTE').trim().toUpperCase()
const kind =
kindRaw === 'FACT' ||
kindRaw === 'CHECK' ||
kindRaw === 'RISK' ||
kindRaw === 'HANDOFF' ||
kindRaw === 'NOTE'
? kindRaw
: 'NOTE'
const workspace =
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
const memDir =
typeof paths.workspaceMemory === 'string'
? paths.workspaceMemory
: workspace + '/memory'
const dest = args.daily
? memDir +
'/' +
(typeof bareAgentWorkspaceUtcYmd === 'function'
? bareAgentWorkspaceUtcYmd()
: new Date().toISOString().slice(0, 10)) +
'.md'
: workspace + '/MEMORY.md'
const stamp = new Date().toISOString()
const line = '- ' + kind + ' — ' + text.replace(/\s+/g, ' ').trim()
let prev = ''
try {
prev = await bareAgentReadTextFile(ctx, dest)
} catch {
prev = ''
}
const next =
(prev ? prev.replace(/\s*$/, '') + '\n' : '# Memory\n\n') +
line +
' \n _' +
stamp +
'_\n'
await bareAgentWriteTextFile(ctx, dest, next)
appendProgress('memory_append ' + dest)
return bareAgentJsonResult({ ok: true, path: dest, kind, appended: line })
}
if (toolName === 'list_skills') {
const max =
typeof args.max === 'number' && Number.isFinite(args.max)
? Math.min(Math.max(Math.floor(args.max), 1), 80)
: 40
if (typeof bareAgentDiscoverSkills !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'skills_unavailable' })
}
const skillPaths = {
workspaceSkills:
typeof paths.workspaceSkills === 'string'
? paths.workspaceSkills
: paths.dir + '/workspace/skills',
skillsGlobal:
typeof paths.skillsGlobal === 'string'
? paths.skillsGlobal
: paths.dir + '/skills'
}
const skills = await bareAgentDiscoverSkills(ctx, skillPaths)
appendProgress('list_skills ' + String(skills.length))
return bareAgentJsonResult({
ok: true,
count: Math.min(skills.length, max),
truncated: skills.length > max,
skills: skills.slice(0, max)
})
}
if (toolName === 'schedule_task') {
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : ''
if (!prompt) return bareAgentJsonResult({ ok: false, error: 'prompt_required' })
const parsed =
typeof bareAgentParseScheduleInterval === 'function'
? bareAgentParseScheduleInterval(args.interval)
: { error: 'schedule_unavailable' }
if (parsed.error) return bareAgentJsonResult({ ok: false, error: parsed.error, hint: parsed.hint })
const id =
typeof bareAgentScheduleId === 'function'
? bareAgentScheduleId(args.id || prompt.slice(0, 24))
: 'agent-task'
const dir = (home || '/home') + '/.config/bare-os/timers'
if (!vfs || typeof vfs.readdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
}
try {
if (typeof vfs.mkdir === 'function') await vfs.mkdir(dir, { recursive: true })
} catch {
/* exists */
}
let names = []
try {
names = await vfs.readdir(dir)
} catch {
names = []
}
const existing = Array.isArray(names)
? names.filter(function (n) {
return typeof n === 'string' && n.endsWith('.timer')
})
: []
const dest = dir + '/' + id + '.timer'
const already = existing.indexOf(id + '.timer') >= 0
if (!already && existing.length >= 8) {
return bareAgentJsonResult({
ok: false,
error: 'timer_limit',
max: 8,
hint: 'unschedule_task an existing id first'
})
}
const quoted = bareAgentShellQuote(prompt)
const line = args.auto === false ? 'agent ' + quoted : 'agent --auto ' + quoted
const body =
parsed.kind === 'everyMs'
? '[Timer]\nEveryMs=' + String(parsed.everyMs) + '\nExecLine=' + line + '\n'
: '[Timer]\nOnCalendar=' + parsed.onCalendar + '\nExecLine=' + line + '\n'
await bareAgentWriteTextFile(ctx, dest, body)
appendProgress('schedule_task ' + id)
return bareAgentJsonResult({
ok: true,
id,
path: dest,
interval: parsed,
exec: line
})
}
if (toolName === 'unschedule_task') {
const id =
typeof bareAgentScheduleId === 'function'
? bareAgentScheduleId(args.id)
: String(args.id || '')
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
const dest = (home || '/home') + '/.config/bare-os/timers/' + id + '.timer'
if (!vfs || typeof vfs.unlink !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs.unlink unavailable' })
}
try {
await vfs.unlink(dest)
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
return bareAgentJsonResult({ ok: false, error: msg, path: dest })
}
appendProgress('unschedule_task ' + id)
return bareAgentJsonResult({ ok: true, id, path: dest })
}
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
} catch (e) {
const msg =
+1 -1
View File
@@ -285,7 +285,7 @@ COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the ans
TOOL MAP (schemas are already attached — use them):
- Files: read_file, write_file, edit_file, search_replace, apply_patch, create_directory, list_directory, file_stat, glob_files, grep, search_files, move_path, delete_path, list_bin
- Code / harness: run_command, run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, read_skill, edit_agent_config, git_status
- Code / harness: run_command, run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, list_skills, read_skill, edit_agent_config, git_status, schedule_task, unschedule_task
- 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)
@@ -1,5 +1,13 @@
# PROMPT.md - Reusable Command Templates
## /remember
memory_append a FACT/CHECK/RISK/HANDOFF so the next session can find it.
## /loop
schedule_task with interval 5m/2h/1d (or cron) and a prompt. unschedule_task to drop it.
## /implement
Discover with glob/grep/read, track with todo_write, edit with unique search_replace or apply_patch, verify, then task_complete. NEVER ASK — just do it.
@@ -24,8 +24,9 @@ Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find`
- `run_js_script_at_path` — existing absolute `.mjs`.
- `todo_write` — session todos (`merge=true` to update by id).
- `enter_plan_mode` / `exit_plan_mode` — plan mode is read-only except `~/.agent/plan.md`.
- `memory_search` / `memory_get` `~/.agent/workspace/memory`, MEMORY.md, compact.md.
- `read_skill` — full SKILL.md (workspace skills override `~/.agent/skills/`).
- `memory_search` / `memory_get` / `memory_append` — MEMORY.md and daily logs.
- `list_skills` / `read_skill` — compact catalog then full SKILL.md.
- `schedule_task` / `unschedule_task` — guest timers (`~/.config/bare-os/timers/agent-*.timer`).
- `edit_agent_config` — shallow merge of known `~/.agent/config.json` keys.
## Kernel / ops
@@ -145,7 +145,11 @@ test('agent-tools exposes agent ops tools', async (t) => {
'apply_patch',
'update_goal',
'web_search',
'git_status'
'git_status',
'memory_append',
'list_skills',
'schedule_task',
'unschedule_task'
]) {
t.ok(
TOOLS.includes("name: '" + toolName + "'"),
@@ -493,6 +493,48 @@ test('update_goal / grep dispatch', async (t) => {
t.absent(configRef.current.autonomous_active)
})
test('schedule interval parse and memory_append', async (t) => {
const s = loadPort()
const five = s.bareAgentParseScheduleInterval('5m')
t.is(five.kind, 'everyMs')
t.is(five.everyMs, 300000)
const cron = s.bareAgentParseScheduleInterval('0 * * * *')
t.is(cron.kind, 'calendar')
t.is(s.bareAgentScheduleId('Ping Host'), 'agent-ping-host')
const d = loadDispatch()
const { vfs, files, b4a } = makeVfs({})
await vfs.mkdir('/home/guest/.agent/workspace')
const ctx = { vfs, b4a }
const paths = {
dir: '/home/guest/.agent',
workspace: '/home/guest/.agent/workspace',
workspaceMemory: '/home/guest/.agent/workspace/memory'
}
const mem = await dispatch(d, {
ctx,
paths,
toolName: 'memory_append',
args: { text: 'holesail keys live in state.json', kind: 'FACT' },
configRef: { current: {} },
home: '/home/guest'
})
t.ok(mem.ok)
t.ok(String(files.get(paths.workspace + '/MEMORY.md') || '').includes('FACT'))
const sched = await dispatch(d, {
ctx,
paths,
toolName: 'schedule_task',
args: { id: 'heartbeat', interval: '30m', prompt: 'report peer count' },
configRef: { current: {} },
home: '/home/guest'
})
t.ok(sched.ok)
t.ok(String(files.get(sched.path) || '').includes('EveryMs=1800000'))
t.ok(String(files.get(sched.path) || '').includes('agent --auto'))
})
test('search result parser extracts DDG-style topics', async (t) => {
const s = loadPort()
const rows = s.bareAgentParseSearchResults(