diff --git a/.omp/skills/system-prompts/SKILL.md b/.omp/skills/system-prompts/SKILL.md index 8a51bf64b..b8632c9f7 100644 --- a/.omp/skills/system-prompts/SKILL.md +++ b/.omp/skills/system-prompts/SKILL.md @@ -187,7 +187,7 @@ READ-ONLY if applicable — list prohibited actions explicitly. What to return. Schema requirements. -Call `submit_result` with findings when done. +Call `yield` with findings when done. @@ -577,11 +577,11 @@ READ-ONLY. You are STRICTLY PROHIBITED from: 2. Read key sections (not entire files) 3. Identify types, interfaces, key functions 4. Note dependencies between files -5. Call `submit_result` with findings +5. Call `yield` with findings -Read-only. Call `submit_result` when done. This matters. +Read-only. Call `yield` when done. This matters. ``` diff --git a/docs/sdk.md b/docs/sdk.md index fa7ef94f7..bf877cbca 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -228,12 +228,12 @@ Related APIs: - Built-ins come from `createTools(...)` and `BUILTIN_TOOLS`. - `toolNames` acts as an allowlist for built-ins. - `customTools` and extension-registered tools are still included. -- Hidden tools (for example `submit_result`) are opt-in unless required by options. +- Hidden tools (for example `yield`) are opt-in unless required by options. ```ts const { session } = await createAgentSession({ toolNames: ["read", "grep", "find", "write"], - requireSubmitResultTool: true, + requireYieldTool: true, }); ``` @@ -274,7 +274,7 @@ Use these when you want partial control without recreating internal discovery lo For SDK consumers building orchestrators (similar to task executor flow): - `outputSchema`: passes structured output expectation into tool context -- `requireSubmitResultTool`: forces `submit_result` tool inclusion +- `requireYieldTool`: forces `yield` tool inclusion - `taskDepth`: recursion-depth context for nested task sessions - `parentTaskPrefix`: artifact naming prefix for nested task outputs diff --git a/docs/task-agent-discovery.md b/docs/task-agent-discovery.md index 16c52e0f4..f1b2c4d2e 100644 --- a/docs/task-agent-discovery.md +++ b/docs/task-agent-discovery.md @@ -31,7 +31,7 @@ Task agents normalize into `AgentDefinition` (`src/task/types.ts`): Parsing comes from frontmatter via `parseAgentFields()` (`src/discovery/helpers.ts`): - missing `name` or `description` => invalid (`null`), caller treats as parse failure -- `tools` accepts CSV or array; if provided, `submit_result` is auto-added +- `tools` accepts CSV or array; if provided, `yield` is auto-added - `spawns` accepts `*`, CSV, or array - backward-compat behavior: if `spawns` missing but `tools` includes `task`, `spawns` becomes `*` - `output` is passed through as opaque schema data diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index 5b03ecfa2..02ef74185 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -544,7 +544,7 @@ describe("agentLoop with AgentMessage", () => { const stream = new MockAssistantStream(); queueMicrotask(() => { const partial = createAssistantMessage( - [{ type: "toolCall", id: "tool-1", name: "submit_result", arguments: { data: { ok: true } } }], + [{ type: "toolCall", id: "tool-1", name: "yield", arguments: { data: { ok: true } } }], "toolUse", ); stream.push({ type: "start", partial }); diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 1fcb0d001..46a69a8c1 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -617,7 +617,7 @@ async function createClient( // Azure OpenAI requires /deployments/{id}/chat/completions?api-version=YYYY-MM-DD. // The generic openai-completions path adds neither, producing silent 404s. let azureDefaultQuery: Record | undefined; - if (baseUrl && baseUrl.includes(".openai.azure.com")) { + if (baseUrl?.includes(".openai.azure.com")) { const apiVersion = $env.AZURE_OPENAI_API_VERSION || "2024-10-21"; if (!baseUrl.includes("/deployments/")) { baseUrl = `${baseUrl}/deployments/${model.id}`; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 131cdbb9e..320f59444 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog ## [Unreleased] - ### Breaking Changes +- Renamed the subagent completion contract from `submit_result` to `yield`, so subagent sessions must now finish with the `yield` tool and the `requireYieldTool` option; `submit_result`/`requireSubmitResultTool` and old completion calls are no longer recognized - Changed the hashline and chunk anchor ID format from the prior hex-like tokens to two-letter BPE bigrams (for example `#th`), which invalidates previously captured `LINE#ID`/chunk selectors and requires re-reading to refresh anchors ### Added @@ -13,6 +13,7 @@ ### Changed +- Updated subagent reminders, prompts, and rendered subagent output to reference `yield` completion and report missing/final results from `yield` tool data - Updated the `edit` workflow to treat `atom` mode like hashline mode for read output, so hashline anchors are shown when `atom` is selected - Adjusted patch/replace/chunk tooling to accept optional entry paths and to apply a top-level path default - Updated hashline/chunk selector parsing to the new stable bigram token set used for checksums diff --git a/packages/coding-agent/DEVELOPMENT.md b/packages/coding-agent/DEVELOPMENT.md index 2746bdee3..45055a69d 100644 --- a/packages/coding-agent/DEVELOPMENT.md +++ b/packages/coding-agent/DEVELOPMENT.md @@ -395,7 +395,7 @@ A `ToolFactory` is `(session: ToolSession) => Tool | null | Promise 4. Computes effective gating (`isToolAllowed`) from settings and runtime state: - feature toggles (`find.enabled`, `grep.enabled`, etc.) - recursion guard for `task` (`task.maxRecursionDepth` vs `session.taskDepth`) - - submit-result mode (`requireSubmitResultTool`) and `todo_write` suppression + - yield mode (`requireYieldTool`) and `todo_write` suppression 5. Instantiates selected tools in parallel with `Promise.all`, records slow factory timings when `PI_TIMING=1`, and wraps results with `wrapToolWithMetaNotice`. 6. Includes `resolve` only when at least one instantiated tool has `deferrable: true` (deferred preview/apply workflows). @@ -864,7 +864,7 @@ mapWithConcurrencyLimit(...) └── ... │ ▼ -submit_result/fallback normalization +yield/fallback normalization │ ▼ aggregated task results (+ optional worktree patches) @@ -924,21 +924,21 @@ What _is_ isolated is execution context and artifacts, not process memory: - Adds `task` tool automatically when `agent.spawns` is set and recursion depth permits. - Removes `task` when max recursion depth is reached (`task.maxRecursionDepth`). - Expands legacy `exec` alias into `python` and/or `bash` based on `python.toolMode`. -- Forces `requireSubmitResultTool: true` in `createAgentSession(...)`. +- Forces `requireYieldTool: true` in `createAgentSession(...)`. - Filters parent-owned tools out of child tools (`todo_write` is removed). If parent MCP connections exist, executor creates in-process MCP proxy tools with `createMCPProxyTools(...)` so children reuse parent MCP connectivity rather than creating independent MCP sessions. ## Submit/Result Contract and Completion Semantics -`executor.ts` enforces structured completion around `submit_result`: +`executor.ts` enforces structured completion around `yield`: - Tracks tool events and extracted data through `subprocessToolRegistry` handlers. -- Retries reminder prompts up to 3 times (`MAX_SUBMIT_RESULT_RETRIES`) using `subagent-submit-reminder.md` if `submit_result` was not called. +- Retries reminder prompts up to 3 times (`MAX_YIELD_RETRIES`) using `subagent-yield-reminder.md` if `yield` was not called. - Final output normalization is centralized in `finalizeSubprocessOutput(...)`: - - If `submit_result.status === "aborted"`, task is converted to an aborted result payload. - - If missing `submit_result`, fallback attempts JSON parse/validation against output schema. - - Emits warnings when `submit_result` is missing/null and fallback cannot safely validate. + - If `yield.status === "aborted"`, task is converted to an aborted result payload. + - If missing `yield`, fallback attempts JSON parse/validation against output schema. + - Emits warnings when `yield` is missing/null and fallback cannot safely validate. This module also accumulates token/cost usage from assistant `message_end` events and truncates returned output with `truncateTail(...)` using `MAX_OUTPUT_BYTES` and `MAX_OUTPUT_LINES`. @@ -1132,7 +1132,7 @@ Primary file: `packages/coding-agent/src/tools/index.ts`. - `export const BUILTIN_TOOLS: Record = { ... }` - Key is the external tool name (e.g. `"read"`, `"web_search"`). 4. If it should be hidden/system-only, register under `HIDDEN_TOOLS` instead. - - Existing hidden names: `submit_result`, `report_finding`, `exit_plan_mode`, `resolve`. + - Existing hidden names: `yield`, `report_finding`, `exit_plan_mode`, `resolve`. 5. Wire feature gates in `isToolAllowed(name)` when the tool needs runtime enable/disable behavior. - Existing gates use `session.settings.get(".enabled")` and recursion limits for `task`. 6. If the tool should be selectable by type, update `ToolName = keyof typeof BUILTIN_TOOLS` consumers as needed. @@ -1141,7 +1141,7 @@ Notes from current behavior: - `createTools()` always injects `exit_plan_mode` when `toolNames` are specified. - `resolve` is included only when at least one active tool is marked `deferrable: true` (built-in or extension/custom). -- `submit_result` is force-added when `session.requireSubmitResultTool === true`. +- `yield` is force-added when `session.requireYieldTool === true`. - Python/Bash availability is mode-driven (`PI_PY`, `python.toolMode`) and can auto-fallback to bash. ### Playbook: add an RPC command diff --git a/packages/coding-agent/src/commit/agentic/prompts/analyze-file.md b/packages/coding-agent/src/commit/agentic/prompts/analyze-file.md index 0cad6bbb8..25ba7b4cf 100644 --- a/packages/coding-agent/src/commit/agentic/prompts/analyze-file.md +++ b/packages/coding-agent/src/commit/agentic/prompts/analyze-file.md @@ -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. diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index a47582386..630aa2648 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -221,9 +221,9 @@ export function parseAgentFields(frontmatter: Record): 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) diff --git a/packages/coding-agent/src/export/html/template.generated.ts b/packages/coding-agent/src/export/html/template.generated.ts index 8ed8ba91e..2aae86fb9 100644 --- a/packages/coding-agent/src/export/html/template.generated.ts +++ b/packages/coding-agent/src/export/html/template.generated.ts @@ -1,2 +1,2 @@ // Auto-generated by scripts/generate-template.ts - DO NOT EDIT -export const TEMPLATE = "\n\n\n \n \n Session Export\n \n \n\n\n \n
\n
\n \n
\n
\n
\n
\n
\n
\n \"\"\n
\n
\n\n \n \n \n \n\n\n"; +export const TEMPLATE = "\n\n\n \n \n Session Export\n \n \n\n\n \n
\n
\n \n
\n
\n
\n
\n
\n
\n \"\"\n
\n
\n\n \n \n \n \n\n\n"; diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js index f2f9c88d9..69c8129cd 100644 --- a/packages/coding-agent/src/export/html/template.js +++ b/packages/coding-agent/src/export/html/template.js @@ -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 += '
' + escapeHtml(JSON.stringify(args.data, null, 2)) + '
'; } @@ -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, diff --git a/packages/coding-agent/src/prompts/agents/librarian.md b/packages/coding-agent/src/prompts/agents/librarian.md index 3098e7dfd..e4af7fc7a 100644 --- a/packages/coding-agent/src/prompts/agents/librarian.md +++ b/packages/coding-agent/src/prompts/agents/librarian.md @@ -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-*`. diff --git a/packages/coding-agent/src/prompts/agents/reviewer.md b/packages/coding-agent/src/prompts/agents/reviewer.md index 804eca52f..9d9916450 100644 --- a/packages/coding-agent/src/prompts/agents/reviewer.md +++ b/packages/coding-agent/src/prompts/agents/reviewer.md @@ -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 `) 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. @@ -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 diff --git a/packages/coding-agent/src/prompts/review-request.md b/packages/coding-agent/src/prompts/review-request.md index 5f82bb52a..b355ed0fd 100644 --- a/packages/coding-agent/src/prompts/review-request.md +++ b/packages/coding-agent/src/prompts/review-request.md @@ -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 diff --git a/packages/coding-agent/src/prompts/system/subagent-submit-reminder.md b/packages/coding-agent/src/prompts/system/subagent-submit-reminder.md deleted file mode 100644 index 038eed18b..000000000 --- a/packages/coding-agent/src/prompts/system/subagent-submit-reminder.md +++ /dev/null @@ -1,11 +0,0 @@ - -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. - diff --git a/packages/coding-agent/src/prompts/system/subagent-system-prompt.md b/packages/coding-agent/src/prompts/system/subagent-system-prompt.md index 1e7e9b9b0..09b2266c6 100644 --- a/packages/coding-agent/src/prompts/system/subagent-system-prompt.md +++ b/packages/coding-agent/src/prompts/system/subagent-system-prompt.md @@ -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. diff --git a/packages/coding-agent/src/prompts/system/subagent-yield-reminder.md b/packages/coding-agent/src/prompts/system/subagent-yield-reminder.md new file mode 100644 index 000000000..d13b73f22 --- /dev/null +++ b/packages/coding-agent/src/prompts/system/subagent-yield-reminder.md @@ -0,0 +1,11 @@ + +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. + diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 5a1e5b6f3..a095c0087 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -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, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index b933e61d9..50fd08062 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -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 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 { if (isAgentEvent(event)) { try { @@ -1087,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise 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 | 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) { diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 72a58832d..24f6da2d6 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -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; @@ -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 = { }; export const HIDDEN_TOOLS: Record = { - 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 { - 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), ]; diff --git a/packages/coding-agent/src/tools/review.ts b/packages/coding-agent/src/tools/review.ts index 5269c1ed5..17300e731 100644 --- a/packages/coding-agent/src/tools/review.ts +++ b/packages/coding-agent/src/tools/review.ts @@ -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 = { 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 { - readonly name = "submit_result"; +export class YieldTool implements AgentTool { + 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 _toolCallId: string, params: Static, _signal?: AbortSignal, - _onUpdate?: AgentToolUpdateCallback, + _onUpdate?: AgentToolUpdateCallback, _context?: AgentToolContext, - ): Promise> { + ): Promise> { const raw = params as Record; const rawResult = raw.result; if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) { @@ -169,7 +169,7 @@ export class SubmitResultTool implements AgentTool 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 } // Register subprocess tool handler for extraction + termination. -subprocessToolRegistry.register("submit_result", { +subprocessToolRegistry.register("yield", { extractData: event => { const details = event.result?.details; if (!details || typeof details !== "object") return undefined; diff --git a/packages/coding-agent/test/discovery/agent-fields.test.ts b/packages/coding-agent/test/discovery/agent-fields.test.ts index 4491400a9..f5adc9cba 100644 --- a/packages/coding-agent/test/discovery/agent-fields.test.ts +++ b/packages/coding-agent/test/discovery/agent-fields.test.ts @@ -64,6 +64,6 @@ describe("parseAgentFields", () => { tools: ["Read", "Grep"], }); - expect(fields?.tools).toEqual(["read", "grep", "submit_result"]); + expect(fields?.tools).toEqual(["read", "grep", "yield"]); }); }); diff --git a/packages/coding-agent/test/model-registry-runtime-provider.test.ts b/packages/coding-agent/test/model-registry-runtime-provider.test.ts index c6b9af499..f0af769c0 100644 --- a/packages/coding-agent/test/model-registry-runtime-provider.test.ts +++ b/packages/coding-agent/test/model-registry-runtime-provider.test.ts @@ -234,7 +234,6 @@ describe("ModelRegistry runtime provider registration", () => { expect(model?.api).toBe("openai-completions"); }); - test("headers-only runtime override preserves existing baseUrl across refresh", async () => { const registry = new ModelRegistry(authStorage, modelsJsonPath); const modelId = "runtime-headers-only-baseurl-survivor"; @@ -292,11 +291,7 @@ describe("ModelRegistry runtime provider registration", () => { const registry = new ModelRegistry(authStorage, modelsJsonPath); expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(configHeaderValue); - registry.registerProvider( - "anthropic", - { headers: { [sharedHeader]: runtimeHeaderValue } }, - "ext://runtime", - ); + registry.registerProvider("anthropic", { headers: { [sharedHeader]: runtimeHeaderValue } }, "ext://runtime"); await expectProviderHeaderAcrossRefresh(registry, "anthropic", sharedHeader, runtimeHeaderValue); registry.clearSourceRegistrations("ext://runtime"); @@ -433,18 +428,10 @@ describe("ModelRegistry runtime provider registration", () => { const sourceAHeader = "X-Source-A-Header"; const sourceBHeader = "X-Source-B-Header"; - registry.registerProvider( - providerName, - { headers: { [sourceAHeader]: "from-source-a" } }, - "ext://a", - ); + registry.registerProvider(providerName, { headers: { [sourceAHeader]: "from-source-a" } }, "ext://a"); expectProviderHeader(registry, providerName, sourceAHeader, "from-source-a"); - registry.registerProvider( - providerName, - { headers: { [sourceBHeader]: "from-source-b" } }, - "ext://b", - ); + registry.registerProvider(providerName, { headers: { [sourceBHeader]: "from-source-b" } }, "ext://b"); await expectProviderHeaderAcrossRefresh(registry, providerName, sourceAHeader, undefined); expectProviderHeader(registry, providerName, sourceBHeader, "from-source-b"); }); diff --git a/packages/coding-agent/test/task/executor-subagent-reminders.test.ts b/packages/coding-agent/test/task/executor-subagent-reminders.test.ts index a16017a19..3e3194849 100644 --- a/packages/coding-agent/test/task/executor-subagent-reminders.test.ts +++ b/packages/coding-agent/test/task/executor-subagent-reminders.test.ts @@ -6,7 +6,7 @@ import type { CreateAgentSessionResult } from "../../src/sdk"; import * as sdkModule from "../../src/sdk"; import type { AgentSession, AgentSessionEvent, PromptOptions } from "../../src/session/agent-session"; import type { AuthStorage } from "../../src/session/auth-storage"; -import { runSubprocess, SUBAGENT_WARNING_MISSING_SUBMIT_RESULT } from "../../src/task/executor"; +import { runSubprocess, SUBAGENT_WARNING_MISSING_YIELD } from "../../src/task/executor"; import type { AgentDefinition } from "../../src/task/types"; import { EventBus } from "../../src/utils/event-bus"; @@ -55,7 +55,7 @@ function createMockSession( sessionManager: { appendSessionInit: () => {}, }, - getActiveToolNames: () => ["read", "submit_result"], + getActiveToolNames: () => ["read", "yield"], setActiveToolsByName: async (_toolNames: string[]) => {}, subscribe: (listener: (event: AgentSessionEvent) => void) => { listeners.push(listener); @@ -90,7 +90,7 @@ function mockCreateAgentSession(session: AgentSession) { return vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session)); } -describe("runSubprocess submit_result reminders", () => { +describe("runSubprocess yield reminders", () => { afterEach(() => { vi.restoreAllMocks(); }); @@ -114,7 +114,7 @@ describe("runSubprocess submit_result reminders", () => { enableLsp: false, }; - it("sends reminder prompt when subagent stops without submit_result", async () => { + it("sends reminder prompt when subagent stops without yield", async () => { const prompts: string[] = []; const promptOptions: Array = []; const session = createMockSession(({ text, options, promptIndex, emit, state }) => { @@ -129,7 +129,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: "tool-1", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success", data: { done: true } }, @@ -145,12 +145,12 @@ describe("runSubprocess submit_result reminders", () => { expect(promptOptions).toHaveLength(2); expect(promptOptions[0]?.attribution).toBe("agent"); expect(promptOptions[1]?.attribution).toBe("agent"); - expect(prompts[1]).toContain("You stopped without calling submit_result"); + expect(prompts[1]).toContain("You stopped without calling yield"); expect(result.output).toContain('"done": true'); expect(result.output.includes("SYSTEM WARNING")).toBe(false); }); - it("keeps null submit_result warning when subagent submits success without data", async () => { + it("keeps null yield warning when subagent submits success without data", async () => { const session = createMockSession(({ promptIndex, emit, state }) => { if (promptIndex === 1) { const assistant = createAssistantStopMessage("partial output"); @@ -161,7 +161,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: "tool-2", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success" }, @@ -173,21 +173,21 @@ describe("runSubprocess submit_result reminders", () => { mockCreateAgentSession(session); const result = await runSubprocess({ ...baseOptions, id: "subagent-2" }); - expect(result.output).toContain("SYSTEM WARNING: Subagent called submit_result with null data."); + expect(result.output).toContain("SYSTEM WARNING: Subagent called yield with null data."); }); - it("retries when submit_result tool returns an error before succeeding", async () => { + it("retries when yield tool returns an error before succeeding", async () => { const prompts: string[] = []; const session = createMockSession(({ text, promptIndex, emit, state }) => { prompts.push(text); if (promptIndex === 1) { - const assistant = createAssistantStopMessage("attempted submit_result"); + const assistant = createAssistantStopMessage("attempted yield"); state.messages.push(assistant); emit({ type: "message_end", message: assistant }); emit({ type: "tool_execution_end", toolCallId: "tool-error", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Output does not match schema" }], details: { status: "error", error: "Output does not match schema" }, @@ -199,7 +199,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: "tool-success", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success", data: { ok: true } }, @@ -221,7 +221,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: "tool-thinking-fallback", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success", data: { ok: true } }, @@ -268,7 +268,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: `tool-thinking-override-${index}`, - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success", data: { ok: true } }, @@ -292,11 +292,11 @@ describe("runSubprocess submit_result reminders", () => { expect(createAgentSessionSpy.mock.calls[0]?.[0]?.thinkingLevel).toBe(cases[0].expectedThinkingLevel); expect(createAgentSessionSpy.mock.calls[1]?.[0]?.thinkingLevel).toBe(cases[1].expectedThinkingLevel); }); - it("fails after 3 reminders when submit_result is never called for a structured task", async () => { + it("fails after 3 reminders when yield is never called for a structured task", async () => { const prompts: string[] = []; const session = createMockSession(({ text, promptIndex, emit, state }) => { prompts.push(text); - const assistant = createAssistantStopMessage(promptIndex === 1 ? "did work" : "still no submit_result"); + const assistant = createAssistantStopMessage(promptIndex === 1 ? "did work" : "still no yield"); state.messages.push(assistant); emit({ type: "message_end", message: assistant }); }); @@ -311,11 +311,11 @@ describe("runSubprocess submit_result reminders", () => { expect(prompts).toHaveLength(4); expect(result.exitCode).toBe(1); expect(result.aborted).toBe(false); - expect(result.stderr).toBe(SUBAGENT_WARNING_MISSING_SUBMIT_RESULT); + expect(result.stderr).toBe(SUBAGENT_WARNING_MISSING_YIELD); expect(result.abortReason).toBeUndefined(); }); - it("surfaces abort reason when submit_result reports aborted status", async () => { + it("surfaces abort reason when yield reports aborted status", async () => { const session = createMockSession(({ promptIndex, emit, state }) => { if (promptIndex === 1) { const assistant = createAssistantStopMessage("cannot proceed"); @@ -325,7 +325,7 @@ describe("runSubprocess submit_result reminders", () => { emit({ type: "tool_execution_end", toolCallId: "tool-abort", - toolName: "submit_result", + toolName: "yield", result: { content: [{ type: "text", text: "Task aborted: blocked by permissions" }], details: { status: "aborted", error: "blocked by permissions" }, @@ -336,7 +336,7 @@ describe("runSubprocess submit_result reminders", () => { mockCreateAgentSession(session); - const result = await runSubprocess({ ...baseOptions, id: "subagent-aborted-submit-result" }); + const result = await runSubprocess({ ...baseOptions, id: "subagent-aborted-yield" }); expect(result.aborted).toBe(true); expect(result.abortReason).toBe("blocked by permissions"); }); diff --git a/packages/coding-agent/test/task/executor-warnings.test.ts b/packages/coding-agent/test/task/executor-warnings.test.ts index db13f5fce..385151b69 100644 --- a/packages/coding-agent/test/task/executor-warnings.test.ts +++ b/packages/coding-agent/test/task/executor-warnings.test.ts @@ -1,39 +1,39 @@ import { describe, expect, it } from "bun:test"; import { finalizeSubprocessOutput, - SUBAGENT_WARNING_MISSING_SUBMIT_RESULT, - SUBAGENT_WARNING_NULL_SUBMIT_RESULT, + SUBAGENT_WARNING_MISSING_YIELD, + SUBAGENT_WARNING_NULL_YIELD, } from "../../src/task/executor"; describe("subagent warning injection", () => { - it("injects null-data warning when submit_result is success without data", () => { + it("injects null-data warning when yield is success without data", () => { const result = finalizeSubprocessOutput({ rawOutput: "partial output", exitCode: 0, stderr: "", doneAborted: false, signalAborted: false, - submitResultItems: [{ status: "success" }], + yieldItems: [{ status: "success" }], outputSchema: undefined, }); - expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\npartial output`); - expect(result.hasSubmitResult).toBe(true); + expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_NULL_YIELD}\n\npartial output`); + expect(result.hasYield).toBe(true); }); - it("injects missing-submit warning when subagent exits cleanly without submit_result", () => { + it("injects missing-submit warning when subagent exits cleanly without yield", () => { const result = finalizeSubprocessOutput({ rawOutput: "", exitCode: 0, stderr: "", doneAborted: false, signalAborted: false, - submitResultItems: undefined, + yieldItems: undefined, outputSchema: { properties: { ok: { type: "boolean" } } }, }); - expect(result.rawOutput).toBe(SUBAGENT_WARNING_MISSING_SUBMIT_RESULT); - expect(result.hasSubmitResult).toBe(false); + expect(result.rawOutput).toBe(SUBAGENT_WARNING_MISSING_YIELD); + expect(result.hasYield).toBe(false); }); it("does not inject missing-submit warning when fallback completion is recoverable", () => { @@ -43,7 +43,7 @@ describe("subagent warning injection", () => { stderr: "", doneAborted: false, signalAborted: false, - submitResultItems: undefined, + yieldItems: undefined, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, }); @@ -58,13 +58,11 @@ describe("subagent warning injection", () => { stderr: "", doneAborted: false, signalAborted: false, - submitResultItems: undefined, + yieldItems: undefined, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, }); - expect(result.rawOutput).toBe( - `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\nagent stopped after writing analysis`, - ); + expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_MISSING_YIELD}\n\nagent stopped after writing analysis`); }); it("does not inject missing-submit warning when execution exits non-zero", () => { @@ -74,7 +72,7 @@ describe("subagent warning injection", () => { stderr: "subagent terminated", doneAborted: true, signalAborted: false, - submitResultItems: undefined, + yieldItems: undefined, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, }); @@ -83,32 +81,32 @@ describe("subagent warning injection", () => { expect(result.exitCode).toBe(1); }); - it("normalizes explicit aborted submit_result into aborted payload", () => { + it("normalizes explicit aborted yield into aborted payload", () => { const result = finalizeSubprocessOutput({ rawOutput: "partial output", exitCode: 1, stderr: "old error", doneAborted: false, signalAborted: false, - submitResultItems: [{ status: "aborted", error: "blocked by permissions" }], + yieldItems: [{ status: "aborted", error: "blocked by permissions" }], outputSchema: undefined, }); - expect(result.abortedViaSubmitResult).toBe(true); + expect(result.abortedViaYield).toBe(true); expect(result.exitCode).toBe(0); expect(result.stderr).toBe("blocked by permissions"); expect(result.rawOutput).toContain('"aborted": true'); expect(result.rawOutput).toContain('"blocked by permissions"'); }); - it("accepts successful submit_result data without warning", () => { + it("accepts successful yield data without warning", () => { const result = finalizeSubprocessOutput({ rawOutput: "should be replaced", exitCode: 1, stderr: "should clear", doneAborted: false, signalAborted: false, - submitResultItems: [{ status: "success", data: { ok: true } }], + yieldItems: [{ status: "success", data: { ok: true } }], outputSchema: undefined, }); @@ -125,7 +123,7 @@ describe("subagent warning injection", () => { stderr: "", doneAborted: false, signalAborted: false, - submitResultItems: undefined, + yieldItems: undefined, outputSchema: undefined, }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index 34ea76f29..a3a583377 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -177,12 +177,12 @@ describe("createTools", () => { expect(names).toEqual(["report_finding", "exit_plan_mode"]); }); - it("includes submit_result tool when required", async () => { - const session = createTestSession({ requireSubmitResultTool: true }); + it("includes yield tool when required", async () => { + const session = createTestSession({ requireYieldTool: true }); const tools = await createTools(session); const names = tools.map(t => t.name); - expect(names).toContain("submit_result"); + expect(names).toContain("yield"); }); it("excludes ask tool when hasUI is false", async () => { @@ -250,7 +250,7 @@ describe("createTools", () => { "report_finding", "report_tool_issue", "resolve", - "submit_result", + "yield", ]); }); }); diff --git a/packages/coding-agent/test/tools/submit-result-extraction.test.ts b/packages/coding-agent/test/tools/yield-extraction.test.ts similarity index 67% rename from packages/coding-agent/test/tools/submit-result-extraction.test.ts rename to packages/coding-agent/test/tools/yield-extraction.test.ts index 8a9d8ca89..899240a11 100644 --- a/packages/coding-agent/test/tools/submit-result-extraction.test.ts +++ b/packages/coding-agent/test/tools/yield-extraction.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "bun:test"; -import "../../src/tools/submit-result"; +import "../../src/tools/yield"; import { subprocessToolRegistry } from "../../src/task/subprocess-tool-registry"; -describe("submit_result subprocess extraction", () => { - const handler = subprocessToolRegistry.getHandler("submit_result"); +describe("yield subprocess extraction", () => { + const handler = subprocessToolRegistry.getHandler("yield"); - it("extracts valid submit_result payloads", () => { + it("extracts valid yield payloads", () => { expect(handler?.extractData).toBeDefined(); const data = handler?.extractData?.({ - toolName: "submit_result", + toolName: "yield", toolCallId: "call-1", result: { content: [{ type: "text", text: "Result submitted." }], @@ -19,9 +19,9 @@ describe("submit_result subprocess extraction", () => { expect(data).toEqual({ status: "success", data: { ok: true }, error: undefined }); }); - it("ignores malformed submit_result details without status", () => { + it("ignores malformed yield details without status", () => { const data = handler?.extractData?.({ - toolName: "submit_result", + toolName: "yield", toolCallId: "call-2", result: { content: [{ type: "text", text: "Tool execution was aborted." }], diff --git a/packages/coding-agent/test/tools/submit-result.test.ts b/packages/coding-agent/test/tools/yield.test.ts similarity index 91% rename from packages/coding-agent/test/tools/submit-result.test.ts rename to packages/coding-agent/test/tools/yield.test.ts index 8ebfee9da..1a285cb3b 100644 --- a/packages/coding-agent/test/tools/submit-result.test.ts +++ b/packages/coding-agent/test/tools/yield.test.ts @@ -4,7 +4,7 @@ import { enforceStrictSchema } from "@oh-my-pi/pi-ai/utils/schema"; import { validateToolArguments } from "@oh-my-pi/pi-ai/utils/validation"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { SubmitResultTool } from "@oh-my-pi/pi-coding-agent/tools/submit-result"; +import { YieldTool } from "@oh-my-pi/pi-coding-agent/tools/yield"; function createSession(overrides: Partial = {}): ToolSession { return { @@ -34,9 +34,9 @@ function getSuccessDataSchema(parameters: Record): Record { +describe("YieldTool", () => { it("exposes top-level object parameters with required result union", () => { - const tool = new SubmitResultTool(createSession()); + const tool = new YieldTool(createSession()); const schema = tool.parameters as { type?: string; properties?: Record; @@ -48,19 +48,19 @@ describe("SubmitResultTool", () => { }); it("accepts success payload with data", async () => { - const tool = new SubmitResultTool(createSession()); + const tool = new YieldTool(createSession()); const result = await tool.execute("call-1", { result: { data: { ok: true } } } as never); expect(result.details).toEqual({ data: { ok: true }, status: "success", error: undefined }); }); it("accepts aborted payload with error only", async () => { - const tool = new SubmitResultTool(createSession()); + const tool = new YieldTool(createSession()); const result = await tool.execute("call-2", { result: { error: "blocked" } } as never); expect(result.details).toEqual({ data: undefined, status: "aborted", error: "blocked" }); }); it("accepts arbitrary data when outputSchema is null", async () => { - const tool = new SubmitResultTool(createSession({ outputSchema: null })); + const tool = new YieldTool(createSession({ outputSchema: null })); const result = await tool.execute("call-null", { result: { data: { nested: { x: 1 }, ok: true } } } as never); expect(result.details).toEqual({ data: { nested: { x: 1 }, ok: true }, @@ -70,7 +70,7 @@ describe("SubmitResultTool", () => { }); it("treats outputSchema true as unconstrained and accepts primitive and array data", async () => { - const tool = new SubmitResultTool(createSession({ outputSchema: true })); + const tool = new YieldTool(createSession({ outputSchema: true })); const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record); expect(dataSchema.type).toBeUndefined(); @@ -85,7 +85,7 @@ describe("SubmitResultTool", () => { }); }); it("repairs strict schema generation for required-only object output schemas", () => { - const tool = new SubmitResultTool( + const tool = new YieldTool( createSession({ outputSchema: { type: "object", @@ -103,7 +103,7 @@ describe("SubmitResultTool", () => { }); it("normalizes object/null type arrays into strict-compatible data variants", () => { - const tool = new SubmitResultTool( + const tool = new YieldTool( createSession({ outputSchema: { type: ["object", "null"], @@ -129,7 +129,7 @@ describe("SubmitResultTool", () => { }); it("converts mixed JTD and JSON Schema output definitions into provider-valid schemas", async () => { - const tool = new SubmitResultTool( + const tool = new YieldTool( createSession({ outputSchema: { type: "object", @@ -189,7 +189,7 @@ describe("SubmitResultTool", () => { }, ], }; - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); const parametersRecord = tool.parameters as unknown as Record; // $defs should NOT be in parameters — refs are inlined expect(parametersRecord.$defs).toBeUndefined(); @@ -232,7 +232,7 @@ describe("SubmitResultTool", () => { ]); }); it("falls back to unconstrained object data when output schema is invalid", async () => { - const tool = new SubmitResultTool( + const tool = new YieldTool( createSession({ outputSchema: { type: "object", @@ -263,7 +263,7 @@ describe("SubmitResultTool", () => { const circularSchema: Record = { type: "object" }; circularSchema.self = circularSchema; - const tool = new SubmitResultTool(createSession({ outputSchema: circularSchema })); + const tool = new YieldTool(createSession({ outputSchema: circularSchema })); const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record); expect(tool.strict).toBe(false); @@ -299,7 +299,7 @@ describe("SubmitResultTool", () => { return root; }; - const tool = new SubmitResultTool(createSession({ outputSchema: buildDeepSchema(20_000) })); + const tool = new YieldTool(createSession({ outputSchema: buildDeepSchema(20_000) })); const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record); expect(tool.strict).toBe(false); @@ -311,7 +311,7 @@ describe("SubmitResultTool", () => { it("handles non-object output schemas without blocking successful result submission", async () => { for (const outputSchema of [[], 123, false]) { - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); const result = await tool.execute("call-non-object-schema", { result: { data: { value: outputSchema } }, } as never); @@ -333,7 +333,7 @@ describe("SubmitResultTool", () => { }, required: ["token"], }; - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record); const tokenSchema = toRecord(toRecord(dataSchema.properties).token); @@ -357,7 +357,7 @@ describe("SubmitResultTool", () => { }, required: ["token"], }; - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); await expect(tool.execute("call-short-1", { result: { data: { token: "ab" } } } as never)).rejects.toThrow( "Output does not match schema", @@ -384,7 +384,7 @@ describe("SubmitResultTool", () => { }, required: ["token"], }; - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); const firstResult = await tool.execute("call-valid-1", { result: { data: { token: "abcd" } } } as never); expect(firstResult.content).toEqual([{ type: "text", text: "Result submitted." }]); @@ -408,7 +408,7 @@ describe("SubmitResultTool", () => { }, required: ["token"], }; - const tool = new SubmitResultTool(createSession({ outputSchema })); + const tool = new YieldTool(createSession({ outputSchema })); await expect(tool.execute("call-struct-1", { result: { data: { token: "ab" } } } as never)).rejects.toThrow( "Output does not match schema", @@ -422,13 +422,13 @@ describe("SubmitResultTool", () => { ); }); it("rejects submissions without a result object", async () => { - const tool = new SubmitResultTool(createSession()); + const tool = new YieldTool(createSession()); await expect(tool.execute("call-3", {} as never)).rejects.toThrow( "result must be an object containing either data or error", ); }); it("sets lenientArgValidation so agent-loop bypasses validation errors", () => { - const tool = new SubmitResultTool(createSession()); + const tool = new YieldTool(createSession()); expect(tool.lenientArgValidation).toBe(true); }); });