Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
resolve_computer,
tool_namespace,
)
from .tool_context import ToolContext
from .tool_guardrails import (
ToolGuardrailFunctionOutput,
ToolInputGuardrail,
Expand Down Expand Up @@ -413,6 +414,7 @@ def enable_verbose_stdout_logging():
"CompactionItem",
"AgentHookContext",
"RunContextWrapper",
"ToolContext",
"TContext",
"RunErrorDetails",
"RunErrorData",
Expand Down
3 changes: 3 additions & 0 deletions src/agents/run_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions src/agents/run_internal/turn_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set conversation history for resumed interruption turns

This history assignment only runs in get_single_step_result_from_response, but resumed approval flows execute tools through resolve_interrupted_turn and can use a rebuilt/overridden RunContextWrapper (for example after deserializing RunState) where _generated_items is empty. In that path, ToolContext.from_agent_context copies stale history, so tools resumed after an interruption see missing prior conversation items; initialize _generated_items in the resume path before tool execution as well.

Useful? React with 👍 / 👎.


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)
Expand Down
12 changes: 12 additions & 0 deletions src/agents/tool_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent item-level mutation through conversation_history

Returning list(self._generated_items) only copies the outer list, so each RunItem object is still shared with run state; tool code can mutate conversation_history entries (for example raw_item dict/model fields) and unintentionally alter subsequent model input/persistence. That contradicts the read-only guarantee in this property docstring and can corrupt run behavior; expose immutable/deep-copied entries instead.

Useful? React with 👍 / 👎.


@property
def qualified_tool_name(self) -> str:
"""Return the tool name qualified by namespace when available."""
Expand Down
63 changes: 63 additions & 0 deletions tests/test_tool_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]