fix(delegation): report failed children as failed, not completed

A subagent whose loop gave up on a structured failure (e.g. "API call
failed after 3 retries: HTTP 524") returns that error message as
final_response together with completed=False / failed=True /
failure_reason. _run_single_child derived the batch-entry status from
the summary alone (`elif summary and not _empty_sentinel: status =
"completed"`), so the non-empty error text made the batch report show
the task as "✓ status=completed" — the `failed` flag was never
consulted anywhere in delegate_tool.py. Only the "(empty)" sentinel was
mapped to failed.

Fix, at the single status-determination choke point both the single-task
and batch paths share:

- `failed=True` on the child result now wins over a non-empty summary:
  status = "failed".
- The child's classified failure_reason (rate_limit / billing /
  server_error / ...) is propagated onto the batch entry so the parent
  can tell a quota wall from a real task error without parsing prose.
- exit_reason for a structured failure is "error" instead of falling
  through to "max_iterations" (which also wrongly set truncated=True).

Successful children (completed=True, no failed flag) are untouched —
covered by an explicit control test alongside the regression test,
which is red on the old code and green with the fix.
This commit is contained in:
itskaism
2026-08-16 16:49:42 +09:00
committed by Teknium
parent 5a3edc7467
commit 1b6ea1a2c2
2 changed files with 75 additions and 0 deletions
+6
View File
@@ -3317,6 +3317,12 @@ def _run_single_child(
)
if status == "failed":
entry["error"] = result.get("error", "Subagent did not produce a response.")
# Classified reason from the child loop (e.g. "rate_limit",
# "billing", "server_error") — lets the parent distinguish a
# quota wall from a real task error without parsing prose.
_failure_reason = result.get("failure_reason")
if isinstance(_failure_reason, str) and _failure_reason:
entry["failure_reason"] = _failure_reason
# T1-24: schema-validation outcome — emitted ONLY when a schema was
# requested, so legacy (schema-less) payloads keep their exact shape.