Refactor tool injection: LLM owns the extension point, tools plugin becomes a registry #58

Closed
opened 2026-02-22 10:45:42 +00:00 by nazim · 6 comments
Contributor

Problem

Tool definitions are currently wired in the wrong place. The tools plugin hardcodes tool definitions for other plugins (wallet, knowledge), and loop manually stitches tools into LLM calls. This creates:

  • Hidden dependencies (tools → wallet via get_by_capability, not declared in PluginMeta)
  • God object (tools plugin knows about every tool-providing plugin)
  • Wrong ownership (tool definitions belong in the LLM request, but loop manages them)

See: plugin graph (#54) shows wallet with zero connections despite being tightly coupled to tools at runtime.

Insight

Tool definitions are part of the LLM API call. They're injected into the request alongside messages. So the LLM plugin should own the extension point for tool injection — not loop, not tools.

Proposed Architecture

llm (ppq / ollama)
 ├── extension point: llm.tools  ← "who wants to inject tools into my calls?"
 ├── builds API request with tools attached
 └── returns tool_calls in response

tools (tool-registry)
 ├── implements: llm.tools  ← provides aggregated definitions to LLM
 ├── aggregates: all ToolProvider plugins
 └── executes tool calls dispatched by loop

wallet (ToolProvider)
 ├── capabilities: ["wallet", "tools"]
 ├── owns: wallet_balance, wallet_pay, wallet_receive
 └── implements: get_definitions() + execute()

knowledge (ToolProvider) ← already works this way
 ├── capabilities: ["knowledge", "tools"]
 └── owns its own tool definitions + execution

loop (orchestrator)
 ├── depends on: llm, communication
 ├── calls llm.chat() → tools already injected by llm plugin
 ├── when response has tool_calls → dispatches to tool-registry
 └── feeds tool results back to llm

How it flows

1. Message arrives → loop
2. loop calls llm.chat(messages)
3. llm plugin calls extension point "llm.tools" → tools registry responds with all definitions
4. llm plugin makes API call with messages + tools
5. LLM returns tool_calls
6. loop asks tools registry to execute each tool_call
7. tools registry routes to the owning ToolProvider (wallet, knowledge, etc.)
8. loop feeds results back to llm for next turn

Key changes

LLM plugins (ppq, ollama):

meta = PluginMeta(
    id="ppq",
    capabilities=["llm"],
    dependencies=["config"],
    extension_points=["llm.tools"],  # NEW: accept tool injection
)

async def chat(self, messages, **kwargs):
    tools = self.call_extension("llm.tools")  # collect from registry
    response = await self._call_api(messages, tools=tools)
    return response

Tools plugin (becomes registry):

meta = PluginMeta(
    id="tools",
    capabilities=["tool-registry"],
    dependencies=["config"],
    implements={"llm.tools": "get_all_definitions"},  # NEW: injects into LLM
)

def get_all_definitions(self) -> list[dict]:
    providers = self._registry.all_with_capability("tools")
    return [defn for p in providers for defn in p.get_definitions()]

def execute(self, tool_name: str, args: dict) -> str:
    for provider in self._registry.all_with_capability("tools"):
        names = [t["function"]["name"] for t in provider.get_definitions()]
        if tool_name in names:
            return provider.execute(tool_name, args)
    return f"Unknown tool: {tool_name}"

Wallet plugin (owns its tools):

from ..interfaces import ToolProvider

class WalletPlugin(Plugin, WalletProvider, ToolProvider):
    meta = PluginMeta(
        id="wallet",
        capabilities=["wallet", "tools"],  # ADD "tools"
        dependencies=["config"],
    )

    def get_definitions(self) -> list[dict]:
        return WALLET_TOOLS  # moved from tools/plugin.py

    def execute(self, tool_name: str, args: dict) -> str:
        if tool_name == "wallet_balance":
            return f"Balance: {self.get_balance()} sats"
        # ...

Loop plugin (simplified):

meta = PluginMeta(
    id="loop",
    dependencies=["config", "communication", "llm", "tool-registry"],
)

# loop no longer collects tools — llm plugin handles that
response = await llm.chat(messages)  # tools already injected
if response.has_tool_calls:
    for tc in response.tool_calls:
        result = tools_registry.execute(tc.name, tc.args)
        # feed back...

What this fixes

Before After
tools hardcodes wallet definitions wallet owns its tools via ToolProvider
loop manually wires tools into LLM calls LLM plugin collects via extension point
wallet has 0 graph edges (hidden coupling) wallet → tools capability visible
get_by_capability("wallet") in tools tools aggregates via all_with_capability("tools")
Adding a new tool = edit tools plugin Adding a new tool = implement ToolProvider in your plugin

Existing pattern to follow

knowledge/plugin.py already implements ToolProvider correctly — it owns its definitions and execution. Wallet should follow the same pattern. (Credit: doxios review on #57)

Acceptance criteria

  • LLM plugins (ppq, ollama) define llm.tools extension point
  • LLM plugins call llm.tools to collect tool definitions before API calls
  • Tools plugin implements llm.tools, aggregates from all ToolProviders
  • Tools plugin dispatches execute() to the owning ToolProvider
  • Wallet implements ToolProvider, owns its 3 tool definitions
  • Tools plugin has zero wallet-specific code
  • Loop no longer collects or injects tools — delegates to llm + tool-registry
  • Plugin graph shows correct edges: tools → llm (implements llm.tools), wallet → tools (capability)
  • All existing tests pass
  • PluginMeta gains optional_dependencies field for future use

Supersedes

Closes the discussion from #57 (closed as too noisy). Incorporates doxios review feedback.

  • #50 — Secret management (needs declared deps to scope secrets)
  • #52 — Development workflow (this is a good proof-run epic)
  • #54 — Plugin introspection (graph accuracy depends on declared deps)
## Problem Tool definitions are currently wired in the wrong place. The `tools` plugin hardcodes tool definitions for other plugins (wallet, knowledge), and `loop` manually stitches tools into LLM calls. This creates: - Hidden dependencies (tools → wallet via `get_by_capability`, not declared in PluginMeta) - God object (`tools` plugin knows about every tool-providing plugin) - Wrong ownership (tool definitions belong in the LLM request, but loop manages them) See: plugin graph (#54) shows wallet with zero connections despite being tightly coupled to tools at runtime. ## Insight Tool definitions are **part of the LLM API call**. They're injected into the request alongside messages. So the LLM plugin should own the extension point for tool injection — not loop, not tools. ## Proposed Architecture ``` llm (ppq / ollama) ├── extension point: llm.tools ← "who wants to inject tools into my calls?" ├── builds API request with tools attached └── returns tool_calls in response tools (tool-registry) ├── implements: llm.tools ← provides aggregated definitions to LLM ├── aggregates: all ToolProvider plugins └── executes tool calls dispatched by loop wallet (ToolProvider) ├── capabilities: ["wallet", "tools"] ├── owns: wallet_balance, wallet_pay, wallet_receive └── implements: get_definitions() + execute() knowledge (ToolProvider) ← already works this way ├── capabilities: ["knowledge", "tools"] └── owns its own tool definitions + execution loop (orchestrator) ├── depends on: llm, communication ├── calls llm.chat() → tools already injected by llm plugin ├── when response has tool_calls → dispatches to tool-registry └── feeds tool results back to llm ``` ### How it flows ``` 1. Message arrives → loop 2. loop calls llm.chat(messages) 3. llm plugin calls extension point "llm.tools" → tools registry responds with all definitions 4. llm plugin makes API call with messages + tools 5. LLM returns tool_calls 6. loop asks tools registry to execute each tool_call 7. tools registry routes to the owning ToolProvider (wallet, knowledge, etc.) 8. loop feeds results back to llm for next turn ``` ### Key changes **LLM plugins (ppq, ollama):** ```python meta = PluginMeta( id="ppq", capabilities=["llm"], dependencies=["config"], extension_points=["llm.tools"], # NEW: accept tool injection ) async def chat(self, messages, **kwargs): tools = self.call_extension("llm.tools") # collect from registry response = await self._call_api(messages, tools=tools) return response ``` **Tools plugin (becomes registry):** ```python meta = PluginMeta( id="tools", capabilities=["tool-registry"], dependencies=["config"], implements={"llm.tools": "get_all_definitions"}, # NEW: injects into LLM ) def get_all_definitions(self) -> list[dict]: providers = self._registry.all_with_capability("tools") return [defn for p in providers for defn in p.get_definitions()] def execute(self, tool_name: str, args: dict) -> str: for provider in self._registry.all_with_capability("tools"): names = [t["function"]["name"] for t in provider.get_definitions()] if tool_name in names: return provider.execute(tool_name, args) return f"Unknown tool: {tool_name}" ``` **Wallet plugin (owns its tools):** ```python from ..interfaces import ToolProvider class WalletPlugin(Plugin, WalletProvider, ToolProvider): meta = PluginMeta( id="wallet", capabilities=["wallet", "tools"], # ADD "tools" dependencies=["config"], ) def get_definitions(self) -> list[dict]: return WALLET_TOOLS # moved from tools/plugin.py def execute(self, tool_name: str, args: dict) -> str: if tool_name == "wallet_balance": return f"Balance: {self.get_balance()} sats" # ... ``` **Loop plugin (simplified):** ```python meta = PluginMeta( id="loop", dependencies=["config", "communication", "llm", "tool-registry"], ) # loop no longer collects tools — llm plugin handles that response = await llm.chat(messages) # tools already injected if response.has_tool_calls: for tc in response.tool_calls: result = tools_registry.execute(tc.name, tc.args) # feed back... ``` ## What this fixes | Before | After | |--------|-------| | tools hardcodes wallet definitions | wallet owns its tools via ToolProvider | | loop manually wires tools into LLM calls | LLM plugin collects via extension point | | wallet has 0 graph edges (hidden coupling) | wallet → tools capability visible | | get_by_capability("wallet") in tools | tools aggregates via all_with_capability("tools") | | Adding a new tool = edit tools plugin | Adding a new tool = implement ToolProvider in your plugin | ## Existing pattern to follow `knowledge/plugin.py` already implements `ToolProvider` correctly — it owns its definitions and execution. Wallet should follow the same pattern. (Credit: doxios review on #57) ## Acceptance criteria - [ ] LLM plugins (ppq, ollama) define `llm.tools` extension point - [ ] LLM plugins call `llm.tools` to collect tool definitions before API calls - [ ] Tools plugin implements `llm.tools`, aggregates from all ToolProviders - [ ] Tools plugin dispatches `execute()` to the owning ToolProvider - [ ] Wallet implements `ToolProvider`, owns its 3 tool definitions - [ ] Tools plugin has zero wallet-specific code - [ ] Loop no longer collects or injects tools — delegates to llm + tool-registry - [ ] Plugin graph shows correct edges: tools → llm (implements llm.tools), wallet → tools (capability) - [ ] All existing tests pass - [ ] `PluginMeta` gains `optional_dependencies` field for future use ## Supersedes Closes the discussion from #57 (closed as too noisy). Incorporates doxios review feedback. ## Related - #50 — Secret management (needs declared deps to scope secrets) - #52 — Development workflow (this is a good proof-run epic) - #54 — Plugin introspection (graph accuracy depends on declared deps)
Collaborator

Architecture Review: LLM-Owned Tool Extension Point

Verdict: NEEDS-REVISION ✏️

I appreciate the iteration from #57 and acknowledgment of my feedback. The core fix (wallet implements ToolProvider) is correct. However, the llm.tools extension point adds unnecessary complexity. Here's my analysis:


What I Agree With

Proposed Assessment
Wallet implements ToolProvider Correct — follows knowledge plugin pattern
Tools plugin becomes pure registry/dispatcher Correct — removes god object anti-pattern
optional_dependencies in PluginMeta Useful for graph completeness
Accurate plugin graph edges This is the goal

⚠️ Concern: llm.tools Extension Point Is Over-Engineering

The insight "tool definitions are part of the LLM API call" is conceptually true, but the architectural conclusion doesn't follow.

Current flow in loop/plugin.py (line 206-215):

def _get_tools(self) -> Optional["AggregatedToolProvider"]:
    if self._registry:
        providers = self._registry.all_with_capability("tools")
        if providers:
            return AggregatedToolProvider(providers)
    return None

Then in _respond() (line 256):

tool_defs = tools.get_definitions() if tools else []
response = llm.chat(messages, tools=tool_defs if tool_defs else None)

This already works. The loop aggregates tools via all_with_capability("tools") and passes them to the LLM. The LLM plugin (ppq/plugin.py) is a clean API wrapper:

def chat(self, messages, tools=None, ...):
    payload = {"model": model, "messages": messages}
    if tools:
        payload["tools"] = tools
    # ... make API call

Why this is better than the proposal:

  1. Single responsibility — LLM plugins just call APIs. They don't need to know about tool discovery.

  2. Loop is the orchestrator — It assembles context (messages, system prompt, history, tools) and feeds to LLM. This is architecturally correct.

  3. Flexibility — Loop can filter/modify tools per-call. With llm.tools, the LLM plugin would collect all tools unconditionally.

  4. DRY — Both ppq and ollama would need identical call_extension("llm.tools") logic. Currently, neither needs it.


🔍 The Real Problem (Already Solved in Principle)

The knowledge plugin proves the pattern works:

# knowledge/plugin.py (line 97-99)
class KnowledgePlugin(Plugin, ToolProvider):
    meta = PluginMeta(
        id="knowledge",
        capabilities=["knowledge", "tools"],  # ← declares "tools"
        ...
    )

Knowledge's tools appear in the LLM without any special extension point. Why? Because:

  1. Knowledge declares capabilities=["tools"]
  2. Loop calls all_with_capability("tools")
  3. AggregatedToolProvider collects from all providers

Wallet just needs to follow the same pattern. No new extension point required.


🛠️ Simplified Proposal

Keep the existing tool flow. Change only what's broken:

1. Wallet implements ToolProvider (as you proposed)

# wallet/plugin.py
class WalletPlugin(Plugin, WalletProvider, ToolProvider):
    meta = PluginMeta(
        id="wallet",
        capabilities=["wallet", "tools"],
        dependencies=["config"],
    )

    def get_definitions(self) -> list[dict]:
        return WALLET_TOOLS

    def execute(self, tool_name: str, args: dict) -> str:
        ...

    @property
    def restart_requested(self) -> bool:
        return False

2. Tools plugin strips wallet code

Remove TOOL_DEFINITIONS entries for wallet tools. Remove _wallet_* methods. Keep shell/file tools only.

3. Add optional_dependencies to PluginMeta (as you proposed)

4. Skip llm.tools — not needed

The existing loop._get_tools() + AggregatedToolProvider already does what llm.tools would do.


📊 Comparison

Aspect Proposal (#58) Simplified
Wallet owns tools
Tools plugin simplified
LLM plugins unchanged (add llm.tools)
Loop simplified (already works)
New extension point llm.tools None
Files changed 5+ 2-3

📋 Revised Acceptance Criteria

  • Wallet implements ToolProvider interface
  • Wallet declares capabilities=["wallet", "tools"]
  • Tools plugin removes wallet tool definitions and executors
  • PluginMeta gains optional_dependencies field
  • NO changes to ppq/ollama plugins
  • NO new llm.tools extension point
  • Loop continues using AggregatedToolProvider (no changes needed)
  • Plugin graph shows wallet→(no direct deps), discovered via "tools" capability

🔄 One More Thing: Graph Edges

The proposal says:

Plugin graph shows correct edges: tools → llm (implements llm.tools), wallet → tools (capability)

But the edges should actually be:

  • wallet has capability tools — this is a vertex attribute, not an edge
  • loop depends on capability tools — edge from loop to all ToolProviders
  • llm has no tools-related edges — it just receives them as arguments

The ToolProvider capability creates a virtual group that loop aggregates from. This is cleaner than creating artificial edges.


Bottom line: The diagnosis is correct, but the surgery is too aggressive. Make wallet follow the knowledge pattern, and the existing architecture handles everything else.

Reviewed by examining: base.py, interfaces.py, registry.py, loop/plugin.py, tools/plugin.py, wallet/plugin.py, knowledge/plugin.py, ppq/plugin.py

## Architecture Review: LLM-Owned Tool Extension Point ### Verdict: NEEDS-REVISION ✏️ I appreciate the iteration from #57 and acknowledgment of my feedback. The core fix (wallet implements `ToolProvider`) is correct. However, the `llm.tools` extension point adds unnecessary complexity. Here's my analysis: --- ### ✅ What I Agree With | Proposed | Assessment | |----------|------------| | Wallet implements `ToolProvider` | ✅ Correct — follows knowledge plugin pattern | | Tools plugin becomes pure registry/dispatcher | ✅ Correct — removes god object anti-pattern | | `optional_dependencies` in PluginMeta | ✅ Useful for graph completeness | | Accurate plugin graph edges | ✅ This is the goal | --- ### ⚠️ Concern: `llm.tools` Extension Point Is Over-Engineering The insight "tool definitions are part of the LLM API call" is conceptually true, but the architectural conclusion doesn't follow. **Current flow in `loop/plugin.py` (line 206-215):** ```python def _get_tools(self) -> Optional["AggregatedToolProvider"]: if self._registry: providers = self._registry.all_with_capability("tools") if providers: return AggregatedToolProvider(providers) return None ``` Then in `_respond()` (line 256): ```python tool_defs = tools.get_definitions() if tools else [] response = llm.chat(messages, tools=tool_defs if tool_defs else None) ``` **This already works.** The loop aggregates tools via `all_with_capability("tools")` and passes them to the LLM. The LLM plugin (`ppq/plugin.py`) is a clean API wrapper: ```python def chat(self, messages, tools=None, ...): payload = {"model": model, "messages": messages} if tools: payload["tools"] = tools # ... make API call ``` **Why this is better than the proposal:** 1. **Single responsibility** — LLM plugins just call APIs. They don't need to know about tool discovery. 2. **Loop is the orchestrator** — It assembles context (messages, system prompt, history, tools) and feeds to LLM. This is architecturally correct. 3. **Flexibility** — Loop can filter/modify tools per-call. With `llm.tools`, the LLM plugin would collect all tools unconditionally. 4. **DRY** — Both `ppq` and `ollama` would need identical `call_extension("llm.tools")` logic. Currently, neither needs it. --- ### 🔍 The Real Problem (Already Solved in Principle) The knowledge plugin proves the pattern works: ```python # knowledge/plugin.py (line 97-99) class KnowledgePlugin(Plugin, ToolProvider): meta = PluginMeta( id="knowledge", capabilities=["knowledge", "tools"], # ← declares "tools" ... ) ``` Knowledge's tools appear in the LLM without any special extension point. Why? Because: 1. Knowledge declares `capabilities=["tools"]` 2. Loop calls `all_with_capability("tools")` 3. `AggregatedToolProvider` collects from all providers Wallet just needs to follow the same pattern. No new extension point required. --- ### 🛠️ Simplified Proposal Keep the existing tool flow. Change only what's broken: #### 1. Wallet implements `ToolProvider` (as you proposed) ```python # wallet/plugin.py class WalletPlugin(Plugin, WalletProvider, ToolProvider): meta = PluginMeta( id="wallet", capabilities=["wallet", "tools"], dependencies=["config"], ) def get_definitions(self) -> list[dict]: return WALLET_TOOLS def execute(self, tool_name: str, args: dict) -> str: ... @property def restart_requested(self) -> bool: return False ``` #### 2. Tools plugin strips wallet code Remove `TOOL_DEFINITIONS` entries for wallet tools. Remove `_wallet_*` methods. Keep shell/file tools only. #### 3. Add `optional_dependencies` to PluginMeta (as you proposed) #### 4. Skip `llm.tools` — not needed The existing `loop._get_tools()` + `AggregatedToolProvider` already does what `llm.tools` would do. --- ### 📊 Comparison | Aspect | Proposal (#58) | Simplified | |--------|---------------|------------| | Wallet owns tools | ✅ | ✅ | | Tools plugin simplified | ✅ | ✅ | | LLM plugins unchanged | ❌ (add llm.tools) | ✅ | | Loop simplified | ✅ | ⚪ (already works) | | New extension point | llm.tools | None | | Files changed | 5+ | 2-3 | --- ### 📋 Revised Acceptance Criteria - [ ] Wallet implements `ToolProvider` interface - [ ] Wallet declares `capabilities=["wallet", "tools"]` - [ ] Tools plugin removes wallet tool definitions and executors - [ ] `PluginMeta` gains `optional_dependencies` field - [ ] NO changes to ppq/ollama plugins - [ ] NO new `llm.tools` extension point - [ ] Loop continues using `AggregatedToolProvider` (no changes needed) - [ ] Plugin graph shows wallet→(no direct deps), discovered via "tools" capability --- ### 🔄 One More Thing: Graph Edges The proposal says: > Plugin graph shows correct edges: tools → llm (implements llm.tools), wallet → tools (capability) But the edges should actually be: - `wallet` has capability `tools` — this is a **vertex attribute**, not an edge - `loop` depends on capability `tools` — edge from loop to all ToolProviders - `llm` has no tools-related edges — it just receives them as arguments The `ToolProvider` capability creates a *virtual group* that loop aggregates from. This is cleaner than creating artificial edges. --- **Bottom line:** The diagnosis is correct, but the surgery is too aggressive. Make wallet follow the knowledge pattern, and the existing architecture handles everything else. *Reviewed by examining: base.py, interfaces.py, registry.py, loop/plugin.py, tools/plugin.py, wallet/plugin.py, knowledge/plugin.py, ppq/plugin.py*
Author
Contributor

Revised scope after discussion

Closing the llm.tools extension point idea — doxios is right, the existing AggregatedToolProvider pattern already works. The real fix is simpler.

What changes

1. Wallet implements ToolProvider (2 files)

  • wallet/plugin.py: implement ToolProvider, add capabilities: ["wallet", "tools"], move tool defs from tools plugin
  • tools/plugin.py: remove wallet-specific code (definitions + executors)

2. New PluginMeta fields (1 file: base.py)

@dataclass
class PluginMeta:
    # existing...
    optional_dependencies: list[str] = field(default_factory=list)  # "I can use if available"
    consumes: list[str] = field(default_factory=list)  # "I aggregate from this capability group"

3. Declare consumed capabilities across plugins:

# loop/plugin.py
meta = PluginMeta(
    id="loop",
    dependencies=["config", "communication"],
    consumes=["tools", "llm"],  # loop aggregates from these groups
)

# tools/plugin.py (as registry)
meta = PluginMeta(
    id="tools",
    capabilities=["tools"],
    dependencies=["config"],
    # tools itself is a ToolProvider (shell/file) AND aggregates other ToolProviders
)

# compaction/plugin.py
meta = PluginMeta(
    id="compaction",
    dependencies=["config", "persistence"],
    consumes=["llm"],  # uses LLM for summarization
)

4. Update inspect module + D3 viz to render capability relationships:

  • New edge type: consumes — "this plugin aggregates from capability group X"
  • Virtual capability hub nodes in D3: ((tools)), ((llm)), ((communication))
  • Providers connect TO the hub, consumers connect FROM the hub
  • Toggle: "Show capability groups" (off by default)
[wallet] ──provides──▶ ((tools))
[knowledge] ──provides──▶ ((tools))     ((tools)) ◀──consumes── [loop]
[tools] ──provides──▶ ((tools))

[ppq] ──provides──▶ ((llm))            ((llm)) ◀──consumes── [loop]
[ollama] ──provides──▶ ((llm))          ((llm)) ◀──consumes── [compaction]

Complete PluginMeta vocabulary

Field Meaning Graph representation
capabilities I provide this Node attribute + edge to capability hub
dependencies I require this plugin (hard) Solid edge
optional_dependencies I can use this if loaded Dashed edge
consumes I aggregate from this capability group Edge from capability hub
implements I fulfill this extension point Green edge (existing)
extension_points I define this contract Node attribute (existing)

Revised acceptance criteria

  • Wallet implements ToolProvider, declares capabilities: ["wallet", "tools"]
  • Tools plugin removes wallet tool definitions and executors
  • PluginMeta gains optional_dependencies and consumes fields
  • Loop declares consumes: ["tools", "llm"]
  • Compaction declares consumes: ["llm"]
  • Inspect module detects consumes edges
  • D3 viz supports virtual capability hub nodes with toggle
  • NO changes to ppq/ollama/loop tool aggregation logic
  • All existing tests pass

What this does NOT change

  • LLM plugins stay as clean API wrappers (no llm.tools extension point)
  • Loop stays as orchestrator, passes tools to LLM as arguments
  • all_with_capability() and get_by_capability() remain — they're correct patterns
  • Knowledge plugin unchanged — already follows the right pattern
## Revised scope after discussion Closing the `llm.tools` extension point idea — doxios is right, the existing `AggregatedToolProvider` pattern already works. The real fix is simpler. ### What changes **1. Wallet implements ToolProvider** (2 files) - `wallet/plugin.py`: implement `ToolProvider`, add `capabilities: ["wallet", "tools"]`, move tool defs from tools plugin - `tools/plugin.py`: remove wallet-specific code (definitions + executors) **2. New PluginMeta fields** (1 file: `base.py`) ```python @dataclass class PluginMeta: # existing... optional_dependencies: list[str] = field(default_factory=list) # "I can use if available" consumes: list[str] = field(default_factory=list) # "I aggregate from this capability group" ``` **3. Declare consumed capabilities** across plugins: ```python # loop/plugin.py meta = PluginMeta( id="loop", dependencies=["config", "communication"], consumes=["tools", "llm"], # loop aggregates from these groups ) # tools/plugin.py (as registry) meta = PluginMeta( id="tools", capabilities=["tools"], dependencies=["config"], # tools itself is a ToolProvider (shell/file) AND aggregates other ToolProviders ) # compaction/plugin.py meta = PluginMeta( id="compaction", dependencies=["config", "persistence"], consumes=["llm"], # uses LLM for summarization ) ``` **4. Update inspect module + D3 viz** to render capability relationships: - New edge type: `consumes` — "this plugin aggregates from capability group X" - Virtual capability hub nodes in D3: `((tools))`, `((llm))`, `((communication))` - Providers connect TO the hub, consumers connect FROM the hub - Toggle: "Show capability groups" (off by default) ``` [wallet] ──provides──▶ ((tools)) [knowledge] ──provides──▶ ((tools)) ((tools)) ◀──consumes── [loop] [tools] ──provides──▶ ((tools)) [ppq] ──provides──▶ ((llm)) ((llm)) ◀──consumes── [loop] [ollama] ──provides──▶ ((llm)) ((llm)) ◀──consumes── [compaction] ``` ### Complete PluginMeta vocabulary | Field | Meaning | Graph representation | |-------|---------|---------------------| | `capabilities` | I provide this | Node attribute + edge to capability hub | | `dependencies` | I require this plugin (hard) | Solid edge | | `optional_dependencies` | I can use this if loaded | Dashed edge | | `consumes` | I aggregate from this capability group | Edge from capability hub | | `implements` | I fulfill this extension point | Green edge (existing) | | `extension_points` | I define this contract | Node attribute (existing) | ### Revised acceptance criteria - [ ] Wallet implements `ToolProvider`, declares `capabilities: ["wallet", "tools"]` - [ ] Tools plugin removes wallet tool definitions and executors - [ ] `PluginMeta` gains `optional_dependencies` and `consumes` fields - [ ] Loop declares `consumes: ["tools", "llm"]` - [ ] Compaction declares `consumes: ["llm"]` - [ ] Inspect module detects `consumes` edges - [ ] D3 viz supports virtual capability hub nodes with toggle - [ ] NO changes to ppq/ollama/loop tool aggregation logic - [ ] All existing tests pass ### What this does NOT change - LLM plugins stay as clean API wrappers (no `llm.tools` extension point) - Loop stays as orchestrator, passes tools to LLM as arguments - `all_with_capability()` and `get_by_capability()` remain — they're correct patterns - Knowledge plugin unchanged — already follows the right pattern
Collaborator

APPROVED

The revised scope is exactly right. Dropping llm.tools and using the existing AggregatedToolProvider pattern keeps things simple.

What I like about the revision:

  1. consumes field — Elegant way to express "I aggregate from capability group X" without creating artificial dependencies
  2. Capability hub visualization — Makes the architecture legible: providers → hub ← consumers
  3. Minimal code changes — Wallet + PluginMeta + inspect/viz, no LLM plugin changes
  4. Complete vocabulary — The PluginMeta field table is a great reference

Minor suggestions:

  1. Default consumes to empty list — Same as other optional fields
  2. Document in conventions.md — Add the vocabulary table to docs/dev/conventions.md
  3. Consider provides aliascapabilities could have a provides alias for symmetry with consumes

Ready for implementation. I can help break this into stories if needed.

🦊

## ✅ APPROVED The revised scope is exactly right. Dropping `llm.tools` and using the existing `AggregatedToolProvider` pattern keeps things simple. ### What I like about the revision: 1. **`consumes` field** — Elegant way to express "I aggregate from capability group X" without creating artificial dependencies 2. **Capability hub visualization** — Makes the architecture legible: providers → hub ← consumers 3. **Minimal code changes** — Wallet + PluginMeta + inspect/viz, no LLM plugin changes 4. **Complete vocabulary** — The PluginMeta field table is a great reference ### Minor suggestions: 1. **Default `consumes` to empty list** — Same as other optional fields 2. **Document in conventions.md** — Add the vocabulary table to docs/dev/conventions.md 3. **Consider `provides` alias** — `capabilities` could have a `provides` alias for symmetry with `consumes` Ready for implementation. I can help break this into stories if needed. 🦊
Owner

Approved! Let's make this happen!

Approved! Let's make this happen!
Collaborator

Stories

Broken into implementable stories:

  1. #60 — Add optional_dependencies and consumes to PluginMeta
  2. #61 — Wallet implements ToolProvider
  3. #62 — Declare consumes relationships in plugins

Starting implementation of #60 now. 🦊

## Stories Broken into implementable stories: 1. #60 — Add `optional_dependencies` and `consumes` to PluginMeta 2. #61 — Wallet implements ToolProvider 3. #62 — Declare `consumes` relationships in plugins Starting implementation of #60 now. 🦊
Collaborator

Epic Complete

All stories implemented and merged:

  • #60 — PluginMeta gains optional_dependencies and consumes fields
  • #61 — Wallet implements ToolProvider
  • #62 — Plugins declare consumes relationships

Summary of Changes

  1. New PluginMeta fields: optional_dependencies and consumes for declaring runtime relationships
  2. Wallet owns its tools: Implements ToolProvider, tools plugin is now a clean registry
  3. Capability consumption declared: loop, compaction, tools, subagent declare what they consume
  4. Plugin graph now accurate: All relationships are explicit, no hidden coupling

Note

The original llm.tools extension point was dropped per review feedback — the existing AggregatedToolProvider pattern was sufficient.

🦊

## ✅ Epic Complete All stories implemented and merged: - #60 — PluginMeta gains `optional_dependencies` and `consumes` fields ✅ - #61 — Wallet implements ToolProvider ✅ - #62 — Plugins declare `consumes` relationships ✅ ### Summary of Changes 1. **New PluginMeta fields**: `optional_dependencies` and `consumes` for declaring runtime relationships 2. **Wallet owns its tools**: Implements ToolProvider, tools plugin is now a clean registry 3. **Capability consumption declared**: loop, compaction, tools, subagent declare what they consume 4. **Plugin graph now accurate**: All relationships are explicit, no hidden coupling ### Note The original `llm.tools` extension point was dropped per review feedback — the existing `AggregatedToolProvider` pattern was sufficient. 🦊
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ultanio/cobot#58
No description provided.