refactor(exec-render): show turn and item in header
This commit is contained in:
@@ -28,11 +28,14 @@ def format_elapsed(elapsed_s: float) -> str:
|
|||||||
return f"{seconds}s"
|
return f"{seconds}s"
|
||||||
|
|
||||||
|
|
||||||
def format_header(elapsed_s: float, turn: int | None, label: str) -> str:
|
def format_header(elapsed_s: float, turn: int | None, item: int | None, label: str) -> str:
|
||||||
elapsed = format_elapsed(elapsed_s)
|
elapsed = format_elapsed(elapsed_s)
|
||||||
|
parts = [label, elapsed]
|
||||||
if turn is not None:
|
if turn is not None:
|
||||||
return f"{label}{HEADER_SEP}{elapsed}{HEADER_SEP}turn {turn}"
|
parts.append(f"turn {turn}")
|
||||||
return f"{label}{HEADER_SEP}{elapsed}"
|
if item is not None:
|
||||||
|
parts.append(f"item {item}")
|
||||||
|
return HEADER_SEP.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def is_command_log_line(line: str) -> bool:
|
def is_command_log_line(line: str) -> bool:
|
||||||
@@ -60,10 +63,10 @@ def _shorten_path(path: str, width: int) -> str:
|
|||||||
|
|
||||||
def format_event(
|
def format_event(
|
||||||
event: dict[str, Any],
|
event: dict[str, Any],
|
||||||
last_turn: int | None,
|
last_item: int | None,
|
||||||
) -> tuple[int | None, list[str], str | None, str | None]:
|
) -> tuple[int | None, list[str], str | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Returns (new_last_turn, cli_lines, progress_line, progress_prefix).
|
Returns (new_last_item, cli_lines, progress_line, progress_prefix).
|
||||||
progress_prefix is only set when progress_line is set, and is used for
|
progress_prefix is only set when progress_line is set, and is used for
|
||||||
replacing a preceding "running" line on completion.
|
replacing a preceding "running" line on completion.
|
||||||
"""
|
"""
|
||||||
@@ -71,51 +74,51 @@ def format_event(
|
|||||||
|
|
||||||
match event["type"]:
|
match event["type"]:
|
||||||
case "thread.started":
|
case "thread.started":
|
||||||
return last_turn, ["thread started"], None, None
|
return last_item, ["thread started"], None, None
|
||||||
case "turn.started":
|
case "turn.started":
|
||||||
return last_turn, ["turn started"], None, None
|
return last_item, ["turn started"], None, None
|
||||||
case "turn.completed":
|
case "turn.completed":
|
||||||
return last_turn, ["turn completed"], None, None
|
return last_item, ["turn completed"], None, None
|
||||||
case "turn.failed":
|
case "turn.failed":
|
||||||
return last_turn, [f"turn failed: {event['error']['message']}"], None, None
|
return last_item, [f"turn failed: {event['error']['message']}"], None, None
|
||||||
case "error":
|
case "error":
|
||||||
return last_turn, [f"stream error: {event['message']}"], None, None
|
return last_item, [f"stream error: {event['message']}"], None, None
|
||||||
case "item.started" | "item.updated" | "item.completed" as etype:
|
case "item.started" | "item.updated" | "item.completed" as etype:
|
||||||
item = event["item"]
|
item = event["item"]
|
||||||
item_num = extract_numeric_id(item["id"], last_turn)
|
item_num = extract_numeric_id(item["id"], last_item)
|
||||||
last_turn = item_num if item_num is not None else last_turn
|
last_item = item_num if item_num is not None else last_item
|
||||||
prefix = f"[{item_num if item_num is not None else '?'}] "
|
prefix = f"[{item_num if item_num is not None else '?'}] "
|
||||||
|
|
||||||
match (item["type"], etype):
|
match (item["type"], etype):
|
||||||
case ("agent_message", "item.completed"):
|
case ("agent_message", "item.completed"):
|
||||||
lines.append("assistant:")
|
lines.append("assistant:")
|
||||||
lines.extend(indent(item["text"], " ").splitlines())
|
lines.extend(indent(item["text"], " ").splitlines())
|
||||||
return last_turn, lines, None, None
|
return last_item, lines, None, None
|
||||||
case ("reasoning", "item.completed"):
|
case ("reasoning", "item.completed"):
|
||||||
line = prefix + item["text"]
|
line = prefix + item["text"]
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("command_execution", "item.started"):
|
case ("command_execution", "item.started"):
|
||||||
command = f"`{_shorten(item['command'], MAX_CMD_LEN)}`"
|
command = f"`{_shorten(item['command'], MAX_CMD_LEN)}`"
|
||||||
line = prefix + f"{STATUS_RUNNING} running: {command}"
|
line = prefix + f"{STATUS_RUNNING} running: {command}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("command_execution", "item.completed"):
|
case ("command_execution", "item.completed"):
|
||||||
command = f"`{_shorten(item['command'], MAX_CMD_LEN)}`"
|
command = f"`{_shorten(item['command'], MAX_CMD_LEN)}`"
|
||||||
exit_code = item["exit_code"]
|
exit_code = item["exit_code"]
|
||||||
exit_part = f" (exit {exit_code})" if exit_code is not None else ""
|
exit_part = f" (exit {exit_code})" if exit_code is not None else ""
|
||||||
line = prefix + f"{STATUS_DONE} ran: {command}{exit_part}"
|
line = prefix + f"{STATUS_DONE} ran: {command}{exit_part}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("mcp_tool_call", "item.started"):
|
case ("mcp_tool_call", "item.started"):
|
||||||
name = ".".join(part for part in (item["server"], item["tool"]) if part) or "tool"
|
name = ".".join(part for part in (item["server"], item["tool"]) if part) or "tool"
|
||||||
line = prefix + f"{STATUS_RUNNING} tool: {name}"
|
line = prefix + f"{STATUS_RUNNING} tool: {name}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("mcp_tool_call", "item.completed"):
|
case ("mcp_tool_call", "item.completed"):
|
||||||
name = ".".join(part for part in (item["server"], item["tool"]) if part) or "tool"
|
name = ".".join(part for part in (item["server"], item["tool"]) if part) or "tool"
|
||||||
line = prefix + f"{STATUS_DONE} tool: {name}"
|
line = prefix + f"{STATUS_DONE} tool: {name}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("web_search", "item.completed"):
|
case ("web_search", "item.completed"):
|
||||||
query = _shorten(item["query"], MAX_QUERY_LEN)
|
query = _shorten(item["query"], MAX_QUERY_LEN)
|
||||||
line = prefix + f"{STATUS_DONE} searched: {query}"
|
line = prefix + f"{STATUS_DONE} searched: {query}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("file_change", "item.completed"):
|
case ("file_change", "item.completed"):
|
||||||
paths = [change["path"] for change in item["changes"] if change.get("path")]
|
paths = [change["path"] for change in item["changes"] if change.get("path")]
|
||||||
if not paths:
|
if not paths:
|
||||||
@@ -126,20 +129,22 @@ def format_event(
|
|||||||
else:
|
else:
|
||||||
desc = f"updated {len(paths)} files"
|
desc = f"updated {len(paths)} files"
|
||||||
line = prefix + f"{STATUS_DONE} {desc}"
|
line = prefix + f"{STATUS_DONE} {desc}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case ("error", "item.completed"):
|
case ("error", "item.completed"):
|
||||||
warning = _shorten(item["message"], 120)
|
warning = _shorten(item["message"], 120)
|
||||||
line = prefix + f"{STATUS_DONE} warning: {warning}"
|
line = prefix + f"{STATUS_DONE} warning: {warning}"
|
||||||
return last_turn, [line], line, prefix
|
return last_item, [line], line, prefix
|
||||||
case _:
|
case _:
|
||||||
return last_turn, [], None, None
|
return last_item, [], None, None
|
||||||
case _:
|
case _:
|
||||||
return last_turn, [], None, None
|
return last_item, [], None, None
|
||||||
|
|
||||||
|
|
||||||
def render_event_cli(event: dict[str, Any], last_turn: int | None = None) -> tuple[int | None, list[str]]:
|
def render_event_cli(
|
||||||
last_turn, cli_lines, _, _ = format_event(event, last_turn)
|
event: dict[str, Any], last_item: int | None = None
|
||||||
return last_turn, cli_lines
|
) -> tuple[int | None, list[str]]:
|
||||||
|
last_item, cli_lines, _, _ = format_event(event, last_item)
|
||||||
|
return last_item, cli_lines
|
||||||
|
|
||||||
|
|
||||||
class ExecProgressRenderer:
|
class ExecProgressRenderer:
|
||||||
@@ -147,13 +152,17 @@ class ExecProgressRenderer:
|
|||||||
self.max_actions = max_actions
|
self.max_actions = max_actions
|
||||||
self.max_chars = max_chars
|
self.max_chars = max_chars
|
||||||
self.recent_actions: deque[str] = deque(maxlen=max_actions)
|
self.recent_actions: deque[str] = deque(maxlen=max_actions)
|
||||||
self.last_turn: int | None = None
|
self.turn_count: int | None = None
|
||||||
|
self.last_item: int | None = None
|
||||||
|
|
||||||
def note_event(self, event: dict[str, Any]) -> bool:
|
def note_event(self, event: dict[str, Any]) -> bool:
|
||||||
if event["type"] in {"thread.started", "turn.started"}:
|
if event["type"] == "thread.started":
|
||||||
|
return True
|
||||||
|
if event["type"] == "turn.started":
|
||||||
|
self.turn_count = 1 if self.turn_count is None else self.turn_count + 1
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.last_turn, _, progress_line, progress_prefix = format_event(event, self.last_turn)
|
self.last_item, _, progress_line, progress_prefix = format_event(event, self.last_item)
|
||||||
if progress_line is None:
|
if progress_line is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -167,12 +176,12 @@ class ExecProgressRenderer:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def render_progress(self, elapsed_s: float) -> str:
|
def render_progress(self, elapsed_s: float) -> str:
|
||||||
header = format_header(elapsed_s, self.last_turn, label="working")
|
header = format_header(elapsed_s, self.turn_count, self.last_item, label="working")
|
||||||
message = self._assemble(header, list(self.recent_actions))
|
message = self._assemble(header, list(self.recent_actions))
|
||||||
return message if len(message) <= self.max_chars else header
|
return message if len(message) <= self.max_chars else header
|
||||||
|
|
||||||
def render_final(self, elapsed_s: float, answer: str, status: str = "done") -> str:
|
def render_final(self, elapsed_s: float, answer: str, status: str = "done") -> str:
|
||||||
header = format_header(elapsed_s, self.last_turn, label=status)
|
header = format_header(elapsed_s, self.turn_count, self.last_item, label=status)
|
||||||
lines = list(self.recent_actions)
|
lines = list(self.recent_actions)
|
||||||
if status == "done":
|
if status == "done":
|
||||||
lines = [line for line in lines if not is_command_log_line(line)]
|
lines = [line for line in lines if not is_command_log_line(line)]
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ def test_progress_renderer_renders_progress_and_final() -> None:
|
|||||||
r.note_event(evt)
|
r.note_event(evt)
|
||||||
|
|
||||||
progress = r.render_progress(3.0)
|
progress = r.render_progress(3.0)
|
||||||
assert progress.startswith("working · 3s · turn 3")
|
assert progress.startswith("working · 3s · turn 1 · item 3")
|
||||||
assert "[1] ✓ ran: `bash -lc ls` (exit 0)" in progress
|
assert "[1] ✓ ran: `bash -lc ls` (exit 0)" in progress
|
||||||
|
|
||||||
final = r.render_final(3.0, "answer", status="done")
|
final = r.render_final(3.0, "answer", status="done")
|
||||||
assert final.startswith("done · 3s · turn 3")
|
assert final.startswith("done · 3s · turn 1 · item 3")
|
||||||
assert "running:" not in final
|
assert "running:" not in final
|
||||||
assert "ran:" not in final
|
assert "ran:" not in final
|
||||||
assert final.endswith("answer")
|
assert final.endswith("answer")
|
||||||
|
|||||||
Reference in New Issue
Block a user