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/metadatafields propagated at each layer intocore/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.