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:
@@ -2,8 +2,7 @@
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Self
|
||||
from typing import Any, Self
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -101,11 +100,16 @@ class LLMClient:
|
||||
f"Model '{model}' not found. Available models: {available_str}"
|
||||
)
|
||||
|
||||
async def stream_chat(self, messages: list[Message]) -> AsyncIterator[dict]:
|
||||
async def stream_chat(
|
||||
self,
|
||||
messages: list[Message],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
) -> AsyncIterator[dict]:
|
||||
"""Stream a chat completion request, yielding parsed SSE chunks.
|
||||
|
||||
Args:
|
||||
messages: Conversation history to send to the model.
|
||||
tools: Optional OpenAI function-calling tool schemas.
|
||||
|
||||
Yields:
|
||||
Parsed JSON dicts from each SSE data line.
|
||||
@@ -115,7 +119,7 @@ class LLMClient:
|
||||
LLMResponseError: On non-2xx HTTP status.
|
||||
LLMStreamError: On malformed SSE data (only if every line fails).
|
||||
"""
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"model": self._config.model,
|
||||
"messages": [m.to_api_dict() for m in messages],
|
||||
"stream": True,
|
||||
@@ -123,6 +127,9 @@ class LLMClient:
|
||||
"max_tokens": self._config.max_tokens,
|
||||
}
|
||||
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
|
||||
try:
|
||||
async with self._client.stream(
|
||||
"POST", self._config.api_path, json=payload
|
||||
|
||||
Reference in New Issue
Block a user