Source code for ffai.agent.agent_result

# Copyright (c) 2025 Antonio Quinonez / Far Finer LLC
# SPDX-License-Identifier: MIT
# Contact: antquinonez@farfiner.com

"""Data structures for agentic execution results."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Literal


[docs] @dataclass class ToolCallRecord: """Record of a single tool call within an agentic loop. Attributes: round: The round number (1-indexed) when this call was made. tool_name: Name of the tool that was called. tool_call_id: Provider-specific ID for the tool call. arguments: Arguments passed to the tool. result: The tool's return value as a string. duration_ms: Wall-clock duration of tool execution in milliseconds. error: Error message if the tool execution failed, None otherwise. """ round: int tool_name: str tool_call_id: str = "" arguments: dict[str, Any] = field(default_factory=dict) result: str = "" duration_ms: float = 0.0 error: str | None = None
[docs] def to_dict(self) -> dict[str, Any]: """Convert to dictionary for serialization.""" return { "round": self.round, "tool_name": self.tool_name, "tool_call_id": self.tool_call_id, "arguments": self.arguments, "result": self.result, "duration_ms": self.duration_ms, "error": self.error, }
[docs] @classmethod def from_dict(cls, data: dict[str, Any]) -> ToolCallRecord: """Create from dictionary.""" return cls( round=data.get("round", 0), tool_name=data.get("tool_name", ""), tool_call_id=data.get("tool_call_id", ""), arguments=data.get("arguments", {}), result=data.get("result", ""), duration_ms=data.get("duration_ms", 0.0), error=data.get("error"), )
AgentStatus = Literal["success", "failed", "max_rounds_exceeded"]
[docs] @dataclass class AgentResult: """Result of an agentic execution loop. Attributes: response: The final response text from the LLM. tool_calls: List of tool call records from all rounds. total_rounds: Number of rounds executed. total_llm_calls: Total number of LLM API calls made. status: Execution status - "success", "failed", or "max_rounds_exceeded". """ response: str = "" tool_calls: list[ToolCallRecord] = field(default_factory=list) total_rounds: int = 0 total_llm_calls: int = 0 status: AgentStatus = "success" @property def tool_calls_count(self) -> int: """Total number of tool calls made.""" return len(self.tool_calls) @property def last_tool_name(self) -> str: """Name of the last tool called, or empty string if no tools used.""" if self.tool_calls: return self.tool_calls[-1].tool_name return "" @property def failed_tool_calls(self) -> list[ToolCallRecord]: """List of tool calls that resulted in errors.""" return [tc for tc in self.tool_calls if tc.error is not None]
[docs] def to_dict(self) -> dict[str, Any]: """Convert to dictionary for serialization.""" return { "response": self.response, "tool_calls": [tc.to_dict() for tc in self.tool_calls], "total_rounds": self.total_rounds, "total_llm_calls": self.total_llm_calls, "status": self.status, }
[docs] @classmethod def from_dict(cls, data: dict[str, Any]) -> AgentResult: """Create from dictionary.""" tool_calls = [ToolCallRecord.from_dict(tc) for tc in data.get("tool_calls", [])] return cls( response=data.get("response", ""), tool_calls=tool_calls, total_rounds=data.get("total_rounds", 0), total_llm_calls=data.get("total_llm_calls", 0), status=data.get("status", "success"), )