From f1a65c2647f79a26dc86405800f2e985aa3c0e77 Mon Sep 17 00:00:00 2001 From: Nik-Reddy Date: Wed, 15 Apr 2026 16:12:24 -0700 Subject: [PATCH] feat: expose conversation history to tools via ToolContext.conversation_history Add a conversation_history property to ToolContext that gives tools read-only access to the items generated so far in the current agent run. This enables tools to inspect prior messages, tool calls, and other conversation items during execution without requiring streaming mode. Changes: - Add _generated_items field to RunContextWrapper (internal, not part of the public constructor contract) - Add conversation_history read-only property to ToolContext that returns a defensive copy of the generated items - Populate _generated_items on the context wrapper in get_single_step_result_from_response before tool execution, which covers both streaming and non-streaming paths - Export ToolContext from the top-level gents package - Add 4 tests covering default empty state, population from wrapper, copy semantics, and direct constructor usage Fixes #904 --- src/agents/__init__.py | 2 + src/agents/run_context.py | 3 ++ src/agents/run_internal/turn_resolution.py | 4 ++ src/agents/tool_context.py | 12 +++++ tests/test_tool_context.py | 63 ++++++++++++++++++++++ 5 files changed, 84 insertions(+) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index e3b34d244b..9474a24251 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -176,6 +176,7 @@ resolve_computer, tool_namespace, ) +from .tool_context import ToolContext from .tool_guardrails import ( ToolGuardrailFunctionOutput, ToolInputGuardrail, @@ -413,6 +414,7 @@ def enable_verbose_stdout_logging(): "CompactionItem", "AgentHookContext", "RunContextWrapper", + "ToolContext", "TContext", "RunErrorDetails", "RunErrorData", diff --git a/src/agents/run_context.py b/src/agents/run_context.py index df7047eb38..453bb5477e 100644 --- a/src/agents/run_context.py +++ b/src/agents/run_context.py @@ -61,6 +61,9 @@ class RunContextWrapper(Generic[TContext]): tool_input: Any | None = None """Structured input for the current agent tool run, when available.""" + _generated_items: list[Any] = field(default_factory=list, repr=False) + """Internal: conversation items generated so far in the current run.""" + @staticmethod def _to_str_or_none(value: Any) -> str | None: if isinstance(value, str): diff --git a/src/agents/run_internal/turn_resolution.py b/src/agents/run_internal/turn_resolution.py index e7c059c701..f98d28ecfd 100644 --- a/src/agents/run_internal/turn_resolution.py +++ b/src/agents/run_internal/turn_resolution.py @@ -1890,6 +1890,10 @@ async def get_single_step_result_from_response( tool_use_tracker.record_processed_response(item_agent, processed_response) + # Expose the conversation history so tools can inspect prior items via + # ToolContext.conversation_history (addresses #904). + context_wrapper._generated_items = list(pre_step_items) + if event_queue is not None and processed_response.new_items: handoff_items = [ item for item in processed_response.new_items if isinstance(item, HandoffCallItem) diff --git a/src/agents/tool_context.py b/src/agents/tool_context.py index 7ee140e8a9..2df910ae73 100644 --- a/src/agents/tool_context.py +++ b/src/agents/tool_context.py @@ -72,6 +72,7 @@ def __init__( turn_input: list[TResponseInputItem] | None = None, _approvals: dict[str, _ApprovalRecord] | None = None, tool_input: Any | None = None, + _generated_items: list[Any] | None = None, ) -> None: """Preserve the v0.7 positional constructor while accepting new context fields.""" resolved_usage = Usage() if usage is _MISSING else cast(Usage, usage) @@ -81,6 +82,7 @@ def __init__( turn_input=list(turn_input or []), _approvals={} if _approvals is None else _approvals, tool_input=tool_input, + _generated_items=list(_generated_items or []), ) self.tool_name = ( _assert_must_pass_tool_name() if tool_name is _MISSING else cast(str, tool_name) @@ -104,6 +106,16 @@ def __init__( self.agent = agent self.run_config = run_config + @property + def conversation_history(self) -> list[Any]: + """The items generated so far in the current agent run. + + This is a snapshot of the conversation history at the time the tool was invoked, + allowing tools to inspect prior messages, tool calls, and other items produced + during the run. Returns a copy so that mutations do not affect the run state. + """ + return list(self._generated_items) + @property def qualified_tool_name(self) -> str: """Return the tool name qualified by namespace when available.""" diff --git a/tests/test_tool_context.py b/tests/test_tool_context.py index a4579e8fb4..de5e1509bb 100644 --- a/tests/test_tool_context.py +++ b/tests/test_tool_context.py @@ -353,3 +353,66 @@ async def on_invoke_tool( assert captured_context is not None assert not isinstance(captured_context, ToolContext) assert captured_context.tool_input == {"city": "Tokyo"} + + +def test_conversation_history_defaults_to_empty() -> None: + """conversation_history is empty when no generated items are set.""" + tool_ctx: ToolContext[None] = ToolContext( + context=None, + tool_name="tool", + tool_call_id="call-1", + tool_arguments="{}", + ) + assert tool_ctx.conversation_history == [] + + +def test_conversation_history_populated_via_wrapper() -> None: + """conversation_history is populated from RunContextWrapper._generated_items.""" + wrapper: RunContextWrapper[None] = RunContextWrapper(context=None) + wrapper._generated_items = ["item_a", "item_b"] + + tool_call = ResponseFunctionToolCall( + type="function_call", + name="my_tool", + call_id="call-10", + arguments="{}", + ) + tool_ctx = ToolContext.from_agent_context( + wrapper, + tool_call_id="call-10", + tool_call=tool_call, + ) + assert tool_ctx.conversation_history == ["item_a", "item_b"] + + +def test_conversation_history_returns_copy() -> None: + """Mutating the returned list must not affect the underlying state.""" + wrapper: RunContextWrapper[None] = RunContextWrapper(context=None) + wrapper._generated_items = ["item_a"] + + tool_call = ResponseFunctionToolCall( + type="function_call", + name="my_tool", + call_id="call-11", + arguments="{}", + ) + tool_ctx = ToolContext.from_agent_context( + wrapper, + tool_call_id="call-11", + tool_call=tool_call, + ) + history = tool_ctx.conversation_history + history.append("extra") + assert tool_ctx.conversation_history == ["item_a"] + + +def test_conversation_history_via_constructor() -> None: + """conversation_history can be set via _generated_items kwarg.""" + tool_ctx: ToolContext[None] = ToolContext( + context=None, + tool_name="tool", + tool_call_id="call-12", + tool_arguments="{}", + _generated_items=["x", "y"], + ) + assert tool_ctx.conversation_history == ["x", "y"]