Sync code¶
Every way to run an agent has a sync twin, for code without an event loop: scripts, Django and Flask views, Celery tasks, notebooks.
| Async | Sync |
|---|---|
await agent.run(...) |
agent.run_sync(...) |
async for event in agent.stream(...) |
for event in agent.stream_sync(...) |
async for events in run.batches() |
for events in run.batches() |
async for text in run.ai_sdk(), run.ag_ui(...) |
for text in run.ai_sdk(), run.ag_ui(...) |
await ctx.ask(...), await ctx.step(...) |
ctx.ask_sync(...), ctx.step_sync(...) |
The arguments are the same.
How it works¶
The agent loop already runs in Rust, so a sync run needs no event loop at all. Your thread waits in Rust with the GIL released (other threads keep running), and runs your sync Python code when the run needs it:
| Python code | async run | sync run |
|---|---|---|
sync tools, hooks, history stores, on_request |
a worker thread | your thread |
@tool(run_in_thread=True) |
a worker thread | a worker thread |
| async tools, hooks, stores, handlers | your event loop | pyox's own event loop thread |
| Rust tools | a worker thread, no Python | the same |
So sync functions never block an event loop, and in sync runs they behave like any function you call: Django's ORM
sees the view's transaction, and thread-locals and context variables just work. Parallel tool calls of a sync run
run one after the other; give slow tools run_in_thread=True to run them in parallel.
Async tools and MCP servers work in sync runs too. They run on one event loop thread that pyox starts the first time it needs it, and keeps for the life of the process, so async clients made by one run work in the next.
Django and Flask¶
from django.http import JsonResponse, StreamingHttpResponse
from pyox_agents import ai_sdk
def chat(request):
result = agent.run_sync(request.POST["prompt"], deps=request.user)
return JsonResponse({"text": result.text})
def chat_stream(request): # for `useChat`, see Web frontends
chat = ai_sdk.parse(request.body)
run = agent.stream_sync(
chat.prompt, history=chat.history, answers=chat.answers, frontend_tools=chat.frontend_tools, state=chat.state
)
return StreamingHttpResponse(run.ai_sdk(), headers=ai_sdk.HEADERS)
When the client goes away, the server closes the stream, which ends the run.
A sync run holds its thread until the model answers (seconds), like any sync view that calls an API. Use Gunicorn's
gthread workers to serve several runs per process, or the async API with an ASGI server for many at once.
Stopping¶
break,run.close()or leavingwith agent.stream_sync(...) as run:ends the run at once.run.stop()ends it at the next safe point and keeps what it did, from any thread:threading.Timer(60, run.stop).start()gives a run at most a minute.- Ctrl+C raises
KeyboardInterruptwithin 0.1 s and ends the run, also inside a tool.
Limits¶
- gevent and eventlet workers: a sync run blocks the worker's other greenlets until it ends. Use
gthreadworkers, or the async API. - Forking: a process forked after pyox-agents ran (Gunicorn
--preloadwith a run in the master, Celery prefork with a run at import) cannot run agents and raises aRuntimeErrorthat says so. Run agents only after the fork. - Inside async code:
run_syncworks inside a running event loop (a notebook cell), but blocks it for the whole run. Inasync deffunctions useawait agent.run(...). - MCP servers: open them with
with McpTools...(notasync with) for sync runs.