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

1"""Agent contracts for the Lexigram framework. 

2 

3Exceptions, types, and protocols for agents, tools, and strategies. 

4""" 

5 

6from __future__ import annotations 

7 

8from dataclasses import dataclass, field 

9from enum import Enum 

10from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

11 

12from lexigram.contracts.exceptions.base import LexigramError 

13 

14if TYPE_CHECKING: 

15 from lexigram.contracts.core.module import CompiledModuleGraphProtocol 

16 from lexigram.contracts.core.result import Result 

17 

18 

19class AgentEventType(str, Enum): 

20 """Event types emitted during agent streaming execution.""" 

21 

22 STARTED = "started" 

23 """Agent execution has started.""" 

24 

25 THOUGHT = "thought" 

26 """Agent emitted a thought/reasoning step.""" 

27 

28 TOOL_START = "tool_start" 

29 """Tool execution is about to start.""" 

30 

31 TOOL_END = "tool_end" 

32 """Tool execution has completed.""" 

33 

34 MESSAGE = "message" 

35 """Agent emitted a message (intermediate or final).""" 

36 

37 ERROR = "error" 

38 """An error occurred during execution.""" 

39 

40 FINISHED = "finished" 

41 """Agent execution has finished.""" 

42 

43 

44@dataclass(frozen=True) 

45class AgentEvent: 

46 """Event emitted during agent streaming execution. 

47 

48 Attributes: 

49 type: The type of event. 

50 data: Event payload data. 

51 run_id: Unique identifier for this execution run. 

52 """ 

53 

54 type: AgentEventType 

55 """The type of event.""" 

56 

57 data: dict[str, Any] 

58 """Event payload data.""" 

59 

60 run_id: str 

61 """Unique identifier for this execution run.""" 

62 

63 

64# --------------------------------------------------------------------------- 

65# Section 1: Exceptions 

66# --------------------------------------------------------------------------- 

67 

68 

69class AgentError(LexigramError): 

70 """Base exception for all agent errors.""" 

71 

72 _code = "LEX_ERR_AGT_001" 

73 

74 def __init__(self, message: str = "Agent error", **kwargs: Any) -> None: 

75 super().__init__( 

76 message=message, 

77 **kwargs, 

78 ) 

79 

80 

81class ToolError(AgentError): 

82 """Base exception for tool errors.""" 

83 

84 _code = "LEX_ERR_AGT_002" 

85 

86 def __init__(self, message: str = "Tool error", **kwargs: Any) -> None: 

87 super().__init__( 

88 message=message, 

89 **kwargs, 

90 ) 

91 

92 

93class StrategyError(AgentError): 

94 """Reasoning strategy failed. 

95 

96 Raised when the agent's strategy encounters an error 

97 during reasoning (LLM failure, invalid response, etc.). 

98 """ 

99 

100 _code = "LEX_ERR_AGT_003" 

101 

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 ) 

117 

118 

119# --------------------------------------------------------------------------- 

120# Section 2: Types 

121# --------------------------------------------------------------------------- 

122 

123 

124@dataclass(frozen=True) 

125class AgentResponse: 

126 """Complete response from an agent execution. 

127 

128 Contains the final message, the full reasoning trace (steps), 

129 all tool calls made, token usage, cost, and timing metadata. 

130 

131 Note: ToolCall and ReasoningStep are defined in lexigram-ai-agents 

132 and imported here for use in this type's field annotations. 

133 """ 

134 

135 message: str 

136 """The agent's final response to the user.""" 

137 

138 steps: list[Any] = field(default_factory=list) 

139 """Full reasoning trace — every thought, action, and observation.""" 

140 

141 tool_calls: list[Any] = field(default_factory=list) 

142 """All tool invocations made during this execution.""" 

143 

144 total_tokens: int = 0 

145 """Total LLM tokens consumed (prompt + completion).""" 

146 

147 prompt_tokens: int = 0 

148 """Input (prompt) LLM tokens consumed. ``0`` when unknown.""" 

149 

150 completion_tokens: int = 0 

151 """Output (completion) LLM tokens consumed. ``0`` when unknown.""" 

152 

153 total_cost: float = 0.0 

154 """Estimated cost in USD.""" 

155 

156 duration_ms: float = 0.0 

157 """Total execution time in milliseconds.""" 

158 

159 session_id: str | None = None 

160 """Session identifier for multi-turn conversations.""" 

161 

162 metadata: dict[str, Any] = field(default_factory=dict) 

163 """Additional metadata (model name, strategy used, etc.).""" 

164 

165 @property 

166 def tool_call_count(self) -> int: 

167 """Number of tool calls made.""" 

168 return len(self.tool_calls) 

169 

170 @property 

171 def step_count(self) -> int: 

172 """Number of reasoning steps taken.""" 

173 return len(self.steps) 

174 

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)] 

179 

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)] 

184 

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 } 

199 

200 

201@dataclass(frozen=True) 

202class ToolDefinition: 

203 """Schema for a tool.""" 

204 

205 name: str 

206 description: str 

207 parameters: dict[str, Any] 

208 

209 

210@dataclass(frozen=True) 

211class ToolResult: 

212 """Result of tool execution.""" 

213 

214 success: bool 

215 output: str | None = None 

216 error: str | None = None 

217 metadata: dict[str, Any] | None = None 

218 

219 

220@dataclass(frozen=True) 

221class AgentExecutionContext: 

222 """Typed context for agent execution.""" 

223 

224 session_id: str | None = None 

225 tools: list[ToolDefinition] | None = None 

226 config: dict[str, Any] | None = None 

227 

228 

229# --------------------------------------------------------------------------- 

230# Section 3: Protocols 

231# --------------------------------------------------------------------------- 

232 

233 

234@runtime_checkable 

235class ToolProtocol(Protocol): 

236 """Protocol for agent tools. 

237 

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. 

241 

242 Satisfied by: 

243 - @tool decorated functions (FunctionTool) 

244 - Classes extending Tool base class 

245 """ 

246 

247 @property 

248 def name(self) -> str: 

249 """Unique tool identifier.""" 

250 ... 

251 

252 @property 

253 def description(self) -> str: 

254 """Human-readable description for the LLM.""" 

255 ... 

256 

257 @property 

258 def parameters_schema(self) -> dict[str, Any]: 

259 """JSON Schema describing the tool's parameters. 

260 

261 Auto-generated from type hints by the @tool decorator, 

262 or manually defined for class-based tools. 

263 

264 Format follows OpenAI function calling schema:: 

265 

266 { 

267 "type": "object", 

268 "properties": { 

269 "order_id": {"type": "string"}, 

270 "reason": {"type": "string"} 

271 }, 

272 "required": ["order_id"] 

273 } 

274 """ 

275 ... 

276 

277 async def execute(self, **kwargs: Any) -> Any: 

278 """Execute the tool with the given arguments. 

279 

280 Returns the tool's result. Errors should be raised as 

281 exceptions — the executor wraps them in Result. 

282 """ 

283 ... 

284 

285 

286@runtime_checkable 

287class AgentProtocol(Protocol): 

288 """Protocol for AI agents. 

289 

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 """ 

294 

295 @property 

296 def name(self) -> str: 

297 """Unique agent identifier.""" 

298 ... 

299 

300 @property 

301 def tools(self) -> list[ToolProtocol]: 

302 """Tools available to this agent.""" 

303 ... 

304 

305 @property 

306 def system_prompt(self) -> str: 

307 """System prompt defining the agent's persona and constraints.""" 

308 ... 

309 

310 

311@runtime_checkable 

312class StrategyProtocol(Protocol): 

313 """Protocol for agent reasoning strategies. 

314 

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 """ 

319 

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. 

329 

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.) 

337 

338 Returns: 

339 Result[AgentResponse, Exception] 

340 """ 

341 ... 

342 

343 

344@runtime_checkable 

345class AgentExecutorProtocol(Protocol): 

346 """Protocol for agent execution engines. 

347 

348 The executor runs an agent with governance checks, memory 

349 management, and observability integration. 

350 """ 

351 

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. 

361 

362 Returns Result[AgentResponse, AgentError]. 

363 """ 

364 ... 

365 

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. 

375 

376 Yields AgentEvent objects as the agent executes, enabling 

377 real-time monitoring of thoughts, tool calls, and messages. 

378 

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. 

385 

386 Yields: 

387 AgentEvent objects with type, data, and run_id. 

388 """ 

389 ... 

390 

391 

392@runtime_checkable 

393class ToolRegistryProtocol(Protocol): 

394 """Protocol for tool registries. 

395 

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 """ 

400 

401 def register( 

402 self, 

403 tool: ToolProtocol, 

404 module_class: type | None = None, 

405 ) -> None: 

406 """Register a tool.""" 

407 ... 

408 

409 def get(self, name: str) -> ToolProtocol | None: 

410 """Get a tool by name.""" 

411 ... 

412 

413 def list_tools(self) -> list[ToolProtocol]: 

414 """List all registered tools.""" 

415 ... 

416 

417 def list_visible_tools(self) -> list[ToolProtocol]: 

418 """List tools visible to the current caller module.""" 

419 ... 

420 

421 def list_visible_tool_names(self) -> list[str]: 

422 """List names of tools visible to the current caller module.""" 

423 ... 

424 

425 def set_module_graph(self, graph: CompiledModuleGraphProtocol | None) -> None: 

426 """Set the compiled module graph for visibility enforcement.""" 

427 ... 

428 

429 def set_caller_module(self, module_class: type | None) -> None: 

430 """Set the calling module for visibility checks.""" 

431 ... 

432 

433 async def execute( 

434 self, 

435 name: str, 

436 **kwargs: Any, 

437 ) -> Result[Any, ToolError]: 

438 """Execute a tool by name. 

439 

440 Returns Result[Any, ToolError]. 

441 """ 

442 ... 

443 

444 

445@runtime_checkable 

446class MemoryProtocol(Protocol): 

447 """Protocol for conversation memory.""" 

448 

449 async def add_message(self, message: Any) -> None: 

450 """Add a message to memory. 

451 

452 Args: 

453 message: Message to add. 

454 """ 

455 ... 

456 

457 async def get_messages(self) -> list[Any]: 

458 """Get all messages from memory. 

459 

460 Returns: 

461 List of messages. 

462 """ 

463 ... 

464 

465 async def clear(self) -> None: 

466 """Clear all messages from memory.""" 

467 ... 

468 

469 

470@runtime_checkable 

471class AgentStrategyProtocol(Protocol): 

472 """Protocol for pluggable agent reasoning strategies. 

473 

474 Implementations encode a particular reasoning loop (ReAct, 

475 Plan-and-Execute, Chain-of-Thought, Reflexion, etc.). 

476 """ 

477 

478 @property 

479 def name(self) -> str: 

480 """Human-readable strategy identifier.""" 

481 ... 

482 

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. 

489 

490 Args: 

491 objective: Top-level task description. 

492 context: Agent execution context (tools, memory, config). 

493 

494 Returns: 

495 Strategy-specific result object. 

496 """ 

497 ... 

498 

499 

500@runtime_checkable 

501class SkillComposerProtocol(Protocol): 

502 """Protocol for composing multiple skills or tools into a pipeline.""" 

503 

504 async def get_tools(self) -> list[Any]: 

505 """Return all tools provided by composed skills.""" 

506 ... 

507 

508 

509class RunnableAgentProtocol(Protocol): 

510 """Protocol for runnable agents (G-05 parity).""" 

511 

512 async def plan(self, input: str) -> str: 

513 """Plan the next steps.""" 

514 ... 

515 

516 async def execute(self, plan: str) -> str: 

517 """Execute the plan.""" 

518 ... 

519 

520 

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]