Skip to content

Commit 01d5c10

Browse files
Disambiguate tool_call vs tool_result item_id fallback
cursor[bot] 3151054134 (Low): _map_tool_call and _map_tool_output both used ``item_id = str(getattr(raw, "id", "") or call_id)``. When the SDK's raw item lacks an ``id``, both fall back to the same ``call_id``, so the two AgentContextItems collide on item_id. AgentContext._index is keyed by item_id, so the second append() silently overwrites the first — get_context_item then returns the wrong item even though both still live in self.items. Namespace the fallback: tool-call-{call_id} for the assistant call and tool-result-{call_id} for the tool result. Items with intact raw.id are unaffected. Includes a regression test that constructs a tool_call/tool_output pair without raw.id and asserts distinct item_ids; verified the test fails when the fix is reverted.
1 parent b60f620 commit 01d5c10

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

engine/engine/agents/openai_event_mapper.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,11 @@ def _map_tool_call(self, item: Any, *, execution: AgentExecution) -> MappedEvent
152152
arguments=str(getattr(raw, "arguments", "") or ""),
153153
),
154154
)
155-
item_id = str(getattr(raw, "id", "") or call_id)
155+
# Namespace the fallback so a tool_call and its tool_output don't
156+
# collide on the same call_id when the SDK doesn't populate raw.id
157+
# — AgentContext._index keys by item_id and the second append would
158+
# silently overwrite the first, breaking get_context_item.
159+
item_id = str(getattr(raw, "id", "") or f"tool-call-{call_id}")
156160
context_item = AgentContextItem(
157161
item_id=item_id,
158162
role="assistant",
@@ -181,7 +185,9 @@ def _map_tool_output(self, item: Any, *, execution: AgentExecution) -> MappedEve
181185
output = getattr(raw, "output", "")
182186
content = str(output)
183187
name = getattr(raw, "name", None) or getattr(item, "name", None)
184-
item_id = str(getattr(raw, "id", "") or call_id)
188+
# See ``_map_tool_call``: namespaced fallback prevents the tool_call
189+
# and tool_output for the same call_id from sharing an item_id.
190+
item_id = str(getattr(raw, "id", "") or f"tool-result-{call_id}")
185191
context_item = AgentContextItem(
186192
item_id=item_id,
187193
role="tool",

engine/tests/unit/agents/test_openai_event_mapper.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,36 @@ def test_raw_text_delta_produces_delta_only() -> None:
138138
assert mapped.output_item is None
139139
assert mapped.delta is not None
140140
assert mapped.delta.text_delta == "par"
141+
142+
143+
def test_tool_call_and_output_have_distinct_item_ids_when_raw_id_missing() -> None:
144+
"""Regression: when the SDK's raw item lacks an ``id``, both the
145+
tool_call and tool_output events used to fall back to the same
146+
``call_id`` for ``item_id``, so ``AgentContext._index`` overwrote the
147+
first entry and ``get_context_item`` returned the wrong item.
148+
"""
149+
mapper = OpenAiEventMapper()
150+
call_event = _wrap_item(
151+
SimpleNamespace(
152+
type="tool_call_item",
153+
raw_item=SimpleNamespace(
154+
# No ``id`` attribute — exercises the fallback.
155+
call_id="call_xyz",
156+
name="query_traces",
157+
arguments="{}",
158+
),
159+
)
160+
)
161+
output_event = _wrap_item(
162+
SimpleNamespace(
163+
type="tool_call_output_item",
164+
raw_item=SimpleNamespace(call_id="call_xyz", name="query_traces"),
165+
output="ok",
166+
)
167+
)
168+
169+
call_mapped = mapper.to_mapped_event(call_event, execution=_exec(), is_root=True)
170+
output_mapped = mapper.to_mapped_event(output_event, execution=_exec(), is_root=True)
171+
assert call_mapped.context_item is not None
172+
assert output_mapped.context_item is not None
173+
assert call_mapped.context_item.item_id != output_mapped.context_item.item_id

0 commit comments

Comments
 (0)