Tools Design — External Integration Layer (Revision 3)
Revision note: One change from Revision 2: the Tool Registry's class: field moves from a dotted-import-path string to a name resolved against an explicit TOOL_CLASS_MAP, matching the resolution strategy skill-factory-design.md established for Skills — see §4 below. This corrects an inconsistency between how Tools and Skills resolved the identical "registry entry names a Python class" problem; there was no principled reason for the two to differ. Everything else — the Tool contract, error handling, testing approach, LLMTool removal — is unchanged from Revision 2.
1. Purpose (Unchanged, Scope Narrowed)
A Tool wraps exactly one external, non-LLM capability behind a typed interface: a data API, a database read, a file operation. Tools contain no business logic and no routing logic, and — as of this revision — no inference logic either. The Tools layer is now purely about external data integrations; reasoning is entirely the LLM Service's domain.
2. Tool Contract (Unchanged)
BaseTool (abstract):
name: str
async def call(**kwargs) -> ToolResult
ToolResult:
data: Any
success: bool
error: str | None
latency_ms: float
source: str
3. Initial Tools (Phase 1, Updated — LLMTool Removed)
| Tool | Wraps | Notes |
|---|---|---|
CompanyProfileTool |
Company fundamentals/profile API | Ticker → structured profile |
FinancialMetricsTool |
Financial statement/metrics API | Ticker → normalized metrics |
NewsTool |
News API | Ticker/company → recent headlines with dates and sources |
SECFilingsTool |
SEC EDGAR (or equivalent) | Ticker → recent filing metadata/links |
~~LLMTool~~ — removed. Inference now flows Skill → LLMService → llm/factory.py → provider SDK, with no Tool in the path. See llm-service-design.md for where this logic now lives.
4. Tool Registry (Updated Location and Contents)
config/tools/registry.yaml (moved from backend/src/tools/registry.yaml):
tools:
company_profile_tool:
class: CompanyProfileTool
config_key: company_data_provider
financial_metrics_tool:
class: FinancialMetricsTool
config_key: financial_data_provider
news_tool:
class: NewsTool
config_key: news_provider
sec_filings_tool:
class: SECFilingsTool
config_key: sec_provider
class: is a name resolved against an explicit TOOL_CLASS_MAP, not a dotted import path. This corrects an inconsistency with skill-factory-design.md §5, which resolves the identical problem (a registry entry naming a Python class) via a hand-maintained dict in backend/src/tools/registry_loader.py, specifically rejecting importlib-style string-based dynamic import — string-based import is functionally equivalent to the plugin auto-discovery architecture-overview.md §16 explicitly rules out, just triggered by YAML instead of a filesystem scan. There's no principled reason for Tools to resolve their class: field differently from how Skills do, so this revision brings them in line:
# backend/src/tools/registry_loader.py
TOOL_CLASS_MAP: dict[str, type[BaseTool]] = {
"CompanyProfileTool": CompanyProfileTool,
"FinancialMetricsTool": FinancialMetricsTool,
"NewsTool": NewsTool,
"SECFilingsTool": SECFilingsTool,
}
Adding a Tool still requires: (1) write the Tool class, (2) add one line to TOOL_CLASS_MAP, (3) add the registry entry — the same three-step shape as adding a Skill via the Skill Factory. A registry entry referencing a class absent from TOOL_CLASS_MAP fails fast at startup, consistent with every other config/ subtree's fail-fast validation.
config_key still points into backend/src/config/settings.py, unchanged from v1.
5. Independence and Testability (Unchanged)
Each Tool is independently unit-testable with mocked HTTP clients; a tagged integration suite exercises real/sandboxed APIs separately. Dependency direction remains strictly one-way: Skills → Tools, never the reverse, and now, never Skills → LLM through a Tool either.
6. Error Handling Philosophy (Unchanged)
Tools fail loud in logs, quiet in contract (ToolResult(success=False, error=...)), never raising past their boundary.
7. Anti-Patterns Explicitly Rejected (Updated)
- ❌ A Tool making a routing decision.
- ❌ A Tool holding conversation/session state.
- ❌ Skills bypassing the Tool Registry to instantiate a Tool directly.
- ❌ Reintroducing an
LLMToolto keep "all invocable things in one registry" — this was identified in review as conflating a platform capability (reasoning) with an external integration (data fetch), and is the specific anti-pattern this revision corrects. If a future need arises to swap LLM providers dynamically per-request, that logic belongs inLLMService's model-selection policy, not in a resurrected Tool.