Refactor tool injection: LLM owns the extension point, tools plugin becomes a registry #58
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Competitor
Kind/Documentation
Kind/Enhancement
Kind/Epic
Kind/Feature
Kind/Security
Kind/Story
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Scope/Core
Scope/Cross-Plugin
Scope/Plugin-System
Scope/Single-Plugin
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ultanio/cobot#58
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Tool definitions are currently wired in the wrong place. The
toolsplugin hardcodes tool definitions for other plugins (wallet, knowledge), andloopmanually stitches tools into LLM calls. This creates:get_by_capability, not declared in PluginMeta)toolsplugin knows about every tool-providing plugin)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
How it flows
Key changes
LLM plugins (ppq, ollama):
Tools plugin (becomes registry):
Wallet plugin (owns its tools):
Loop plugin (simplified):
What this fixes
Existing pattern to follow
knowledge/plugin.pyalready implementsToolProvidercorrectly — it owns its definitions and execution. Wallet should follow the same pattern. (Credit: doxios review on #57)Acceptance criteria
llm.toolsextension pointllm.toolsto collect tool definitions before API callsllm.tools, aggregates from all ToolProvidersexecute()to the owning ToolProviderToolProvider, owns its 3 tool definitionsPluginMetagainsoptional_dependenciesfield for future useSupersedes
Closes the discussion from #57 (closed as too noisy). Incorporates doxios review feedback.
Related
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, thellm.toolsextension point adds unnecessary complexity. Here's my analysis:✅ What I Agree With
ToolProvideroptional_dependenciesin PluginMeta⚠️ Concern:
llm.toolsExtension Point Is Over-EngineeringThe 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):Then in
_respond()(line 256):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:Why this is better than the proposal:
Single responsibility — LLM plugins just call APIs. They don't need to know about tool discovery.
Loop is the orchestrator — It assembles context (messages, system prompt, history, tools) and feeds to LLM. This is architecturally correct.
Flexibility — Loop can filter/modify tools per-call. With
llm.tools, the LLM plugin would collect all tools unconditionally.DRY — Both
ppqandollamawould need identicalcall_extension("llm.tools")logic. Currently, neither needs it.🔍 The Real Problem (Already Solved in Principle)
The knowledge plugin proves the pattern works:
Knowledge's tools appear in the LLM without any special extension point. Why? Because:
capabilities=["tools"]all_with_capability("tools")AggregatedToolProvidercollects from all providersWallet 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)2. Tools plugin strips wallet code
Remove
TOOL_DEFINITIONSentries for wallet tools. Remove_wallet_*methods. Keep shell/file tools only.3. Add
optional_dependenciesto PluginMeta (as you proposed)4. Skip
llm.tools— not neededThe existing
loop._get_tools()+AggregatedToolProvideralready does whatllm.toolswould do.📊 Comparison
📋 Revised Acceptance Criteria
ToolProviderinterfacecapabilities=["wallet", "tools"]PluginMetagainsoptional_dependenciesfieldllm.toolsextension pointAggregatedToolProvider(no changes needed)🔄 One More Thing: Graph Edges
The proposal says:
But the edges should actually be:
wallethas capabilitytools— this is a vertex attribute, not an edgeloopdepends on capabilitytools— edge from loop to all ToolProvidersllmhas no tools-related edges — it just receives them as argumentsThe
ToolProvidercapability 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
Revised scope after discussion
Closing the
llm.toolsextension point idea — doxios is right, the existingAggregatedToolProviderpattern already works. The real fix is simpler.What changes
1. Wallet implements ToolProvider (2 files)
wallet/plugin.py: implementToolProvider, addcapabilities: ["wallet", "tools"], move tool defs from tools plugintools/plugin.py: remove wallet-specific code (definitions + executors)2. New PluginMeta fields (1 file:
base.py)3. Declare consumed capabilities across plugins:
4. Update inspect module + D3 viz to render capability relationships:
consumes— "this plugin aggregates from capability group X"((tools)),((llm)),((communication))Complete PluginMeta vocabulary
capabilitiesdependenciesoptional_dependenciesconsumesimplementsextension_pointsRevised acceptance criteria
ToolProvider, declarescapabilities: ["wallet", "tools"]PluginMetagainsoptional_dependenciesandconsumesfieldsconsumes: ["tools", "llm"]consumes: ["llm"]consumesedgesWhat this does NOT change
llm.toolsextension point)all_with_capability()andget_by_capability()remain — they're correct patterns✅ APPROVED
The revised scope is exactly right. Dropping
llm.toolsand using the existingAggregatedToolProviderpattern keeps things simple.What I like about the revision:
consumesfield — Elegant way to express "I aggregate from capability group X" without creating artificial dependenciesMinor suggestions:
consumesto empty list — Same as other optional fieldsprovidesalias —capabilitiescould have aprovidesalias for symmetry withconsumesReady for implementation. I can help break this into stories if needed.
🦊
Approved! Let's make this happen!
Stories
Broken into implementable stories:
optional_dependenciesandconsumesto PluginMetaconsumesrelationships in pluginsStarting implementation of #60 now. 🦊
✅ Epic Complete
All stories implemented and merged:
optional_dependenciesandconsumesfields ✅consumesrelationships ✅Summary of Changes
optional_dependenciesandconsumesfor declaring runtime relationshipsNote
The original
llm.toolsextension point was dropped per review feedback — the existingAggregatedToolProviderpattern was sufficient.🦊