From b5ee1f3e44a658b92fc2ca46a7432fcf96f44cab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 12 Dec 2025 10:59:58 +0100 Subject: [PATCH] fix: use run() instead of run_stream() for scoped tools to avoid Ollama 400 bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PydanticAI + Ollama streaming with tool calls has known issues: - Issue #1292: Streaming stops after tool call due to empty TextPart - Issue #2256: Empty text part causes run to end prematurely This change uses run() for the actual tool execution while still yielding the response in chunks to maintain the streaming UX. The orchestration loop can emit updates between await calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/agents/tatlock.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/agents/tatlock.py b/src/agents/tatlock.py index e26a945..1c74175 100644 --- a/src/agents/tatlock.py +++ b/src/agents/tatlock.py @@ -587,16 +587,23 @@ class TatlockAgent(AgentInterface): ModelResponse(parts=[TextPart(content=content)]) ) - # Stream with scoped tools and tracker - async with scoped_agent.run_stream( + # Use run() instead of run_stream() to avoid Ollama 400 bug + # with streaming + tool calls (PydanticAI issues #1292, #2256) + # We yield the final response in chunks to maintain streaming interface + result = await scoped_agent.run( enriched_message, message_history=pydantic_history if pydantic_history else None, deps=tool_tracker - ) as stream: - async for chunk in stream.stream_text(delta=True): - yield chunk + ) - logger.info("tatlock_stream_complete") + # Stream the final response in chunks to maintain UX + response_text = result.output + chunk_size = 50 # characters per chunk + + for i in range(0, len(response_text), chunk_size): + yield response_text[i:i + chunk_size] + + logger.info("tatlock_scoped_run_complete") async def get_capabilities(self) -> dict: """Return current capabilities."""