Files
SneakyCode/app/models/message.py
Phillip Tarrant 91187a0728 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>
2026-03-11 08:37:22 -05:00

52 lines
1.7 KiB
Python

"""Message schema for LLM conversation history."""
from typing import Any, Literal
from pydantic import BaseModel, Field
from app.models.tool_call import ToolCall
class Message(BaseModel):
"""A single message in the conversation history.
Follows the OpenAI chat completions message format with support for
system, user, assistant, and tool roles.
"""
role: Literal["system", "user", "assistant", "tool"] = Field(
description="Role of the message sender"
)
content: str | None = Field(default=None, description="Text content of the message")
tool_calls: list[ToolCall] | None = Field(
default=None, description="Tool calls made by the assistant"
)
tool_call_id: str | None = Field(
default=None, description="ID of the tool call this message responds to (role=tool)"
)
name: str | None = Field(
default=None, description="Name of the tool that produced this message (role=tool)"
)
def to_api_dict(self) -> dict[str, Any]:
"""Serialize to a dict suitable for the OpenAI-compatible API.
Strips None-valued fields to keep the payload clean.
"""
data: dict[str, Any] = {"role": self.role}
# Ollama requires content to be a string, never null/missing —
# even on assistant messages that only contain tool_calls.
data["content"] = self.content or ""
if self.tool_calls is not None:
data["tool_calls"] = [tc.model_dump() for tc in self.tool_calls]
if self.tool_call_id is not None:
data["tool_call_id"] = self.tool_call_id
if self.name is not None:
data["name"] = self.name
return data