fix: include prior tool results in session_history for tool-level evaluators - #338
Conversation
|
@strandly-the-agent review this PR. Consolidate findings into a single comment. |
This comment was marked as outdated.
This comment was marked as outdated.
16d417f to
14ece34
Compare
14ece34 to
8f0abaa
Compare
|
@strandly-the-agent review this PR. Consolidate findings into a single comment. As for your questions, the limitations are already being tracked as part of: #336 |
This comment was marked as resolved.
This comment was marked as resolved.
|
@strandly-the-agent review this PR. Consolidate findings into a single comment. I am deferring the error-rendering change because it is unrelated to this one. |
This comment was marked as resolved.
This comment was marked as resolved.
|
@strandly-the-agent review this PR. Consolidate findings into a single comment. For the questions:
|
|
Round 4 on Verified fixed this round:
One new 🟡 below — a one-line clamp I'd land before merge if it's cheap, otherwise worth tracking, because it's the same bug class as #337. The tie-break rule was my suggestion, so I had an independent pass attack it specifically; that's how this surfaced. Reviewed by 🟡 An inverted span (
|
|
@strandly-the-agent review this PR. Consolidate findings into a single comment. |
|
Round 5 on Independently verified at this head — five fresh-context passes (adversarial
I'm 🟡 Docstring: the executed counterexample, and the fixSpan So the comparison is against a derived value, not the raw field — and it's the candidate's value against the target's """Input for tool-level evaluators.
`session_history` never includes the call under evaluation. Within the same trace, a
tool execution is included only if `max(end_time, start_time)` is at or before this tool's
`start_time` (the max guards against spans with a negative duration), with list position
as tiebreaker for equal timestamps. Cross-trace history is unfiltered.
"""🟡 Two tests that close the gap (verified: pass at head, each kills its mutants)Both follow the file's existing convention of a local def test_extract_tool_level_clamps_inverted_end_time():
"""A tool with a corrupted (earlier-than-start) end_time is not treated as completing early."""
from datetime import timedelta, timezone
base = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
agent_span = AgentInvocationSpan(
span_info=SpanInfo(session_id="test", span_id="a0", start_time=base, end_time=base + timedelta(seconds=20)),
user_prompt="x",
agent_response="y",
available_tools=[ToolConfig(name="broken_tool"), ToolConfig(name="tool_at_5")],
)
# starts at t=10 but reports end_time=t=1 (negative duration / corrupted telemetry)
broken_tool = ToolExecutionSpan(
span_info=SpanInfo(
session_id="test",
span_id="tb",
parent_span_id="a0",
start_time=base + timedelta(seconds=10),
end_time=base + timedelta(seconds=1),
),
tool_call=ToolCall(name="broken_tool", arguments={}),
tool_result=ToolResult(content="broken_result"),
)
tool_at_5 = ToolExecutionSpan(
span_info=SpanInfo(
session_id="test",
span_id="t5",
parent_span_id="a0",
start_time=base + timedelta(seconds=5),
end_time=base + timedelta(seconds=6),
),
tool_call=ToolCall(name="tool_at_5", arguments={}),
tool_result=ToolResult(content="5_result"),
)
trace = Trace(spans=[agent_span, broken_tool, tool_at_5], trace_id="trace1", session_id="test")
result = TraceExtractor(EvaluationLevel.TOOL_LEVEL).extract(Session(traces=[trace], session_id="test"))
tool_at_5_input = next(r for r in result if r.tool_execution_details.tool_call.name == "tool_at_5")
priors = [e.tool_call.name for g in tool_at_5_input.session_history if isinstance(g, list) for e in g]
assert "broken_tool" not in priors, f"broken_tool (starts t=10) must not be a prior of tool_at_5 (t=5): {priors}"
def test_extract_tool_level_equal_timestamps_break_ties_by_position():
"""Two zero-duration spans sharing a timestamp are ordered by list position, not excluded."""
from datetime import timedelta, timezone
base = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
agent_span = AgentInvocationSpan(
span_info=SpanInfo(session_id="test", span_id="a0", start_time=base, end_time=base + timedelta(seconds=5)),
user_prompt="x",
agent_response="y",
available_tools=[ToolConfig(name="tool_first"), ToolConfig(name="tool_second")],
)
tool_first = ToolExecutionSpan(
span_info=SpanInfo(session_id="test", span_id="tf", parent_span_id="a0", start_time=base, end_time=base),
tool_call=ToolCall(name="tool_first", arguments={}),
tool_result=ToolResult(content="first_result"),
)
tool_second = ToolExecutionSpan(
span_info=SpanInfo(session_id="test", span_id="ts", parent_span_id="a0", start_time=base, end_time=base),
tool_call=ToolCall(name="tool_second", arguments={}),
tool_result=ToolResult(content="second_result"),
)
trace = Trace(spans=[agent_span, tool_first, tool_second], trace_id="trace1", session_id="test")
result = TraceExtractor(EvaluationLevel.TOOL_LEVEL).extract(Session(traces=[trace], session_id="test"))
priors_by_tool = {
r.tool_execution_details.tool_call.name: [
e.tool_call.name for g in r.session_history if isinstance(g, list) for e in g
]
for r in result
}
assert priors_by_tool == {"tool_first": [], "tool_second": ["tool_first"]}, (
f"ties must resolve by list position only, got {priors_by_tool}"
)Mutation results (each mutant run against these two tests): The second test has to assert on both tools' prior sets: with the tie-break dropped, ⚪ Nit, no action needed
|
Description
TraceExtractor._extract_tool_levelbuildssession_historyat trace-level granularity rather than per-tool-span. All tool spans within the same trace receive an identical history snapshot — one that contains only the user prompt and results from previous traces, but none of the tool results from sibling tool calls earlier in the same trace.This causes
ToolSelectionAccuracyEvaluatorandToolParameterAccuracyEvaluatorto produce false negatives: the judge model sees a tool call using values that appear "hallucinated" because it cannot see that a prior tool call already returned those values.Related Issues
Fixes #337
Documentation PR
N/A
Type of Change
Bug fix
Testing
Added
test_extract_tool_level_incremental_session_historywhich verifies that:square_root) sees only the user prompt insession_historymultiply_numbers) sees the user prompt plus the priorsquare_rootcall and its result (42.0)The test fails without the fix (
assert 1 == 2— second tool only sees user prompt) and passes with it.hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.