Skip to content

Conversation

uzair330
Copy link
Contributor

Description

This PR addresses Issue #939 by enhancing the observability of tool calls within the Agents SDK. It exposes the raw JSON arguments of a tool call in the ToolContext, making them accessible in lifecycle hooks like on_tool_start and on_tool_end.

Why These Changes Are Required and Their Importance

Currently, developers using lifecycle hooks have visibility into which tool is being called (tool_name) but lack access to the arguments being passed to it. This limits the ability to effectively log, debug, or implement custom logic based on the tool's inputs.

This change introduces an arguments field to the ToolContext, providing the raw JSON string of the arguments sent by the model. The importance of this enhancement is threefold:

  1. Enhanced Observability: Developers can now create sophisticated logging and monitoring hooks that record the exact inputs for every tool call, which is invaluable for debugging agent behavior.
  2. Improved Customization: It enables advanced use cases, such as input validation or dynamic logic within hooks, before a tool is even executed.
  3. Backward Compatibility: The change is implemented in a non-breaking way. Existing agents and hooks will continue to function without any modification, while new capabilities are available for those who need them.

This aligns with the SDK's core goals of extensibility and traceability, giving developers deeper insights into the agent's execution flow.

Examples

The following examples demonstrate how to access context.arguments within a custom RunHooks implementation across synchronous, asynchronous, and streaming executions.

1. Hello World (Sync)

This example shows a simple agent with a custom hook that prints the arguments passed to the say_hello tool during a synchronous run.

import os

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext
@function_tool
def say_hello(name: str) -> str:
    return f"Hello, {name}!"

class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        pass

    async def on_end(self, context, agent, result):
        pass

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool '{tool.name}' started with args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool '{tool.name}' ended with args: {context.arguments}, result: {result}")


agent = Agent(
    name="HelloSyncAgent",
    instructions="Use the say_hello tool when asked to greet someone.",
    tools=[say_hello],
    hooks=LoggingArgsHooks(),
)

if __name__ == "__main__":
    result = Runner.run_sync(agent, "Please greet Muhammad Uzair.")
    print("Final output:", result.final_output)

2. Hello World (Async)

This example demonstrates the same argument logging hook within an asynchronous agent run.

import asyncio
import os

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext

@function_tool
def say_hello(name: str) -> str:
    return f"Hello, {name}!"


class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        pass

    async def on_end(self, context, agent, result):
        pass

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool '{tool.name}' started with args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool '{tool.name}' ended with args: {context.arguments}, result: {result}")


agent = Agent(
    name="HelloAsyncAgent",
    instructions="Use the say_hello tool when asked to greet someone.",
    tools=[say_hello],
    hooks=LoggingArgsHooks(),
)

async def main():
    result = await Runner.run(agent, "Please greet Muhammad Uzair.")
    print("Final output:", result.final_output)


if __name__ == "__main__":
    asyncio.run(main())```

#### 3. Hello World (Streaming)
This example shows that the hooks function correctly and can access arguments even when the agent's response is streamed.

```python
import asyncio
import os

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext
from openai.types.responses import ResponseTextDeltaEvent


@function_tool
def say_hello(name: str) -> str:
    return f"Hello, {name}!"


class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        pass

    async def on_end(self, context, agent, result):
        pass

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool '{tool.name}' started with args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool '{tool.name}' ended with args: {context.arguments}, result: {result}")


agent = Agent(
    name="HelloStreamAgent",
    instructions="Use the say_hello tool when asked to greet someone.",
    tools=[say_hello],
    hooks=LoggingArgsHooks(),
)


async def main():
    result = Runner.run_streamed(agent, input="Please greet Muhammad Uzair.")
    async for event in result.stream_events():
        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
            print(event.data.delta, end="", flush=True)
    print("\nDone streaming.")


if __name__ == "__main__":
    asyncio.run(main())

4. Math World (Sync)

This example demonstrates argument logging for an agent that performs multiple tool calls in a synchronous run.

import os

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext
@function_tool
def add(x: float, y: float) -> float:
    return x + y


@function_tool
def subtract(x: float, y: float) -> float:
    return x - y


@function_tool
def divide(x: float, y: float) -> float:
    return x / y if y != 0 else float("inf")


class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        print(f"[HOOK] {agent.name} starting")

    async def on_end(self, context, agent, result):
        print(f"[HOOK] {agent.name} finished: {result}")

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}, result: {result}")


agent = Agent(
    name="MathSyncAgent",
    instructions="You can add, subtract, and divide numbers.",
    tools=[add, subtract, divide],
    hooks=LoggingArgsHooks(),

)

if __name__ == "__main__":
    result = Runner.run_sync(agent, "What is (10 + 5) divided by (12 - 7)?")
    print("Final output:", result.final_output)

5. Math World (Async)

This shows the same multi-tool agent using an asynchronous runner.

import asyncio
import os

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext

@function_tool
def add(x: float, y: float) -> float:
    return x + y


@function_tool
def subtract(x: float, y: float) -> float:
    return x - y


@function_tool
def divide(x: float, y: float) -> float:
    return x / y if y != 0 else float("inf")


class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        print(f"[HOOK] {agent.name} starting")

    async def on_end(self, context, agent, result):
        print(f"[HOOK] {agent.name} finished: {result}")

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}, result: {result}")


agent = Agent(
    name="MathAsyncAgent",
    instructions="You can add, subtract, and divide numbers.",
    tools=[add, subtract, divide],
    hooks=LoggingArgsHooks(),
)


async def main():
    result = await Runner.run(agent, "What is (20 - 4) divided by (3 + 1)?")
    print("Final output:", result.final_output)


if __name__ == "__main__":
    asyncio.run(main())

6. Math World (Streaming)

Finally, this example confirms the hooks work as expected during a streaming run with a multi-tool agent.

import asyncio
import os

from openai.types.responses import ResponseTextDeltaEvent

from agents import Agent, RunHooks, Runner, function_tool
from agents.tool_context import ToolContext

@function_tool
def add(x: float, y: float) -> float:
    return x + y


@function_tool
def subtract(x: float, y: float) -> float:
    return x - y


@function_tool
def divide(x: float, y: float) -> float:
    return x / y if y != 0 else float("inf")


class LoggingArgsHooks(RunHooks):
    async def on_start(self, context, agent):
        print(f"[HOOK] {agent.name} starting")

    async def on_end(self, context, agent, result):
        print(f"[HOOK] {agent.name} finished: {result}")

    async def on_tool_start(self, context: ToolContext, agent, tool):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}")

    async def on_tool_end(self, context: ToolContext, agent, tool, result):
        print(f"[HOOK] Tool {tool.name} args: {context.arguments}, result: {result}")


agent = Agent(
    name="MathStreamAgent",
    instructions="You can add, subtract, and divide numbers.",
    tools=[add, subtract, divide],
    hooks=LoggingArgsHooks(),
)


async def main():
    result = Runner.run_streamed(agent, input="Compute (100 - 20) / (5 + 5).")
    async for event in result.stream_events():
        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
            print(event.data.delta, end="", flush=True)
    print("\nDone streaming.")


if __name__ == "__main__":
    asyncio.run(main())

@uzair330 uzair330 closed this Aug 27, 2025
@uzair330 uzair330 deleted the feat/tool--context-arguments branch August 27, 2025 19:54
@ihower
Copy link
Contributor

ihower commented Sep 12, 2025

@uzair330 +1 for this feature 🙌
But I noticed you closed this PR yourself — is there any reason why it wasn’t continued?

@uzair330
Copy link
Contributor Author

I closed it because I ran into merge conflicts and wanted to clean things up before reopening. I’ll submit a fresh PR once it’s resolved.

rm-openai pushed a commit that referenced this pull request Sep 22, 2025
## Background 

Currently, the `RunHooks` lifecycle (`on_tool_start`, `on_tool_end`)
exposes the `Tool` and `ToolContext`, but does not include the actual
arguments passed to the tool call.

resolves #939

## Solution

This implementation is inspired by [PR
#1598](#1598).

* Add a new `tool_arguments` field to `ToolContext` and populate it via
from_agent_context with tool_call.arguments.
* Update `lifecycle_example.py` to demonstrate tool_arguments in hooks
* Unlike the proposal in [PR
#253](#253), this
solution is not expected to introduce breaking changes, making it easier
to adopt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants