Attempts at agent skills
This commit is contained in:
+302
-3
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user