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:
53
app/main.py
53
app/main.py
@@ -8,16 +8,18 @@ from pathlib import Path
|
||||
import structlog
|
||||
|
||||
from app.agent.context import SessionContext
|
||||
from app.agent.loop import AgentLoop
|
||||
from app.models.config import AppConfig, load_config
|
||||
from app.services.llm import LLMClient, LLMConnectionError, LLMError
|
||||
from app.services.permissions import PermissionsService
|
||||
from app.services.streaming import StreamHandler
|
||||
from app.tools.registry import create_default_registry
|
||||
from app.utils.display import (
|
||||
print_banner,
|
||||
print_error,
|
||||
print_history,
|
||||
print_info,
|
||||
print_success,
|
||||
print_token_usage,
|
||||
print_user_message,
|
||||
print_warning,
|
||||
)
|
||||
@@ -73,8 +75,12 @@ async def _run_repl(
|
||||
config: Application configuration.
|
||||
logger: Structured logger instance.
|
||||
"""
|
||||
registry = create_default_registry(config.agent.workspace_root, config)
|
||||
permissions = PermissionsService(config.permissions)
|
||||
|
||||
async with LLMClient(config.llm) as client:
|
||||
handler = StreamHandler(config.display)
|
||||
agent = AgentLoop(config, ctx, client, handler, registry, permissions)
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -102,50 +108,9 @@ async def _run_repl(
|
||||
print_warning(f"Unknown command: {user_input}")
|
||||
continue
|
||||
|
||||
# Add user message and display it
|
||||
ctx.add_message("user", user_input)
|
||||
print_user_message(user_input)
|
||||
|
||||
# Stream LLM response
|
||||
try:
|
||||
chunk_iter = client.stream_chat(ctx.get_history())
|
||||
assistant_msg = await handler.process_stream(chunk_iter)
|
||||
except KeyboardInterrupt:
|
||||
print_warning("Response interrupted.")
|
||||
handler.reset()
|
||||
continue
|
||||
except LLMConnectionError as e:
|
||||
print_error(f"Connection error: {e}")
|
||||
continue
|
||||
except LLMError as e:
|
||||
print_error(f"LLM error: {e}")
|
||||
continue
|
||||
|
||||
# Handle empty response
|
||||
if assistant_msg.content is None and assistant_msg.tool_calls is None:
|
||||
print_warning("Received empty response from model.")
|
||||
|
||||
# Record assistant message in history
|
||||
ctx.add_message(
|
||||
"assistant",
|
||||
assistant_msg.content,
|
||||
tool_calls=assistant_msg.tool_calls,
|
||||
)
|
||||
|
||||
# Record API token usage if available, fall back to heuristic
|
||||
if handler.usage:
|
||||
ctx.token_counter.count_usage(handler.usage)
|
||||
|
||||
# Show token usage if configured
|
||||
if config.display.show_token_usage:
|
||||
print_token_usage(
|
||||
ctx.token_counter.cumulative_usage.total_tokens
|
||||
or ctx.estimated_tokens,
|
||||
ctx.token_counter.budget,
|
||||
)
|
||||
|
||||
handler.reset()
|
||||
logger.debug("message_exchanged", message_count=ctx.message_count)
|
||||
await agent.run_turn(user_input)
|
||||
logger.debug("turn_complete", message_count=ctx.message_count)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user