Skip to content

Commit f69cd41

Browse files
committed
fix(converter): simplify full-history trim to last message and add edge case tests
1 parent d1c57e7 commit f69cd41

3 files changed

Lines changed: 58 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12-
- Agent and workflow output conversion now removes only confirmed repeated input
13-
history, preserves multiple terminal assistant messages, and reports the
14-
standard `tool_call` finish reason when an output requests a tool and no
15-
source finish reason is available.
12+
- Agent and workflow output conversion now removes confirmed repeated input
13+
history and keeps only the last message as the terminal output; intermediate
14+
tool-call and tool-response messages are no longer included in
15+
`gen_ai.output.messages` for full-history spans. The `tool_call` finish reason
16+
is inferred when an output requests a tool and no source finish reason is
17+
available.
1618

1719
## [0.2.1] - 2026-08-07
1820

src/splunk_ao/converter/attribute_mapping.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,7 @@ def _set_orchestration_content(attrs: MutableMapping[str, AttributeValue], span:
451451
if output_messages is None:
452452
return
453453
if full_history and input_messages and output_messages[: len(input_messages)] == input_messages:
454-
output_messages = output_messages[len(input_messages) :]
455-
456-
terminal_start = len(output_messages)
457-
while terminal_start > 0 and output_messages[terminal_start - 1].get("role") == "assistant":
458-
terminal_start -= 1
459-
output_messages = output_messages[terminal_start:]
454+
output_messages = output_messages[len(input_messages) :][-1:]
460455
attrs["gen_ai.output.messages"] = _json_string(_with_finish_reasons(output_messages))
461456

462457

tests/test_attribute_mapping.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,9 @@ def test_orchestration_output_omits_repeated_input_history() -> None:
419419

420420

421421
@pytest.mark.parametrize("span_type", [WorkflowSpan, AgentSpan])
422-
def test_orchestration_full_history_preserves_all_terminal_assistant_messages(
422+
def test_orchestration_full_history_keeps_last_terminal_message(
423423
span_type: type[WorkflowSpan] | type[AgentSpan],
424424
) -> None:
425-
# Given: a full-history result with two terminal assistant outputs after the exact input history.
426425
user = {"role": "user", "content": "Give me two alternatives"}
427426
first = {"role": "assistant", "content": "First alternative"}
428427
second = {"role": "assistant", "content": "Second alternative"}
@@ -434,13 +433,10 @@ def test_orchestration_full_history_preserves_all_terminal_assistant_messages(
434433
if span_type is AgentSpan:
435434
span_kwargs["agent_type"] = AgentType.planner
436435

437-
# When: the orchestration content is converted.
438436
attrs = build_span_attributes(span_type(**span_kwargs))
439437

440-
# Then: the repeated input prefix is removed without reducing the terminal outputs to one message.
441438
assert json.loads(attrs["gen_ai.output.messages"]) == [
442-
_text_message("assistant", "First alternative", finish_reason="unknown"),
443-
_text_message("assistant", "Second alternative", finish_reason="unknown"),
439+
_text_message("assistant", "Second alternative", finish_reason="unknown")
444440
]
445441

446442

@@ -640,6 +636,55 @@ def test_orchestration_message_container_without_input_prefix_match_not_reduced(
640636
assert output_messages[1]["parts"][0]["response"] == "Amlodipine: 5 mg daily"
641637

642638

639+
def test_orchestration_full_history_ends_on_tool_message_keeps_last() -> None:
640+
# return_direct=True tool: run ends on a tool response, no final assistant message.
641+
# The dedup gate fires (prefix matches) but the last message is a tool, not assistant.
642+
# Must return the tool message rather than an empty list.
643+
user = {"role": "user", "content": "Get patient P001"}
644+
ai_toolcall = {
645+
"role": "assistant",
646+
"content": "",
647+
"tool_calls": [{"id": "tc1", "function": {"name": "get_patient", "arguments": '{"id":"P001"}'}}],
648+
}
649+
tool_resp = {"role": "tool", "content": "George Rivera, Lisinopril 10mg", "tool_call_id": "tc1"}
650+
span = AgentSpan(
651+
name="Agent",
652+
agent_type=AgentType.default,
653+
input=json.dumps({"messages": [user, ai_toolcall]}),
654+
output=json.dumps({"messages": [user, ai_toolcall, tool_resp]}),
655+
)
656+
657+
attrs = build_span_attributes(span)
658+
659+
output_messages = json.loads(attrs["gen_ai.output.messages"])
660+
assert len(output_messages) == 1
661+
assert output_messages[0]["parts"][0]["response"] == "George Rivera, Lisinopril 10mg"
662+
663+
664+
def test_orchestration_full_history_ends_on_tool_call_ai_message_keeps_last() -> None:
665+
# interrupt_before=["tools"]: run ends on a tool-call AIMessage with empty content.
666+
# The last message is assistant role but content="" — must not return empty list.
667+
user = {"role": "user", "content": "Search for Lisinopril"}
668+
ai_toolcall = {
669+
"role": "assistant",
670+
"content": "",
671+
"tool_calls": [{"id": "tc1", "function": {"name": "search", "arguments": '{"query":"Lisinopril"}'}}],
672+
}
673+
span = AgentSpan(
674+
name="Agent",
675+
agent_type=AgentType.default,
676+
input=json.dumps({"messages": [user]}),
677+
output=json.dumps({"messages": [user, ai_toolcall]}),
678+
)
679+
680+
attrs = build_span_attributes(span)
681+
682+
output_messages = json.loads(attrs["gen_ai.output.messages"])
683+
assert len(output_messages) == 1
684+
assert output_messages[0]["role"] == "assistant"
685+
assert output_messages[0]["finish_reason"] == "tool_call"
686+
687+
643688
def test_orchestration_preserves_schema_valid_parts_and_tool_calls() -> None:
644689
span = WorkflowSpan(
645690
name="tool-workflow",

0 commit comments

Comments
 (0)