{"record":{"id":"28d6c67633ee2108","repo":"can1357/oh-my-pi","slug":"result-must-contain-either-data-or-error","errorCode":null,"errorMessage":"result must contain either \\`data\\` or \\`error\\`. Use \\`{result: {data: <your output>}}\\` for success or \\`{result: {error: \"message\"}}\\` for failure. Empty untyped result retries remaining before abort: ${remaining}.","messagePattern":"result must contain either \\\\`data\\\\` or \\\\`error\\\\`\\. Use \\\\`(.+?)\\}\\\\` for success or \\\\`(.+?)\\}\\\\` for failure\\. Empty untyped result retries remaining before abort: (.+?)\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/yield.ts","lineNumber":403,"sourceCode":"\t\t\tthis.#emptyResultFailures++;\n\t\t\tif (this.#emptyResultFailures > MAX_EMPTY_RESULT_RETRIES) {\n\t\t\t\tconst attemptCount = this.#emptyResultFailures;\n\t\t\t\tthis.#emptyResultFailures = 0;\n\t\t\t\tconst error =\n\t\t\t\t\t`yield result stayed empty after ${attemptCount} consecutive attempt(s); aborting child instead of retrying forever. ` +\n\t\t\t\t\t'Submit success as `{ \"result\": { \"data\": <your output> } }` or failure as `{ \"result\": { \"error\": \"message\" } }`.';\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Task aborted: ${error}` }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tdata: undefined,\n\t\t\t\t\t\tstatus: \"aborted\",\n\t\t\t\t\t\terror,\n\t\t\t\t\t\ttype: yieldType,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst remaining = MAX_EMPTY_RESULT_RETRIES - this.#emptyResultFailures;\n\t\t\tthrow new Error(\n\t\t\t\t`result must contain either \\`data\\` or \\`error\\`. Use \\`{result: {data: <your output>}}\\` for success or \\`{result: {error: \"message\"}}\\` for failure. Empty untyped result retries remaining before abort: ${remaining}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst status = errorMessage !== undefined ? \"aborted\" : \"success\";\n\t\tlet schemaValidationOverridden = false;\n\t\t// Unknown incremental labels are a hard contract mismatch with the closed caller\n\t\t// schema. Reject before the last-turn short-circuit too: `type: [\"findings\"], result: {}`\n\t\t// would otherwise be accepted as a typed last-turn incremental yield, then a sibling\n\t\t// section's MAX_SCHEMA_RETRIES override flips schemaOverridden in finalization and the\n\t\t// stale section rides along untouched.\n\t\tif (status === \"success\" && isIncremental) {\n\t\t\tconst unknownLabels = this.#unknownIncrementalLabels(yieldType as string[]);\n\t\t\tif (unknownLabels.length > 0) {\n\t\t\t\tconst validLabels =\n\t\t\t\t\tthis.#knownSectionLabels.length > 0 ? formatYieldLabels(this.#knownSectionLabels) : \"none\";\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Section ${formatYieldLabels(yieldType as string[])} uses unknown incremental yield label(s): ${formatYieldLabels(unknownLabels)}. Resubmit with one of the schema's labels: ${validLabels}.`,","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/yield.ts#L385-L421","documentation":"The yield tool requires every result to carry either a `data` payload or an `error` message. When a result has neither (and no incremental `type` sections to fall back on), the tool counts the empty submission and throws, telling the caller how many retries remain before the subagent aborts. After MAX_EMPTY_RESULT_RETRIES empty results the tool aborts the task instead of retrying again.","triggerScenarios":"Calling the yield tool with {result: {}} or an all-undefined payload while no incremental section type is supplied; repeated empty yields (up to MAX_EMPTY_RESULT_RETRIES) then abort.","commonSituations":"A model emits a bare {result:{}} because it thinks the parent reads its message text; a templated caller leaves both data and error unset; an integration passes args through JSON round-trips that drop null/undefined payloads.","solutions":["Resubmit immediately with {result: {data: <output>}} — the error message itself prescribes this shape and shows remaining retries","If the task genuinely failed, resubmit with {result: {error: \"description\"}}","If the output is already in the last assistant turn, use the useLastTurn yield variant instead of an empty data payload","Fix the calling harness so it never submits an empty result object"],"exampleFix":"// before\nyield({ result: {} });\n// after\nyield({ result: { data: { summary: \"done\", files: [\"a.ts\"] } } });","handlingStrategy":"validation","validationCode":"function yieldPayloadIsValid(r) { return r.data !== undefined || r.error !== undefined || r.type !== undefined; }","typeGuard":"function hasContent(r) {\n  return r.data !== undefined || r.error !== undefined || (r.useLastTurn === true);\n}","tryCatchPattern":"try {\n  yield({ result });\n} catch (err) {\n  if (err.message.includes('must contain either')) {\n    yield({ result: { data: buildFallbackData() } });\n  } else throw err;\n}","preventionTips":["Treat an empty result object as a bug — always construct data or error explicitly","Track the retry budget in the error message; fix the shape on the first attempt","Use the useLastTurn variant when output lives in the final assistant message"],"tags":["tool-call","yield","api-misuse","subagent","retry"],"backgroundTag":"empty-required-payload","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}