feat: renamed subagent handoff flow to use yield instead of submit_result

- Renamed subagent completion flow from `submit_result` to `yield` across SDK tools, prompts, and docs.
- Updated executor/task handling to require and parse `yield` calls, replacing legacy submit-result extraction and state flags.
- Added `subagent-yield-reminder` and updated system prompts to require `yield` with `result.data` or `result.error`.
- Renamed hidden-tool and registration plumbing to `yield`, including discovery helpers and renderer/test surface.
This commit is contained in:
can1357
2026-04-26 00:29:52 +02:00
parent 6bd4cf24fd
commit ecd1554eba
30 changed files with 200 additions and 221 deletions
+10 -10
View File
@@ -53,9 +53,9 @@ import { ResolveTool } from "./resolve";
import { reportFindingTool } from "./review";
import { SearchToolBm25Tool } from "./search-tool-bm25";
import { loadSshTool } from "./ssh";
import { SubmitResultTool } from "./submit-result";
import { type TodoPhase, TodoWriteTool } from "./todo-write";
import { WriteTool } from "./write";
import { YieldTool } from "./yield";
// Exa MCP tools (22 tools)
@@ -91,10 +91,10 @@ export * from "./resolve";
export * from "./review";
export * from "./search-tool-bm25";
export * from "./ssh";
export * from "./submit-result";
export * from "./todo-write";
export * from "./vim";
export * from "./write";
export * from "./yield";
/** Tool type (AgentTool from pi-ai) */
export type Tool = AgentTool<any, any, any>;
@@ -131,8 +131,8 @@ export interface ToolSession {
eventBus?: EventBus;
/** Output schema for structured completion (subagents) */
outputSchema?: unknown;
/** Whether to include the submit_result tool by default */
requireSubmitResultTool?: boolean;
/** Whether to include the yield tool by default */
requireYieldTool?: boolean;
/** Task recursion depth (0 = top-level, 1 = first child, etc.) */
taskDepth?: number;
/** Get session file */
@@ -245,7 +245,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
};
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
submit_result: s => new SubmitResultTool(s),
yield: s => new YieldTool(s),
report_finding: () => reportFindingTool,
report_tool_issue: s => createReportToolIssueTool(s),
exit_plan_mode: s => new ExitPlanModeTool(s),
@@ -288,7 +288,7 @@ function getPythonModeFromEnv(): PythonToolMode | null {
* Create tools from BUILTIN_TOOLS registry.
*/
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
const includeSubmitResult = session.requireSubmitResultTool === true;
const includeYield = session.requireYieldTool === true;
const enableLsp = session.enableLsp ?? true;
const requestedTools =
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
@@ -390,7 +390,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
if (name === "bash") return allowBash;
if (name === "python") return allowPython;
if (name === "debug") return session.settings.get("debug.enabled");
if (name === "todo_write") return !includeSubmitResult && session.settings.get("todo.enabled");
if (name === "todo_write") return !includeYield && session.settings.get("todo.enabled");
if (name === "find") return session.settings.get("find.enabled");
if (name === "grep") return session.settings.get("grep.enabled");
if (name.startsWith("gh_")) return session.settings.get("github.enabled");
@@ -411,8 +411,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
}
return true;
};
if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {
requestedTools.push("submit_result");
if (includeYield && requestedTools && !requestedTools.includes("yield")) {
requestedTools.push("yield");
}
const filteredRequestedTools = requestedTools?.filter(name => name in allTools && isToolAllowed(name));
@@ -421,7 +421,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
: [
...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
...(includeSubmitResult ? ([["submit_result", HIDDEN_TOOLS.submit_result]] as const) : []),
...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
];
+3 -3
View File
@@ -3,7 +3,7 @@
*
* Used by the reviewer agent to report findings in a structured way.
* Hidden by default - only enabled when explicitly listed in agent's tools.
* Reviewers finish via `submit_result` tool with SubmitReviewDetails schema.
* Reviewers finish via `yield` tool with SubmitReviewDetails schema.
*/
// ─────────────────────────────────────────────────────────────────────────────
// Subprocess tool handlers - registered for extraction/rendering in task tool
@@ -131,7 +131,7 @@ export function parseReportFindingDetails(value: unknown): ReportFindingDetails
export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
name: "report_finding",
label: "Report Finding",
description: "Report a code review finding. Use this for each issue found. Call submit_result when done.",
description: "Report a code review finding. Use this for each issue found. Call yield when done.",
parameters: ReportFindingParams,
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const { title, body, priority, confidence, file_path, line_start, line_end } = params;
@@ -186,7 +186,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
},
};
/** SubmitReviewDetails - used for rendering review results from submit_result tool */
/** SubmitReviewDetails - used for rendering review results from yield tool */
export interface SubmitReviewDetails {
overall_correctness: "correct" | "incorrect";
explanation: string;
@@ -12,7 +12,7 @@ import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
import type { ToolSession } from ".";
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
export interface SubmitResultDetails {
export interface YieldDetails {
data: unknown;
status: "success" | "aborted";
error?: string;
@@ -40,8 +40,8 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
.join("; ");
}
export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails> {
readonly name = "submit_result";
export class YieldTool implements AgentTool<TSchema, YieldDetails> {
readonly name = "yield";
readonly label = "Submit Result";
readonly description =
"Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
@@ -143,9 +143,9 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
_toolCallId: string,
params: Static<TSchema>,
_signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback<SubmitResultDetails>,
_onUpdate?: AgentToolUpdateCallback<YieldDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<SubmitResultDetails>> {
): Promise<AgentToolResult<YieldDetails>> {
const raw = params as Record<string, unknown>;
const rawResult = raw.result;
if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
@@ -169,7 +169,7 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
let schemaValidationOverridden = false;
if (status === "success") {
if (data === undefined || data === null) {
throw new Error("data is required when submit_result indicates success");
throw new Error("data is required when yield indicates success");
}
if (this.#validate && !this.#validate(data)) {
this.#schemaValidationFailures++;
@@ -194,7 +194,7 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
}
// Register subprocess tool handler for extraction + termination.
subprocessToolRegistry.register<SubmitResultDetails>("submit_result", {
subprocessToolRegistry.register<YieldDetails>("yield", {
extractData: event => {
const details = event.result?.details;
if (!details || typeof details !== "object") return undefined;