Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,20 +186,22 @@ subagent_spawn({

每个 Subagent 都是新的进程内 Pi SDK Session:

- 默认继承父会话的 Provider、模型与 Thinking Level;
- 默认继承父会话的 Provider 与模型;用户可明确指定 Thinking Level,否则模型根据角色建议、任务难度与目标模型实际支持的档位选择
- 继承普通 child-safe 工具、Skills、项目说明与 Trust 决策;
- 最多 4 个模型发起的 Subagent 并发运行,结束后自动回传;
- 可 `check`、`wait`、`cancel`,也可用 `subagent_send` 继续同一子会话;
- 输入框下方显示实时摘要,空输入时按 `↓` 聚焦,`Enter` 或 `→` 打开管理界面。

内置角色由 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,而不是悄悄退回更宽松的能力。

Expand Down
28 changes: 21 additions & 7 deletions extensions/subagents/agent-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
},
{
Expand All @@ -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,
},
{
Expand All @@ -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,
},
{
Expand All @@ -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", () => {
Expand Down
23 changes: 13 additions & 10 deletions extensions/subagents/docs/agent-types.md
Original file line number Diff line number Diff line change
@@ -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`.

Expand Down Expand Up @@ -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
Expand Down
52 changes: 6 additions & 46 deletions extensions/subagents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -57,7 +56,6 @@ import {
BACKEND_NAMES,
formatElapsed,
latestText,
REASONING_EFFORTS,
type SubagentSnapshot,
} from "./src/domain.ts";
import {
Expand All @@ -77,21 +75,18 @@ 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,
SUBAGENT_CHECK_TOOL_DESCRIPTION,
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";
Expand Down Expand Up @@ -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];
Expand Down
131 changes: 119 additions & 12 deletions extensions/subagents/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
}

test("the generated agent_type schema exposes each effective capability and effort default", () => {
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 a compact, enforced role index", () => {
const parentOnlyType: AgentType = {
name: "parent-only",
description: "Attempts parent orchestration.",
Expand All @@ -43,20 +73,97 @@ test("the generated agent_type schema exposes each effective capability and effo
...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, /parent-only.*reasoning_effort: inherits parent/);
assert.match(description, /parent-only.*only: read/);
assert.doesNotMatch(description, /only: read, subagent_spawn/);
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(
description,
/explicit spawn model > selected type file model > configured built-in role model > parent model/,
SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
/user's requested level/i,
);
assert.match(
description,
/explicit spawn reasoning_effort > selected type default > parent reasoning effort/,
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) <=
SUBAGENT_SCHEMA_BUDGETS.defaultSpawnSurfaceBytes,
);
});

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", () => {
Expand Down
Loading
Loading