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"]