first-class async tools via ctx.update()#5841
Conversation
| items: list[ChatItem] | ||
| # AsyncRunContext is a backwards-compat alias β the unified RunContext now carries | ||
| # the same surface, so user tools typing `ctx: AsyncRunContext` keep working | ||
| __all__ = ["AsyncRunContext", "AsyncToolset"] |
There was a problem hiding this comment.
Since AsyncToolset was in beta, I think we can safely remove AsyncRunContext?
There was a problem hiding this comment.
maybe log a deprecation warning and notify them to use RunContext directly when it's used
| raise ToolError( | ||
| f"Tool call {call_id} is not cancellable because interruptions are disallowed" | ||
| ) | ||
| await utils.aio.cancel_and_wait(task.exe_task) |
There was a problem hiding this comment.
Should we cancel the asyncio.Task? Or just mark the SpeechHandle as cancelled/interrupted?
My main concern is that most users donβt really understand asyncio cancellation logic. If we can hide that complexity from users, that would be great. (e.g not having to worry about all your method to be cancellation-safe)
There was a problem hiding this comment.
we have allow_cancellation default with False, I think user should be aware of it if they enable it specifically. it could be useful to stop or revert some write operations like updating the db or ordering something, etc...
There was a problem hiding this comment.
But should the user catch asyncio.CancelledErrorvs having something like if speech_handle.cancelled: ...?
There was a problem hiding this comment.
perhaps something like ctx.wait_if_not_cancelled and ctx.cancelled since the speech handle is done after the first update. we have something similar using speech_handle.wait_if_not_interrupted in
agents/examples/voice_agents/long_running_function.py
Lines 44 to 51 in e3e992f
| """A toolset for running long-running functions in the background. | ||
| """Session-scoped toolset whose tools survive agent handoff. | ||
|
|
||
| Tools with an :class:`AsyncRunContext` parameter are wrapped to run in the background. | ||
| Each ``ctx.update()`` and the final ``return`` inject a tool output into the conversation; | ||
| the agent then generates a natural-language reply to the user based on that output. | ||
| Background updates from tools in this toolset are delivered to whichever agent | ||
| is current at delivery time, so a ``ctx.update()`` started under agent A still | ||
| completes after a handoff to agent B. Tools placed on ``Agent(tools=...)`` | ||
| instead use the activity-scoped executor and are cancelled/awaited on handoff. |
3a10d51 to
381b113
Compare
| except Exception as e: | ||
| output = e | ||
| logger.exception( | ||
| "error in tool execution", | ||
| extra={"call_id": call_id, "function": fnc_name}, |
There was a problem hiding this comment.
π‘ _execute_tool logs ToolError and StopResponse with full traceback via logger.exception
In the new _ToolExecutor._execute_tool, all exceptions (including expected ToolError and signal-like StopResponse) are caught by the generic except Exception handler and logged with logger.exception, which emits a full traceback. These exceptions then propagate through first_update_fut to the caller in generation.py:658-674, where ToolError is properly handled as a warning and StopResponse is silently consumed. This results in expected errors being double-logged β once with a noisy full traceback in the executor and once properly in the caller.
In the old code, ToolError from prepare_function_arguments was caught directly in _execute_tools_task and handled at the appropriate log level. Now it flows through the executor's generic handler first. This is particularly noisy in production since ToolError is common (LLM sends bad arguments) and StopResponse is a deliberate signal, not an error at all.
| except Exception as e: | |
| output = e | |
| logger.exception( | |
| "error in tool execution", | |
| extra={"call_id": call_id, "function": fnc_name}, | |
| except Exception as e: | |
| output = e | |
| if not isinstance(e, (ToolError, StopResponse)): | |
| logger.exception( | |
| "error in tool execution", | |
| extra={"call_id": call_id, "function": fnc_name}, | |
| ) |
Was this helpful? React with π or π to provide feedback.
Overview
Async tools are now first-class. Any
@function_toolcan callctx.update()to stream progress and run in the background β no special type hint needed.AsyncRunContextis now an alias ofRunContext, and per-tool duplicate policy / cancellation are explicit flags on@function_tool.Summary
AsyncRunContextis now a module-level alias ofRunContext. Tools typedctx: AsyncRunContextkeep working unchanged.@function_tool(on_duplicate="allow|reject|replace|confirm", allow_cancellation=False)._ToolExecutor(voice/tool_executor.py). The default executor is owned byAgentActivity. EachAsyncToolsetowns its own executor scoped to that toolset β when placed underAgentSession(tools=[...])its in-flight tools and deferred replies survive agent handoff; underAgent(tools=[...])they're bound to that agent.cancel_task/get_running_taskscompanion tools are auto-exposed byAgentActivity.toolswhenever any registered tool declaresallow_cancellation=True(always-on, so the LLM-visible schema stays stable across turns and the prompt cache stays warm).AsyncToolPromptstemplate fields accept either astr.format()template or a callable receiving typedUpdatePromptArgs/DuplicatePromptArgs/ReplyPromptArgs. Resolved viaAgentSessionβAgentβAsyncToolset(most specific wins).Deprecations
AsyncToolset(on_duplicate_call=...)logs a warning at construction and is applied as a fallback override onto contained tools'on_duplicate. Prefer the per-tool@function_tool(on_duplicate=...)flag.