Skip to content
← How we build

architecture

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 LLMTool to 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 in LLMService's model-selection policy, not in a resurrected Tool.

Source: docs/architecture/tools-design.md

Follow the work.

Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.

No spam. Unsubscribe anytime.