feat: add plugin system with entry point discovery#73
Conversation
Add PluginManager and MiniAgentPlugin base class for loading external plugins via the mini_agent.plugins entry point group. Integrate plugin hooks into agent lifecycle (init, session start, turn complete, session end). Add /plugins command and --plugins CLI flag. Assisted-by: mini-agent:deepseek-v4-flash
The /plugins slash command already existed in the interactive loop but was missing from the tab-completion list. The --plugins CLI flag was redundant — plugins can be listed via /plugins inside a session. Assisted-by: mini-agent:deepseek-v4-flash
- Replace module-level PluginManager.discover() call with a lazy initializer to prevent circular imports when external plugins import from mini_agent during entry point loading - Exclude plugin.py from mypyc compilation so MiniAgentPlugin remains an interpreted class that external plugins can inherit from Assisted-by: mini-agent:deepseek-v4-flash
Remove MiniAgentPlugin and PluginManager from mini-agent.__init__ to break the circular import chain: 1. __init__.py imports cli.main -> calls PluginManager.discover() 2. discover() loads langfuse plugin -> from mini_agent import MiniAgentPlugin 3. MiniAgentPlugin not yet in namespace (__init__ still executing) External plugins must now import from mini_agent.plugin subpackage: from mini_agent.plugin import MiniAgentPlugin Also update mini-agent-langfuse to use the new import path.
|
@greptile |
|
| Filename | Overview |
|---|---|
| src/mini_agent/init.py | Adds a module docstring but fails to re-export MiniAgentPlugin, breaking the documented from mini_agent import MiniAgentPlugin import for all plugin authors. |
| src/mini_agent/cli/main.py | Wires plugin lifecycle hooks throughout the interactive and non-interactive paths; on_session_end is missing from non-interactive mode and not fired before /new//resume transitions. |
| src/mini_agent/plugin/manager.py | Solid entry-point discovery and lifecycle dispatch; uses contextlib.suppress for all hook calls with no fallback logging, making plugin errors invisible at runtime. |
| src/mini_agent/plugin/base.py | Clean base class with no-op lifecycle hooks; correctly excluded from mypyc compilation so external packages can subclass it. |
| setup.py | Correctly excludes plugin/base.py from mypyc compilation to keep it subclassable by external plugin packages. |
| docs/plugins.md | New plugin authoring guide; from mini_agent import MiniAgentPlugin example is broken until the top-level __init__.py is fixed. |
Sequence Diagram
sequenceDiagram
participant CLI as cli/main.py
participant PM as PluginManager
participant P as Plugin(s)
Note over CLI,PM: Module import time
CLI->>PM: PluginManager.discover()
PM->>PM: "entry_points(group="mini_agent.plugins")"
PM->>P: ep.load()() — instantiate each plugin
Note over CLI,PM: main() called
CLI->>PM: on_agent_init()
PM->>P: p.on_agent_init()
Note over CLI,PM: Session begins
CLI->>PM: on_session_start(session_id)
PM->>P: p.on_session_start(session_id)
loop Each turn
CLI->>CLI: agent_loop(history)
CLI->>PM: on_turn_complete(session_id, history, usages)
PM->>P: p.on_turn_complete(...)
end
Note over CLI,PM: /new or /resume (no on_session_end for old session)
CLI->>PM: on_session_start(new_session_id)
PM->>P: p.on_session_start(new_session_id)
Note over CLI,PM: Interactive exit (break/EOFError)
CLI->>PM: on_session_end(session_id, history, usages)
PM->>P: p.on_session_end(...)
Note over CLI,PM: Non-interactive (-p flag) — on_session_end never called
Comments Outside Diff (1)
-
src/mini_agent/cli/main.py, line 102-121 (link)on_session_endnot called for the old session on/newor/resumeWhen the user types
/new(line 102) or/resume(line 113), the current session is abandoned andon_session_startis called for the new one, buton_session_endis never called for the session that just ended. A plugin tracking active sessions or holding per-session resources would see a leaked session.
Reviews (1): Last reviewed commit: "fix(plugin): stop re-exporting MiniAgent..." | Re-trigger Greptile
| """Mini-agent: a minimal agent.""" | ||
|
|
||
| from .cli.main import main | ||
|
|
||
| __all__ = ["main"] |
There was a problem hiding this comment.
MiniAgentPlugin not importable from the top-level package
The plugin docs show from mini_agent import MiniAgentPlugin as the canonical import, but __init__.py only re-exports main — no reference to MiniAgentPlugin exists. Any plugin author following the documented example will get ImportError: cannot import name 'MiniAgentPlugin' from 'mini_agent' at install time.
| """Mini-agent: a minimal agent.""" | |
| from .cli.main import main | |
| __all__ = ["main"] | |
| """Mini-agent: a minimal agent.""" | |
| from .cli.main import main | |
| from .plugin.base import MiniAgentPlugin | |
| __all__ = ["main", "MiniAgentPlugin"] |
| def _run_non_interactive(prompt: str) -> None: | ||
| history: list[MessageParam] = [{"role": "user", "content": prompt}] | ||
| session_id = session_manager.new_id() | ||
| plugin_manager.on_session_start(session_id) | ||
| history_len = len(history) | ||
| agent_loop(history) | ||
| if len(history) > history_len and token_tracker.get() is not None: | ||
| session_manager.save(session_id, history, token_tracker.round_usages) | ||
| plugin_manager.on_turn_complete(session_id, history, token_tracker.round_usages) |
There was a problem hiding this comment.
on_session_end never fires in non-interactive mode
_run_non_interactive calls on_session_start and on_turn_complete but never on_session_end. Plugins that implement cleanup, flushing, or finalization logic in on_session_end will silently skip it for every mini -p "..." invocation, leaving e.g. buffered telemetry unsent or open resources unclosed.
| def on_agent_init(self) -> None: | ||
| for p in self.plugins: | ||
| with contextlib.suppress(Exception): | ||
| p.on_agent_init() | ||
|
|
||
| def on_session_start(self, session_id: str) -> None: | ||
| for p in self.plugins: | ||
| with contextlib.suppress(Exception): | ||
| p.on_session_start(session_id) | ||
|
|
||
| def on_turn_complete( | ||
| self, | ||
| session_id: str, | ||
| history: list[dict[str, Any]], | ||
| round_usages: list[Any] | None, | ||
| ) -> None: | ||
| for p in self.plugins: | ||
| with contextlib.suppress(Exception): | ||
| p.on_turn_complete(session_id, history, round_usages) | ||
|
|
||
| def on_session_end( | ||
| self, | ||
| session_id: str, | ||
| history: list[dict[str, Any]], | ||
| round_usages: list[Any] | None, | ||
| ) -> None: | ||
| for p in self.plugins: | ||
| with contextlib.suppress(Exception): | ||
| p.on_session_end(session_id, history, round_usages) |
There was a problem hiding this comment.
Plugin errors silently swallowed after load — no observable signal
contextlib.suppress(Exception) in every lifecycle dispatch discards all plugin exceptions with no log line, warning, or counter. A plugin that raises on every call (e.g. network error, missing config key) will appear to work fine. At minimum, a warnings.warn (matching the load-failure path) would let operators notice misbehaving plugins without crashing the agent.
Description
Adds a plugin system for mini-agent with entry-point-based discovery
MiniAgentPluginbase class with lifecycle hooks (on_agent_init,on_session_start,on_turn_complete,on_session_end)PluginManagerdiscovers plugins via themini_agent.pluginsentry point group/pluginscommand to tab-completion listType of change
Test
/pluginstab-completion in interactive sessionsAssisted-by: mini-agent:deepseek-v4-flash