Coverage for src / lexigram / contracts / ai / agents.py: 2%
146 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Agent contracts for the Lexigram framework.
3Exceptions, types, and protocols for agents, tools, and strategies.
4"""
6from __future__ import annotations
8from dataclasses import dataclass, field
9from enum import Enum
10from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
12from lexigram.contracts.exceptions.base import LexigramError
14if TYPE_CHECKING:
15 from lexigram.contracts.core.module import CompiledModuleGraphProtocol
16 from lexigram.contracts.core.result import Result
19class AgentEventType(str, Enum):
20 """Event types emitted during agent streaming execution."""
22 STARTED = "started"
23 """Agent execution has started."""
25 THOUGHT = "thought"
26 """Agent emitted a thought/reasoning step."""
28 TOOL_START = "tool_start"
29 """Tool execution is about to start."""
31 TOOL_END = "tool_end"
32 """Tool execution has completed."""
34 MESSAGE = "message"
35 """Agent emitted a message (intermediate or final)."""
37 ERROR = "error"
38 """An error occurred during execution."""
40 FINISHED = "finished"
41 """Agent execution has finished."""
44@dataclass(frozen=True)
45class AgentEvent:
46 """Event emitted during agent streaming execution.
48 Attributes:
49 type: The type of event.
50 data: Event payload data.
51 run_id: Unique identifier for this execution run.
52 """
54 type: AgentEventType
55 """The type of event."""
57 data: dict[str, Any]
58 """Event payload data."""
60 run_id: str
61 """Unique identifier for this execution run."""
64# ---------------------------------------------------------------------------
65# Section 1: Exceptions
66# ---------------------------------------------------------------------------
69class AgentError(LexigramError):
70 """Base exception for all agent errors."""
72 _code = "LEX_ERR_AGT_001"
74 def __init__(self, message: str = "Agent error", **kwargs: Any) -> None:
75 super().__init__(
76 message=message,
77 **kwargs,
78 )
81class ToolError(AgentError):
82 """Base exception for tool errors."""
84 _code = "LEX_ERR_AGT_002"
86 def __init__(self, message: str = "Tool error", **kwargs: Any) -> None:
87 super().__init__(
88 message=message,
89 **kwargs,
90 )
93class StrategyError(AgentError):
94 """Reasoning strategy failed.
96 Raised when the agent's strategy encounters an error
97 during reasoning (LLM failure, invalid response, etc.).
98 """
100 _code = "LEX_ERR_AGT_003"
102 def __init__(
103 self,
104 message: str = "Strategy execution failed",
105 *,
106 strategy_name: str | None = None,
107 **kwargs: Any,
108 ) -> None:
109 details = kwargs.pop("details", {})
110 if strategy_name:
111 details["strategy"] = strategy_name
112 super().__init__(
113 message=message,
114 details=details,
115 **kwargs,
116 )
119# ---------------------------------------------------------------------------
120# Section 2: Types
121# ---------------------------------------------------------------------------
124@dataclass(frozen=True)
125class AgentResponse:
126 """Complete response from an agent execution.
128 Contains the final message, the full reasoning trace (steps),
129 all tool calls made, token usage, cost, and timing metadata.
131 Note: ToolCall and ReasoningStep are defined in lexigram-ai-agents
132 and imported here for use in this type's field annotations.
133 """
135 message: str
136 """The agent's final response to the user."""
138 steps: list[Any] = field(default_factory=list)
139 """Full reasoning trace — every thought, action, and observation."""
141 tool_calls: list[Any] = field(default_factory=list)
142 """All tool invocations made during this execution."""
144 total_tokens: int = 0
145 """Total LLM tokens consumed (prompt + completion)."""
147 prompt_tokens: int = 0
148 """Input (prompt) LLM tokens consumed. ``0`` when unknown."""
150 completion_tokens: int = 0
151 """Output (completion) LLM tokens consumed. ``0`` when unknown."""
153 total_cost: float = 0.0
154 """Estimated cost in USD."""
156 duration_ms: float = 0.0
157 """Total execution time in milliseconds."""
159 session_id: str | None = None
160 """Session identifier for multi-turn conversations."""
162 metadata: dict[str, Any] = field(default_factory=dict)
163 """Additional metadata (model name, strategy used, etc.)."""
165 @property
166 def tool_call_count(self) -> int:
167 """Number of tool calls made."""
168 return len(self.tool_calls)
170 @property
171 def step_count(self) -> int:
172 """Number of reasoning steps taken."""
173 return len(self.steps)
175 @property
176 def successful_tool_calls(self) -> list[Any]:
177 """Tool calls that completed without error."""
178 return [tc for tc in self.tool_calls if getattr(tc, "succeeded", True)]
180 @property
181 def failed_tool_calls(self) -> list[Any]:
182 """Tool calls that failed."""
183 return [tc for tc in self.tool_calls if not getattr(tc, "succeeded", True)]
185 def to_dict(self) -> dict[str, Any]:
186 """Serialize to a JSON-compatible dict."""
187 return {
188 "message": self.message,
189 "steps": len(self.steps),
190 "tool_calls": len(self.tool_calls),
191 "total_tokens": self.total_tokens,
192 "prompt_tokens": self.prompt_tokens,
193 "completion_tokens": self.completion_tokens,
194 "total_cost": self.total_cost,
195 "duration_ms": self.duration_ms,
196 "session_id": self.session_id,
197 "metadata": self.metadata,
198 }
201@dataclass(frozen=True)
202class ToolDefinition:
203 """Schema for a tool."""
205 name: str
206 description: str
207 parameters: dict[str, Any]
210@dataclass(frozen=True)
211class ToolResult:
212 """Result of tool execution."""
214 success: bool
215 output: str | None = None
216 error: str | None = None
217 metadata: dict[str, Any] | None = None
220@dataclass(frozen=True)
221class AgentExecutionContext:
222 """Typed context for agent execution."""
224 session_id: str | None = None
225 tools: list[ToolDefinition] | None = None
226 config: dict[str, Any] | None = None
229# ---------------------------------------------------------------------------
230# Section 3: Protocols
231# ---------------------------------------------------------------------------
234@runtime_checkable
235class ToolProtocol(Protocol):
236 """Protocol for agent tools.
238 Tools are the atomic capabilities an agent can invoke during
239 reasoning. Each tool has a name, description, JSON parameter
240 schema (for LLM function calling), and an async execute method.
242 Satisfied by:
243 - @tool decorated functions (FunctionTool)
244 - Classes extending Tool base class
245 """
247 @property
248 def name(self) -> str:
249 """Unique tool identifier."""
250 ...
252 @property
253 def description(self) -> str:
254 """Human-readable description for the LLM."""
255 ...
257 @property
258 def parameters_schema(self) -> dict[str, Any]:
259 """JSON Schema describing the tool's parameters.
261 Auto-generated from type hints by the @tool decorator,
262 or manually defined for class-based tools.
264 Format follows OpenAI function calling schema::
266 {
267 "type": "object",
268 "properties": {
269 "order_id": {"type": "string"},
270 "reason": {"type": "string"}
271 },
272 "required": ["order_id"]
273 }
274 """
275 ...
277 async def execute(self, **kwargs: Any) -> Any:
278 """Execute the tool with the given arguments.
280 Returns the tool's result. Errors should be raised as
281 exceptions — the executor wraps them in Result.
282 """
283 ...
286@runtime_checkable
287class AgentProtocol(Protocol):
288 """Protocol for AI agents.
290 An agent declares its identity, capabilities (tools), and
291 persona (system prompt). The AgentExecutor uses this
292 protocol to drive the reasoning loop.
293 """
295 @property
296 def name(self) -> str:
297 """Unique agent identifier."""
298 ...
300 @property
301 def tools(self) -> list[ToolProtocol]:
302 """Tools available to this agent."""
303 ...
305 @property
306 def system_prompt(self) -> str:
307 """System prompt defining the agent's persona and constraints."""
308 ...
311@runtime_checkable
312class StrategyProtocol(Protocol):
313 """Protocol for agent reasoning strategies.
315 A strategy implements the reasoning loop that drives an agent's
316 behavior. Built-in strategies: ReActStrategy (reason → act →
317 observe) and PlanAndExecuteStrategy (plan → execute steps).
318 """
320 async def execute(
321 self,
322 message: str,
323 tools: list[ToolProtocol],
324 history: list[dict[str, Any]],
325 llm: Any,
326 **kwargs: Any,
327 ) -> Any:
328 """Execute the reasoning strategy.
330 Args:
331 message: The user's input message.
332 tools: Tools available to the agent.
333 history: Conversation history as list of message dicts.
334 llm: LLM client for reasoning.
335 **kwargs: Additional strategy-specific parameters
336 (system_prompt, temperature, tool_registry, etc.)
338 Returns:
339 Result[AgentResponse, Exception]
340 """
341 ...
344@runtime_checkable
345class AgentExecutorProtocol(Protocol):
346 """Protocol for agent execution engines.
348 The executor runs an agent with governance checks, memory
349 management, and observability integration.
350 """
352 async def run(
353 self,
354 agent: AgentProtocol,
355 message: str,
356 session_id: str | None = None,
357 user_id: str | None = None,
358 **kwargs: Any,
359 ) -> Any:
360 """Execute an agent and return the response.
362 Returns Result[AgentResponse, AgentError].
363 """
364 ...
366 async def astream(
367 self,
368 agent: AgentProtocol,
369 message: str,
370 session_id: str | None = None,
371 user_id: str | None = None,
372 **kwargs: Any,
373 ) -> Any:
374 """Stream agent execution events.
376 Yields AgentEvent objects as the agent executes, enabling
377 real-time monitoring of thoughts, tool calls, and messages.
379 Args:
380 agent: The agent to execute.
381 message: User's input message.
382 session_id: Session ID for multi-turn memory.
383 user_id: User ID for governance tracking.
384 **kwargs: Additional parameters passed to the strategy.
386 Yields:
387 AgentEvent objects with type, data, and run_id.
388 """
389 ...
392@runtime_checkable
393class ToolRegistryProtocol(Protocol):
394 """Protocol for tool registries.
396 A registry stores tools by name and provides execution with
397 error handling. When module visibility is enabled, tool access
398 is checked against the compiled module graph.
399 """
401 def register(
402 self,
403 tool: ToolProtocol,
404 module_class: type | None = None,
405 ) -> None:
406 """Register a tool."""
407 ...
409 def get(self, name: str) -> ToolProtocol | None:
410 """Get a tool by name."""
411 ...
413 def list_tools(self) -> list[ToolProtocol]:
414 """List all registered tools."""
415 ...
417 def list_visible_tools(self) -> list[ToolProtocol]:
418 """List tools visible to the current caller module."""
419 ...
421 def list_visible_tool_names(self) -> list[str]:
422 """List names of tools visible to the current caller module."""
423 ...
425 def set_module_graph(self, graph: CompiledModuleGraphProtocol | None) -> None:
426 """Set the compiled module graph for visibility enforcement."""
427 ...
429 def set_caller_module(self, module_class: type | None) -> None:
430 """Set the calling module for visibility checks."""
431 ...
433 async def execute(
434 self,
435 name: str,
436 **kwargs: Any,
437 ) -> Result[Any, ToolError]:
438 """Execute a tool by name.
440 Returns Result[Any, ToolError].
441 """
442 ...
445@runtime_checkable
446class MemoryProtocol(Protocol):
447 """Protocol for conversation memory."""
449 async def add_message(self, message: Any) -> None:
450 """Add a message to memory.
452 Args:
453 message: Message to add.
454 """
455 ...
457 async def get_messages(self) -> list[Any]:
458 """Get all messages from memory.
460 Returns:
461 List of messages.
462 """
463 ...
465 async def clear(self) -> None:
466 """Clear all messages from memory."""
467 ...
470@runtime_checkable
471class AgentStrategyProtocol(Protocol):
472 """Protocol for pluggable agent reasoning strategies.
474 Implementations encode a particular reasoning loop (ReAct,
475 Plan-and-Execute, Chain-of-Thought, Reflexion, etc.).
476 """
478 @property
479 def name(self) -> str:
480 """Human-readable strategy identifier."""
481 ...
483 async def run(
484 self,
485 objective: str,
486 context: Any,
487 ) -> Result[Any, ToolError]:
488 """Execute the reasoning loop for the given objective.
490 Args:
491 objective: Top-level task description.
492 context: Agent execution context (tools, memory, config).
494 Returns:
495 Strategy-specific result object.
496 """
497 ...
500@runtime_checkable
501class SkillComposerProtocol(Protocol):
502 """Protocol for composing multiple skills or tools into a pipeline."""
504 async def get_tools(self) -> list[Any]:
505 """Return all tools provided by composed skills."""
506 ...
509class RunnableAgentProtocol(Protocol):
510 """Protocol for runnable agents (G-05 parity)."""
512 async def plan(self, input: str) -> str:
513 """Plan the next steps."""
514 ...
516 async def execute(self, plan: str) -> str:
517 """Execute the plan."""
518 ...
521__all__ = [
522 "AgentError",
523 "AgentEvent",
524 "AgentEventType",
525 "AgentExecutionContext",
526 "AgentExecutorProtocol",
527 "AgentProtocol",
528 "AgentResponse",
529 "AgentStrategyProtocol",
530 "MemoryProtocol",
531 "RunnableAgentProtocol",
532 "SkillComposerProtocol",
533 "StrategyError",
534 "StrategyProtocol",
535 "ToolDefinition",
536 "ToolError",
537 "ToolProtocol",
538 "ToolRegistryProtocol",
539 "ToolResult",
540]