Skip to content

fix(pydantic): make streamed runs work with auto_wrap_tools tool calls - #224

Open
QuocThuanTruong wants to merge 1 commit into
restatedev:mainfrom
QuocThuanTruong:fix-pydantic-streaming-tool-calls
Open

fix(pydantic): make streamed runs work with auto_wrap_tools tool calls#224
QuocThuanTruong wants to merge 1 commit into
restatedev:mainfrom
QuocThuanTruong:fix-pydantic-streaming-tool-calls

Conversation

@QuocThuanTruong

Copy link
Copy Markdown

Summary

Fixes the two bugs that break streamed runs with auto_wrap_tools=True on the first tool call (RestateAgent with an event_stream_handler). Text-only streamed runs work today; the first tool call crashes the invocation.

Fixes #222
Fixes #223

Bug 1 — "run event" journal entry crashes with TypeError: Object of type coroutine is not JSON serializable (#222)

wrapped_event_stream_handler journaled each tool-side event via:

await context.run_typed("run event", lambda: fn(ctx, single_event()))

fn (the user's event_stream_handler) is async, but the sync lambda means create_run_coroutine takes the non-coroutine branch: the handler is never awaited (so it never runs) and the returned coroutine object is handed to serde.serialize, which raises. Replaced the lambda with an async action so ctx.run() awaits it.

Bug 2 — KeyError('call_...') from Turnstile.wait_for on streamed tool calls (#223)

Tool executions in RestateContextRunToolSet.call_tool are ordered by a Turnstile built from the model response's tool-call ids — but only the non-streaming RestateModelWrapper.request() armed it. request_stream() left the default empty Turnstile([]), so the first tool call of a streamed run failed the id lookup. Now request_stream() arms it from the recorded response after the journaled "Model stream call" step, mirroring request() (deterministic on replay, since the response comes from the journal).

Testing

Verified on a production-shaped app (FastAPI + Virtual Object per conversation, pydantic-ai 2.4.0, Python 3.12, Restate server 1.7.2):

  • Before: first tool call of any streamed run → TypeError (and, with bug 1 worked around, KeyError('call_...')); invocation retries until paused.
  • After: streamed multi-tool turns complete — a run with 3 tool calls shows the expected journal (Model stream callCalling <tool> × N → Model stream call → output) and the live event stream reaches the handler for every step.

Two bugs that stack on the first tool call of a streamed run
(RestateAgent with an event_stream_handler):

1. wrapped_event_stream_handler journaled each tool-side event via
   run_typed("run event", lambda: fn(ctx, single_event())). The sync
   lambda returns the async handler's coroutine un-awaited, so the
   handler never runs and the journal entry fails with
   "TypeError: Object of type coroutine is not JSON serializable".
   Use an async action so ctx.run() awaits it.

2. request_stream() never armed the turnstile that orders journaled
   tool executions — only the non-streaming request() did — so the
   first tool call of a streamed run raised KeyError('call_...') in
   Turnstile.wait_for. Arm it from the recorded response, mirroring
   request().

Fixes restatedev#222
Fixes restatedev#223
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@QuocThuanTruong

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant