Harness spec 2.0.0: background work is a job — commands and subagents alike, seen and steered
One registry per process, every job owned by a session: bash's background commands (and a command still running at its timeout, which now goes to the background instead of being killed) and subagents started with task background: true. job_list, job_output, job_stop (the former bash_list, bash_output, bash_kill) and the new job_send and report; a finished job wakes the owning session in every front end with its marked note. The TUI counts jobs, lists them (/jobs), opens a command's output, enters a subagent and messages it, moves a command to the background (ctrl+b) and asks before quitting with jobs running. The link gains the jobs capability. Subagents get a session row of their own, the spec's preamble and agent texts, and their errors reach the report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
75 files changed
+3614
-437
No files matched your search
+17
-10
@@ -7,6 +7,7 @@ import { parse } from "yaml"
|
||||
import { paths } from "../config/paths.ts"
|
||||
import { asMode, type Mode } from "../config/schema.ts"
|
||||
import { canonicalToolNames } from "../tool/names.ts"
|
||||
import { promptText } from "../prompt/assemble.ts"
|
||||
|
||||
export interface AgentSpec {
|
||||
name: string
|
||||
@@ -22,28 +23,29 @@ export interface AgentSpec {
|
||||
source: "builtin" | "global" | "project"
|
||||
}
|
||||
|
||||
/** Never given to a subagent: it cannot spawn more, and it has no user of its own to ask. */
|
||||
export const NOT_FOR_SUBAGENTS = new Set(["task", "ask_user", "plan_submit", "plan_write", "plan_update", "tasks", "decisions", "memory", "skill_manage", "settings", "note_manage"])
|
||||
/** Never given to a subagent: it cannot spawn more (so nothing to send to), and it has no user of
|
||||
* its own to ask. */
|
||||
export const NOT_FOR_SUBAGENTS = new Set(["task", "job_send", "ask_user", "plan_submit", "plan_write", "plan_update", "tasks", "decisions", "memory", "skill_manage", "settings", "note_manage"])
|
||||
|
||||
export const BUILTIN_AGENTS: AgentSpec[] = [
|
||||
{
|
||||
name: "explore",
|
||||
description: "read-only research: find where things are, how they work, what a change would touch",
|
||||
instructions:
|
||||
"You can read, search and fetch, but not change anything. Look widely — several searches in one step when they are independent — then report what you found, not what you did: the answer first, then the evidence as file:line references.",
|
||||
// The texts are the spec's (harness/prompts/agents), so LLeMbas's helper says the same words.
|
||||
instructions: promptText("agents/explore.md"),
|
||||
mode: "plan",
|
||||
tools: ["read", "glob", "grep", "list", "bash", "web_search", "web_fetch", "view_image"],
|
||||
tools: ["read", "glob", "grep", "list", "bash", "job_output", "job_list", "job_stop", "web_search", "web_fetch", "view_image"],
|
||||
source: "builtin",
|
||||
},
|
||||
{
|
||||
name: "general",
|
||||
description: "a self-contained piece of work with the full toolset, under the same permission mode",
|
||||
instructions: "Do the task completely, check it, and report what you changed and how you verified it.",
|
||||
instructions: promptText("agents/general.md"),
|
||||
source: "builtin",
|
||||
},
|
||||
]
|
||||
|
||||
function load(dir: string, source: AgentSpec["source"]): AgentSpec[] {
|
||||
function load(dir: string, source: AgentSpec["source"], warnings?: string[]): AgentSpec[] {
|
||||
if (!existsSync(dir)) return []
|
||||
const out: AgentSpec[] = []
|
||||
for (const f of readdirSync(dir)) {
|
||||
@@ -53,7 +55,11 @@ function load(dir: string, source: AgentSpec["source"]): AgentSpec[] {
|
||||
let meta: Record<string, unknown> = {}
|
||||
try {
|
||||
meta = fm ? ((parse(fm[1]!) as Record<string, unknown>) ?? {}) : {}
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
// The agent is still offered, with its body: said, so a broken frontmatter is not a silent
|
||||
// loss of its model, mode or tools.
|
||||
warnings?.push(`agent ${join(dir, f)}: frontmatter not read (${(e as Error).message.split("\n")[0]}); its description, model, mode and tools are ignored`)
|
||||
}
|
||||
const body = (fm ? raw.slice(fm[0].length) : raw).trim()
|
||||
out.push({
|
||||
name: basename(f, ".md").toLowerCase().replace(/[^a-z0-9_-]/g, "-"),
|
||||
@@ -69,8 +75,9 @@ function load(dir: string, source: AgentSpec["source"]): AgentSpec[] {
|
||||
return out
|
||||
}
|
||||
|
||||
export function agents(projectDir: string | undefined): AgentSpec[] {
|
||||
/** `warnings`: where a file that could not be read whole is said (a notice at start). */
|
||||
export function agents(projectDir: string | undefined, warnings?: string[]): AgentSpec[] {
|
||||
const byName = new Map<string, AgentSpec>()
|
||||
for (const a of [...BUILTIN_AGENTS, ...load(join(paths.config, "agents"), "global"), ...(projectDir ? load(join(projectDir, "agents"), "project") : [])]) byName.set(a.name, a)
|
||||
for (const a of [...BUILTIN_AGENTS, ...load(join(paths.config, "agents"), "global", warnings), ...(projectDir ? load(join(projectDir, "agents"), "project", warnings) : [])]) byName.set(a.name, a)
|
||||
return [...byName.values()]
|
||||
}
|
||||
Reference in new issue
Block a user