multi-agent-lab / docs /architecture /tool-contract.md
agharsallah
feat: update model endpoint URLs and enhance documentation with mermaid diagrams
2a8eab2
|
Raw
History Blame Contribute Delete
4.4 kB
# Tool Contract
Tools are the fourth stable contract. An agent never holds a tool directly; it
asks the `ToolRegistry`, which enforces the agent's capability grant before
dispatching. "The Artist gets image-gen, the Critic does not" is enforced by the
runtime, not by convention.
## Shape
A tool is a `(name, description, run)` triple. `run(**params)` returns a
JSON-serialisable dict that the calling agent folds into its emitted event.
```python
registry = ToolRegistry()
registry.register("oracle", "Draw a cryptic omen. Params: {seed: str}.", oracle_fn)
```
## Capability enforcement
```python
registry.call(agent_name, manifest, tool, params)
# raises CapabilityViolation if tool not in manifest.tools
# raises KeyError if tool is granted but not registered
# else -> tool's dict result
```
`ManifestAgent` wraps this as `self.call_tool(name, **params)` and injects the
descriptions of granted tools into the prompt (`AVAILABLE TOOLS` block).
## Worked example: the oracle path
`oracle-grove` pairs a tool-using agent with a tool-less one:
```
fortune-teller handler: fortune-teller tools: [oracle] emits: oracle.spoke
scene-whisperer (generic) tools: [] emits: world.observed
```
`FortuneTeller` (`src/agents/handlers.py`) calls the deterministic `oracle` tool,
weaves the omen into its prompt, and records the omen on the event payload β€” so
the tool output is first-class ledger data. `scene-whisperer` has no grant, so a
call would raise `CapabilityViolation`. `tests/test_tools.py` proves both.
## Transports: in-process and MCP (ADR-0017, realized)
The `(name, description, run)` interface fronts an in-process callable **and** a
tool running out-of-process over the Model Context Protocol (MCP) β€” same contract,
swappable transport, invisible to agents. The capability check is the security
boundary; MCP is only transport.
```mermaid
flowchart TD
A["agent.call_tool(name, params)"] --> B["ToolRegistry.call()"]
B --> C{"name in manifest.tools?"}
C -->|no| X["raise CapabilityViolation"]
C -->|yes| D{"registered in-process?"}
D -->|yes| E["run() β†’ dict (default)"]
D -->|no| F{"resolver has it?"}
F -->|yes| G["MCP client β†’ stdio server β†’ dict"]
F -->|no| Y["raise KeyError"]
```
The capability check (node C) runs **first** β€” before any transport is touched β€”
so swapping in-process for MCP never weakens the security boundary.
`ToolRegistry.call(...)` enforces `tool in manifest.tools` and raises
`CapabilityViolation` on a denied call **before any transport is touched** β€” then
dispatches in-process if the tool is registered locally, otherwise to an attached
`ToolResolver` (the MCP client). `describe()` prefers in-process descriptions and
falls back to the resolver's, so prompt assembly is identical across transports.
### Config gate
`default_tool_registry()` is in-process by default. Set one of these to resolve
granted tools over MCP instead (the grant check is unchanged either way):
- `MCP_SERVERS` β€” `::`-separated stdio command lines, e.g.
`MCP_SERVERS="python -m src.tools.mcp_server"` or
`MCP_SERVERS="python -m src.tools.mcp_server :: node other-server.js"`.
- `MCP_ORACLE=1` β€” shorthand for the built-in oracle server
(`python -m src.tools.mcp_server`); ignored when `MCP_SERVERS` is set.
With neither set the registry stays fully in-process β€” the offline default the
test-suite exercises. `mcp` is an optional extra (`pip install -e '.[mcp]'`) and
is imported lazily, so `import src.*` and `import app` work with it not installed.
The server exposes the *same* `oracle` implementation, so for a given seed the
omen drawn over MCP is byte-identical to the in-process one: `oracle-grove`
produces the same ledger in both modes.
## Code
- `src/tools/registry.py` β€” `ToolRegistry`, `ToolSpec`, `CapabilityViolation`, `ToolResolver`
- `src/tools/builtins.py` β€” `oracle`, `default_tool_registry()` (MCP config gate)
- `src/tools/mcp_server.py` β€” FastMCP stdio server exposing the built-in tools
- `src/tools/mcp_client.py` β€” `MCPToolClient`, `MCPResolver`, env gate (`mcp_resolver_from_env`)
- `src/agents/handlers.py` β€” `FortuneTeller` (handler that calls a tool)
- `config/agents/fortune-teller.yaml`, `config/scenarios/oracle-grove.yaml`
- `tests/test_tools.py` (in-process), `tests/test_mcp.py` (transport + capability)