Further agent harness updates
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user