Skip to content
← How we build

adr

ADR-003: Separate Skills Layer from Tools Layer

Status

Accepted

Context

Phase 1 must decide whether "business capability" (e.g. "summarize a company") and "external integration" (e.g. "call the company profile API") live in a single layer or two. The current repository has no such split yet — chat_agent.py calls the LLM provider directly.

Decision

Introduce two distinct packages, skills/ and tools/, with a one-way dependency: Skills depend on Tools; Tools never depend on Skills; Skills never call other Skills directly (composition happens in the Orchestrator).

Alternatives Considered

A. Single "Capabilities" layer (merge Skills and Tools into one concept) Rejected: conflates two different rates of change. Tools change when an external API/provider changes (e.g. switching a news provider, adding a new LLM provider). Skills change when the business analysis logic changes (e.g. what a "financial analysis" should contain). Merging them means every provider swap risks touching business logic, and every business logic change risks touching integration code.

B. Skills calling Skills directly (allow composition inside the Skill layer) Rejected: it recreates the exact problem the Orchestrator exists to solve — execution order and composition would become implicit and scattered across Skill implementations instead of centralized and visible in one place (the Orchestrator). This would directly undermine Phase 4's multi-step planner, which needs a single place to reorder/reuse Skill calls.

C. No separation — put everything in the Orchestrator Rejected: would produce a large, untestable orchestrator handling both "which API to call" and "what the business logic means," the opposite of the project's stated goal of "strong separation of concerns" and "clean software engineering practices."

Consequences

Positive:

  • Tools are swappable per-provider via config (tools/registry.yaml + settings.py) without touching Skills.
  • Skills are independently unit-testable with mocked Tools — no network access needed for the bulk of the test suite.
  • Adding Phase 3's Rules Engine or Phase 2's Retriever is just "a new Tool a Skill can declare a dependency on" — no architectural change.
  • Matches the exact layer boundaries already specified in project-context.md, so future conversations/design reviews don't need to re-litigate this decision.

Negative / accepted tradeoffs:

  • Two more packages and two more registries (YAML files) than a simpler single-layer design — accepted because the alternative (layer conflation) causes exactly the coupling problems the project explicitly wants to avoid, and the registries are small, flat YAML, not a heavyweight plugin system.
  • Slight indirection cost when tracing a request (query → Router → Skill → Tool) — mitigated by the reasoning/metadata fields propagated at each layer into core/tracing.py, so the indirection is visible, not hidden.

Interview Talking Point

This is the same "capability vs. integration" split seen in mature agent frameworks (e.g. LangChain's "agents/tools," or classic hexagonal architecture's "application services vs. adapters"). The framing here is deliberately generic — Skills/Tools — rather than tied to any one library's vocabulary, keeping the design portable if the underlying LLM SDK or agent framework changes later.

Source: docs/adr/ADR-003-skills-tools-separation.md

Follow the work.

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

No spam. Unsubscribe anytime.