Subagents in the CLI (harness spec 3.3.0): the model, capacity, the offer, /context
ci / check (push) Waiting to run
ci / check (push) Waiting to run
- A task subagent or a team member runs on the session's model unless the person named another in this session's own messages (team/subagents.ts; engine.personSaid reads every prompt and mid-turn message, compacted ones included). Not named: refused; an agent file's own model is not used. - Another model on the session's one-model server is refused; a one-request session model is not its own subagent, and nothing else is picked. - The team is offered when a model could serve: the session's with two slots, or any usable model off its one-model server; the size note adds team_own when the session's model cannot serve its own. - "Subagents" in /effort, /team, /settings and the config schema; the status bar reads "agents 3/4 · 2 subagents working". - /context: used of the window, one bar by category, each category's share, what is free, and "N until auto-compact". - Conformance runner for the subagents area (26 cases); helper -> task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
21 files changed
+401
-104
No files matched your search
@@ -0,0 +1,101 @@
|
||||
// Subagents (harness spec 3.3.0, harness/subagents.md): which model a subagent — a task subagent or
|
||||
// a team's member — runs on, and whether subagents are offered at all. The rules alone, without an
|
||||
// engine: the conformance area `subagents` drives them, and the app (spawn, team_add) asks them.
|
||||
import { LOOP } from "../harness.ts"
|
||||
import type { Message } from "../provider/types.ts"
|
||||
import { parallelFrom, type Capacity } from "./team.ts"
|
||||
|
||||
export const SUBAGENTS = LOOP.subagents
|
||||
const TEXTS = SUBAGENTS.texts
|
||||
|
||||
/** A model the person may use: its served id (the ref), its id at its connection, its display name. */
|
||||
export interface ModelInfo {
|
||||
ref: string
|
||||
id?: string
|
||||
name?: string
|
||||
capacity: Capacity
|
||||
}
|
||||
|
||||
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
const BEFORE = new RegExp(`${SUBAGENTS.named.before}$`, "u")
|
||||
const AFTER = new RegExp(`^(?:${SUBAGENTS.named.after})`, "u")
|
||||
|
||||
/** The names a model goes by: the served id, the part after its provider, the display name. */
|
||||
export function namesOf(m: Pick<ModelInfo, "ref" | "id" | "name">): string[] {
|
||||
const out = [m.ref]
|
||||
if (m.id) out.push(m.id)
|
||||
const slash = m.ref.lastIndexOf("/")
|
||||
if (slash >= 0) out.push(m.ref.slice(slash + 1))
|
||||
if (m.name?.trim()) out.push(m.name.trim())
|
||||
return [...new Set(out.filter(Boolean))]
|
||||
}
|
||||
|
||||
/** Whether `name` occurs in `text` as a whole word, without case (subagents.md §2). White space in
|
||||
* the name matches any run of white space. */
|
||||
export function occursAsWord(text: string, name: string): boolean {
|
||||
const pattern = name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(escape)
|
||||
.join("\\s+")
|
||||
if (!pattern) return false
|
||||
const re = new RegExp(pattern, "giu")
|
||||
for (let m = re.exec(text); m; m = re.exec(text)) {
|
||||
const before = text.slice(0, m.index)
|
||||
const after = text.slice(m.index + m[0].length)
|
||||
if (!BEFORE.test(before) && !AFTER.test(after)) return true
|
||||
re.lastIndex = m.index + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Whether the person named the model in any of the messages they typed. */
|
||||
export function named(m: Pick<ModelInfo, "ref" | "id" | "name">, said: readonly string[]): boolean {
|
||||
const names = namesOf(m)
|
||||
return said.some((t) => names.some((n) => occursAsWord(t, n)))
|
||||
}
|
||||
|
||||
/** What the person typed in a session, oldest first: their prompts and what they sent mid-turn
|
||||
* (after `steerMark`) — never a note, a job's or a team's marked message, or a machine turn. */
|
||||
export function personTexts(messages: readonly Message[], steerMark: string): string[] {
|
||||
const out: string[] = []
|
||||
for (const m of messages) {
|
||||
if (m.role !== "user" || m.machine) continue
|
||||
const first = m.parts[0]
|
||||
if (!first || first.type !== "text") continue
|
||||
if (m.turn === false) {
|
||||
if (first.text.startsWith(steerMark)) out.push(first.text.slice(steerMark.length).trim())
|
||||
continue
|
||||
}
|
||||
out.push(first.text)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Whether a model can be its own session's subagent: it serves two requests at once, or has no limit. */
|
||||
export function servesOwn(c: Capacity): boolean {
|
||||
return parallelFrom(c) >= 2
|
||||
}
|
||||
|
||||
/** subagents.md §4: whether subagents are offered for a session on `session`, given the models the
|
||||
* person may use, and whether the session's model can serve its own. */
|
||||
export function offer(session: ModelInfo, models: readonly ModelInfo[], subagents = true): { offered: boolean; own: boolean } {
|
||||
const own = servesOwn(session.capacity)
|
||||
const group = session.capacity.group
|
||||
const other = models.some((m) => m.ref !== session.ref && (group === undefined || m.capacity.group !== group))
|
||||
return { offered: subagents && (own || other), own }
|
||||
}
|
||||
|
||||
/** subagents.md §2, §3: the model a subagent runs on — the session's, or `wanted` (already resolved
|
||||
* to a model the person may use) — or the refusal, word for word. */
|
||||
export function subagentModel(session: ModelInfo, wanted: ModelInfo | undefined, said: readonly string[]): { model: string } | { refused: string } {
|
||||
const m = wanted ?? session
|
||||
if (m.ref !== session.ref) {
|
||||
if (!named(m, said)) return { refused: TEXTS.not_named }
|
||||
const group = session.capacity.group
|
||||
if (group !== undefined && m.capacity.group === group) return { refused: TEXTS.same_group }
|
||||
return { model: m.ref }
|
||||
}
|
||||
if (!servesOwn(session.capacity)) return { refused: TEXTS.one_request }
|
||||
return { model: session.ref }
|
||||
}
|
||||
Reference in new issue
Block a user