Add Phase 5: ReAct-style agent loop with tool execution

Implement the core autonomy layer — AgentLoop streams LLM responses,
parses tool calls, executes them with permission checks, feeds results
back, and repeats until the task completes or finish is called.

- Add FinishTool for explicit loop termination
- Add tools parameter to LLMClient.stream_chat() for function calling
- Add compact tool result display (status line, not full output)
- Refactor REPL to delegate to AgentLoop.run_turn()
- Fix Ollama null content rejection (always send content as string)
- Add finish to auto_approve permissions
- 9 unit tests for agent loop (34 total, zero regressions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 08:37:22 -05:00
parent 501bf5c45b
commit 91187a0728
10 changed files with 609 additions and 54 deletions

View File

@@ -61,8 +61,33 @@ def print_assistant_message(content: str) -> None:
def print_tool_call(name: str, args: str) -> None:
"""Print a tool call summary (stub for Phase 4)."""
console.print(f"[tool]Tool: {name}[/tool] [dim]{args}[/dim]")
"""Print a compact tool call line — tool name + truncated key args."""
truncated_args = args[:80] + "..." if len(args) > 80 else args
console.print(f" [tool]{name}[/tool] [dim]{truncated_args}[/dim]")
def print_tool_result(name: str, output: str, is_error: bool = False) -> None:
"""Print a compact tool result — status line only for success, detail for errors.
Args:
name: Tool name.
output: Tool output or error message.
is_error: Whether this is an error result.
"""
if is_error:
# Errors are shown prominently so the user knows something went wrong
truncated = output[:200] + "..." if len(output) > 200 else output
console.print(f" [error]{name}: {truncated}[/error]")
else:
# Success: just show a compact byte/line summary
lines = output.count("\n") + 1 if output else 0
chars = len(output)
console.print(f" [dim]{name}{lines} lines, {chars} chars[/dim]")
def print_iteration_header(iteration: int, max_iterations: int) -> None:
"""Print the current agent loop iteration."""
console.print(f"[dim]── iteration {iteration}/{max_iterations} ──[/dim]")
def print_token_usage(usage_tokens: int, budget: int) -> None: