Attempts at agent skills

This commit is contained in:
Raven Scott
2026-04-22 02:45:44 -04:00
parent 5b12254fe1
commit bc737094ca
55 changed files with 1571 additions and 124 deletions
+302 -3
View File
@@ -1070,7 +1070,9 @@ function bareAgentPaths(home) {
context: base + '/context.md',
cmdOut: base + '/last_command_out.txt',
workspace: base + '/workspace',
workspaceMemory: base + '/workspace/memory'
workspaceMemory: base + '/workspace/memory',
workspaceSkills: base + '/workspace/skills',
skillsGlobal: base + '/skills'
}
}
@@ -1476,6 +1478,13 @@ var BARE_AGENT_WORKSPACE_FILES = Object.freeze([
/** System-drive templates (kernel share) */
var BARE_AGENT_WORKSPACE_SHARE = '/share/agent-workspace'
/** Relative paths under workspace/ and share root for skill templates */
var BARE_AGENT_SKILL_SEED_REL = Object.freeze([
'skills/.gitkeep',
'skills/p2p-os-status/SKILL.md',
'skills/bare-os-kernel-proc/SKILL.md'
])
/**
* @param {Record<string, unknown>} ctx
* @param {Uint8Array} buf
@@ -1506,7 +1515,7 @@ function bareAgentWorkspaceUtcYmd() {
/**
* Seed ~/.agent/workspace from /share/agent-workspace when SOUL.md is absent.
* @param {Record<string, unknown>} ctx
* @param {{ dir: string, workspace: string, workspaceMemory: string }} paths
* @param {{ dir: string, workspace: string, workspaceMemory: string, workspaceSkills: string }} paths
*/
async function bareAgentEnsureWorkspace(ctx, paths) {
const vfs = ctx.vfs
@@ -1544,6 +1553,7 @@ async function bareAgentEnsureWorkspace(ctx, paths) {
const stubs = [
['loader.stub.js', paths.dir + '/loader.js'],
['index.stub.js', paths.dir + '/index.js'],
['skill-loader.stub.js', paths.dir + '/skill-loader.js'],
['README-agent.md', paths.dir + '/README-agent.md']
]
for (const [srcName, dest] of stubs) {
@@ -1556,6 +1566,55 @@ async function bareAgentEnsureWorkspace(ctx, paths) {
}
}
/**
* Ensure workspace/skills templates and ~/.agent/skill-loader.js exist (idempotent; for upgrades).
* @param {Record<string, unknown>} ctx
* @param {{ workspace: string, workspaceSkills: string, dir: string }} paths
*/
async function bareAgentEnsureSkillTemplates(ctx, paths) {
const vfs = ctx.vfs
if (
!vfs ||
typeof vfs.readFile !== 'function' ||
typeof vfs.writeFile !== 'function' ||
typeof vfs.mkdir !== 'function'
)
return
try {
await vfs.mkdir(paths.workspaceSkills, { recursive: true })
} catch {
return
}
const share = BARE_AGENT_WORKSPACE_SHARE
for (const rel of BARE_AGENT_SKILL_SEED_REL) {
const dest = paths.workspace + '/' + rel
try {
const b = await vfs.readFile(dest)
if (b && b.length) continue
} catch {
/* missing — copy */
}
try {
const buf = await vfs.readFile(share + '/' + rel)
const parent = dest.replace(/\/[^/]+$/, '')
await vfs.mkdir(parent, { recursive: true })
await vfs.writeFile(dest, buf)
} catch {
/* template missing on image */
}
}
try {
await vfs.readFile(paths.dir + '/skill-loader.js')
} catch {
try {
const buf = await vfs.readFile(share + '/skill-loader.stub.js')
await vfs.writeFile(paths.dir + '/skill-loader.js', buf)
} catch {
/* optional */
}
}
}
/**
* Build concatenated system prompt block (Markdown) from workspace files.
* @param {Record<string, unknown>} ctx
@@ -1588,6 +1647,180 @@ async function bareAgentLoadWorkspacePrompt(ctx, paths, maxChars) {
return out
}
/**
* Agent skills: discover SKILL.md under workspace/skills/ (highest precedence) then ~/.agent/skills/.
* Depends on bareAgentWorkspaceDecode from agent-workspace.js (same preamble order).
*/
/**
* @param {string} text
* @returns {{ front: Record<string, string>, body: string }}
*/
function bareAgentParseSkillFrontmatter(text) {
const t = String(text || '')
if (!t.startsWith('---')) return { front: {}, body: t.trim() }
const nl = t.indexOf('\n')
const afterFirst = nl === -1 ? '' : t.slice(nl + 1)
const end = afterFirst.search(/\n---\s*(?:\n|$)/)
if (end === -1) return { front: {}, body: t.trim() }
const yamlBlock = afterFirst.slice(0, end)
const body = afterFirst.slice(end + 1).replace(/^---\s*/, '').replace(/^\r?\n/, '')
/** @type {Record<string, string>} */
const front = {}
for (const line of yamlBlock.split(/\r?\n/)) {
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
if (m) front[m[1]] = m[2].trim()
}
return { front, body: body.trim() }
}
/**
* @param {string} full SKILL.md text
* @param {string} folderName directory basename
*/
function bareAgentSkillMetaFromMarkdown(full, folderName) {
const { front } = bareAgentParseSkillFrontmatter(full)
const name = (front.name || folderName || 'unnamed').trim() || folderName
const descFromFront =
front.description && String(front.description).trim()
? String(front.description).trim()
: ''
const descFromBody =
full
.split(/\r?\n/)
.find((l) => {
const x = l.trim()
return x && !x.startsWith('---') && !x.startsWith('#')
})
?.trim() || ''
const description = (descFromFront || descFromBody || 'Skill').slice(0, 400)
return { name, description }
}
/**
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @returns {Promise<{ id: string, name: string, description: string, path: string, source: string }[]>}
*/
async function bareAgentDiscoverSkills(ctx, paths) {
const vfs = ctx.vfs
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function')
return []
/** @type {{ id: string, name: string, description: string, path: string, source: string }[]} */
const out = []
const seen = new Set()
/**
* @param {string} root
* @param {string} source
*/
async function scanRoot(root, source) {
let names = []
try {
names = await vfs.readdir(root)
} catch {
return
}
if (!Array.isArray(names)) return
for (const raw of names) {
const entry = String(raw)
if (!entry || entry.startsWith('.')) continue
const skillMd = root.replace(/\/+$/, '') + '/' + entry + '/SKILL.md'
try {
const buf = await vfs.readFile(skillMd)
if (!buf || !buf.length) continue
const full = bareAgentWorkspaceDecode(ctx, buf)
const meta = bareAgentSkillMetaFromMarkdown(full, entry)
const keys = [entry.toLowerCase(), meta.name.toLowerCase()]
let dup = false
for (const k of keys) {
if (seen.has(k)) dup = true
}
if (dup) continue
for (const k of keys) seen.add(k)
out.push({
id: entry,
name: meta.name,
description: meta.description,
path: skillMd,
source
})
} catch {
/* not a skill dir */
}
}
}
await scanRoot(paths.workspaceSkills, 'workspace')
await scanRoot(paths.skillsGlobal, 'global')
return out
}
/**
* Compact Markdown block for system prompt (names + short descriptions only).
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @param {number} [maxChars]
*/
async function bareAgentSkillsCompactPrompt(ctx, paths, maxChars) {
const cap = Math.min(Math.max(Number(maxChars) || 4000, 500), 12000)
const skills = await bareAgentDiscoverSkills(ctx, paths)
let block =
'## Available skills (compact index)\n' +
'Each skill is a directory with **SKILL.md** (optional YAML frontmatter: `name`, `description`, …).\n' +
'**Workspace skills** (`~/.agent/workspace/skills/`) override **global** (`~/.agent/skills/`) when names match.\n' +
'To run one: call the **read_skill** tool with the skill id or frontmatter `name` before following its instructions.\n\n'
if (!skills.length) {
block += '(No skills discovered yet — add folders under `workspace/skills/<id>/SKILL.md`.)\n'
return block.length > cap ? block.slice(0, cap) + '\n…\n' : block
}
block += '| id | name | source | description |\n| --- | --- | --- | --- |\n'
for (const s of skills) {
const desc = s.description.replace(/\|/g, '/').replace(/\r?\n/g, ' ').slice(0, 160)
block +=
'| `' +
s.id.replace(/`/g, "'") +
'` | ' +
s.name.replace(/\|/g, '/').replace(/\r?\n/g, ' ') +
' | ' +
s.source +
' | ' +
desc +
' |\n'
}
if (block.length > cap) block = block.slice(0, cap) + '\n… truncated\n'
return block
}
/**
* Load full SKILL.md for a skill matched by folder id or frontmatter name (case-insensitive).
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @param {string} skillQuery
*/
async function bareAgentLoadSkillMarkdown(ctx, paths, skillQuery) {
const q = String(skillQuery || '')
.trim()
.toLowerCase()
if (!q) return { ok: false, error: 'empty_skill', content: '', path: '' }
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function')
return { ok: false, error: 'vfs unavailable', content: '', path: '' }
const skills = await bareAgentDiscoverSkills(ctx, paths)
const hit =
skills.find((s) => s.id.toLowerCase() === q) ||
skills.find((s) => s.name.toLowerCase() === q)
if (!hit) return { ok: false, error: 'skill_not_found', content: '', path: '' }
try {
const buf = await vfs.readFile(hit.path)
if (!buf || !buf.length)
return { ok: false, error: 'empty_file', content: '', path: hit.path }
const t = bareAgentWorkspaceDecode(ctx, buf)
return { ok: true, skill: hit.name, id: hit.id, path: hit.path, source: hit.source, content: t }
} catch (e) {
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
return { ok: false, error: msg, content: '', path: hit.path }
}
}
/** SSE line split + data payload parse (shared by agent-openai + tests). */
/**
@@ -2916,6 +3149,28 @@ function bareAgentToolDefinitions() {
}
}
},
{
type: 'function',
function: {
name: 'read_skill',
description:
'Load the full SKILL.md for a modular agent skill (folder id or frontmatter name, case-insensitive). Workspace ~/.agent/workspace/skills/ overrides ~/.agent/skills/. Use after checking the compact skills index in the system prompt.',
parameters: {
type: 'object',
properties: {
skill: {
type: 'string',
description: 'Skill folder name (e.g. p2p-os-status) or YAML frontmatter name'
},
max_bytes: {
type: 'integer',
description: 'Max bytes of SKILL.md (default 256000)'
}
},
required: ['skill']
}
}
},
{
type: 'function',
function: {
@@ -2965,7 +3220,7 @@ function bareAgentPathAllowed(absPath) {
* ctx: Record<string, unknown>,
* toolName: string,
* argsJson: string,
* paths: { dir: string, config: string, cmdOut: string },
* paths: { dir: string, config: string, cmdOut: string, workspaceSkills?: string, skillsGlobal?: string },
* signal?: AbortSignal,
* appendProgress: (line: string) => void,
* home: string,
@@ -3059,6 +3314,44 @@ async function bareAgentDispatchTool(o) {
}
try {
if (toolName === 'read_skill') {
const skill = typeof args.skill === 'string' ? args.skill.trim() : ''
const maxB =
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
? Math.min(Math.floor(args.max_bytes), 512_000)
: 256_000
if (!skill) {
return bareAgentJsonResult({ ok: false, error: 'skill_required' })
}
const skillPaths = {
workspaceSkills:
typeof paths.workspaceSkills === 'string'
? paths.workspaceSkills
: paths.dir + '/workspace/skills',
skillsGlobal:
typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills'
}
appendProgress('read_skill ' + skill)
const loaded = await bareAgentLoadSkillMarkdown(ctx, skillPaths, skill)
if (!loaded.ok) {
return bareAgentJsonResult({
ok: false,
error: loaded.error || 'load_failed',
skill
})
}
let content = loaded.content
if (content.length > maxB) content = content.slice(0, maxB) + '\n… truncated'
return bareAgentJsonResult({
ok: true,
id: loaded.id,
name: loaded.skill,
path: loaded.path,
source: loaded.source,
content
})
}
if (toolName === 'task_complete') {
const summary = typeof args.summary === 'string' ? args.summary : ''
appendProgress('task_complete: ' + summary.slice(0, 200))
@@ -3854,6 +4147,8 @@ Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privileg
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory instead of \`ls\` in run_command when only listing.
Skills: modular instructions live under ~/.agent/workspace/skills/ (and optionally ~/.agent/skills/). The system message includes a compact skill index; use the read_skill tool to load full SKILL.md when a task matches a listed skill.
Kernel features / capabilities that are **actually enabled or disabled** in this runtime come from **read_proc_file** on \`/proc/bare_os/features\` (same payload as \`/proc/bare_os/features.json\`) and \`/proc/bare_os/capabilities.json\`. **Do not** infer current kernel state from \`apropos_man\` or man pages—that only searches documentation keywords.
Prefer tools over guessing for filesystem and shell facts.
@@ -4077,11 +4372,13 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
const manDigest = await bareAgentManDigest(ctx)
await bareAgentEnsureWorkspace(ctx, paths)
await bareAgentEnsureSkillTemplates(ctx, paths)
const workspacePromptBlock = await bareAgentLoadWorkspacePrompt(
ctx,
paths,
24000
)
const skillsPromptBlock = await bareAgentSkillsCompactPrompt(ctx, paths, 4000)
let systemContent =
BARE_AGENT_STATIC_SYSTEM +
@@ -4093,6 +4390,8 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
systemContent += '\n\n## Session notes\n' + instructions.slice(0, 8000)
if (workspacePromptBlock && String(workspacePromptBlock).trim())
systemContent += '\n\n' + String(workspacePromptBlock).trim()
if (skillsPromptBlock && String(skillsPromptBlock).trim())
systemContent += '\n\n' + String(skillsPromptBlock).trim()
if (!messages.length) {
messages = [