Skip to content

Commit 7c1d78d

Browse files
committed
feat: add conversation history to ToolContext
1 parent 8db2ed2 commit 7c1d78d

15 files changed

+426
-2
lines changed

docs/context.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ plus additional fields specific to the current tool call:
126126
- `tool_arguments` – the raw argument string passed to the tool
127127
- `tool_namespace` – the Responses namespace for the tool call, when the tool was loaded through `tool_namespace()` or another namespaced surface
128128
- `qualified_tool_name` – the tool name qualified with the namespace when one is available
129+
- `conversation_history` – a visible history snapshot available to the tool at invocation time. For local function tools in non-streaming runs, this includes the current input plus prior visible run items that can be represented as model input.
129130

130131
Use `ToolContext` when you need tool-level metadata during execution.
131132
For general context sharing between agents and tools, `RunContextWrapper` remains sufficient. Because `ToolContext` extends `RunContextWrapper`, it can also expose `.tool_input` when a nested `Agent.as_tool()` run supplied structured input.

src/agents/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ async def _run_agent_impl(context: ToolContext, input_json: str) -> Any:
602602
tool_namespace=context.tool_namespace,
603603
agent=context.agent,
604604
run_config=resolved_run_config,
605+
conversation_history=context.conversation_history,
605606
)
606607
set_agent_tool_state_scope(nested_context, tool_state_scope_id)
607608
if should_capture_tool_input:

src/agents/result.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def _populate_state_from_result(
9999
state._reasoning_item_id_policy = getattr(result, "_reasoning_item_id_policy", None)
100100

101101
interruptions = list(getattr(result, "interruptions", []))
102+
if interruptions:
103+
state._interrupted_turn_input = copy.deepcopy(result.context_wrapper.turn_input)
104+
else:
105+
state._interrupted_turn_input = None
102106
if interruptions:
103107
state._current_step = NextStepInterruption(interruptions=interruptions)
104108

src/agents/run_internal/agent_runner_helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import copy
56
from typing import Any, cast
67

78
from ..agent import Agent
@@ -311,6 +312,9 @@ def update_run_state_for_interruption(
311312
run_state._session_items = list(session_items)
312313
run_state._current_step = next_step
313314
run_state._current_turn = current_turn
315+
run_state._interrupted_turn_input = (
316+
copy.deepcopy(run_state._context.turn_input) if run_state._context is not None else None
317+
)
314318

315319

316320
async def save_turn_items_if_needed(

src/agents/run_internal/run_loop.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,7 @@ async def get_new_response(
15871587
)
15881588
if isinstance(filtered.input, list):
15891589
filtered.input = deduplicate_input_items_preferring_latest(filtered.input)
1590+
context_wrapper.turn_input = list(filtered.input)
15901591

15911592
model = get_model(agent, run_config)
15921593
model_settings = agent.model_settings.resolve(run_config.model_settings)

src/agents/run_internal/tool_execution.py

Lines changed: 8 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,17 @@ 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 = (
1297+
list(conversation_history) if conversation_history is not None else None
1298+
)
12941299
self.isolate_parallel_failures = (
12951300
len(tool_runs) > 1 if isolate_parallel_failures is None else isolate_parallel_failures
12961301
)
@@ -1465,6 +1470,7 @@ async def _run_single_tool(
14651470
tool_namespace=tool_context_namespace,
14661471
agent=self.agent,
14671472
run_config=self.config,
1473+
conversation_history=self.conversation_history,
14681474
)
14691475
agent_hooks = self.agent.hooks
14701476
if self.config.trace_include_sensitive_data:
@@ -1797,6 +1803,7 @@ async def execute_function_tool_calls(
17971803
hooks: RunHooks[Any],
17981804
context_wrapper: RunContextWrapper[Any],
17991805
config: RunConfig,
1806+
conversation_history: list[TResponseInputItem] | None = None,
18001807
isolate_parallel_failures: bool | None = None,
18011808
) -> tuple[
18021809
list[FunctionToolResult], list[ToolInputGuardrailResult], list[ToolOutputGuardrailResult]
@@ -1808,6 +1815,7 @@ async def execute_function_tool_calls(
18081815
hooks=hooks,
18091816
context_wrapper=context_wrapper,
18101817
config=config,
1818+
conversation_history=conversation_history,
18111819
isolate_parallel_failures=isolate_parallel_failures,
18121820
).execute()
18131821

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import copy
45
import inspect
56
from collections.abc import Awaitable, Callable, Mapping, Sequence
67
from typing import Any, Literal, cast
@@ -87,7 +88,10 @@
8788
from .items import (
8889
REJECTION_MESSAGE,
8990
apply_patch_rejection_item,
91+
deduplicate_input_items_preferring_latest,
9092
function_rejection_item,
93+
prepare_model_input_items,
94+
run_items_to_input_items,
9195
shell_rejection_item,
9296
)
9397
from .run_steps import (
@@ -139,6 +143,7 @@
139143
_make_unique_item_appender,
140144
_select_function_tool_runs_for_resume,
141145
)
146+
from .turn_preparation import maybe_filter_model_input
142147

143148
__all__ = [
144149
"execute_final_output_step",
@@ -153,6 +158,13 @@
153158
]
154159

155160

161+
def _build_function_tool_conversation_history(
162+
turn_input: Sequence[TResponseInputItem],
163+
) -> list[TResponseInputItem]:
164+
"""Build the visible history snapshot for a local function tool invocation."""
165+
return list(turn_input)
166+
167+
156168
async def _maybe_finalize_from_tool_results(
157169
*,
158170
agent: Agent[TContext],
@@ -528,6 +540,8 @@ async def execute_tools_and_side_effects(
528540
new_items=processed_response.new_items,
529541
)
530542

543+
conversation_history = _build_function_tool_conversation_history(context_wrapper.turn_input)
544+
531545
(
532546
function_results,
533547
tool_input_guardrail_results,
@@ -542,6 +556,7 @@ async def execute_tools_and_side_effects(
542556
hooks=hooks,
543557
context_wrapper=context_wrapper,
544558
run_config=run_config,
559+
conversation_history=conversation_history,
545560
)
546561
new_step_items.extend(
547562
_build_tool_result_items(
@@ -1103,6 +1118,33 @@ def _add_unmatched_pending(approval: ToolApprovalItem) -> None:
11031118
apply_patch_calls=approved_apply_patch_calls,
11041119
)
11051120

1121+
resolved_reasoning_item_id_policy = (
1122+
run_config.reasoning_item_id_policy
1123+
if run_config.reasoning_item_id_policy is not None
1124+
else (run_state._reasoning_item_id_policy if run_state is not None else None)
1125+
)
1126+
if run_state is not None and isinstance(run_state._interrupted_turn_input, list):
1127+
context_wrapper.turn_input = copy.deepcopy(run_state._interrupted_turn_input)
1128+
else:
1129+
reconstructed_turn_input = prepare_model_input_items(
1130+
ItemHelpers.input_to_new_input_list(original_input),
1131+
run_items_to_input_items(original_pre_step_items, resolved_reasoning_item_id_policy),
1132+
)
1133+
system_prompt = await agent.get_system_prompt(context_wrapper)
1134+
filtered_model_input = await maybe_filter_model_input(
1135+
agent=agent,
1136+
run_config=run_config,
1137+
context_wrapper=context_wrapper,
1138+
input_items=reconstructed_turn_input,
1139+
system_instructions=system_prompt,
1140+
)
1141+
if isinstance(filtered_model_input.input, list):
1142+
filtered_model_input.input = deduplicate_input_items_preferring_latest(
1143+
filtered_model_input.input
1144+
)
1145+
context_wrapper.turn_input = list(filtered_model_input.input)
1146+
conversation_history = _build_function_tool_conversation_history(context_wrapper.turn_input)
1147+
11061148
(
11071149
function_results,
11081150
tool_input_guardrail_results,
@@ -1117,6 +1159,7 @@ def _add_unmatched_pending(approval: ToolApprovalItem) -> None:
11171159
hooks=hooks,
11181160
context_wrapper=context_wrapper,
11191161
run_config=run_config,
1162+
conversation_history=conversation_history,
11201163
)
11211164

11221165
for interruption in _collect_tool_interruptions(

src/agents/run_state.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@
118118
# 3. to_json() always emits CURRENT_SCHEMA_VERSION.
119119
# 4. Forward compatibility is intentionally fail-fast (older SDKs reject newer or unsupported
120120
# versions).
121-
CURRENT_SCHEMA_VERSION = "1.6"
121+
CURRENT_SCHEMA_VERSION = "1.7"
122122
SUPPORTED_SCHEMA_VERSIONS = frozenset(
123-
{"1.0", "1.1", "1.2", "1.3", "1.4", "1.5", CURRENT_SCHEMA_VERSION}
123+
{"1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", CURRENT_SCHEMA_VERSION}
124124
)
125125

126126
_FUNCTION_OUTPUT_ADAPTER: TypeAdapter[FunctionCallOutput] = TypeAdapter(FunctionCallOutput)
@@ -187,6 +187,9 @@ class RunState(Generic[TContext, TAgent]):
187187
_reasoning_item_id_policy: Literal["preserve", "omit"] | None = None
188188
"""How reasoning item IDs are represented in next-turn model input."""
189189

190+
_interrupted_turn_input: list[TResponseInputItem] | None = None
191+
"""Filtered turn input snapshot for the currently interrupted turn, if any."""
192+
190193
_input_guardrail_results: list[InputGuardrailResult] = field(default_factory=list)
191194
"""Results from input guardrails applied to the run."""
192195

@@ -240,6 +243,7 @@ def __init__(
240243
self._previous_response_id = previous_response_id
241244
self._auto_previous_response_id = auto_previous_response_id
242245
self._reasoning_item_id_policy = None
246+
self._interrupted_turn_input = None
243247
self._model_responses = []
244248
self._generated_items = []
245249
self._session_items = []
@@ -657,6 +661,7 @@ def to_json(
657661
"previous_response_id": self._previous_response_id,
658662
"auto_previous_response_id": self._auto_previous_response_id,
659663
"reasoning_item_id_policy": self._reasoning_item_id_policy,
664+
"interrupted_turn_input": copy.deepcopy(self._interrupted_turn_input),
660665
}
661666

662667
generated_items = self._merge_generated_items_with_processed()
@@ -2288,6 +2293,11 @@ async def _build_run_state_from_json(
22882293
state._reasoning_item_id_policy = cast(Literal["preserve", "omit"], serialized_policy)
22892294
else:
22902295
state._reasoning_item_id_policy = None
2296+
serialized_interrupted_turn_input = state_json.get("interrupted_turn_input")
2297+
if isinstance(serialized_interrupted_turn_input, list):
2298+
state._interrupted_turn_input = copy.deepcopy(serialized_interrupted_turn_input)
2299+
else:
2300+
state._interrupted_turn_input = None
22912301
state.set_tool_use_tracker_snapshot(state_json.get("tool_use_tracker", {}))
22922302
trace_data = state_json.get("trace")
22932303
if isinstance(trace_data, Mapping):

src/agents/tool_context.py

Lines changed: 10 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.
@@ -137,6 +143,9 @@ def from_agent_context(
137143
tool_run_config = run_config
138144
if tool_run_config is None and isinstance(context, ToolContext):
139145
tool_run_config = context.run_config
146+
tool_conversation_history = conversation_history
147+
if tool_conversation_history is None and isinstance(context, ToolContext):
148+
tool_conversation_history = context.conversation_history
140149

141150
tool_context = cls(
142151
tool_name=tool_name,
@@ -155,6 +164,7 @@ def from_agent_context(
155164
),
156165
agent=tool_agent,
157166
run_config=tool_run_config,
167+
conversation_history=tool_conversation_history,
158168
**base_values,
159169
)
160170
set_agent_tool_state_scope(tool_context, get_agent_tool_state_scope(context))

0 commit comments

Comments
 (0)