Skip to content

Commit bd2a003

Browse files
committed
feat: add conversation history to ToolContext
1 parent 34ff848 commit bd2a003

File tree

6 files changed

+132
-1
lines changed

6 files changed

+132
-1
lines changed

src/agents/run_internal/tool_execution.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
RunItemBase,
5656
ToolApprovalItem,
5757
ToolCallOutputItem,
58+
TResponseInputItem,
5859
)
5960
from ..logger import logger
6061
from ..model_settings import ModelSettings
@@ -1284,13 +1285,15 @@ def __init__(
12841285
hooks: RunHooks[Any],
12851286
context_wrapper: RunContextWrapper[Any],
12861287
config: RunConfig,
1288+
conversation_history: list[TResponseInputItem] | None,
12871289
isolate_parallel_failures: bool | None,
12881290
) -> None:
12891291
self.agent = agent
12901292
self.tool_runs = tool_runs
12911293
self.hooks = hooks
12921294
self.context_wrapper = context_wrapper
12931295
self.config = config
1296+
self.conversation_history = list(conversation_history or [])
12941297
self.isolate_parallel_failures = (
12951298
len(tool_runs) > 1 if isolate_parallel_failures is None else isolate_parallel_failures
12961299
)
@@ -1465,6 +1468,7 @@ async def _run_single_tool(
14651468
tool_namespace=tool_context_namespace,
14661469
agent=self.agent,
14671470
run_config=self.config,
1471+
conversation_history=self.conversation_history,
14681472
)
14691473
agent_hooks = self.agent.hooks
14701474
if self.config.trace_include_sensitive_data:
@@ -1797,6 +1801,7 @@ async def execute_function_tool_calls(
17971801
hooks: RunHooks[Any],
17981802
context_wrapper: RunContextWrapper[Any],
17991803
config: RunConfig,
1804+
conversation_history: list[TResponseInputItem] | None = None,
18001805
isolate_parallel_failures: bool | None = None,
18011806
) -> tuple[
18021807
list[FunctionToolResult], list[ToolInputGuardrailResult], list[ToolOutputGuardrailResult]
@@ -1808,6 +1813,7 @@ async def execute_function_tool_calls(
18081813
hooks=hooks,
18091814
context_wrapper=context_wrapper,
18101815
config=config,
1816+
conversation_history=conversation_history,
18111817
isolate_parallel_failures=isolate_parallel_failures,
18121818
).execute()
18131819

src/agents/run_internal/tool_planning.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ToolApprovalItem,
2121
ToolCallItem,
2222
ToolCallOutputItem,
23+
TResponseInputItem,
2324
)
2425
from ..run_context import RunContextWrapper
2526
from ..tool import FunctionTool, MCPToolApprovalRequest
@@ -522,6 +523,7 @@ async def _execute_tool_plan(
522523
hooks,
523524
context_wrapper: RunContextWrapper[Any],
524525
run_config,
526+
conversation_history: list[TResponseInputItem] | None = None,
525527
parallel: bool = True,
526528
) -> tuple[
527529
list[Any],
@@ -556,6 +558,7 @@ async def _execute_tool_plan(
556558
hooks=hooks,
557559
context_wrapper=context_wrapper,
558560
config=run_config,
561+
conversation_history=conversation_history,
559562
isolate_parallel_failures=isolate_function_tool_failures,
560563
),
561564
execute_computer_actions(
@@ -598,6 +601,7 @@ async def _execute_tool_plan(
598601
hooks=hooks,
599602
context_wrapper=context_wrapper,
600603
config=run_config,
604+
conversation_history=conversation_history,
601605
isolate_parallel_failures=isolate_function_tool_failures,
602606
)
603607
computer_results = await execute_computer_actions(

src/agents/run_internal/turn_resolution.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from ..agent import Agent, ToolsToFinalOutputResult
4343
from ..agent_output import AgentOutputSchemaBase
4444
from ..agent_tool_state import get_agent_tool_state_scope, peek_agent_tool_run_result
45-
from ..exceptions import ModelBehaviorError, UserError
45+
from ..exceptions import AgentsException, ModelBehaviorError, UserError
4646
from ..handoffs import Handoff, HandoffInputData, nest_handoff_history
4747
from ..items import (
4848
CompactionItem,
@@ -153,6 +153,24 @@
153153
]
154154

155155

156+
def _build_function_tool_conversation_history(
157+
original_input: str | list[TResponseInputItem],
158+
pre_step_items: Sequence[RunItem],
159+
) -> list[TResponseInputItem]:
160+
"""Build the visible history snapshot for a local function tool invocation.
161+
162+
This intentionally includes only items that can be represented as model input.
163+
Internal bookkeeping items such as approval placeholders are skipped.
164+
"""
165+
history = ItemHelpers.input_to_new_input_list(original_input)
166+
for item in pre_step_items:
167+
try:
168+
history.append(item.to_input_item())
169+
except AgentsException:
170+
continue
171+
return history
172+
173+
156174
async def _maybe_finalize_from_tool_results(
157175
*,
158176
agent: Agent[TContext],
@@ -528,6 +546,11 @@ async def execute_tools_and_side_effects(
528546
new_items=processed_response.new_items,
529547
)
530548

549+
conversation_history = _build_function_tool_conversation_history(
550+
original_input,
551+
pre_step_items,
552+
)
553+
531554
(
532555
function_results,
533556
tool_input_guardrail_results,
@@ -542,6 +565,7 @@ async def execute_tools_and_side_effects(
542565
hooks=hooks,
543566
context_wrapper=context_wrapper,
544567
run_config=run_config,
568+
conversation_history=conversation_history,
545569
)
546570
new_step_items.extend(
547571
_build_tool_result_items(
@@ -1103,6 +1127,11 @@ def _add_unmatched_pending(approval: ToolApprovalItem) -> None:
11031127
apply_patch_calls=approved_apply_patch_calls,
11041128
)
11051129

1130+
conversation_history = _build_function_tool_conversation_history(
1131+
original_input,
1132+
original_pre_step_items,
1133+
)
1134+
11061135
(
11071136
function_results,
11081137
tool_input_guardrail_results,
@@ -1117,6 +1146,7 @@ def _add_unmatched_pending(approval: ToolApprovalItem) -> None:
11171146
hooks=hooks,
11181147
context_wrapper=context_wrapper,
11191148
run_config=run_config,
1149+
conversation_history=conversation_history,
11201150
)
11211151

11221152
for interruption in _collect_tool_interruptions(

src/agents/tool_context.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class ToolContext(RunContextWrapper[TContext]):
5757
run_config: RunConfig | None = None
5858
"""The active run config for this tool call, when available."""
5959

60+
conversation_history: list[TResponseInputItem] = field(default_factory=list)
61+
"""Visible conversation history snapshot available when this tool is invoked."""
62+
6063
def __init__(
6164
self,
6265
context: TContext,
@@ -69,6 +72,7 @@ def __init__(
6972
tool_namespace: str | None = None,
7073
agent: AgentBase[Any] | None = None,
7174
run_config: RunConfig | None = None,
75+
conversation_history: list[TResponseInputItem] | None = None,
7276
turn_input: list[TResponseInputItem] | None = None,
7377
_approvals: dict[str, _ApprovalRecord] | None = None,
7478
tool_input: Any | None = None,
@@ -103,6 +107,7 @@ def __init__(
103107
)
104108
self.agent = agent
105109
self.run_config = run_config
110+
self.conversation_history = list(conversation_history or [])
106111

107112
@property
108113
def qualified_tool_name(self) -> str:
@@ -119,6 +124,7 @@ def from_agent_context(
119124
*,
120125
tool_namespace: str | None = None,
121126
run_config: RunConfig | None = None,
127+
conversation_history: list[TResponseInputItem] | None = None,
122128
) -> ToolContext:
123129
"""
124130
Create a ToolContext from a RunContextWrapper.
@@ -155,6 +161,7 @@ def from_agent_context(
155161
),
156162
agent=tool_agent,
157163
run_config=tool_run_config,
164+
conversation_history=conversation_history,
158165
**base_values,
159166
)
160167
set_agent_tool_state_scope(tool_context, get_agent_tool_state_scope(context))

tests/test_agent_runner.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,68 @@ def foo(context: ToolContext[Any]) -> str:
813813
assert captured_contexts[0].agent is agent
814814

815815

816+
@pytest.mark.asyncio
817+
async def test_tool_call_context_includes_conversation_history_snapshot() -> None:
818+
model = FakeModel()
819+
captured_contexts: list[ToolContext[Any]] = []
820+
821+
@function_tool(name_override="foo")
822+
def foo(context: ToolContext[Any]) -> str:
823+
captured_contexts.append(context)
824+
return "tool_result"
825+
826+
agent = Agent(
827+
name="test",
828+
model=model,
829+
tools=[foo],
830+
)
831+
832+
model.add_multiple_turn_outputs(
833+
[
834+
[get_function_tool_call("foo", "{}")],
835+
[get_text_message("done")],
836+
]
837+
)
838+
839+
result = await Runner.run(agent, input="user_message")
840+
841+
assert result.final_output == "done"
842+
assert len(captured_contexts) == 1
843+
assert captured_contexts[0].conversation_history == [get_text_input_item("user_message")]
844+
845+
846+
@pytest.mark.asyncio
847+
async def test_tool_call_context_conversation_history_includes_prior_session_turns() -> None:
848+
model = FakeModel()
849+
captured_contexts: list[ToolContext[Any]] = []
850+
851+
@function_tool(name_override="foo")
852+
def foo(context: ToolContext[Any]) -> str:
853+
captured_contexts.append(context)
854+
return "tool_result"
855+
856+
agent = Agent(name="test", model=model, tools=[foo])
857+
session = SimpleListSession()
858+
859+
model.add_multiple_turn_outputs(
860+
[
861+
[get_text_message("first_done")],
862+
[get_function_tool_call("foo", "{}")],
863+
[get_text_message("second_done")],
864+
]
865+
)
866+
867+
first_result = await Runner.run(agent, input="first_user", session=session)
868+
second_result = await Runner.run(agent, input="second_user", session=session)
869+
870+
assert first_result.final_output == "first_done"
871+
assert second_result.final_output == "second_done"
872+
assert len(captured_contexts) == 1
873+
history = captured_contexts[0].conversation_history
874+
assert any(isinstance(item, dict) and item.get("content") == "first_user" for item in history)
875+
assert any(isinstance(item, dict) and item.get("content") == "second_user" for item in history)
876+
877+
816878
@pytest.mark.asyncio
817879
async def test_handoffs():
818880
model = FakeModel()

tests/test_tool_context.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from openai.types.responses import ResponseFunctionToolCall
55

66
from agents import Agent
7+
from agents.items import TResponseInputItem
78
from agents.run_config import RunConfig
89
from agents.run_context import RunContextWrapper
910
from agents.tool import FunctionTool, invoke_function_tool
@@ -51,6 +52,27 @@ def test_tool_context_from_agent_context_populates_fields() -> None:
5152
assert tool_ctx.agent is agent
5253

5354

55+
def test_tool_context_from_agent_context_copies_conversation_history() -> None:
56+
tool_call = ResponseFunctionToolCall(
57+
type="function_call",
58+
name="test_tool",
59+
call_id="call-history",
60+
arguments="{}",
61+
)
62+
ctx = make_context_wrapper()
63+
history: list[TResponseInputItem] = [{"role": "user", "content": "hello"}]
64+
65+
tool_ctx = ToolContext.from_agent_context(
66+
ctx,
67+
tool_call_id="call-history",
68+
tool_call=tool_call,
69+
conversation_history=history,
70+
)
71+
72+
assert tool_ctx.conversation_history == history
73+
assert tool_ctx.conversation_history is not history
74+
75+
5476
def test_tool_context_agent_none_by_default() -> None:
5577
tool_call = ResponseFunctionToolCall(
5678
type="function_call",

0 commit comments

Comments
 (0)