AutoGPT is the vision of accessible AI for everyone, to use and to build on. Our mission is to provide the tools, so that you can focus on what matters.
fix(backend/blocks): orchestrator EXTENDED_THINKING emits final-answer only (#13188)
### Why / What / How
**Why:** Discovered while debugging a debate-style orchestrator agent on
dev preview. The two execution modes were producing structurally
different `finished` outputs for the same block:
| Mode | `finished` content | What's wrong with it |
|---|---|---|
| `BUILT_IN` (agent loop via `tool_call_loop`) |
`response.response_text` from the last LLM call (i.e. the model's final
text-only response after it stopped calling tools) | **Correct** — this
is the agent's composed answer |
| `EXTENDED_THINKING` (Claude Agent SDK) | `"".join(every TextBlock from
every AssistantMessage)` | **Buggy** — accumulates every narration
emitted between tool calls; the answer gets buried in working-log noise,
and worse, *masks* prompts that never compose a real answer |
The original-direction commit (`0f4d348d0d`, now force-replaced) tried
to fix the divergence by making BUILT_IN match EXTENDED_THINKING's
accumulation. That was the wrong direction: BUILT_IN's last-answer
behaviour is the correct contract, and the accumulation in
EXTENDED_THINKING was hiding composition bugs that dry-run / autopilot
should be able to detect.
**What:** Replace `response_parts.append(...)` (every TextBlock) with
`final_response_parts = list(text_parts)` only when the assistant
message has **no tool calls** — i.e. the model has stopped calling tools
and is emitting its composed answer. The last such message wins.
**How:**
```diff
- response_parts: list[str] = []
+ final_response_parts: list[str] = [] # only the final-answer message's text
...
if isinstance(sdk_msg, AssistantMessage):
text_parts = []
tool_use_parts = []
for content_block in sdk_msg.content:
if isinstance(content_block, TextBlock):
text_parts.append(content_block.text)
- response_parts.append(content_block.text)
elif isinstance(content_block, ToolUseBlock):
...
+ # Capture the agent's final answer: last text-only message wins.
+ if text_parts and not tool_use_parts:
+ final_response_parts = list(text_parts)
...
- response_text = "".join(response_parts)
+ response_text = "".join(final_response_parts)
```
**Edge cases:**
- *Agent never stops calling tools (e.g. hits max iterations)*:
`final_response_parts` stays empty → `finished` = `""`. This is the
**correct** signal for dry-run / autopilot diagnostics: the prompt
didn't compose a final answer, so the agent author needs to fix it.
Previously the over-accumulation returned a transcript-shaped string
that masked this failure mode.
- *Agent emits multiple text-only messages over the course of the run*
(rare): the **last** one wins, matching "the model's most recent
composed answer."
- *Agent emits text + tool calls in the same message*: that message's
text is treated as narration belonging to `conversations`, not the
answer (the `not tool_use_parts` guard).
**Existing tests:** all 14 in `test_orchestrator_execution_mode.py`
still pass. No new SDK-mocking infrastructure exists in the test suite
for this code path, so behavioural verification will happen on the next
dev preview redeploy (combo branch redeployment to follow).
### Changes 🏗️
- `_execute_tools_sdk_mode` captures only the last assistant message's
text-only content into `final_response_parts`.
- `finished` output yields `"".join(final_response_parts)` — empty
string when the agent never composed a final answer (useful diagnostic
signal).
- No change to `conversations` output (still carries the full
back-and-forth).
### Checklist 📋
#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] `poetry run pytest
backend/blocks/test/test_orchestrator_execution_mode.py` — 14 tests pass
- [x] `poetry run black` + `poetry run ruff check` clean
- [ ] Manual verification on dev preview after combo redeploy
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Z
Zamil Majdy committed
b50bf6b1ba1d7a55bcf75fa4fc0b3023f34d59d8
Parent: 72e3995
Committed by GitHub <noreply@github.com>
on 5/22/2026, 2:26:41 AM