Skip to content

fix(python-llamaindex-agent): disable streaming so LLM retries work - #1098

Merged
l2ysho merged 4 commits into
masterfrom
claude/affectionate-euler-a59eb3
Nov 4, 2026
Merged

l2ysho merged 4 commits into
masterfrom
claude/affectionate-euler-a59eb3

Conversation

@l2ysho

@l2ysho l2ysho commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Note

TL;DR
autofix tests

Mechanism

ReActAgent(tools=..., llm=llm) leaves streaming at its default of True, so every LLM call goes through llama_index.llms.openai.base.OpenAI._astream_chat. That method carries @llm_retry_decorator, but the network call lives inside an inner async def gen() that the caller iterates after the decorated function has returned. Tenacity therefore wraps a function that performs no I/O and can never fire.

The openai SDK's own max_retries=3 is out of reach for the same shape of reason: with stream=True the retry-guarded _send_request returns as soon as headers arrive, and the body is consumed later in AsyncStream.__stream__, which has no transport-error handling.

So a mid-stream connection drop propagates as a raw httpx.ReadError out of await handler in agent.py, into run_query's except in my_actor/main.py, which calls Actor.fail() and exits 1.

With streaming=False the call routes to _achat, where the request sits directly in the decorated function body and a stream=False response has its body read inside the SDK's retry-guarded _send_request. Both retry layers engage.

Verified against the installed library

  • ReActAgent(tools=[], llm=llm).streaming is True; passing streaming=False sets it to False.
  • The template does not consume AgentStream events — its stream_events() loop handles only ToolCall and ToolCallResult, which MultiAgentWorkflow.call_tool emits independently of self.streaming. Verbose logging is unchanged.
  • The two branches of ReActAgent.take_step differ only in whether AgentStream deltas are written; both return the same AgentOutput with the same tool_calls.

Evidence

Run 38986102096, job 117257819030 (2026-10-17, windows-latest / Python 3.12). The traceback runs react_agent.py:158 → _get_streaming_response → llama_index/llms/openai/base.py:831 in gen → openai/_streaming.py → httpx.ReadError, with no tenacity frame in it.

Honesty about confidence

This is n=1: one failure in 30 days, and the same SHA passed on the nightly schedule on 10-16, 10-18 and 10-19. The fix is unproven against a live failure — nobody can reproduce a dropped connection on demand, and I did not run the template against the OpenAI API. What is proven is the code path: the retry decorator demonstrably wraps no I/O on the streaming path, and does wrap it on the non-streaming path. The change removes a retry layer that was silently doing nothing; whether that particular ReadError would have been retried away is an inference, not a measurement.

Archives

dist/templates/python-llamaindex-agent.zip is rebuilt automatically — the "Build template archives" workflow runs on every push to master and commits chore: Update template archives [skip ci]. Not hand-edited here, matching #1073.

Checks run locally

ruff check, ruff format --check, ty check on the template; pnpm run lint, pnpm run build, pnpm run test-without-templates at the repo root. All pass. (pnpm run format:check flags an untracked local .claude/settings.local.json, unrelated to this change.)

🤖 Generated with Claude Code

ReActAgent.streaming defaults to True, which routes every LLM call through
OpenAI._astream_chat. The network call lives in an inner generator that the
caller iterates after the @llm_retry_decorator function has already returned,
so tenacity wraps a function that performs no I/O. The openai SDK's own
max_retries is out of reach too: with stream=True its retry-guarded
_send_request returns once headers arrive, and the body is consumed later in
AsyncStream.__stream__, which has no transport-error handling.

Result: a mid-stream connection drop surfaces as a raw httpx.ReadError and
fails the whole Actor run. With streaming=False the call goes through _achat,
where the request sits directly in the decorated body and the response body is
read inside the SDK's retry-guarded _send_request, so both retry layers engage.

The template does not consume AgentStream events, only ToolCall and
ToolCallResult, which the call_tool step emits regardless of this setting, so
verbose logging is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@l2ysho
l2ysho requested a review from DaveHanns as a code owner October 22, 2026 19:13
@apify-service-account apify-service-account added the t-builders Issues owned by the Builders team. label Oct 22, 2026
@l2ysho
l2ysho requested review from apify-factory and removed request for DaveHanns October 22, 2026 19:14

@apify-factory apify-factory left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, nothing blocking.

🏭 Apify Factory auto approved ✅ — we looked twice, just to be sure

Every gate passed, and neither the reviewer nor a juror found anything a human needs to look at.

Approval locked to 4671eb454 · workflow run

@l2ysho l2ysho added the adhoc Ad-hoc unplanned task added during the sprint. label Oct 22, 2026
@l2ysho
l2ysho merged commit 04ddd0c into master Nov 4, 2026
117 of 119 checks passed
@l2ysho
l2ysho deleted the claude/affectionate-euler-a59eb3 branch November 4, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-builders Issues owned by the Builders team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants