Merge PR #1859: reviewed focused maintenance fix

This commit is contained in:
Haozhe Wu committed 2026-10-15 16:54:55 +08:00
commit 2d89bf3674
2 files changed
+89 -6

No files matched your search

+19 -6
View File
@@ -44,6 +44,7 @@ class RailStep:
title: str
tool: str = ""
args: Any = None
call_id: str = ""
status: StepStatus = "active"
lines: list[str] = field(default_factory=list)
started_at: float = field(default_factory=time.monotonic)
@@ -346,8 +347,9 @@ class RailRunDashboard:
self.thinking_active = False
tool = str(data.get("tool") or "tool")
args = data.get("arguments") or {}
call_id = str(data.get("call_id") or "")
self.iterations += 1
step = RailStep(title=_tool_title(tool, args), tool=tool, args=args)
step = RailStep(title=_tool_title(tool, args), tool=tool, args=args, call_id=call_id)
detail = _initial_detail(tool, args)
if detail:
step.lines.append(detail)
@@ -358,7 +360,7 @@ class RailRunDashboard:
if event_type == "tool_progress":
tool = str(data.get("tool") or "")
step = self._active_step(tool)
step = self._active_step(tool, call_id=str(data.get("call_id") or ""))
pieces: list[str] = []
stage = _collapse_ws(data.get("stage"))
current = data.get("current")
@@ -381,14 +383,14 @@ class RailRunDashboard:
if event_type == "tool_heartbeat":
tool = str(data.get("tool") or "")
self._active_step(tool)
self._active_step(tool, call_id=str(data.get("call_id") or ""))
self.refresh()
return
if event_type == "tool_result":
tool = str(data.get("tool") or "")
status = str(data.get("status") or "ok")
step = self._active_step(tool)
step = self._active_step(tool, call_id=str(data.get("call_id") or ""))
step.status = "done" if status == "ok" else "error"
step.duration_s = float(data.get("elapsed_ms") or 0.0) / 1000
for line in _result_summary(tool, status, data.get("preview")):
@@ -458,12 +460,23 @@ class RailRunDashboard:
lines = list(step.lines)
return lines[-7:]
def _active_step(self, tool: str) -> RailStep:
def _active_step(self, tool: str, call_id: str = "") -> RailStep:
raw_tool = str(tool or "")
raw_call_id = str(call_id or "")
# Concurrent calls to the same tool (the loop runs consecutive
# readonly tool calls in parallel) can have more than one step
# "active" at once with the same tool name. Matching on the call's
# own id -- always present on tool_call/tool_progress/tool_heartbeat/
# tool_result payloads -- keeps each event paired with the step it
# actually belongs to, instead of guessing by tool name and recency.
if raw_call_id:
for step in self.steps:
if step.call_id == raw_call_id:
return step
for step in reversed(self.steps):
if step.status == "active" and (not raw_tool or step.tool == raw_tool):
return step
step = RailStep(title=_tool_title(raw_tool or "tool", None), tool=raw_tool)
step = RailStep(title=_tool_title(raw_tool or "tool", None), tool=raw_tool, call_id=raw_call_id)
self.steps.append(step)
self.steps = self.steps[-10:]
return step
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the CLI rail dashboard's tool_call/tool_result pairing."""
from __future__ import annotations
from typing import Iterator
import pytest
from cli.ui.rail import RailRunDashboard
@pytest.fixture()
def dashboard() -> Iterator[RailRunDashboard]:
dash = RailRunDashboard("test prompt", max_iter=10)
try:
yield dash
finally:
dash.close()
class TestConcurrentToolCallPairing:
"""Two concurrent calls to the same tool must not have their results swapped.
The agent loop runs consecutive readonly tool calls in parallel, so more
than one step for the same tool name can be "active" at once. Matching a
tool_result to a step by tool name and recency alone pairs the first
arriving result with the most recently started step instead of the one
it actually belongs to.
"""
def test_results_pair_with_their_own_call_via_call_id(self, dashboard: RailRunDashboard) -> None:
dashboard.handle_event(
"tool_call", {"tool": "get_market_data", "arguments": {"symbol": "AAPL"}, "call_id": "call-1"}
)
dashboard.handle_event(
"tool_call", {"tool": "get_market_data", "arguments": {"symbol": "MSFT"}, "call_id": "call-2"}
)
dashboard.handle_event(
"tool_result",
{"tool": "get_market_data", "status": "ok", "preview": '{"summary": "AAPL result"}', "call_id": "call-1"},
)
dashboard.handle_event(
"tool_result",
{"tool": "get_market_data", "status": "ok", "preview": '{"summary": "MSFT result"}', "call_id": "call-2"},
)
aapl_step = next(s for s in dashboard.steps if s.args == {"symbol": "AAPL"})
msft_step = next(s for s in dashboard.steps if s.args == {"symbol": "MSFT"})
assert any("AAPL result" in line for line in aapl_step.lines)
assert not any("MSFT result" in line for line in aapl_step.lines)
assert any("MSFT result" in line for line in msft_step.lines)
assert not any("AAPL result" in line for line in msft_step.lines)
def test_progress_event_updates_its_own_call_via_call_id(self, dashboard: RailRunDashboard) -> None:
dashboard.handle_event(
"tool_call", {"tool": "backtest", "arguments": {"symbol": "AAPL"}, "call_id": "call-1"}
)
dashboard.handle_event(
"tool_call", {"tool": "backtest", "arguments": {"symbol": "MSFT"}, "call_id": "call-2"}
)
dashboard.handle_event(
"tool_progress", {"tool": "backtest", "message": "halfway for MSFT", "call_id": "call-2"}
)
aapl_step = next(s for s in dashboard.steps if s.args == {"symbol": "AAPL"})
msft_step = next(s for s in dashboard.steps if s.args == {"symbol": "MSFT"})
assert not any("halfway for MSFT" in line for line in aapl_step.lines)
assert any("halfway for MSFT" in line for line in msft_step.lines)