Maybe Runtime subsystem done
This commit is contained in:
157
doc/Plugin-Lifecycle-Tool-vs-Provider.md
Normal file
157
doc/Plugin-Lifecycle-Tool-vs-Provider.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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.**
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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.**
|
||||
Reference in New Issue
Block a user