from __future__ import annotations from collections.abc import Callable, Sequence from enum import StrEnum from typing import Any, TypeAlias from pydantic import BaseModel, Field class Role(StrEnum): SYSTEM = "system" USER = "user" ASSISTANT = "assistant" TOOL = "tool" class Message(BaseModel): """A single message in the conversation.""" role: Role content: str | None = None tool_calls: list[ToolCall] | None = None tool_call_id: str | None = None name: str | None = None # Model reasoning / chain-of-thought text (OpenRouter); not sent back in API history. reasoning: str | None = None def to_api_dict(self) -> dict[str, Any]: """Convert to OpenAI-compatible API format.""" data: dict[str, Any] = {"role": self.role.value} if self.content is not None: data["content"] = self.content if self.tool_calls: data["tool_calls"] = [tc.to_api_dict() for tc in self.tool_calls] if self.tool_call_id: data["tool_call_id"] = self.tool_call_id if self.name: data["name"] = self.name return data CompletionGuard: TypeAlias = Callable[ [Message, Sequence[Message]], str | None, ] """Completion callback. Return ``None`` to accept an assistant response without tool calls as the final answer. Return feedback text to reject completion and continue the agent loop; the feedback is appended as a user message before the next iteration. The guard does not run for the forced final answer after ``max_iterations`` is reached. """ class ToolCall(BaseModel): """A tool invocation requested by the model.""" id: str name: str arguments: str # JSON string def to_api_dict(self) -> dict[str, Any]: return { "id": self.id, "type": "function", "function": { "name": self.name, "arguments": self.arguments, }, } class ToolDefinition(BaseModel): """OpenAI-compatible tool schema.""" name: str description: str parameters: dict[str, Any] = Field(default_factory=lambda: { "type": "object", "properties": {}, "required": [], }) def to_api_dict(self) -> dict[str, Any]: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, }, } class ToolResult(BaseModel): """Result of executing a tool.""" tool_call_id: str name: str content: str is_error: bool = False class SkillInfo(BaseModel): """Metadata and content for a loaded skill.""" name: str description: str content: str path: str class AgentEventType(StrEnum): """Events emitted during agent execution.""" THINKING = "thinking" TOOL_CALL = "tool_call" TOOL_RESULT = "tool_result" MESSAGE = "message" SKILL_LOADED = "skill_loaded" DONE = "done" ERROR = "error" class AgentEvent(BaseModel): """A streaming event from the agent loop.""" type: AgentEventType data: dict[str, Any] = Field(default_factory=dict) class AgentResult(BaseModel): """Final result of an agent run.""" content: str messages: list[Message] iterations: int tool_calls_made: int skills_used: list[str] = Field(default_factory=list)