Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 147 additions & 1 deletion code_agent/agent/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion code_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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",
):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading