diff --git a/code_agent/agent/base_agent.py b/code_agent/agent/base_agent.py index 081a0a7..ee762c0 100644 --- a/code_agent/agent/base_agent.py +++ b/code_agent/agent/base_agent.py @@ -14,6 +14,8 @@ import time import contextlib from abc import ABC, abstractmethod +from datetime import datetime +from pathlib import Path logger = logging.getLogger(__name__) @@ -79,6 +81,7 @@ def __init__( self._tool_caller = ToolExecutor(self._tools) self._cli_console: CLIConsole | None = None + self._history_path: Path | None = None # Cancellation support for run_in_executor LLM calls. # Using a private executor (not the default) means asyncio.run() shutdown @@ -519,11 +522,154 @@ async def _run_llm_step_branch( async def _finalize_step( self, step: "AgentStep", messages: list["LLMMessage"], execution: "AgentExecution" ) -> None: - step.state = AgentStepState.COMPLETED + if step.state != AgentStepState.ERROR: + step.state = AgentStepState.COMPLETED + if messages is not None: + dumped: list[dict[str, object]] = [] + for m in messages: + item: dict[str, object] = {"role": m.role} + if m.content: + item["content"] = m.content + if m.tool_call is not None: + tc = m.tool_call + item["tool_call"] = { + "name": tc.name, + "call_id": tc.call_id, + "arguments": tc.arguments, + } + if m.tool_result is not None: + tr = m.tool_result + item["tool_result"] = { + "name": tr.name, + "call_id": tr.call_id, + "success": tr.success, + "result": tr.result, + "error": tr.error, + } + dumped.append(item) + step.extra = dict(step.extra or {}) + step.extra["next_messages"] = dumped self._record_handler(step, messages) + self._append_step_history(step=step, execution=execution) self._update_cli_console(step, execution) + if self.cli_console: + try: + self.cli_console.debug_step(step, execution, agent_type=self.agent_type) + except Exception: + pass execution.steps.append(step) + def _get_history_path(self) -> Path: + if self._history_path is not None: + return self._history_path + + history_dir = Path(__file__).resolve().parents[1] / "history" + history_dir.mkdir(parents=True, exist_ok=True) + + base = datetime.now().strftime("%Y%m%d_%H%M%S") + try: + if self._trajectory_recorder and hasattr(self._trajectory_recorder, "trajectory_path"): + base = str(getattr(self._trajectory_recorder, "trajectory_path").stem) or base + except Exception: + pass + + pid = os.getpid() + self._history_path = (history_dir / f"{base}__{self.agent_type}__{pid}.jsonl").resolve() + return self._history_path + + def _append_step_history(self, *, step: AgentStep, execution: AgentExecution) -> None: + try: + lr = step.llm_response + tool_calls = step.tool_calls or (lr.tool_calls if lr else None) or [] + tool_results = step.tool_results or [] + display_level = getattr(self._cli_console, "display_level", "log") if self._cli_console else "log" + + info = { + "state": getattr(step.state, "value", str(step.state)), + "finish_reason": getattr(lr, "finish_reason", None) if lr else None, + "tool_calls": [tc.name for tc in tool_calls], + "tool_results": [{"name": tr.name, "success": tr.success} for tr in tool_results], + "execution_success": execution.success, + "agent_state": getattr(execution.agent_state, "value", str(execution.agent_state)), + } + + debug = { + "thought": step.thought, + "llm_response": ( + { + "content": lr.content, + "model": lr.model, + "finish_reason": lr.finish_reason, + "usage": ( + { + "input_tokens": lr.usage.input_tokens, + "output_tokens": lr.usage.output_tokens, + "cache_creation_input_tokens": lr.usage.cache_creation_input_tokens, + "cache_read_input_tokens": lr.usage.cache_read_input_tokens, + "reasoning_tokens": lr.usage.reasoning_tokens, + } + if lr.usage + else None + ), + "tool_calls": ( + [ + { + "name": tc.name, + "call_id": tc.call_id, + "arguments": tc.arguments, + "id": getattr(tc, "id", None), + } + for tc in (lr.tool_calls or []) + ] + if lr.tool_calls is not None + else None + ), + } + if lr + else None + ), + "tool_calls": [ + { + "name": tc.name, + "call_id": tc.call_id, + "arguments": tc.arguments, + "id": getattr(tc, "id", None), + } + for tc in tool_calls + ], + "tool_results": [ + { + "name": tr.name, + "call_id": tr.call_id, + "success": tr.success, + "result": tr.result, + "error": tr.error, + "id": getattr(tr, "id", None), + } + for tr in tool_results + ], + "reflection": step.reflection, + "error": step.error, + "next_messages": (step.extra or {}).get("next_messages") if isinstance(step.extra, dict) else None, + } + + record = { + "ts": datetime.now().isoformat(), + "level": display_level, + "agent_type": self.agent_type, + "task_kind": (self._task or {}).get("task_kind") if isinstance(self._task, dict) else None, + "project_path": (self._extra_args or {}).get("project_path"), + "step_number": step.step_number, + "info": info, + "debug": debug, + } + + path = self._get_history_path() + with open(path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + except Exception: + return + def reflect_on_result(self, tool_results: list[ToolResult]) -> str | None: """Reflect on tool execution result. Override for custom reflection logic.""" if len(tool_results) == 0: diff --git a/code_agent/cli.py b/code_agent/cli.py index a82a9d1..9eb56cc 100644 --- a/code_agent/cli.py +++ b/code_agent/cli.py @@ -84,6 +84,13 @@ def cli(): type=click.Choice(["simple", "rich"], case_sensitive=False), help="Type of console to use (simple or rich)", ) +@click.option( + "--display-level", + "-dl", + default="log", + type=click.Choice(["log", "debug"], case_sensitive=False), + help="Display verbosity: log (default) or debug (per-step debug panels)", +) @click.option( "--agent-type", "-at", type=click.Choice(["code_agent", "plan_agent", "test_agent"], case_sensitive=False), @@ -92,7 +99,7 @@ def cli(): def run_command( task, file_path, provider, model, model_base_url, api_key, max_steps, working_dir, must_patch, config_file, trajectory_file, patch_path, - console_type, agent_type, + console_type, display_level, agent_type, ): """Run a task using the agent (CLI entry point).""" is_success, _, _ = run( @@ -101,6 +108,7 @@ def run_command( api_key=api_key, max_steps=max_steps, working_dir=working_dir, must_patch=must_patch, config_file=config_file, trajectory_file=trajectory_file, console_type=console_type, + display_level=display_level, agent_type=agent_type, ) if not is_success: @@ -121,6 +129,7 @@ def run( config_file: str = "opencook_config.yaml", trajectory_file: str | None = None, console_type: str | None = "simple", + display_level: str = "log", agent_type: str | None = "code_agent", ): """ @@ -190,6 +199,8 @@ def run( cli_console = ConsoleFactory.create_console( console_type=selected_console_type, mode=console_mode ) + if hasattr(cli_console, "set_display_level"): + cli_console.set_display_level(display_level) # For rich console in RUN mode, set the initial task if (selected_console_type is not None and selected_console_type @@ -324,6 +335,13 @@ def _has_recent_session(store: "SessionStore") -> bool: default="auto", help="Console type: auto (default) | textual | chat | simple", ) +@click.option( + "--display-level", + "-dl", + default="log", + type=click.Choice(["log", "debug"], case_sensitive=False), + help="Display verbosity: log (default) or debug (per-step debug panels)", +) def interactive( session_id: str | None = None, force_new: bool = False, @@ -337,6 +355,7 @@ def interactive( config_file: str = "opencook_config.yaml", max_steps: int | None = None, console_type: str = "auto", + display_level: str = "log", ): """Start an interactive session with OpenCook.""" import hashlib @@ -405,6 +424,8 @@ def interactive( cli_console = ConsoleFactory.create_interactive_console( console_type=console_type.lower(), ) + if hasattr(cli_console, "set_display_level"): + cli_console.set_display_level(display_level) runner = SessionRunner( config=config, diff --git a/code_agent/utils/cli/chat_console.py b/code_agent/utils/cli/chat_console.py index 625967e..7953f76 100644 --- a/code_agent/utils/cli/chat_console.py +++ b/code_agent/utils/cli/chat_console.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025-2026 weAIDB +# Copyright (c) 2025-2026 weAIDB # OpenCook: Start with a generic project. End with a perfectly tailored solution. # SPDX-License-Identifier: MIT @@ -8,6 +8,7 @@ import asyncio import difflib +import json import logging from typing import TYPE_CHECKING @@ -25,7 +26,9 @@ def override(func): from rich.live import Live from rich.markdown import Markdown from rich.markup import escape +from rich.panel import Panel from rich.syntax import Syntax +from rich.table import Table from rich.text import Text from code_agent.agent.agent_basics import AgentExecution, AgentState, AgentStep, AgentStepState @@ -481,6 +484,170 @@ def print(self, message: str, color: str = "blue", bold: bool = False) -> None: safe = f"[{color}]{safe}[/{color}]" self.console.print(safe) + def debug_step( + self, + agent_step: AgentStep, + agent_execution: AgentExecution | None = None, + *, + agent_type: str | None = None, + ) -> None: + if not self.is_debug: + return + + def _clip(text: str, limit: int = 1400) -> str: + t = (text or "").strip() + return t if len(t) <= limit else (t[: limit - 1] + "…") + + def _pretty(value) -> str: + if value is None: + return "" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, indent=2) + return str(value) + + self._stop_live() + title = f"[bold magenta]DEBUG[/bold magenta] Step {agent_step.step_number}" + if agent_type: + title += f" • {escape(agent_type)}" + + lr = agent_step.llm_response + usage = lr.usage if lr else None + tool_calls = agent_step.tool_calls or (lr.tool_calls if lr else None) or [] + tool_results = agent_step.tool_results or [] + + meta = Table(show_header=False, expand=True, pad_edge=False) + meta.add_column(style="dim", width=16) + meta.add_column() + meta.add_row("State", escape(getattr(agent_step.state, "value", str(agent_step.state)))) + if lr and lr.model: + meta.add_row("Model", escape(lr.model)) + if lr and lr.finish_reason: + meta.add_row("Finish", escape(lr.finish_reason)) + if usage: + meta.add_row( + "Tokens", + escape( + f"in={usage.input_tokens} out={usage.output_tokens}" + f" cache_write={usage.cache_creation_input_tokens} cache_read={usage.cache_read_input_tokens}" + f" reasoning={usage.reasoning_tokens}" + ), + ) + + layout = Table.grid(expand=True) + layout.add_row(meta) + + if agent_step.thought and agent_step.thought.strip(): + layout.add_row( + Panel( + Text(_clip(agent_step.thought, limit=1800), overflow="fold"), + title="[dim]Thought[/dim]", + border_style="bright_black", + ) + ) + + if lr and (lr.content or "").strip(): + layout.add_row( + Panel( + Text(_clip(lr.content, limit=1800), overflow="fold"), + title="[dim]LLM Output[/dim]", + border_style="bright_black", + ) + ) + + if tool_calls: + tc_table = Table(show_header=True, expand=True) + tc_table.add_column("#", style="dim", width=3, justify="right") + tc_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tc_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tc_table.add_column("Arguments", overflow="fold") + for i, tc in enumerate(tool_calls, start=1): + tc_table.add_row( + str(i), + escape(tc.name), + escape(tc.call_id), + escape(_clip(_pretty(tc.arguments), limit=900)), + ) + layout.add_row( + Panel(tc_table, title="[dim]Tool Calls[/dim]", border_style="bright_black") + ) + + if tool_results: + tr_table = Table(show_header=True, expand=True) + tr_table.add_column("#", style="dim", width=3, justify="right") + tr_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tr_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tr_table.add_column("OK", style="dim", width=4, justify="center") + tr_table.add_column("Result / Error", overflow="fold") + for i, tr in enumerate(tool_results, start=1): + payload = tr.result if tr.success else (tr.error or tr.result or "") + tr_table.add_row( + str(i), + escape(tr.name), + escape(tr.call_id), + "Y" if tr.success else "N", + escape(_clip(payload, limit=900)), + ) + layout.add_row( + Panel(tr_table, title="[dim]Tool Results[/dim]", border_style="bright_black") + ) + + next_messages = None + if isinstance(agent_step.extra, dict): + next_messages = agent_step.extra.get("next_messages") + if isinstance(next_messages, list) and next_messages: + nm_table = Table(show_header=True, expand=True) + nm_table.add_column("#", style="dim", width=3, justify="right") + nm_table.add_column("Role", style="dim", width=10) + nm_table.add_column("Type", style="dim", width=12) + nm_table.add_column("Content", overflow="fold") + for i, m in enumerate(next_messages, start=1): + role = str(m.get("role", "")) if isinstance(m, dict) else "" + kind = "" + payload = "" + if isinstance(m, dict) and "tool_result" in m: + kind = "tool_result" + tr = m.get("tool_result") or {} + payload = _pretty(tr) if isinstance(tr, (dict, list)) else str(tr) + elif isinstance(m, dict) and "tool_call" in m: + kind = "tool_call" + tc = m.get("tool_call") or {} + payload = _pretty(tc) if isinstance(tc, (dict, list)) else str(tc) + else: + kind = "content" + if isinstance(m, dict): + payload = str(m.get("content", "") or "") + else: + payload = str(m) + nm_table.add_row( + str(i), + escape(_clip(role, limit=60)), + escape(_clip(kind, limit=60)), + escape(_clip(payload, limit=1200)), + ) + layout.add_row( + Panel(nm_table, title="[dim]Next Messages[/dim]", border_style="bright_black") + ) + + if agent_step.reflection: + layout.add_row( + Panel( + Text(_clip(agent_step.reflection, limit=1200), overflow="fold"), + title="[dim]Reflection[/dim]", + border_style="bright_black", + ) + ) + + if agent_step.error: + layout.add_row( + Panel( + Text(_clip(agent_step.error, limit=1200), overflow="fold"), + title="[dim]Step Error[/dim]", + border_style="red", + ) + ) + + self.console.print(Panel(layout, title=title, border_style="magenta")) + @override def get_task_input(self) -> str | None: """Legacy sync path — always returns None. diff --git a/code_agent/utils/cli/cli_console.py b/code_agent/utils/cli/cli_console.py index c6a88a6..016275f 100644 --- a/code_agent/utils/cli/cli_console.py +++ b/code_agent/utils/cli/cli_console.py @@ -77,7 +77,7 @@ class ConsoleStep: class CLIConsole(ABC): """Base class for CLI console implementations.""" - def __init__(self, mode: ConsoleMode = ConsoleMode.RUN): + def __init__(self, mode: ConsoleMode = ConsoleMode.RUN, display_level: str = "log"): """Initialize the CLI console. Args: @@ -86,6 +86,26 @@ def __init__(self, mode: ConsoleMode = ConsoleMode.RUN): self.mode: ConsoleMode = mode self.console_step_history: dict[int, ConsoleStep] = {} self.agent_execution: AgentExecution | None = None + self.display_level: str = str(display_level or "log").lower() + + def set_display_level(self, display_level: str) -> None: + level = str(display_level or "log").lower() + if level not in ("log", "debug"): + level = "log" + self.display_level = level + + @property + def is_debug(self) -> bool: + return self.display_level == "debug" + + def debug_step( + self, + agent_step: AgentStep, + agent_execution: AgentExecution | None = None, + *, + agent_type: str | None = None, + ) -> None: + return # ── Legacy turn-level interface (kept for batch / non-interactive paths) ── diff --git a/code_agent/utils/cli/rich_console.py b/code_agent/utils/cli/rich_console.py index 06cc77b..e79d51a 100644 --- a/code_agent/utils/cli/rich_console.py +++ b/code_agent/utils/cli/rich_console.py @@ -5,6 +5,7 @@ """Rich CLI Console implementation using Textual TUI.""" import asyncio +import json import os try: from typing import override @@ -13,6 +14,7 @@ def override(func): return func from rich.panel import Panel +from rich.table import Table from rich.text import Text from textual import on from textual.app import App, ComposeResult @@ -338,6 +340,173 @@ def print(self, message: str, color: str = "blue", bold: bool = False): formatted_message = f"[{color}]{formatted_message}[/{color}]" _ = self.app.execution_log.write(formatted_message) + def debug_step( + self, + agent_step: AgentStep, + agent_execution: AgentExecution | None = None, + *, + agent_type: str | None = None, + ) -> None: + if not self.is_debug: + return + if not self.app or not self.app.execution_log: + return + + def _clip(text: str, limit: int = 1400) -> str: + t = (text or "").strip() + return t if len(t) <= limit else (t[: limit - 1] + "…") + + def _pretty(value) -> str: + if value is None: + return "" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, indent=2) + return str(value) + + title = f"[bold magenta]DEBUG[/bold magenta] Step {agent_step.step_number}" + if agent_type: + title += f" • {agent_type}" + + lr = agent_step.llm_response + usage = lr.usage if lr else None + tool_calls = agent_step.tool_calls or (lr.tool_calls if lr else None) or [] + tool_results = agent_step.tool_results or [] + + meta = Table(show_header=False, expand=True, pad_edge=False) + meta.add_column(style="dim", width=16) + meta.add_column() + meta.add_row("State", getattr(agent_step.state, "value", str(agent_step.state))) + if lr and lr.model: + meta.add_row("Model", lr.model) + if lr and lr.finish_reason: + meta.add_row("Finish", lr.finish_reason) + if usage: + meta.add_row( + "Tokens", + ( + f"in={usage.input_tokens} out={usage.output_tokens}" + f" cache_write={usage.cache_creation_input_tokens} cache_read={usage.cache_read_input_tokens}" + f" reasoning={usage.reasoning_tokens}" + ), + ) + + layout = Table.grid(expand=True) + layout.add_row(meta) + + if agent_step.thought and agent_step.thought.strip(): + layout.add_row( + Panel( + Text(_clip(agent_step.thought, limit=1800), overflow="fold"), + title="[dim]Thought[/dim]", + border_style="bright_black", + ) + ) + + if lr and (lr.content or "").strip(): + layout.add_row( + Panel( + Text(_clip(lr.content, limit=1800), overflow="fold"), + title="[dim]LLM Output[/dim]", + border_style="bright_black", + ) + ) + + if tool_calls: + tc_table = Table(show_header=True, expand=True) + tc_table.add_column("#", style="dim", width=3, justify="right") + tc_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tc_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tc_table.add_column("Arguments", overflow="fold") + for i, tc in enumerate(tool_calls, start=1): + tc_table.add_row( + str(i), + tc.name, + tc.call_id, + _clip(_pretty(tc.arguments), limit=900), + ) + layout.add_row( + Panel(tc_table, title="[dim]Tool Calls[/dim]", border_style="bright_black") + ) + + if tool_results: + tr_table = Table(show_header=True, expand=True) + tr_table.add_column("#", style="dim", width=3, justify="right") + tr_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tr_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tr_table.add_column("OK", style="dim", width=4, justify="center") + tr_table.add_column("Result / Error", overflow="fold") + for i, tr in enumerate(tool_results, start=1): + payload = tr.result if tr.success else (tr.error or tr.result or "") + tr_table.add_row( + str(i), + tr.name, + tr.call_id, + "Y" if tr.success else "N", + _clip(payload, limit=900), + ) + layout.add_row( + Panel(tr_table, title="[dim]Tool Results[/dim]", border_style="bright_black") + ) + + next_messages = None + if isinstance(agent_step.extra, dict): + next_messages = agent_step.extra.get("next_messages") + if isinstance(next_messages, list) and next_messages: + nm_table = Table(show_header=True, expand=True) + nm_table.add_column("#", style="dim", width=3, justify="right") + nm_table.add_column("Role", style="dim", width=10) + nm_table.add_column("Type", style="dim", width=12) + nm_table.add_column("Content", overflow="fold") + for i, m in enumerate(next_messages, start=1): + role = str(m.get("role", "")) if isinstance(m, dict) else "" + kind = "" + payload = "" + if isinstance(m, dict) and "tool_result" in m: + kind = "tool_result" + tr = m.get("tool_result") or {} + payload = _pretty(tr) if isinstance(tr, (dict, list)) else str(tr) + elif isinstance(m, dict) and "tool_call" in m: + kind = "tool_call" + tc = m.get("tool_call") or {} + payload = _pretty(tc) if isinstance(tc, (dict, list)) else str(tc) + else: + kind = "content" + if isinstance(m, dict): + payload = str(m.get("content", "") or "") + else: + payload = str(m) + nm_table.add_row( + str(i), + _clip(role, limit=60), + _clip(kind, limit=60), + _clip(payload, limit=1200), + ) + layout.add_row( + Panel(nm_table, title="[dim]Next Messages[/dim]", border_style="bright_black") + ) + + if agent_step.reflection: + layout.add_row( + Panel( + Text(_clip(agent_step.reflection, limit=1200), overflow="fold"), + title="[dim]Reflection[/dim]", + border_style="bright_black", + ) + ) + + if agent_step.error: + layout.add_row( + Panel( + Text(_clip(agent_step.error, limit=1200), overflow="fold"), + title="[dim]Step Error[/dim]", + border_style="red", + ) + ) + + _ = self.app.execution_log.write( + Panel(layout, title=title, border_style="magenta") + ) + @override def get_task_input(self) -> str | None: """Get task input from user (for interactive mode).""" diff --git a/code_agent/utils/cli/simple_console.py b/code_agent/utils/cli/simple_console.py index 7612d4f..e4dcb31 100644 --- a/code_agent/utils/cli/simple_console.py +++ b/code_agent/utils/cli/simple_console.py @@ -1,10 +1,11 @@ -# Copyright (c) 2025-2026 weAIDB +# Copyright (c) 2025-2026 weAIDB # OpenCook: Start with a generic project. End with a perfectly tailored solution. # SPDX-License-Identifier: MIT """Simple CLI Console — DBCooker banner + live spinner + OpenCode-style step panels.""" import asyncio +import json import shutil import sys try: @@ -18,6 +19,7 @@ def override(func): from rich.markup import escape from rich.panel import Panel from rich.table import Table +from rich.text import Text from code_agent.agent.agent_basics import AgentExecution, AgentState, AgentStep, AgentStepState from code_agent.utils.cli.cli_console import ( @@ -123,7 +125,7 @@ def __init__( mode: ConsoleMode = ConsoleMode.RUN, ): super().__init__(mode) - self.console: Console = Console() + self.console: Console = Console(encoding="utf-8") _enable_vt_on_windows() self._is_tty: bool = sys.stdout.isatty() self._term_width: int = shutil.get_terminal_size((80, 24)).columns @@ -385,6 +387,186 @@ def print(self, message: str, color: str = "blue", bold: bool = False) -> None: safe = f"[{color}]{safe}[/{color}]" self.console.print(safe) + def debug_step( + self, + agent_step: AgentStep, + agent_execution: AgentExecution | None = None, + *, + agent_type: str | None = None, + ) -> None: + if not self.is_debug: + return + + def _clip(text: str, limit: int = 1400) -> str: + t = (text or "").strip() + return t if len(t) <= limit else (t[: limit - 1] + "…") + + def _pretty(value) -> str: + if value is None: + return "" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, indent=2) + return str(value) + + self._clear_status() + title = f"[bold magenta]DEBUG[/bold magenta] Step {agent_step.step_number}" + if agent_type: + title += f" • {escape(agent_type)}" + + lr = agent_step.llm_response + usage = lr.usage if lr else None + tool_calls = agent_step.tool_calls or (lr.tool_calls if lr else None) or [] + tool_results = agent_step.tool_results or [] + + meta = Table(show_header=False, expand=True, pad_edge=False) + meta.add_column(style="dim", width=16) + meta.add_column() + meta.add_row("State", escape(getattr(agent_step.state, "value", str(agent_step.state)))) + if lr and lr.model: + meta.add_row("Model", escape(lr.model)) + if lr and lr.finish_reason: + meta.add_row("Finish", escape(lr.finish_reason)) + if usage: + meta.add_row( + "Tokens", + escape( + f"in={usage.input_tokens} out={usage.output_tokens}" + f" cache_write={usage.cache_creation_input_tokens} cache_read={usage.cache_read_input_tokens}" + f" reasoning={usage.reasoning_tokens}" + ), + ) + + layout = Table.grid(expand=True) + layout.add_row(meta) + + if agent_step.thought and agent_step.thought.strip(): + layout.add_row( + Panel( + Text(_clip(agent_step.thought, limit=1800), overflow="fold"), + title="[dim]Thought[/dim]", + border_style="bright_black", + ) + ) + + if lr and (lr.content or "").strip(): + layout.add_row( + Panel( + Text(_clip(lr.content, limit=1800), overflow="fold"), + title="[dim]LLM Output[/dim]", + border_style="bright_black", + ) + ) + + if tool_calls: + tc_table = Table(show_header=True, expand=True) + tc_table.add_column("#", style="dim", width=3, justify="right") + tc_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tc_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tc_table.add_column("Arguments", overflow="fold") + for i, tc in enumerate(tool_calls, start=1): + tc_table.add_row( + str(i), + escape(tc.name), + escape(tc.call_id), + escape(_clip(_pretty(tc.arguments), limit=900)), + ) + layout.add_row( + Panel(tc_table, title="[dim]Tool Calls[/dim]", border_style="bright_black") + ) + + if tool_results: + tr_table = Table(show_header=True, expand=True) + tr_table.add_column("#", style="dim", width=3, justify="right") + tr_table.add_column("Tool", style="cyan", width=22, overflow="fold") + tr_table.add_column("Call ID", style="dim", width=18, overflow="fold") + tr_table.add_column("OK", style="dim", width=4, justify="center") + tr_table.add_column("Result / Error", overflow="fold") + for i, tr in enumerate(tool_results, start=1): + payload = tr.result if tr.success else (tr.error or tr.result or "") + tr_table.add_row( + str(i), + escape(tr.name), + escape(tr.call_id), + "Y" if tr.success else "N", + escape(_clip(payload, limit=900)), + ) + layout.add_row( + Panel(tr_table, title="[dim]Tool Results[/dim]", border_style="bright_black") + ) + + next_messages = None + if isinstance(agent_step.extra, dict): + next_messages = agent_step.extra.get("next_messages") + if isinstance(next_messages, list) and next_messages: + nm_table = Table(show_header=True, expand=True) + nm_table.add_column("#", style="dim", width=3, justify="right") + nm_table.add_column("Role", style="dim", width=10) + nm_table.add_column("Type", style="dim", width=12) + nm_table.add_column("Content", overflow="fold") + for i, m in enumerate(next_messages, start=1): + role = str(m.get("role", "")) if isinstance(m, dict) else "" + kind = "" + payload = "" + if isinstance(m, dict) and "tool_result" in m: + kind = "tool_result" + tr = m.get("tool_result") or {} + if isinstance(tr, dict): + payload = _pretty(tr) + else: + payload = str(tr) + elif isinstance(m, dict) and "tool_call" in m: + kind = "tool_call" + tc = m.get("tool_call") or {} + if isinstance(tc, dict): + payload = _pretty(tc) + else: + payload = str(tc) + else: + kind = "content" + if isinstance(m, dict): + payload = str(m.get("content", "") or "") + else: + payload = str(m) + nm_table.add_row( + str(i), + escape(_clip(role, limit=60)), + escape(_clip(kind, limit=60)), + escape(_clip(payload, limit=1200)), + ) + layout.add_row( + Panel( + nm_table, + title="[dim]Next Messages[/dim]", + border_style="bright_black", + ) + ) + + if agent_step.reflection: + layout.add_row( + Panel( + Text(_clip(agent_step.reflection, limit=1200), overflow="fold"), + title="[dim]Reflection[/dim]", + border_style="bright_black", + ) + ) + + if agent_step.error: + layout.add_row( + Panel( + Text(_clip(agent_step.error, limit=1200), overflow="fold"), + title="[dim]Step Error[/dim]", + border_style="red", + ) + ) + + self.console.print( + Panel( + layout, + title=title, + border_style="magenta", + ) + ) + @override def get_task_input(self) -> str | None: if self.mode != ConsoleMode.INTERACTIVE: diff --git a/code_agent/utils/cli/textual_console.py b/code_agent/utils/cli/textual_console.py index 28ae29f..169c0d2 100644 --- a/code_agent/utils/cli/textual_console.py +++ b/code_agent/utils/cli/textual_console.py @@ -1711,6 +1711,109 @@ def _build_progress_message_text( def print(self, message: str, color: str = "blue", bold: bool = False) -> None: self._write(self._build_message_text(message, color=color, bold=bold), plain=message[:80]) + def debug_step( + self, + agent_step: AgentStep, + agent_execution: AgentExecution | None = None, + *, + agent_type: str | None = None, + ) -> None: + if not self.is_debug: + return + + import json + + def _clip(text: str, limit: int = 1400) -> str: + t = (text or "").strip() + return t if len(t) <= limit else (t[: limit - 1] + "…") + + def _pretty(value) -> str: + if value is None: + return "" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, indent=2) + return str(value) + + lr = agent_step.llm_response + usage = lr.usage if lr else None + tool_calls = agent_step.tool_calls or (lr.tool_calls if lr else None) or [] + tool_results = agent_step.tool_results or [] + + head = f"DEBUG Step {agent_step.step_number}" + if agent_type: + head += f" • {agent_type}" + lines = [head] + lines.append(f"state: {getattr(agent_step.state, 'value', str(agent_step.state))}") + if lr and lr.model: + lines.append(f"model: {lr.model}") + if lr and lr.finish_reason: + lines.append(f"finish: {lr.finish_reason}") + if usage: + lines.append( + "tokens: " + f"in={usage.input_tokens} out={usage.output_tokens} " + f"cache_write={usage.cache_creation_input_tokens} cache_read={usage.cache_read_input_tokens} " + f"reasoning={usage.reasoning_tokens}" + ) + if agent_step.thought and agent_step.thought.strip(): + lines.append("thought:") + for ln in _clip(agent_step.thought, limit=1800).splitlines()[:60]: + lines.append(" " + ln) + if lr and (lr.content or "").strip(): + lines.append("llm_output:") + for ln in _clip(lr.content, limit=1800).splitlines()[:60]: + lines.append(" " + ln) + if tool_calls: + lines.append("tool_calls:") + for i, tc in enumerate(tool_calls, start=1): + args = _clip(_pretty(tc.arguments), limit=900) + lines.append(f" {i}. {tc.name} ({tc.call_id})") + if args: + for ln in args.splitlines()[:30]: + lines.append(" " + ln) + if tool_results: + lines.append("tool_results:") + for i, tr in enumerate(tool_results, start=1): + payload = tr.result if tr.success else (tr.error or tr.result or "") + payload = _clip(payload, limit=900) + ok = "OK" if tr.success else "FAIL" + lines.append(f" {i}. {tr.name} ({tr.call_id}) {ok}") + if payload: + for ln in payload.splitlines()[:30]: + lines.append(" " + ln) + next_messages = None + if isinstance(agent_step.extra, dict): + next_messages = agent_step.extra.get("next_messages") + if isinstance(next_messages, list) and next_messages: + lines.append("next_messages:") + for i, m in enumerate(next_messages, start=1): + if not isinstance(m, dict): + lines.append(f" {i}. {str(m)}") + continue + role = str(m.get("role", "")) + if "tool_result" in m: + lines.append(f" {i}. {role} tool_result") + payload = _clip(_pretty(m.get('tool_result')), limit=900) + elif "tool_call" in m: + lines.append(f" {i}. {role} tool_call") + payload = _clip(_pretty(m.get('tool_call')), limit=900) + else: + lines.append(f" {i}. {role} content") + payload = _clip(str(m.get("content", "") or ""), limit=900) + if payload: + for ln in payload.splitlines()[:30]: + lines.append(" " + ln) + if agent_step.reflection: + lines.append("reflection:") + for ln in _clip(agent_step.reflection, limit=1200).splitlines()[:40]: + lines.append(" " + ln) + if agent_step.error: + lines.append("step_error:") + for ln in _clip(agent_step.error, limit=1200).splitlines()[:40]: + lines.append(" " + ln) + + self.print("\n".join(lines), color="magenta", bold=False) + def clear_live_status(self) -> None: self._spinning = False self._current_state = None