Skip to content

feat: add plugin system with entry point discovery#73

Draft
kowyo wants to merge 4 commits into
mainfrom
feat/plugin-system
Draft

feat: add plugin system with entry point discovery#73
kowyo wants to merge 4 commits into
mainfrom
feat/plugin-system

Conversation

@kowyo

@kowyo kowyo commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Description

Adds a plugin system for mini-agent with entry-point-based discovery

  • Introduces MiniAgentPlugin base class with lifecycle hooks (on_agent_init, on_session_start, on_turn_complete, on_session_end)
  • PluginManager discovers plugins via the mini_agent.plugins entry point group
  • Adds /plugins command to tab-completion list

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Test

  • Verified plugin discovery works with installed entry points
  • Verified /plugins tab-completion in interactive sessions

Assisted-by: mini-agent:deepseek-v4-flash

kowyo added 2 commits June 6, 2026 16:41
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
@kowyo kowyo added the enhancement New feature or request label Jun 6, 2026
gemini-code-assist[bot]

This comment was marked as resolved.

kowyo added 2 commits June 7, 2026 10:22
- 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.
@kowyo

kowyo commented Jun 8, 2026

Copy link
Copy Markdown
Owner Author

@greptile

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces an entry-point-based plugin system for mini-agent, allowing external packages to hook into agent lifecycle events (on_agent_init, on_session_start, on_turn_complete, on_session_end) and adds a /plugins slash command.

  • MiniAgentPlugin base class and PluginManager are new; discovery uses importlib.metadata.entry_points and is done at module-import time. The top-level mini_agent.__init__ does not re-export MiniAgentPlugin, so from mini_agent import MiniAgentPlugin — the import shown in every doc example — raises ImportError for plugin authors.
  • on_session_end is wired for interactive mode but never called in non-interactive mode (mini -p ...), so plugins that flush data or release resources in that hook will silently skip cleanup for CLI pipe usage.
  • /new and /resume call on_session_start for the incoming session without first calling on_session_end for the outgoing one, leaving plugins that track per-session state with a leaked session handle.

Confidence Score: 3/5

The plugin system is functional for simple interactive use, but the documented public API (from mini_agent import MiniAgentPlugin) is broken out of the box, which will affect all plugin authors immediately upon install.

Two defects affect currently shipped behavior: the top-level package doesn't re-export MiniAgentPlugin despite the docs showing it as the canonical import, and on_session_end is never called in non-interactive mode. A one-line fix resolves the import issue, but the lifecycle gap in non-interactive mode and the missing end-of-session signal on /new//resume transitions need deliberate design choices before this is ready for plugin authors to depend on.

src/mini_agent/__init__.py (broken public import) and src/mini_agent/cli/main.py (incomplete lifecycle wiring in non-interactive path and on session transitions)

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. src/mini_agent/cli/main.py, line 102-121 (link)

    P2 on_session_end not called for the old session on /new or /resume

    When the user types /new (line 102) or /resume (line 113), the current session is abandoned and on_session_start is called for the new one, but on_session_end is 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

Comment on lines +1 to 5
"""Mini-agent: a minimal agent."""

from .cli.main import main

__all__ = ["main"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
"""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"]

Comment on lines 46 to +54
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +26 to +54
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant