4.9 KiB
4.9 KiB
Tool vs Provider: Different Lifecycles
The Problem
"How can text plugin receive a tool call when it is not activated to main panel?"
Answer: Tool and Provider have different lifecycles.
Plugin (EditorPlugin) ── lives as long as the app runs (or until plugin is unloaded)
├── Tool: open_file ── always available
├── Tool: save_file ── always available
├── Tool: find_and_replace ── always available
│
└── Provider: EditorProvider ── created/destroyed per Main Panel instance
├── Instance A (showing main.rs)
├── Instance B (showing lib.rs) ← if we support split later
└── destroyed when user closes the file
Key Insight
Tool is registered at Plugin level, not Provider level.
pub trait Plugin {
fn bootstrap(registrar: &mut dyn Registrar);
}
pub trait Registrar {
// Tool lives with the Plugin
fn register_tool(&mut self, desc: ToolDesc, handler: Box<dyn Tool>);
// Provider is a factory — created on demand when panel needs it
fn register_provider(&mut self, desc: ProviderDesc, factory: ProviderFactory);
}
How It Works
Plugin State (Shared)
pub struct EditorPlugin {
// Plugin-level state: all open documents
documents: HashMap<PathBuf, Document>,
config: EditorConfig,
}
impl Plugin for EditorPlugin {
fn bootstrap(registrar: &mut dyn Registrar) {
let plugin = Arc::new(Mutex::new(EditorPlugin::new()));
// Register tools — these live with the plugin, not any provider instance
registrar.register_tool(
ToolDesc { name: "open_file", ... },
Box::new(OpenFileTool { plugin: plugin.clone() }),
);
registrar.register_tool(
ToolDesc { name: "find_and_replace", ... },
Box::new(FindReplaceTool { plugin: plugin.clone() }),
);
// Register provider factory
registrar.register_provider(
ProviderDesc { id: "editor", panel: PanelType::Main, ... },
Box::new(move |_| Box::new(EditorProvider::new(plugin.clone()))),
);
}
}
Tool Execution (Always Available)
pub struct OpenFileTool {
plugin: Arc<Mutex<EditorPlugin>>,
}
impl Tool for OpenFileTool {
fn execute(&self, input: ToolInput) -> ToolOutput {
let path = input.args["path"].as_str().unwrap();
let content = std::fs::read_to_string(path).unwrap();
// 1. Store in plugin-level state
self.plugin.lock().documents.insert(path.into(), Document::new(content));
// 2. Request UI to open this file in Main Panel
// This goes through a UI action, not the tool system
runtime.ui_actions().open_file(path);
ToolOutput::ok("File opened")
}
}
Provider Render (Reads from Shared State)
pub struct EditorProvider {
plugin: Arc<Mutex<EditorPlugin>>,
current_file: PathBuf,
}
impl PanelProvider for EditorProvider {
fn render(&mut self, area: Rect, buf: &mut Buffer) {
let plugin = self.plugin.lock();
let doc = plugin.documents.get(&self.current_file).unwrap();
// render document content
}
}
Lifecycle Comparison
| Tool | Provider | |
|---|---|---|
| Registered by | Plugin | Plugin |
| Created when | Plugin bootstrap | Panel needs to display content |
| Destroyed when | Plugin unload | Panel switches away (unless keep_alive) |
| Can be called when | Always | Only when panel is active (render/event) |
| Has state | Yes, Plugin-level shared state | Yes, instance-level state (cursor, scroll) |
Agent Opens a File: Full Flow
Agent: "Open src/main.rs"
→ LLM decides to call tool "open_file"
→ Tool Registry finds "open_file" (registered by EditorPlugin)
→ OpenFileTool.execute(path: "src/main.rs")
→ Read file from disk
→ Store in EditorPlugin.documents
→ Call ui_actions.open_file("src/main.rs")
→ UI Framework: Main Panel provider = "editor"
→ UI Framework: EditorProvider.current_file = "src/main.rs"
→ EditorProvider.init()
→ EditorProvider.render() — reads from shared state
→ Tool returns "File opened" to Agent
→ Agent displays "I've opened src/main.rs for you"
Why This Matters
Without this separation:
- Agent calls
open_file→ Editor Provider not active → crash or no-op - Tool must wait for Provider to be created → Agent workflow broken
With this separation:
- Tool operates on Plugin-level state, independent of UI
- UI switches to Provider after Tool completes
- Provider renders from shared state
Design Rule
Plugin = shared state + tools + provider factory. Tool = operates on shared state, always callable. Provider = renders shared state into a panel, only active when visible.