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:10 +02:00
parent 6bd4cf24fd
commit ecd1554eba
30 changed files with 200 additions and 221 deletions
@@ -19,4 +19,4 @@ Return concise JSON object with:
Consider how file's changes relate to above files.
{{/if}}
Call submit_result tool with JSON payload.
Call yield tool with JSON payload.
@@ -221,9 +221,9 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
// Subagents with explicit tool lists always need submit_result
if (tools && !tools.includes("submit_result")) {
tools = [...tools, "submit_result"];
// Subagents with explicit tool lists always need yield
if (tools && !tools.includes("yield")) {
tools = [...tools, "yield"];
}
// Parse spawns field (array, "*", or CSV)
File diff suppressed because one or more lines are too long
@@ -1178,8 +1178,8 @@
return html;
}
function renderSubmitResult(name, args, result, ctx) {
let html = toolHead('submit_result');
function renderYield(name, args, result, ctx) {
let html = toolHead('yield');
if (args.data !== undefined) {
html += '<div class="tool-output"><pre>' + escapeHtml(JSON.stringify(args.data, null, 2)) + '</pre></div>';
}
@@ -1287,7 +1287,7 @@
gh_search_issues: renderGh,
gh_search_prs: renderGh,
render_mermaid: renderMermaid,
submit_result: renderSubmitResult,
yield: renderYield,
report_finding: renderReportFinding,
report_tool_issue: renderReportToolIssue,
calc: renderCalc,
@@ -98,7 +98,7 @@ Before acting, determine what kind of question this is:
- For API signatures: copy verbatim from source. You **MUST NOT** paraphrase or reconstruct from memory.
## 5. Report
- Call `submit_result` with structured findings.
- Call `yield` with structured findings.
- Every `sources` entry **MUST** include a verbatim excerpt.
- The `api` array **MUST** contain exact signatures copied from source.
- Clean up cloned repos: `rm -rf /tmp/librarian-*`.
@@ -63,7 +63,7 @@ Your goal is to identify bugs the author would want fixed before merge.
1. Run `git diff` (or `gh pr diff <number>`) to view patch
2. Read modified files for full context
3. Call `report_finding` per issue
4. Call `submit_result` with verdict
4. Call `yield` with verdict
Bash is read-only: `git diff`, `git log`, `git show`, `gh pr diff`. You **MUST NOT** make file edits or trigger builds.
</procedure>
@@ -111,7 +111,7 @@ Each `report_finding` requires:
- `file_path`: Absolute path
- `line_start`, `line_end`: Range ≤10 lines, must overlap diff
Final `submit_result` call (payload under `result.data`):
Final `yield` call (payload under `result.data`):
- `result.data.overall_correctness`: "correct" (no bugs/blockers) or "incorrect"
- `result.data.explanation`: Plain text, 1-3 sentences summarizing verdict. Don't repeat findings (captured via `report_finding`).
- `result.data.confidence`: 0.0-1.0
@@ -40,7 +40,7 @@ Reviewer **MUST**:
2. {{#if skipDiff}}**MUST** run `git diff`/`git show` for assigned files{{else}}**MUST** use diff hunks below (**MUST NOT** re-run git diff){{/if}}
3. **MAY** read full file context as needed via `read`
4. Call `report_finding` per issue
5. Call `submit_result` with verdict when done
5. Call `yield` with verdict when done
{{#if skipDiff}}
### Diff Previews
@@ -1,11 +0,0 @@
<system-reminder>
You stopped without calling submit_result. This is reminder {{retryCount}} of {{maxRetries}}.
You **MUST** call submit_result as your only action now. Choose one:
- If task is complete: call submit_result with your result in `result.data`
- If task failed: call submit_result with `result.error` describing what happened
You **MUST NOT** give up if you can still complete the task through exploration (using available tools or repo context). If you submit an error, you **MUST** include what you tried and the exact blocker.
You **MUST NOT** output text without a tool call. You **MUST** call submit_result to finish.
</system-reminder>
@@ -15,9 +15,9 @@ If you need additional information, you can find your conversation with the user
{{/if}}
{{SECTION_SEPARATOR "Closure"}}
No TODO tracking, no progress updates. Execute, call `submit_result`, done.
No TODO tracking, no progress updates. Execute, call `yield`, done.
When finished, you **MUST** call `submit_result` exactly once. This is like writing to a ticket, provide what is required, and close it.
When finished, you **MUST** call `yield` exactly once. This is like writing to a ticket, provide what is required, and close it.
This is your only way to return a result. You **MUST NOT** put JSON in plain text, and you **MUST NOT** substitute a text summary for the structured `result.data` parameter.
@@ -29,7 +29,7 @@ Your result **MUST** match this TypeScript interface:
{{/if}}
{{SECTION_SEPARATOR "Giving Up"}}
Giving up is a last resort. If truly blocked, you **MUST** call `submit_result` exactly once with `result.error` describing what you tried and the exact blocker.
Giving up is a last resort. If truly blocked, you **MUST** call `yield` exactly once with `result.error` describing what you tried and the exact blocker.
You **MUST NOT** give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.
You **MUST** keep going until this ticket is closed. This matters.
@@ -0,0 +1,11 @@
<system-reminder>
You stopped without calling yield. This is reminder {{retryCount}} of {{maxRetries}}.
You **MUST** call yield as your only action now. Choose one:
- If task is complete: call yield with your result in `result.data`
- If task failed: call yield with `result.error` describing what happened
You **MUST NOT** give up if you can still complete the task through exploration (using available tools or repo context). If you submit an error, you **MUST** include what you tried and the exact blocker.
You **MUST NOT** output text without a tool call. You **MUST** call yield to finish.
</system-reminder>
+3 -3
View File
@@ -209,8 +209,8 @@ export interface CreateAgentSessionOptions {
/** 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 (for subagent sessions). Default: 0 */
taskDepth?: number;
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
@@ -916,7 +916,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
skills,
eventBus,
outputSchema: options.outputSchema,
requireSubmitResultTool: options.requireSubmitResultTool,
requireYieldTool: options.requireYieldTool,
taskDepth: options.taskDepth ?? 0,
getSessionFile: () => sessionManager.getSessionFile() ?? null,
getPythonKernelOwnerId: () => pythonKernelOwnerId,
+42 -47
View File
@@ -18,8 +18,8 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
import type { Skill } from "../extensibility/skills";
import { callTool } from "../mcp/client";
import type { MCPManager } from "../mcp/manager";
import submitReminderTemplate from "../prompts/system/subagent-submit-reminder.md" with { type: "text" };
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
import { createAgentSession, discoverAuthStorage } from "../sdk";
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
import type { AuthStorage } from "../session/auth-storage";
@@ -223,7 +223,7 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
return { data: candidate };
}
export interface SubmitResultItem {
export interface YieldItem {
data?: unknown;
status?: "success" | "aborted";
error?: string;
@@ -235,7 +235,7 @@ interface FinalizeSubprocessOutputArgs {
stderr: string;
doneAborted: boolean;
signalAborted: boolean;
submitResultItems?: SubmitResultItem[];
yieldItems?: YieldItem[];
reportFindings?: ReviewFinding[];
outputSchema: unknown;
}
@@ -244,44 +244,42 @@ interface FinalizeSubprocessOutputResult {
rawOutput: string;
exitCode: number;
stderr: string;
abortedViaSubmitResult: boolean;
hasSubmitResult: boolean;
abortedViaYield: boolean;
hasYield: boolean;
}
export const SUBAGENT_WARNING_NULL_SUBMIT_RESULT = "SYSTEM WARNING: Subagent called submit_result with null data.";
export const SUBAGENT_WARNING_MISSING_SUBMIT_RESULT =
"SYSTEM WARNING: Subagent exited without calling submit_result tool after 3 reminders.";
export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
export const SUBAGENT_WARNING_MISSING_YIELD =
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
let { rawOutput, exitCode, stderr } = args;
const { submitResultItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
let abortedViaSubmitResult = false;
const hasSubmitResult = Array.isArray(submitResultItems) && submitResultItems.length > 0;
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
let abortedViaYield = false;
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
if (hasSubmitResult) {
const lastSubmitResult = submitResultItems[submitResultItems.length - 1];
if (lastSubmitResult?.status === "aborted") {
abortedViaSubmitResult = true;
if (hasYield) {
const lastYield = yieldItems[yieldItems.length - 1];
if (lastYield?.status === "aborted") {
abortedViaYield = true;
exitCode = 0;
stderr = lastSubmitResult.error || "Subagent aborted task";
stderr = lastYield.error || "Subagent aborted task";
try {
rawOutput = JSON.stringify({ aborted: true, error: lastSubmitResult.error }, null, 2);
rawOutput = JSON.stringify({ aborted: true, error: lastYield.error }, null, 2);
} catch {
rawOutput = `{"aborted":true,"error":"${lastSubmitResult.error || "Unknown error"}"}`;
rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
}
} else {
const submitData = lastSubmitResult?.data;
const submitData = lastYield?.data;
if (submitData === null || submitData === undefined) {
rawOutput = rawOutput
? `${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\n${rawOutput}`
: SUBAGENT_WARNING_NULL_SUBMIT_RESULT;
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
} else {
const completeData = normalizeCompleteData(submitData, reportFindings);
try {
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
rawOutput = `{"error":"Failed to serialize submit_result data: ${errorMessage}"}`;
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
}
exitCode = 0;
stderr = "";
@@ -307,17 +305,15 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
stderr = "";
} else if (exitCode === 0) {
const hasRawOutput = rawOutput.trim().length > 0;
rawOutput = rawOutput
? `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\n${rawOutput}`
: SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
rawOutput = rawOutput ? `${SUBAGENT_WARNING_MISSING_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_MISSING_YIELD;
if (hasOutputSchema || !hasRawOutput) {
exitCode = 1;
stderr = SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
stderr = SUBAGENT_WARNING_MISSING_YIELD;
}
}
}
return { rawOutput, exitCode, stderr, abortedViaSubmitResult, hasSubmitResult };
return { rawOutput, exitCode, stderr, abortedViaYield, hasYield };
}
/**
@@ -564,7 +560,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
const abortSignal = abortController.signal;
let activeSession: AgentSession | null = null;
let unsubscribe: (() => void) | null = null;
let submitResultCalled = false;
let yieldCalled = false;
// Accumulate usage incrementally from message_end events (no memory for streaming events)
const accumulatedUsage = {
@@ -789,8 +785,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
existing.push(data);
}
progress.extractedToolData[event.toolName] = existing;
if (event.toolName === "submit_result") {
submitResultCalled = true;
if (event.toolName === "yield") {
yieldCalled = true;
}
}
}
@@ -955,7 +951,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
thinkingLevel: effectiveThinkingLevel,
toolNames,
outputSchema,
requireSubmitResultTool: true,
requireYieldTool: true,
contextFiles: options.contextFiles,
skills: options.skills,
promptTemplates: options.promptTemplates,
@@ -1070,7 +1066,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
await extensionRunner.emit({ type: "session_start" });
}
const MAX_SUBMIT_RESULT_RETRIES = 3;
const MAX_YIELD_RETRIES = 3;
unsubscribe = session.subscribe(event => {
if (isAgentEvent(event)) {
try {
@@ -1087,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
await session.prompt(task, { attribution: "agent" });
await session.waitForIdle();
const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
let retryCount = 0;
while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
try {
retryCount++;
const reminder = prompt.render(submitReminderTemplate, {
retryCount,
maxRetries: MAX_SUBMIT_RESULT_RETRIES,
maxRetries: MAX_YIELD_RETRIES,
});
await session.prompt(reminder, {
@@ -1111,7 +1107,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
}
await session.waitForIdle();
if (!submitResultCalled && !abortSignal.aborted) {
if (!yieldCalled && !abortSignal.aborted) {
exitCode = 0;
}
@@ -1186,7 +1182,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
// Use final output if available, otherwise accumulated output
let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
const submitResultItems = progress.extractedToolData?.submit_result as SubmitResultItem[] | undefined;
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
const finalized = finalizeSubprocessOutput({
rawOutput,
@@ -1194,17 +1190,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
stderr,
doneAborted: Boolean(done.aborted),
signalAborted: Boolean(signal?.aborted),
submitResultItems,
yieldItems,
reportFindings,
outputSchema,
});
rawOutput = finalized.rawOutput;
exitCode = finalized.exitCode;
stderr = finalized.stderr;
const lastSubmitResult = submitResultItems?.[submitResultItems.length - 1];
const submitResultAbortReason =
lastSubmitResult?.status === "aborted" ? lastSubmitResult.error || "Subagent aborted task" : undefined;
const { abortedViaSubmitResult, hasSubmitResult } = finalized;
const lastYield = yieldItems?.[yieldItems.length - 1];
const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
const { abortedViaYield, hasYield } = finalized;
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
maxBytes: MAX_OUTPUT_BYTES,
maxLines: MAX_OUTPUT_LINES,
@@ -1228,16 +1223,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
}
// Update final progress
const wasAborted = abortedViaSubmitResult || (!hasSubmitResult && (done.aborted || signal?.aborted || false));
const wasAborted = abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
const finalAbortReason = wasAborted
? abortedViaSubmitResult
? submitResultAbortReason
? abortedViaYield
? yieldAbortReason
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
: undefined;
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
scheduleProgress(true);
// Emit lifecycle end event after finalization so submit_result status is reflected
// Emit lifecycle end event after finalization so yield status is reflected
if (options.eventBus) {
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
id,
+11 -13
View File
@@ -101,12 +101,12 @@ function formatTaskId(id: string): string {
return `${indices} ${labels}`;
}
const MISSING_SUBMIT_RESULT_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling submit_result tool";
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
function extractMissingSubmitResultWarning(output: string): { warning?: string; rest: string } {
function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
const lines = output.split("\n");
const firstLine = lines[0]?.trim() ?? "";
if (!firstLine.startsWith(MISSING_SUBMIT_RESULT_WARNING_PREFIX)) {
if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
return { rest: output };
}
const rest = lines
@@ -572,9 +572,9 @@ function renderAgentProgress(
// Render extracted tool data inline (e.g., review findings)
if (progress.extractedToolData) {
// For completed tasks, check for review verdict from submit_result tool
// For completed tasks, check for review verdict from yield tool
if (progress.status === "completed") {
const completeData = progress.extractedToolData.submit_result as Array<{ data: unknown }> | undefined;
const completeData = progress.extractedToolData.yield as Array<{ data: unknown }> | undefined;
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
const reviewData = completeData
?.map(c => c.data as SubmitReviewDetails)
@@ -731,9 +731,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingSubmitResultWarning(
result.output,
);
const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
const aborted = result.aborted ?? false;
const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
const success = !aborted && result.exitCode === 0 && !result.error;
@@ -783,11 +781,11 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
);
}
// Check for review result (submit_result with review schema + report_finding)
const completeData = result.extractedToolData?.submit_result as Array<{ data: unknown }> | undefined;
// Check for review result (yield with review schema + report_finding)
const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
// Extract review verdict from submit_result tool's data field if it matches SubmitReviewDetails
// Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
const reviewData = completeData
?.map(c => c.data as SubmitReviewDetails)
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
@@ -804,7 +802,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
const hasCompleteData = completeData && completeData.length > 0;
const message = hasCompleteData
? "Review verdict missing expected fields"
: "Review incomplete (submit_result not called)";
: "Review incomplete (yield not called)";
lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
@@ -817,7 +815,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
if (result.extractedToolData) {
for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
// Skip review tools - handled above
if (toolName === "submit_result" || toolName === "report_finding") continue;
if (toolName === "yield" || toolName === "report_finding") continue;
const handler = subprocessToolRegistry.getHandler(toolName);
if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
+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;