From 0f375adf3e4505fd698305c37257e22977d34ad1 Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Sun, 23 Aug 2026 14:07:39 +0800 Subject: [PATCH 1/2] perf(subagents): bound the spawn tool schema --- extensions/subagents/index.ts | 52 +------ extensions/subagents/prompt.test.ts | 96 ++++++++++-- extensions/subagents/src/agent-types.ts | 8 +- extensions/subagents/src/prompt.ts | 191 +++++++++++++++++++----- skills/subagents/SKILL.md | 3 +- 5 files changed, 250 insertions(+), 100 deletions(-) diff --git a/extensions/subagents/index.ts b/extensions/subagents/index.ts index e0c042dd..d595392b 100644 --- a/extensions/subagents/index.ts +++ b/extensions/subagents/index.ts @@ -25,7 +25,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionCommandContext, @@ -57,7 +56,6 @@ import { BACKEND_NAMES, formatElapsed, latestText, - REASONING_EFFORTS, type SubagentSnapshot, } from "./src/domain.ts"; import { @@ -77,10 +75,9 @@ import { formatContextUtilization } from "../shared/context-utilization.ts"; import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts"; import { buildSubagentResultMessage, - createAgentTypeParameterSchema, buildSubagentSendResult, buildSubagentSpawnResult, - buildSubagentSpawnToolDescription, + createSubagentSpawnToolSurface, SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS, SUBAGENT_CANCEL_TOOL_DESCRIPTION, SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS, @@ -88,10 +85,8 @@ import { SUBAGENT_LIST_TOOL_DESCRIPTION, SUBAGENT_SEND_PARAMETER_DESCRIPTIONS, SUBAGENT_SEND_TOOL_DESCRIPTION, - SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS, SUBAGENT_SPAWN_PROMPT_GUIDELINES, SUBAGENT_SPAWN_PROMPT_SNIPPET, - SUBAGENT_SPAWN_TOOL_DESCRIPTION, SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS, SUBAGENT_WAIT_TOOL_DESCRIPTION, } from "./src/prompt.ts"; @@ -591,57 +586,22 @@ export default function (pi: ExtensionAPI) { agentTypes = loaded.agentTypes; agentTypeDiagnostics = loaded.diagnostics; agentTypeList = [...agentTypes.values()]; - subagentSpawnTool.description = - buildSubagentSpawnToolDescription(agentTypeList); - subagentSpawnTool.parameters = createSubagentSpawnParameters(); + const surface = createSubagentSpawnToolSurface(agentTypeList); + subagentSpawnTool.description = surface.description; + subagentSpawnTool.parameters = surface.parameters; registerSubagentSpawnTool(); }; // --- Tools ------------------------------------------------------------- - const createSubagentSpawnParameters = () => - Type.Object({ - agent_type: createAgentTypeParameterSchema(agentTypeList), - prompt: Type.String({ - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt, - }), - name: Type.String({ - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name, - }), - harness: Type.Optional( - StringEnum(BACKEND_NAMES, { - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness, - }), - ), - working_dir: Type.Optional( - Type.String({ - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir, - }), - ), - isolation: Type.Optional( - StringEnum(["worktree"] as const, { - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation, - }), - ), - model: Type.Optional( - Type.String({ - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model, - }), - ), - reasoning_effort: Type.Optional( - StringEnum(REASONING_EFFORTS, { - description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort, - }), - ), - }); + const initialSpawnSurface = createSubagentSpawnToolSurface(agentTypeList); const subagentSpawnTool = defineTool({ name: "subagent_spawn", label: "Spawn Subagent", - description: buildSubagentSpawnToolDescription(agentTypeList), + ...initialSpawnSurface, promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET, promptGuidelines: SUBAGENT_SPAWN_PROMPT_GUIDELINES, - parameters: createSubagentSpawnParameters(), async execute(_toolCallId, params, signal, _onUpdate, ctx) { // Only one backend exists; harness is optional and defaults to it. const harness = params.harness ?? BACKEND_NAMES[0]; diff --git a/extensions/subagents/prompt.test.ts b/extensions/subagents/prompt.test.ts index 9786049e..d9236a15 100644 --- a/extensions/subagents/prompt.test.ts +++ b/extensions/subagents/prompt.test.ts @@ -8,15 +8,45 @@ import { MAX_RUNNING } from "./src/manager.ts"; import { buildAgentTypeParameterDescription, buildSubagentSpawnResult, + createSubagentSpawnToolSurface, createAgentTypeParameterSchema, + SUBAGENT_SCHEMA_BUDGETS, SUBAGENT_SPAWN_PROMPT_GUIDELINES, SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS, SUBAGENT_SPAWN_TOOL_DESCRIPTION, SUBAGENT_WAIT_TOOL_DESCRIPTION, } from "./src/prompt.ts"; -import { BUILT_IN_AGENT_TYPES, type AgentType } from "./src/agent-types.ts"; +import { + AGENT_TYPE_LIMITS, + BUILT_IN_AGENT_TYPES, + type AgentType, +} from "./src/agent-types.ts"; + +function spawnSurfaceBytes(agentTypes: readonly AgentType[]) { + const surface = createSubagentSpawnToolSurface(agentTypes); + return Buffer.byteLength( + JSON.stringify({ name: "subagent_spawn", ...surface }), + "utf8", + ); +} + +function maximumRoster(): AgentType[] { + return Array.from({ length: AGENT_TYPE_LIMITS.files }, (_, index) => { + const prefix = `role-${index}-`; + return { + name: prefix + "x".repeat(AGENT_TYPE_LIMITS.nameChars - prefix.length), + description: "界".repeat(AGENT_TYPE_LIMITS.descriptionChars), + tools: Array.from( + { length: AGENT_TYPE_LIMITS.tools }, + (__, toolIndex) => `custom-tool-${index}-${toolIndex}`, + ), + reasoningEffort: "high", + source: `test:${index}`, + }; + }); +} -test("the generated agent_type schema exposes each effective capability and effort default", () => { +test("the generated agent_type schema exposes a compact, enforced role index", () => { const parentOnlyType: AgentType = { name: "parent-only", description: "Attempts parent orchestration.", @@ -46,17 +76,61 @@ test("the generated agent_type schema exposes each effective capability and effo assert.match(description, /explorer.*default reasoning_effort: high/); assert.match(description, /reviewer.*default reasoning_effort: medium/); assert.match(description, /advisor.*default reasoning_effort: xhigh/); - assert.match(description, /parent-only.*reasoning_effort: inherits parent/); - assert.match(description, /parent-only.*only: read/); - assert.doesNotMatch(description, /only: read, subagent_spawn/); - assert.match( - description, - /explicit spawn model > selected type file model > configured built-in role model > parent model/, + assert.match(description, /parent-only.*read-only/); + assert.doesNotMatch(description, /only: read/); + assert.doesNotMatch(description, /subagent_spawn/); + assert.doesNotMatch(description, /precedence/i); +}); + +test("the default spawn surface stays within its resident budget", () => { + assert.ok( + spawnSurfaceBytes(BUILT_IN_AGENT_TYPES) <= + SUBAGENT_SCHEMA_BUDGETS.defaultSpawnSurfaceBytes, ); - assert.match( - description, - /explicit spawn reasoning_effort > selected type default > parent reasoning effort/, +}); + +test("the maximum legal roster keeps every enum value while bounding summaries", () => { + const roster = maximumRoster(); + const schema = createAgentTypeParameterSchema(roster); + const description = buildAgentTypeParameterDescription(roster); + + for (const agentType of roster) { + assert.equal(Value.Check(schema, agentType.name), true, agentType.name); + } + assert.match(description, /presets omitted/i); + assert.doesNotMatch(description, /custom-tool-/); + assert.ok( + Buffer.byteLength(description, "utf8") <= + SUBAGENT_SCHEMA_BUDGETS.roleDirectoryBytes, ); + assert.ok( + spawnSurfaceBytes(roster) <= + SUBAGENT_SCHEMA_BUDGETS.maximumSpawnSurfaceBytes, + ); +}); + +test("role summaries are deterministic and truncate UTF-8 without splitting it", () => { + const long: AgentType = { + name: "long-purpose", + description: "界".repeat(AGENT_TYPE_LIMITS.descriptionChars), + tools: ["read"], + reasoningEffort: "high", + source: "test", + }; + const peer: AgentType = { + name: "alpha", + description: "Alpha role", + source: "test", + }; + const forward = createSubagentSpawnToolSurface([long, peer]); + const reverse = createSubagentSpawnToolSurface([peer, long]); + const description = ( + forward.parameters.properties.agent_type as { description?: string } + ).description; + + assert.deepEqual(forward, reverse); + assert.match(description ?? "", /界…/u); + assert.doesNotMatch(description ?? "", /�/u); }); test("the spawn description derives its concurrency cap from the manager", () => { diff --git a/extensions/subagents/src/agent-types.ts b/extensions/subagents/src/agent-types.ts index 9d2a0655..ec21094f 100644 --- a/extensions/subagents/src/agent-types.ts +++ b/extensions/subagents/src/agent-types.ts @@ -101,7 +101,7 @@ export interface AgentType { readonly source: string; } -const READ_ONLY_TOOLS = [ +export const READ_ONLY_AGENT_TOOLS = [ "read", "grep", "find", @@ -123,7 +123,7 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [ name: "explorer", description: "Read-only codebase exploration. Use high for routine, local, direct tracing; xhigh for interacting state transitions, concurrency or trust boundaries, or subtle multi-path lifecycle/control-flow; max only for exceptionally difficult broad unfamiliar architecture with unresolved competing flows.", - tools: READ_ONLY_TOOLS, + tools: READ_ONLY_AGENT_TOOLS, reasoningEffort: "high", body: "Explore the codebase read-only. Trace the real flow, inspect related callers, and report concise evidence with file paths and line references.", source: "built-in:explorer", @@ -152,7 +152,7 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [ { name: "reviewer", description: "Read-only review for correctness, safety, and regressions.", - tools: READ_ONLY_TOOLS, + tools: READ_ONLY_AGENT_TOOLS, reasoningEffort: "medium", body: "Review the requested code or change read-only. Identify concrete correctness, security, and regression risks with evidence; do not modify files.", source: "built-in:reviewer", @@ -160,7 +160,7 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [ { name: "advisor", description: "Deep read-only analysis and technical advice.", - tools: READ_ONLY_TOOLS, + tools: READ_ONLY_AGENT_TOOLS, reasoningEffort: "xhigh", body: "Analyze the problem deeply without modifying files. Explain the relevant tradeoffs, risks, and recommended next step using repository evidence.", source: "built-in:advisor", diff --git a/extensions/subagents/src/prompt.ts b/extensions/subagents/src/prompt.ts index b246e44b..a360f3fd 100644 --- a/extensions/subagents/src/prompt.ts +++ b/extensions/subagents/src/prompt.ts @@ -3,54 +3,127 @@ import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { effectiveChildToolAllowlist } from "../../shared/child-session.ts"; -import type { AgentType } from "./agent-types.ts"; +import { SUBAGENT_ROLE_NAMES } from "../../shared/subagent-roles.ts"; +import { type AgentType, READ_ONLY_AGENT_TOOLS } from "./agent-types.ts"; +import { BACKEND_NAMES, REASONING_EFFORTS } from "./domain.ts"; import { MAX_RUNNING } from "./manager.ts"; +export const SUBAGENT_SCHEMA_BUDGETS = Object.freeze({ + rolePurposeBytes: 240, + roleDirectoryBytes: 4 * 1024, + defaultSpawnSurfaceBytes: 2.5 * 1024, + maximumSpawnSurfaceBytes: 16 * 1024, +}); + /** Describes subagent_spawn, including the fixed concurrency cap. */ export const SUBAGENT_SPAWN_TOOL_DESCRIPTION = - "Spawn a background subagent: a fully autonomous, headless pi session with its own context window, this environment's tools and config, and normal host permissions. Fire-and-forget: this returns immediately with an id, and the subagent's final output is automatically queued back to you as a message when it settles. In an interactive session, keep working or end your turn so the user remains able to interact; do not block merely because a later step depends on the result. Children cannot orchestrate more agents/workflows or ask the user, and cannot see this conversation, so the prompt must be self-contained. Only use trusted working directories. " + + "Spawn a background in-process Pi subagent with its own context, child-safe tools, and normal host permissions. Returns immediately; its final result is delivered automatically. The child cannot see this conversation, ask the user, or orchestrate agents/workflows. Use only trusted working directories. " + `Max ${MAX_RUNNING} subagents can be running at once.`; -/** - * Appends the configured agent types, if any. They are a runtime resource, so - * the roster has to be baked into the description at registration time. - */ -export function buildSubagentSpawnToolDescription( - agentTypes: readonly AgentType[], -) { - if (agentTypes.length === 0) return SUBAGENT_SPAWN_TOOL_DESCRIPTION; - return `${SUBAGENT_SPAWN_TOOL_DESCRIPTION} This environment also defines agent types (see agent_type): named presets that fix a child's system prompt, and often restrict it to a subset of tools. Prefer one when it matches the task — a type's tool restriction is enforced, not advisory.`; +/** UTF-8 bounded, whitespace-normalized text for the parent-facing roster. */ +function boundedPurpose(description: string) { + const normalized = description.trim().replace(/\s+/gu, " "); + const limit = SUBAGENT_SCHEMA_BUDGETS.rolePurposeBytes; + if (Buffer.byteLength(normalized, "utf8") <= limit) return normalized; + const suffix = "…"; + let used = Buffer.byteLength(suffix, "utf8"); + let output = ""; + for (const character of normalized) { + const bytes = Buffer.byteLength(character, "utf8"); + if (used + bytes > limit) break; + output += character; + used += bytes; + } + return output.trimEnd() + suffix; +} + +function compareNames(left: string, right: string) { + return left < right ? -1 : left > right ? 1 : 0; } -/** Lists each agent type's enforced capabilities and reasoning default. */ +/** Built-ins stay familiar; project/global additions are stable by name. */ +function orderedAgentTypes(agentTypes: readonly AgentType[]) { + const builtInOrder = new Map( + SUBAGENT_ROLE_NAMES.map((name, index) => [name, index]), + ); + return [...agentTypes].sort((left, right) => { + const leftIndex = builtInOrder.get(left.name); + const rightIndex = builtInOrder.get(right.name); + if (leftIndex !== undefined || rightIndex !== undefined) { + if (leftIndex === undefined) return 1; + if (rightIndex === undefined) return -1; + return leftIndex - rightIndex; + } + return compareNames(left.name, right.name); + }); +} + +function capabilityClass(agentType: AgentType) { + if (agentType.tools === undefined) return "inherited-tools"; + const tools = effectiveChildToolAllowlist(agentType.tools) ?? []; + if (tools.length === 0) return "no-tools"; + if (tools.every((tool) => READ_ONLY_AGENT_TOOLS.includes(tool))) { + return "read-only"; + } + if ( + tools.some((tool) => tool === "bash" || tool === "edit" || tool === "write") + ) { + return "workspace-write"; + } + // Third-party child-safe tools may still have side effects, so only claim + // the enforceable fact: this preset has a restricted tool set. + return "restricted"; +} + +function agentTypeSummary(agentType: AgentType) { + const effort = agentType.reasoningEffort + ? ` [default reasoning_effort: ${agentType.reasoningEffort}]` + : ""; + return `"${agentType.name}" — ${boundedPurpose(agentType.description)} [${capabilityClass(agentType)}]${effort}`; +} + +/** A deterministic, bounded selection index; execution details stay in Skill. */ export function buildAgentTypeParameterDescription( agentTypes: readonly AgentType[], ) { - const entries = agentTypes.map((agentType) => { - const tools = agentType.tools - ? (() => { - const effectiveTools = effectiveChildToolAllowlist(agentType.tools); - return effectiveTools?.length - ? ` [only: ${effectiveTools.join(", ")}]` - : " [only: no child-safe tools]"; - })() - : ""; - const effort = agentType.reasoningEffort - ? ` [default reasoning_effort: ${agentType.reasoningEffort}]` - : " [reasoning_effort: inherits parent]"; - return `"${agentType.name}" — ${agentType.description}${tools}${effort}`; - }); - return `Optional agent type: a preset that gives the child a specialized system prompt and, when listed, restricts it to exactly those child-safe tools. Omit for a general-purpose subagent with the normal tool set. Available: ${entries.join("; ")}. Model precedence: explicit spawn model > selected type file model > configured built-in role model > parent model. Reasoning precedence: explicit spawn reasoning_effort > selected type default > parent reasoning effort.`; + const ordered = orderedAgentTypes(agentTypes); + const intro = + "Optional named preset for the child prompt and capability boundary. Omit for a general-purpose child. Available: "; + const outro = + " Preset restrictions are enforced; read the Subagents Skill or role file for full details."; + const entries: string[] = []; + for (const agentType of ordered) { + const summary = agentTypeSummary(agentType); + const next = [...entries, summary]; + const omitted = ordered.length - next.length; + const omission = omitted + ? `; ${omitted} presets omitted from this summary; their exact enum names remain valid.` + : "."; + const candidate = `${intro}${next.join("; ")}${omission}${outro}`; + if ( + Buffer.byteLength(candidate, "utf8") > + SUBAGENT_SCHEMA_BUDGETS.roleDirectoryBytes + ) { + break; + } + entries.push(summary); + } + const omitted = ordered.length - entries.length; + const omission = omitted + ? `; ${omitted} presets omitted from this summary; their exact enum names remain valid.` + : "."; + return `${intro}${entries.join("; ")}${omission}${outro}`; } /** Generated schema for the dynamic agent-type roster. */ export function createAgentTypeParameterSchema( agentTypes: readonly AgentType[], ) { + const ordered = orderedAgentTypes(agentTypes); return Type.Optional( StringEnum( - agentTypes.map((agentType) => agentType.name) as [string, ...string[]], - { description: buildAgentTypeParameterDescription(agentTypes) }, + ordered.map((agentType) => agentType.name) as [string, ...string[]], + { description: buildAgentTypeParameterDescription(ordered) }, ), ); } @@ -61,27 +134,69 @@ export const SUBAGENT_SPAWN_PROMPT_SNIPPET = /** Guides the parent model to delegate standalone tasks and avoid unnecessary blocking waits. */ export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [ - "Reserve subagent_spawn for substantial, self-contained work; give it a complete, standalone prompt. For a single lookup or edit you can do inline, just do it — each subagent spends a fresh context window and cannot see this conversation.", - "After subagent_spawn, keep working on independent work. If none remains in an interactive session, briefly tell the user the subagent is running in the background and end your turn; its result arrives automatically and you are re-invoked when it settles. Do not poll with subagent_check. Do not call subagent_wait merely because your next step depends on the result or because you have nothing else to do. Block only when the user explicitly asks you to keep the current response open for these results, or when a non-interactive automation must return them in the same invocation. Never answer from a guessed result before it arrives.", + "Delegate substantial independent work; do a single lookup or edit inline.", + "After spawning, continue independent work. In an interactive session, end your turn when none remains; automatic delivery will re-invoke you. Do not call subagent_wait merely because the next step depends on the result; use it only when the user explicitly asks to keep the response open, or the same non-interactive invocation must return the result. Never poll or guess the result.", ]; /** Model-facing schema descriptions for subagent_spawn task and execution options. */ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = { prompt: "Task prompt for the subagent. Must be self-contained: include all needed context, file paths, and what to report back.", - name: "Short human-readable name for this subagent, shown in listings and the UI", - harness: - 'Optional. The only harness is "pi" (an in-process Pi session that inherits this environment), which is the default; you can omit this.', + name: "Short human-readable name shown in listings and the UI", + harness: 'Optional; "pi" is the only harness and the default.', workingDir: - "Trusted working directory for the autonomous child (default: current working directory)", + "Trusted child working directory; defaults to the current directory", isolation: - 'Set to "worktree" for concurrent writers and tell the child to commit. Requires Git and a clean checkout. Read the subagents Skill for lifecycle, merge location, and costs.', + 'Use "worktree" for concurrent writers and tell the child to commit. See the Subagents Skill for lifecycle details.', model: - 'Optional model override, as "provider/model-id" or a bare id resolved against the current provider. Precedence: explicit spawn model > selected type file model > configured built-in role model > parent model. Never guess a model name.', + 'Optional "provider/model-id" or current-provider model override. Omit to use the preset, configured role, or parent default. Never guess a model name.', reasoningEffort: - "Optional thinking level for the child. Precedence: explicit spawn reasoning_effort > selected type default > parent reasoning effort.", + "Optional child thinking level; omit to use the preset or parent default.", }; +/** The exact name/description/wire-schema source used by registration/tests. */ +export function createSubagentSpawnToolSurface( + agentTypes: readonly AgentType[], +) { + return { + description: SUBAGENT_SPAWN_TOOL_DESCRIPTION, + parameters: Type.Object({ + agent_type: createAgentTypeParameterSchema(agentTypes), + prompt: Type.String({ + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt, + }), + name: Type.String({ + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name, + }), + harness: Type.Optional( + StringEnum(BACKEND_NAMES, { + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness, + }), + ), + working_dir: Type.Optional( + Type.String({ + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir, + }), + ), + isolation: Type.Optional( + StringEnum(["worktree"] as const, { + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation, + }), + ), + model: Type.Optional( + Type.String({ + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model, + }), + ), + reasoning_effort: Type.Optional( + StringEnum(REASONING_EFFORTS, { + description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort, + }), + ), + }), + }; +} + /** Builds the subagent_spawn result that tells the parent model how to continue or inspect the child. */ export function buildSubagentSpawnResult(options: { id: string; diff --git a/skills/subagents/SKILL.md b/skills/subagents/SKILL.md index 1616986f..7174e337 100644 --- a/skills/subagents/SKILL.md +++ b/skills/subagents/SKILL.md @@ -10,7 +10,8 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati - Delegate substantial independent work, not a lookup or edit the parent can do directly. - Give the child a standalone prompt with paths, constraints, relevant context, and the expected report; it cannot see the parent conversation or ask the user. - Inherit the parent model and thinking level by default. Override them only for an explicit user request or concrete task requirement. -- Prefer a matching agent type when one exists; its tool restriction is enforced. An explicit spawn model or reasoning effort wins, otherwise use the type default then inherit the parent. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see `extensions/subagents/docs/agent-types.md`. +- For the built-in explorer, `high` fits routine local tracing, `xhigh` fits interacting lifecycle/concurrency/trust boundaries, and `max` is only for exceptionally broad unresolved architecture. +- Prefer a matching agent type when one exists; its tool restriction is enforced. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see `extensions/subagents/docs/agent-types.md`. - Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected. - After spawning, continue useful parent work. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation. From 4907ef1785fe06affa5768cbec877028a31fe21e Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Sun, 23 Aug 2026 16:24:27 +0800 Subject: [PATCH 2/2] fix(subagents): make role effort guidance model-aware --- README.md | 16 +++++----- extensions/subagents/agent-types.test.ts | 28 ++++++++++++----- extensions/subagents/docs/agent-types.md | 23 ++++++++------ extensions/subagents/prompt.test.ts | 39 ++++++++++++++++++++++-- extensions/subagents/src/agent-types.ts | 15 +++++---- extensions/subagents/src/prompt.ts | 2 +- skills/subagents/SKILL.md | 3 +- 7 files changed, 88 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 88581350..bb3cdad9 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ subagent_spawn({ 每个 Subagent 都是新的进程内 Pi SDK Session: -- 默认继承父会话的 Provider、模型与 Thinking Level; +- 默认继承父会话的 Provider 与模型;用户可明确指定 Thinking Level,否则模型根据角色建议、任务难度与目标模型实际支持的档位选择; - 继承普通 child-safe 工具、Skills、项目说明与 Trust 决策; - 最多 4 个模型发起的 Subagent 并发运行,结束后自动回传; - 可 `check`、`wait`、`cancel`,也可用 `subagent_send` 继续同一子会话; @@ -194,12 +194,14 @@ subagent_spawn({ 内置角色由 Harness 强制工具边界,不靠 Prompt 自律: -| `agent_type` | 适合 | 默认 effort | 强制能力 | -| ------------- | ---------------- | ----------- | ----------------------------- | -| `explorer` | 代码追踪与探索 | high | 只读发现工具 | -| `implementer` | 聚焦实现 | high | read / bash / edit / write 等 | -| `reviewer` | 正确性与回归审查 | medium | 只读发现工具 | -| `advisor` | 深度技术建议 | xhigh | 只读发现工具 | +| `agent_type` | 适合 | 相对 effort 建议 | 强制能力 | +| ------------- | ---------------- | ------------------- | ----------------------------- | +| `explorer` | 代码追踪与探索 | 中等,难题可提高 | 只读发现工具 | +| `implementer` | 聚焦实现 | 中高,按范围与风险调整 | read / bash / edit / write 等 | +| `reviewer` | 正确性与回归审查 | 较高 | 只读发现工具 | +| `advisor` | 深度技术建议 | 较高 | 只读发现工具 | + +上述只是模型的相对选择提示,不会为内置角色写死具体档位。用户明确指定的 `reasoning_effort` 始终优先;否则模型结合任务难度,从目标模型实际支持的档位中选择。 角色可由全局 `~/.pi/agent/agents/*.md` 或受信任项目 `.pi/agents/*.md` 覆盖。模型优先级是:显式调用 > Agent Type 文件 > `/openpi-setup` 角色模型 > 父模型继承。更高优先级定义损坏时会阻断 fallback,而不是悄悄退回更宽松的能力。 diff --git a/extensions/subagents/agent-types.test.ts b/extensions/subagents/agent-types.test.ts index d4ad712d..7be75c44 100644 --- a/extensions/subagents/agent-types.test.ts +++ b/extensions/subagents/agent-types.test.ts @@ -64,7 +64,7 @@ async function seed( return { agentDir, cwd }; } -test("built-in roles have exact capability boundaries and no model defaults", () => { +test("built-in roles have exact capability boundaries and no fixed model or effort defaults", () => { assert.deepEqual( BUILT_IN_AGENT_TYPES.map((role) => ({ name: role.name, @@ -86,7 +86,7 @@ test("built-in roles have exact capability boundaries and no model defaults", () "git_diff", "git_log", ], - effort: "high", + effort: undefined, model: undefined, }, { @@ -105,7 +105,7 @@ test("built-in roles have exact capability boundaries and no model defaults", () "git_diff", "git_log", ], - effort: "high", + effort: undefined, model: undefined, }, { @@ -121,7 +121,7 @@ test("built-in roles have exact capability boundaries and no model defaults", () "git_diff", "git_log", ], - effort: "medium", + effort: undefined, model: undefined, }, { @@ -137,13 +137,27 @@ test("built-in roles have exact capability boundaries and no model defaults", () "git_diff", "git_log", ], - effort: "xhigh", + effort: undefined, model: undefined, }, ], ); - assert.match(BUILT_IN_AGENT_TYPES[0]?.description ?? "", /xhigh/); - assert.match(BUILT_IN_AGENT_TYPES[0]?.description ?? "", /max only/); + assert.match( + BUILT_IN_AGENT_TYPES[0]?.description ?? "", + /moderate reasoning/, + ); + assert.match( + BUILT_IN_AGENT_TYPES[1]?.description ?? "", + /medium-high reasoning/, + ); + assert.match(BUILT_IN_AGENT_TYPES[2]?.description ?? "", /high reasoning/); + assert.match(BUILT_IN_AGENT_TYPES[3]?.description ?? "", /high reasoning/); + assert.ok( + BUILT_IN_AGENT_TYPES.every( + (role) => + role.description.includes("task") || role.description.includes("tasks"), + ), + ); }); test("a valid agent type parses into prompt, tools, model, and effort", () => { diff --git a/extensions/subagents/docs/agent-types.md b/extensions/subagents/docs/agent-types.md index 3733ce15..49273d85 100644 --- a/extensions/subagents/docs/agent-types.md +++ b/extensions/subagents/docs/agent-types.md @@ -1,8 +1,8 @@ # Agent types An agent type is a reusable child-agent definition shared by -`subagent_spawn` and Workflow `agent()`: a named preset that fixes a child's -system prompt, its model and thinking level, and — the point of the feature — +`subagent_spawn` and Workflow `agent()`: a named preset that can set a child's +system prompt, model, and thinking level, and — the point of the feature — **which tools it may use at all**. Four provider-free built-in roles are always available: `explorer`, `implementer`, `reviewer`, and `advisor`. @@ -48,14 +48,17 @@ All built-ins omit a model, so they inherit the parent model unless configured through `/openpi-setup`. Their complete definitions can be replaced by a custom file with the same name. -| Role | Tools | Effort | Purpose | -| ------------- | ----------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `explorer` | `read grep find ls fd rg git_show git_diff git_log` | `high` | Read-only codebase tracing. Use `high` for routine, local, direct tracing; `xhigh` for interacting state transitions, concurrency or trust boundaries, or subtle multi-path lifecycle/control-flow; `max` only for exceptionally difficult broad unfamiliar architecture with unresolved competing flows. | -| `implementer` | `read bash edit write grep find ls fd rg git_show git_diff git_log` | `high` | Focused implementation and relevant checks. | -| `reviewer` | `read grep find ls fd rg git_show git_diff git_log` | `medium` | Read-only correctness, safety, and regression review. | -| `advisor` | `read grep find ls fd rg git_show git_diff git_log` | `xhigh` | Deep read-only analysis and technical advice. | - -Built-ins have concise role prompts and no provider or model names. Their tool +| Role | Tools | Relative effort guidance | Purpose | +| ------------- | ------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------ | +| `explorer` | `read grep find ls fd rg git_show git_diff git_log` | Moderate | Read-only codebase tracing; increase for harder tasks. | +| `implementer` | `read bash edit write grep find ls fd rg git_show git_diff git_log` | Medium-high | Focused implementation; adjust for scope and risk. | +| `reviewer` | `read grep find ls fd rg git_show git_diff git_log` | High | Read-only correctness, safety, and regression review. | +| `advisor` | `read grep find ls fd rg git_show git_diff git_log` | High | Deep read-only analysis and technical advice. | + +These are relative selection hints, not fixed Pi thinking levels. Built-ins set +no model or reasoning-effort default. An explicit user requirement takes +priority; otherwise the parent model chooses from levels supported by the +resolved child model according to the role and task difficulty. Their tool allowlists still intersect with plan mode and the child denylist. ## Discovery diff --git a/extensions/subagents/prompt.test.ts b/extensions/subagents/prompt.test.ts index d9236a15..3e154602 100644 --- a/extensions/subagents/prompt.test.ts +++ b/extensions/subagents/prompt.test.ts @@ -73,15 +73,48 @@ test("the generated agent_type schema exposes a compact, enforced role index", ( ...BUILT_IN_AGENT_TYPES, parentOnlyType, ]); - assert.match(description, /explorer.*default reasoning_effort: high/); - assert.match(description, /reviewer.*default reasoning_effort: medium/); - assert.match(description, /advisor.*default reasoning_effort: xhigh/); + assert.match(description, /explorer.*moderate reasoning/); + assert.match(description, /implementer.*medium-high reasoning/); + assert.match(description, /reviewer.*high reasoning/); + assert.match(description, /advisor.*high reasoning/); + assert.doesNotMatch(description, /default reasoning_effort/); assert.match(description, /parent-only.*read-only/); assert.doesNotMatch(description, /only: read/); assert.doesNotMatch(description, /subagent_spawn/); assert.doesNotMatch(description, /precedence/i); }); +test("reasoning guidance prioritizes the user and task difficulty without fixing a built-in level", () => { + assert.match( + SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort, + /user's requested level/i, + ); + assert.match( + SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort, + /task difficulty/i, + ); + assert.match( + SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort, + /supported by the resolved child model/i, + ); +}); + +test("an explicit user-selected reasoning level remains available", () => { + const schema = + createSubagentSpawnToolSurface(BUILT_IN_AGENT_TYPES).parameters; + const task = { + agent_type: "reviewer", + prompt: "Review the change.", + name: "review", + }; + + assert.equal(Value.Check(schema, { ...task, reasoning_effort: "max" }), true); + assert.equal( + Value.Check(schema, { ...task, reasoning_effort: "unsupported" }), + false, + ); +}); + test("the default spawn surface stays within its resident budget", () => { assert.ok( spawnSurfaceBytes(BUILT_IN_AGENT_TYPES) <= diff --git a/extensions/subagents/src/agent-types.ts b/extensions/subagents/src/agent-types.ts index ec21094f..dd2d4e84 100644 --- a/extensions/subagents/src/agent-types.ts +++ b/extensions/subagents/src/agent-types.ts @@ -122,15 +122,15 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [ { name: "explorer", description: - "Read-only codebase exploration. Use high for routine, local, direct tracing; xhigh for interacting state transitions, concurrency or trust boundaries, or subtle multi-path lifecycle/control-flow; max only for exceptionally difficult broad unfamiliar architecture with unresolved competing flows.", + "Read-only codebase exploration. Usually use moderate reasoning, increasing it for harder tasks.", tools: READ_ONLY_AGENT_TOOLS, - reasoningEffort: "high", body: "Explore the codebase read-only. Trace the real flow, inspect related callers, and report concise evidence with file paths and line references.", source: "built-in:explorer", }, { name: "implementer", - description: "Focused implementation with repository checks.", + description: + "Focused implementation with repository checks. Usually use medium-high reasoning, adjusted for scope, risk, and task difficulty.", tools: [ "read", "bash", @@ -145,23 +145,22 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [ "git_diff", "git_log", ], - reasoningEffort: "high", body: "Implement the requested change carefully. Trace the affected flow first, make the smallest correct edit, and run relevant checks before reporting results.", source: "built-in:implementer", }, { name: "reviewer", - description: "Read-only review for correctness, safety, and regressions.", + description: + "Read-only review for correctness, safety, and regressions. Usually use high reasoning, adjusted for task difficulty.", tools: READ_ONLY_AGENT_TOOLS, - reasoningEffort: "medium", body: "Review the requested code or change read-only. Identify concrete correctness, security, and regression risks with evidence; do not modify files.", source: "built-in:reviewer", }, { name: "advisor", - description: "Deep read-only analysis and technical advice.", + description: + "Deep read-only analysis and technical advice. Usually use high reasoning, adjusted for task difficulty.", tools: READ_ONLY_AGENT_TOOLS, - reasoningEffort: "xhigh", body: "Analyze the problem deeply without modifying files. Explain the relevant tradeoffs, risks, and recommended next step using repository evidence.", source: "built-in:advisor", }, diff --git a/extensions/subagents/src/prompt.ts b/extensions/subagents/src/prompt.ts index a360f3fd..024fbc90 100644 --- a/extensions/subagents/src/prompt.ts +++ b/extensions/subagents/src/prompt.ts @@ -151,7 +151,7 @@ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = { model: 'Optional "provider/model-id" or current-provider model override. Omit to use the preset, configured role, or parent default. Never guess a model name.', reasoningEffort: - "Optional child thinking level; omit to use the preset or parent default.", + "Optional child thinking level. Honor the user's requested level. Otherwise choose a level supported by the resolved child model based on the selected role and task difficulty. An explicit value overrides a role default.", }; /** The exact name/description/wire-schema source used by registration/tests. */ diff --git a/skills/subagents/SKILL.md b/skills/subagents/SKILL.md index 7174e337..30158f95 100644 --- a/skills/subagents/SKILL.md +++ b/skills/subagents/SKILL.md @@ -9,8 +9,7 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati - Delegate substantial independent work, not a lookup or edit the parent can do directly. - Give the child a standalone prompt with paths, constraints, relevant context, and the expected report; it cannot see the parent conversation or ask the user. -- Inherit the parent model and thinking level by default. Override them only for an explicit user request or concrete task requirement. -- For the built-in explorer, `high` fits routine local tracing, `xhigh` fits interacting lifecycle/concurrency/trust boundaries, and `max` is only for exceptionally broad unresolved architecture. +- Inherit the parent model by default. When choosing the child's reasoning effort, honor an explicit user requirement first; otherwise use the selected role's relative guidance and the task's difficulty, choosing from levels supported by the resolved child model. - Prefer a matching agent type when one exists; its tool restriction is enforced. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see `extensions/subagents/docs/agent-types.md`. - Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected. - After spawning, continue useful parent work. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation.