"""The tool invocation seam (§8.4) — the one interface the TaskRunner calls. The agent layer stays tool-agnostic (INV-7) by invoking every tool through this protocol, never importing a tool module directly. The **tool team owns the implementation** (KM-418); this file defines only the contract the TaskRunner depends on. Frozen guarantees the implementation must hold: 1. **Never throws.** A tool failure returns `ToolOutput(kind="error", error=...)`, not an exception — the TaskRunner's degrade-and-continue (§7.4) relies on this. (The TaskRunner still wraps calls defensively, as a backstop.) 2. **Returns the `ToolOutput` envelope** (§8.1) — structured data only, never rendered tables or prose (that is the Assembler's job). 3. **`tool_name` comes from the registry** (§9.2); unknown names return an error envelope rather than throwing. """ from __future__ import annotations from typing import Any, Protocol, runtime_checkable from ..planner.contracts import ToolOutput @runtime_checkable class ToolInvoker(Protocol): async def invoke(self, tool_name: str, args: dict[str, Any]) -> ToolOutput: ...