Files
2026-07-13 12:39:17 +08:00

831 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tools
Tools let agents take actions: things like fetching data, running code, calling external APIs, and even using a computer. The SDK supports five categories:
- Hosted OpenAI tools: run alongside the model on OpenAI servers.
- Local/runtime execution tools: `ComputerTool` and `ApplyPatchTool` always run in your environment, while `ShellTool` can run locally or in a hosted container.
- Function calling: wrap any Python function as a tool.
- Agents as tools: expose an agent as a callable tool without a full handoff.
- Experimental: Codex tool: run workspace-scoped Codex tasks from a tool call.
## Choosing a tool type
Use this page as a catalog, then jump to the section that matches the runtime you control.
| If you want to... | Start here |
| --- | --- |
| Use OpenAI-managed tools (web search, file search, code interpreter, hosted MCP, image generation) | [Hosted tools](#hosted-tools) |
| Defer large tool surfaces until runtime with tool search | [Hosted tool search](#hosted-tool-search) |
| Run tools in your own process or environment | [Local runtime tools](#local-runtime-tools) |
| Wrap Python functions as tools | [Function tools](#function-tools) |
| Let one agent call another without a handoff | [Agents as tools](#agents-as-tools) |
| Run workspace-scoped Codex tasks from an agent | [Experimental: Codex tool](#experimental-codex-tool) |
## Hosted tools
OpenAI offers a few built-in tools when using the [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]:
- The [`WebSearchTool`][agents.tool.WebSearchTool] lets an agent search the web.
- The [`FileSearchTool`][agents.tool.FileSearchTool] allows retrieving information from your OpenAI Vector Stores.
- The [`CodeInterpreterTool`][agents.tool.CodeInterpreterTool] lets the LLM execute code in a sandboxed environment.
- The [`HostedMCPTool`][agents.tool.HostedMCPTool] exposes a remote MCP server's tools to the model.
- The [`ImageGenerationTool`][agents.tool.ImageGenerationTool] generates images from a prompt.
- The [`ToolSearchTool`][agents.tool.ToolSearchTool] lets the model load deferred tools, namespaces, or hosted MCP servers on demand.
Advanced hosted search options:
- `FileSearchTool` supports `filters`, `ranking_options`, and `include_search_results` in addition to `vector_store_ids` and `max_num_results`.
- `WebSearchTool` supports `filters`, `user_location`, and `search_context_size`.
```python
from agents import Agent, FileSearchTool, Runner, WebSearchTool
agent = Agent(
name="Assistant",
tools=[
WebSearchTool(),
FileSearchTool(
max_num_results=3,
vector_store_ids=["VECTOR_STORE_ID"],
),
],
)
async def main():
result = await Runner.run(agent, "Which coffee shop should I go to, taking into account my preferences and the weather today in SF?")
print(result.final_output)
```
### Hosted tool search
Tool search lets OpenAI Responses models defer large tool surfaces until runtime, so the model loads only the subset it needs for the current turn. This is useful when you have many function tools, namespace groups, or hosted MCP servers and want to reduce tool-schema tokens without exposing every tool up front.
Start with hosted tool search when the candidate tools are already known when you build the agent. If your application needs to decide what to load dynamically, the Responses API also supports client-executed tool search, but the standard `Runner` does not auto-execute that mode.
```python
from typing import Annotated
from agents import Agent, Runner, ToolSearchTool, function_tool, tool_namespace
@function_tool(defer_loading=True)
def get_customer_profile(
customer_id: Annotated[str, "The customer ID to look up."],
) -> str:
"""Fetch a CRM customer profile."""
return f"profile for {customer_id}"
@function_tool(defer_loading=True)
def list_open_orders(
customer_id: Annotated[str, "The customer ID to look up."],
) -> str:
"""List open orders for a customer."""
return f"open orders for {customer_id}"
crm_tools = tool_namespace(
name="crm",
description="CRM tools for customer lookups.",
tools=[get_customer_profile, list_open_orders],
)
agent = Agent(
name="Operations assistant",
model="gpt-5.6-sol",
instructions="Load the crm namespace before using CRM tools.",
tools=[*crm_tools, ToolSearchTool()],
)
result = await Runner.run(agent, "Look up customer_42 and list their open orders.")
print(result.final_output)
```
What to know:
- Hosted tool search is available only with OpenAI Responses models. The current Python SDK support depends on `openai>=2.25.0`.
- Add exactly one `ToolSearchTool()` when you configure deferred-loading surfaces on an agent.
- Searchable surfaces include `@function_tool(defer_loading=True)`, `tool_namespace(name=..., description=..., tools=[...])`, and `HostedMCPTool(tool_config={..., "defer_loading": True})`.
- Deferred-loading function tools must be paired with `ToolSearchTool()`. Namespace-only setups may also use `ToolSearchTool()` to let the model load the right group on demand.
- `tool_namespace()` groups `FunctionTool` instances under a shared namespace name and description. This is usually the best fit when you have many related tools, such as `crm`, `billing`, or `shipping`.
- OpenAI's official best-practice guidance is [Use namespaces where possible](https://developers.openai.com/api/docs/guides/tools-tool-search#use-namespaces-where-possible).
- Prefer namespaces or hosted MCP servers over many individually deferred functions when possible. They usually give the model a better high-level search surface and better token savings.
- Namespaces can mix immediate and deferred tools. Tools without `defer_loading=True` remain callable immediately, while deferred tools in the same namespace are loaded through tool search.
- As a rule of thumb, keep each namespace fairly small, ideally fewer than 10 functions.
- Named `tool_choice` cannot target bare namespace names or deferred-only tools. Prefer `auto`, `required`, or a real top-level callable tool name.
- `ToolSearchTool(execution="client")` is for manual Responses orchestration. If the model emits a client-executed `tool_search_call`, the standard `Runner` raises instead of executing it for you.
- Tool search activity appears in [`RunResult.new_items`](results.md#new-items) and in [`RunItemStreamEvent`](streaming.md#run-item-event-names) with dedicated item and event types.
- See `examples/tools/tool_search.py` for complete runnable examples covering both namespaced loading and top-level deferred tools.
- Official platform guide: [Tool search](https://developers.openai.com/api/docs/guides/tools-tool-search).
### Hosted container shell + skills
`ShellTool` also supports OpenAI-hosted container execution. Use this mode when you want the model to run shell commands in a managed container instead of your local runtime.
```python
from agents import Agent, Runner, ShellTool, ShellToolSkillReference
csv_skill: ShellToolSkillReference = {
"type": "skill_reference",
"skill_id": "skill_698bbe879adc81918725cbc69dcae7960bc5613dadaed377",
"version": "1",
}
agent = Agent(
name="Container shell agent",
model="gpt-5.6-sol",
instructions="Use the mounted skill when helpful.",
tools=[
ShellTool(
environment={
"type": "container_auto",
"network_policy": {"type": "disabled"},
"skills": [csv_skill],
}
)
],
)
result = await Runner.run(
agent,
"Use the configured skill to analyze CSV files in /mnt/data and summarize totals by region.",
)
print(result.final_output)
```
To reuse an existing container in later runs, set `environment={"type": "container_reference", "container_id": "cntr_..."}`.
What to know:
- Hosted shell is available through the Responses API shell tool.
- `container_auto` provisions a container for the request; `container_reference` reuses an existing one.
- `container_auto` can also include `file_ids` and `memory_limit`.
- `environment.skills` accepts skill references and inline skill bundles.
- With hosted environments, do not set `executor`, `needs_approval`, or `on_approval` on `ShellTool`.
- `network_policy` supports `disabled` and `allowlist` modes.
- In allowlist mode, `network_policy.domain_secrets` can inject domain-scoped secrets by name.
- See `examples/tools/container_shell_skill_reference.py` and `examples/tools/container_shell_inline_skill.py` for complete examples.
- OpenAI platform guides: [Shell](https://platform.openai.com/docs/guides/tools-shell) and [Skills](https://platform.openai.com/docs/guides/tools-skills).
## Local runtime tools
Local runtime tools execute outside the model response itself. The model still decides when to call them, but your application or configured execution environment performs the actual work.
`ComputerTool` and `ApplyPatchTool` always require local implementations that you provide. `ShellTool` spans both modes: use the hosted-container configuration above when you want managed execution, or the local runtime configuration below when you want commands to run in your own process.
Local runtime tools require you to supply implementations:
- [`ComputerTool`][agents.tool.ComputerTool]: implement the [`Computer`][agents.computer.Computer] or [`AsyncComputer`][agents.computer.AsyncComputer] interface to enable GUI/browser automation.
- [`ShellTool`][agents.tool.ShellTool]: the latest shell tool for both local execution and hosted container execution.
- [`LocalShellTool`][agents.tool.LocalShellTool]: legacy local-shell integration.
- [`ApplyPatchTool`][agents.tool.ApplyPatchTool]: implement [`ApplyPatchEditor`][agents.editor.ApplyPatchEditor] to apply diffs locally.
- Local shell skills are available with `ShellTool(environment={"type": "local", "skills": [...]})`.
### ComputerTool and the Responses computer tool
`ComputerTool` is still a local harness: you provide a [`Computer`][agents.computer.Computer] or [`AsyncComputer`][agents.computer.AsyncComputer] implementation, and the SDK maps that harness onto the OpenAI Responses API computer surface.
For explicit [`gpt-5.5`](https://developers.openai.com/api/docs/models/gpt-5.5) requests, the SDK sends the GA built-in tool payload `{"type": "computer"}`. The older `computer-use-preview` model keeps the preview payload `{"type": "computer_use_preview", "environment": ..., "display_width": ..., "display_height": ...}`. This mirrors the platform migration described in OpenAI's [Computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use/):
- Model: `computer-use-preview` -> `gpt-5.5`
- Tool selector: `computer_use_preview` -> `computer`
- Computer call shape: one `action` per `computer_call` -> batched `actions[]` on `computer_call`
- Truncation: `ModelSettings(truncation="auto")` required on the preview path -> not required on the GA path
The SDK chooses that wire shape from the effective model on the actual Responses request. If you use a prompt template and the request omits `model` because the prompt owns it, the SDK keeps the preview-compatible computer payload unless you either keep `model="gpt-5.5"` explicit or force the GA selector with `ModelSettings(tool_choice="computer")` or `ModelSettings(tool_choice="computer_use")`.
When a [`ComputerTool`][agents.tool.ComputerTool] is present, `tool_choice="computer"`, `"computer_use"`, and `"computer_use_preview"` are all accepted and normalized to the built-in selector that matches the effective request model. Without a `ComputerTool`, those strings still behave like ordinary function names.
This distinction matters when `ComputerTool` is backed by a [`ComputerProvider`][agents.tool.ComputerProvider] factory. The GA `computer` payload does not need `environment` or dimensions at serialization time, so unresolved factories are fine. Preview-compatible serialization still needs a resolved `Computer` or `AsyncComputer` instance so the SDK can send `environment`, `display_width`, and `display_height`.
At runtime, both paths still use the same local harness. Preview responses emit `computer_call` items with a single `action`; `gpt-5.5` can emit batched `actions[]`, and the SDK executes them in order before producing a `computer_call_output` screenshot item. See `examples/tools/computer_use.py` for a runnable Playwright-based harness.
```python
from agents import Agent, ApplyPatchTool, ShellTool
from agents.computer import AsyncComputer
from agents.editor import ApplyPatchResult, ApplyPatchOperation, ApplyPatchEditor
class NoopComputer(AsyncComputer):
environment = "browser"
dimensions = (1024, 768)
async def screenshot(self): return ""
async def click(self, x, y, button): ...
async def double_click(self, x, y): ...
async def scroll(self, x, y, scroll_x, scroll_y): ...
async def type(self, text): ...
async def wait(self): ...
async def move(self, x, y): ...
async def keypress(self, keys): ...
async def drag(self, path): ...
class NoopEditor(ApplyPatchEditor):
async def create_file(self, op: ApplyPatchOperation): return ApplyPatchResult(status="completed")
async def update_file(self, op: ApplyPatchOperation): return ApplyPatchResult(status="completed")
async def delete_file(self, op: ApplyPatchOperation): return ApplyPatchResult(status="completed")
async def run_shell(request):
return "shell output"
agent = Agent(
name="Local tools agent",
tools=[
ShellTool(executor=run_shell),
ApplyPatchTool(editor=NoopEditor()),
# ComputerTool expects a Computer/AsyncComputer implementation; omitted here for brevity.
],
)
```
## Function tools
You can use any Python function as a tool. The Agents SDK will set up the tool automatically:
- The name of the tool will be the name of the Python function (or you can provide a name)
- Tool description will be taken from the docstring of the function (or you can provide a description)
- The schema for the function inputs is automatically created from the function's arguments
- Descriptions for each input are taken from the docstring of the function, unless disabled
We use Python's `inspect` module to extract the function signature, along with [`griffe`](https://mkdocstrings.github.io/griffe/) to parse docstrings and `pydantic` for schema creation.
When you are using OpenAI Responses models, `@function_tool(defer_loading=True)` hides a function tool until `ToolSearchTool()` loads it. You can also group related function tools with [`tool_namespace()`][agents.tool.tool_namespace]. See [Hosted tool search](#hosted-tool-search) for the full setup and constraints.
```python
import json
from typing_extensions import TypedDict, Any
from agents import Agent, FunctionTool, RunContextWrapper, function_tool
class Location(TypedDict):
lat: float
long: float
@function_tool # (1)!
async def fetch_weather(location: Location) -> str:
# (2)!
"""Fetch the weather for a given location.
Args:
location: The location to fetch the weather for.
"""
# In real life, we'd fetch the weather from a weather API
return "sunny"
@function_tool(name_override="fetch_data") # (3)!
def read_file(ctx: RunContextWrapper[Any], path: str, directory: str | None = None) -> str:
"""Read the contents of a file.
Args:
path: The path to the file to read.
directory: The directory to read the file from.
"""
# In real life, we'd read the file from the file system
return "<file contents>"
agent = Agent(
name="Assistant",
tools=[fetch_weather, read_file], # (4)!
)
for tool in agent.tools:
if isinstance(tool, FunctionTool):
print(tool.name)
print(tool.description)
print(json.dumps(tool.params_json_schema, indent=2))
print()
```
1. You can use any Python types as arguments to your functions, and the function can be sync or async.
2. Docstrings, if present, are used to capture descriptions and argument descriptions
3. Functions can optionally take the `context` (must be the first argument). You can also set overrides, like the name of the tool, description, which docstring style to use, etc.
4. You can pass the decorated functions to the list of tools.
??? note "Expand to see output"
```
fetch_weather
Fetch the weather for a given location.
{
"$defs": {
"Location": {
"properties": {
"lat": {
"title": "Lat",
"type": "number"
},
"long": {
"title": "Long",
"type": "number"
}
},
"required": [
"lat",
"long"
],
"title": "Location",
"type": "object"
}
},
"properties": {
"location": {
"$ref": "#/$defs/Location",
"description": "The location to fetch the weather for."
}
},
"required": [
"location"
],
"title": "fetch_weather_args",
"type": "object"
}
fetch_data
Read the contents of a file.
{
"properties": {
"path": {
"description": "The path to the file to read.",
"title": "Path",
"type": "string"
},
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The directory to read the file from.",
"title": "Directory"
}
},
"required": [
"path"
],
"title": "fetch_data_args",
"type": "object"
}
```
### Returning images or files from function tools
In addition to returning text outputs, you can return one or many images or files as the output of a function tool. To do so, you can return any of:
- Images: [`ToolOutputImage`][agents.tool.ToolOutputImage] (or the TypedDict version, [`ToolOutputImageDict`][agents.tool.ToolOutputImageDict])
- Files: [`ToolOutputFileContent`][agents.tool.ToolOutputFileContent] (or the TypedDict version, [`ToolOutputFileContentDict`][agents.tool.ToolOutputFileContentDict])
- Text: either a string or stringable objects, or [`ToolOutputText`][agents.tool.ToolOutputText] (or the TypedDict version, [`ToolOutputTextDict`][agents.tool.ToolOutputTextDict])
### Custom function tools
Sometimes, you don't want to use a Python function as a tool. You can directly create a [`FunctionTool`][agents.tool.FunctionTool] if you prefer. You'll need to provide:
- `name`
- `description`
- `params_json_schema`, which is the JSON schema for the arguments
- `on_invoke_tool`, which is an async function that receives a [`ToolContext`][agents.tool_context.ToolContext] and the arguments as a JSON string, and returns tool output (for example, text, structured tool output objects, or a list of outputs).
```python
from typing import Any
from pydantic import BaseModel
from agents import RunContextWrapper, FunctionTool
def do_some_work(data: str) -> str:
return "done"
class FunctionArgs(BaseModel):
username: str
age: int
async def run_function(ctx: RunContextWrapper[Any], args: str) -> str:
parsed = FunctionArgs.model_validate_json(args)
return do_some_work(data=f"{parsed.username} is {parsed.age} years old")
tool = FunctionTool(
name="process_user",
description="Processes extracted user data",
params_json_schema=FunctionArgs.model_json_schema(),
on_invoke_tool=run_function,
)
```
### Automatic argument and docstring parsing
As mentioned before, we automatically parse the function signature to extract the schema for the tool, and we parse the docstring to extract descriptions for the tool and for individual arguments. Some notes on that:
1. The signature parsing is done via the `inspect` module. We use type annotations to understand the types for the arguments, and dynamically build a Pydantic model to represent the overall schema. It supports most types, including Python primitives, Pydantic models, TypedDicts, and more.
2. We use `griffe` to parse docstrings. Supported docstring formats are `google`, `sphinx` and `numpy`. We attempt to automatically detect the docstring format, but this is best-effort and you can explicitly set it when calling `function_tool`. You can also disable docstring parsing by setting `use_docstring_info` to `False`.
The code for the schema extraction lives in [`agents.function_schema`][].
### Constraining and describing arguments with Pydantic Field
You can use Pydantic's [`Field`](https://docs.pydantic.dev/latest/concepts/fields/) to add constraints (e.g. min/max for numbers, length or pattern for strings) and descriptions to tool arguments. As in Pydantic, both forms are supported: default-based (`arg: int = Field(..., ge=1)`) and `Annotated` (`arg: Annotated[int, Field(..., ge=1)]`). The generated JSON schema and validation include these constraints.
```python
from typing import Annotated
from pydantic import Field
from agents import function_tool
# Default-based form
@function_tool
def score_a(score: int = Field(..., ge=0, le=100, description="Score from 0 to 100")) -> str:
return f"Score recorded: {score}"
# Annotated form
@function_tool
def score_b(score: Annotated[int, Field(..., ge=0, le=100, description="Score from 0 to 100")]) -> str:
return f"Score recorded: {score}"
```
### Function tool timeouts
You can set per-call timeouts for async function tools with `@function_tool(timeout=...)`.
```python
import asyncio
from agents import Agent, Runner, function_tool
@function_tool(timeout=2.0)
async def slow_lookup(query: str) -> str:
await asyncio.sleep(10)
return f"Result for {query}"
agent = Agent(
name="Timeout demo",
instructions="Use tools when helpful.",
tools=[slow_lookup],
)
```
When a timeout is reached, the default behavior is `timeout_behavior="error_as_result"`, which sends a model-visible timeout message (for example, `Tool 'slow_lookup' timed out after 2 seconds.`).
You can control timeout handling:
- `timeout_behavior="error_as_result"` (default): return a timeout message to the model so it can recover.
- `timeout_behavior="raise_exception"`: raise [`ToolTimeoutError`][agents.exceptions.ToolTimeoutError] and fail the run.
- `timeout_error_function=...`: customize the timeout message when using `error_as_result`.
```python
import asyncio
from agents import Agent, Runner, ToolTimeoutError, function_tool
@function_tool(timeout=1.5, timeout_behavior="raise_exception")
async def slow_tool() -> str:
await asyncio.sleep(5)
return "done"
agent = Agent(name="Timeout hard-fail", tools=[slow_tool])
try:
await Runner.run(agent, "Run the tool")
except ToolTimeoutError as e:
print(f"{e.tool_name} timed out in {e.timeout_seconds} seconds")
```
!!! note
Timeout configuration is supported only for async `@function_tool` handlers.
### Handling errors in function tools
When you create a function tool via `@function_tool`, you can pass a `failure_error_function`. This is a function that provides an error response to the LLM in case the tool call crashes.
- By default (i.e. if you don't pass anything), it runs a `default_tool_error_function` which tells the LLM an error occurred.
- If you pass your own error function, it runs that instead, and sends the response to the LLM.
- If you explicitly pass `None`, then any tool call errors will be re-raised for you to handle. This could be a `ModelBehaviorError` if the model produced invalid JSON, or a `UserError` if your code crashed, etc.
```python
from agents import function_tool, RunContextWrapper
from typing import Any
def my_custom_error_function(context: RunContextWrapper[Any], error: Exception) -> str:
"""A custom function to provide a user-friendly error message."""
print(f"A tool call failed with the following error: {error}")
return "An internal server error occurred. Please try again later."
@function_tool(failure_error_function=my_custom_error_function)
def get_user_profile(user_id: str) -> str:
"""Fetches a user profile from a mock API.
This function demonstrates a 'flaky' or failing API call.
"""
if user_id == "user_123":
return "User profile for user_123 successfully retrieved."
else:
raise ValueError(f"Could not retrieve profile for user_id: {user_id}. API returned an error.")
```
If you are manually creating a `FunctionTool` object, then you must handle errors inside the `on_invoke_tool` function.
## Agents as tools
In some workflows, you may want a central agent to orchestrate a network of specialized agents, instead of handing off control. You can do this by modeling agents as tools.
```python
from agents import Agent, Runner
import asyncio
spanish_agent = Agent(
name="Spanish agent",
instructions="You translate the user's message to Spanish",
)
french_agent = Agent(
name="French agent",
instructions="You translate the user's message to French",
)
orchestrator_agent = Agent(
name="orchestrator_agent",
instructions=(
"You are a translation agent. You use the tools given to you to translate. "
"If asked for multiple translations, you call the relevant tools."
),
tools=[
spanish_agent.as_tool(
tool_name="translate_to_spanish",
tool_description="Translate the user's message to Spanish",
),
french_agent.as_tool(
tool_name="translate_to_french",
tool_description="Translate the user's message to French",
),
],
)
async def main():
result = await Runner.run(orchestrator_agent, input="Say 'Hello, how are you?' in Spanish.")
print(result.final_output)
```
### Customizing tool-agents
The `agent.as_tool` function is a convenience method to make it easy to turn an agent into a tool. It supports common runtime options such as `max_turns`, `run_config`, `hooks`, `previous_response_id`, `conversation_id`, `session`, and `needs_approval`. It also supports structured input with `parameters`, `input_builder`, and `include_input_schema`.
The state options configure the nested agent run started by the tool call; the parent run's conversation state is not inherited automatically. To share client-managed history between the parent and nested runs, explicitly pass the same `session` to both. As with `Runner.run`, choose one state strategy for the nested run: a client-managed `session`, or server-managed continuation through `previous_response_id` or `conversation_id`.
```python
@function_tool
async def run_my_agent() -> str:
"""A tool that runs the agent with custom configs"""
agent = Agent(name="My agent", instructions="...")
result = await Runner.run(
agent,
input="...",
max_turns=5,
run_config=...
)
return str(result.final_output)
```
### Structured input for tool-agents
By default, `Agent.as_tool()` expects a single string input (`{"input": "..."}`), but you can expose a structured schema by passing `parameters` (a Pydantic model or dataclass type).
Additional options:
- `include_input_schema=True` includes the full JSON Schema in the generated nested input.
- `input_builder=...` lets you fully customize how structured tool arguments become nested agent input.
- `RunContextWrapper.tool_input` contains the parsed structured payload inside the nested run context.
```python
from pydantic import BaseModel, Field
class TranslationInput(BaseModel):
text: str = Field(description="Text to translate.")
source: str = Field(description="Source language.")
target: str = Field(description="Target language.")
translator_tool = translator_agent.as_tool(
tool_name="translate_text",
tool_description="Translate text between languages.",
parameters=TranslationInput,
include_input_schema=True,
)
```
See `examples/agent_patterns/agents_as_tools_structured.py` for a complete runnable example.
### Approval gates for tool-agents
`Agent.as_tool(..., needs_approval=...)` uses the same approval flow as `function_tool`. If approval is required, the run pauses and pending items appear in `result.interruptions`; then use `result.to_state()` and resume after calling `state.approve(...)` or `state.reject(...)`. See the [Human-in-the-loop guide](human_in_the_loop.md) for the full pause/resume pattern.
### Custom output extraction
In certain cases, you might want to modify the output of the tool-agents before returning it to the central agent. This may be useful if you want to:
- Extract a specific piece of information (e.g., a JSON payload) from the sub-agent's chat history.
- Convert or reformat the agents final answer (e.g., transform Markdown into plain text or CSV).
- Validate the output or provide a fallback value when the agents response is missing or malformed.
You can do this by supplying the `custom_output_extractor` argument to the `as_tool` method:
```python
async def extract_json_payload(run_result: RunResult) -> str:
# Scan the agents outputs in reverse order until we find a JSON-like message from a tool call.
for item in reversed(run_result.new_items):
if isinstance(item, ToolCallOutputItem) and item.output.strip().startswith("{"):
return item.output.strip()
# Fallback to an empty JSON object if nothing was found
return "{}"
json_tool = data_agent.as_tool(
tool_name="get_data_json",
tool_description="Run the data agent and return only its JSON payload",
custom_output_extractor=extract_json_payload,
)
```
Inside a custom extractor, the nested [`RunResult`][agents.result.RunResult] also exposes [`agent_tool_invocation`][agents.result.RunResultBase.agent_tool_invocation], which is useful when you need the outer tool name, call ID, or raw arguments while post-processing the nested result. See the [Results guide](results.md#agent-as-tool-metadata).
### Streaming nested agent runs
Pass an `on_stream` callback to `as_tool` to listen to streaming events emitted by the nested agent while still returning its final output once the stream completes.
```python
from agents import AgentToolStreamEvent
async def handle_stream(event: AgentToolStreamEvent) -> None:
# Inspect the underlying StreamEvent along with agent metadata.
print(f"[stream] {event['agent'].name} :: {event['event'].type}")
billing_agent_tool = billing_agent.as_tool(
tool_name="billing_helper",
tool_description="Answer billing questions.",
on_stream=handle_stream, # Can be sync or async.
)
```
What to expect:
- Event types mirror `StreamEvent["type"]`: `raw_response_event`, `run_item_stream_event`, `agent_updated_stream_event`.
- Providing `on_stream` automatically runs the nested agent in streaming mode and drains the stream before returning the final output.
- The handler may be synchronous or asynchronous; each event is delivered in order as it arrives.
- `tool_call` is present when the tool is invoked via a model tool call; direct calls may leave it `None`.
- See `examples/agent_patterns/agents_as_tools_streaming.py` for a complete runnable sample.
### Conditional tool enabling
You can conditionally enable or disable agent tools at runtime using the `is_enabled` parameter. This allows you to dynamically filter which tools are available to the LLM based on context, user preferences, or runtime conditions.
```python
import asyncio
from agents import Agent, AgentBase, Runner, RunContextWrapper
from pydantic import BaseModel
class LanguageContext(BaseModel):
language_preference: str = "french_spanish"
def french_enabled(ctx: RunContextWrapper[LanguageContext], agent: AgentBase) -> bool:
"""Enable French for French+Spanish preference."""
return ctx.context.language_preference == "french_spanish"
# Create specialized agents
spanish_agent = Agent(
name="spanish_agent",
instructions="You respond in Spanish. Always reply to the user's question in Spanish.",
)
french_agent = Agent(
name="french_agent",
instructions="You respond in French. Always reply to the user's question in French.",
)
# Create orchestrator with conditional tools
orchestrator = Agent(
name="orchestrator",
instructions=(
"You are a multilingual assistant. You use the tools given to you to respond to users. "
"You must call ALL available tools to provide responses in different languages. "
"You never respond in languages yourself, you always use the provided tools."
),
tools=[
spanish_agent.as_tool(
tool_name="respond_spanish",
tool_description="Respond to the user's question in Spanish",
is_enabled=True, # Always enabled
),
french_agent.as_tool(
tool_name="respond_french",
tool_description="Respond to the user's question in French",
is_enabled=french_enabled,
),
],
)
async def main():
context = RunContextWrapper(LanguageContext(language_preference="french_spanish"))
result = await Runner.run(orchestrator, "How are you?", context=context.context)
print(result.final_output)
asyncio.run(main())
```
The `is_enabled` parameter accepts:
- **Boolean values**: `True` (always enabled) or `False` (always disabled)
- **Callable functions**: Functions that take `(context, agent)` and return a boolean
- **Async functions**: Async functions for complex conditional logic
Disabled tools are completely hidden from the LLM at runtime, making this useful for:
- Feature gating based on user permissions
- Environment-specific tool availability (dev vs prod)
- A/B testing different tool configurations
- Dynamic tool filtering based on runtime state
## Experimental: Codex tool
The `codex_tool` wraps the Codex CLI so an agent can run workspace-scoped tasks (shell, file edits, MCP tools) during a tool call. This surface is experimental and may change.
Use it when you want the main agent to delegate a bounded workspace task to Codex without leaving the current run. By default, the tool name is `codex`. If you set a custom name, it must be `codex` or start with `codex_`. When an agent includes multiple Codex tools, each must use a unique name.
```python
from agents import Agent
from agents.extensions.experimental.codex import ThreadOptions, TurnOptions, codex_tool
agent = Agent(
name="Codex Agent",
instructions="Use the codex tool to inspect the workspace and answer the question.",
tools=[
codex_tool(
sandbox_mode="workspace-write",
working_directory="/path/to/repo",
default_thread_options=ThreadOptions(
model="gpt-5.5",
model_reasoning_effort="low",
network_access_enabled=True,
web_search_mode="disabled",
approval_policy="never",
),
default_turn_options=TurnOptions(
idle_timeout_seconds=60,
),
persist_session=True,
)
],
)
```
Start with these option groups:
- Execution surface: `sandbox_mode` and `working_directory` define where Codex can operate. Pair them together, and set `skip_git_repo_check=True` when the working directory is not inside a Git repository.
- Thread defaults: `default_thread_options=ThreadOptions(...)` configures the model, reasoning effort, approval policy, additional directories, network access, and web search mode. Prefer `web_search_mode` over the legacy `web_search_enabled`.
- Turn defaults: `default_turn_options=TurnOptions(...)` configures per-turn behavior such as `idle_timeout_seconds` and the optional cancellation `signal`.
- Tool I/O: tool calls must include at least one `inputs` item with `{ "type": "text", "text": ... }` or `{ "type": "local_image", "path": ... }`. `output_schema` lets you require structured Codex responses.
Thread reuse and persistence are separate controls:
- `persist_session=True` reuses one Codex thread for repeated calls to the same tool instance.
- `use_run_context_thread_id=True` stores and reuses the thread ID in run context across runs that share the same mutable context object.
- Thread ID precedence is: per-call `thread_id`, then run-context thread ID (if enabled), then the configured `thread_id` option.
- The default run-context key is `codex_thread_id` for `name="codex"` and `codex_thread_id_<suffix>` for `name="codex_<suffix>"`. Override it with `run_context_thread_id_key`.
Runtime configuration:
- Auth: set `CODEX_API_KEY` (preferred) or `OPENAI_API_KEY`, or pass `codex_options={"api_key": "..."}`.
- Runtime: `codex_options.base_url` overrides the CLI base URL.
- Binary resolution: set `codex_options.codex_path_override` (or `CODEX_PATH`) to pin the CLI path. Otherwise the SDK resolves `codex` from `PATH`, then falls back to the bundled vendor binary.
- Environment: `codex_options.env` fully controls the subprocess environment. When it is provided, the subprocess does not inherit `os.environ`.
- Stream limits: `codex_options.codex_subprocess_stream_limit_bytes` (or `OPENAI_AGENTS_CODEX_SUBPROCESS_STREAM_LIMIT_BYTES`) controls stdout/stderr reader limits. Valid range is `65536` to `67108864`; default is `8388608`.
- Streaming: `on_stream` receives thread/turn lifecycle events and item events (`reasoning`, `command_execution`, `mcp_tool_call`, `file_change`, `web_search`, `todo_list`, and `error` item updates).
- Outputs: results include `response`, `usage`, and `thread_id`; usage is added to `RunContextWrapper.usage`.
Reference:
- [Codex tool API reference](ref/extensions/experimental/codex/codex_tool.md)
- [ThreadOptions reference](ref/extensions/experimental/codex/thread_options.md)
- [TurnOptions reference](ref/extensions/experimental/codex/turn_options.md)
- See `examples/tools/codex.py` and `examples/tools/codex_same_thread.py` for complete runnable samples.