Architecture: unify hooks and extension points into a single dispatch mechanism #33
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ultanio/cobot#33
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
Cobot currently has two distinct dispatch mechanisms for plugin communication:
1. Hooks (lifecycle pipeline)
base.py:on_message_received,transform_system_prompt, etc.registry.run_hook()calls all overriders in priority order, flowingctxthrough the chainbase.py+HOOK_METHODSlist2. Extension Points (service contracts)
PluginMeta.extension_points(what a plugin defines)PluginMeta.implementsdict (what a plugin provides)registry.get_implementations("context.system_prompt")and iterate manuallyThe confusion
Both mechanisms achieve similar goals but work differently:
base.py)run_hookrun_hookget_registry()+ iterationConcrete overlap example:
transform_system_promptis a hook that compaction uses, butcontext.system_promptis an extension point that soul/memory/workspace use. Both contribute to the system prompt. A new plugin author must know which to use.Current
get_registry()CouplingThree different patterns exist for registry access:
session.start()callsget_registry()directlyagent.pycallstools.set_registry(registry)andcompaction.set_registry(registry)context._registry = None— hopes someone sets itEvery plugin that defines extension points needs the registry to call
get_implementations(). The current approach doesn't scale — every new extension-point-defining plugin needs custom wiring inagent.py.Proposal: Unify the Mechanism
Core idea
Make extension points and hooks use the same dispatch system. Extension points are just hooks that any plugin can define, where only explicit implementers get called.
Changes
1. Auto-inject registry into all plugins (in
start_all()):This eliminates all
set_registry()methods andget_registry()calls.2. Add
call_extension()to base Plugin class:3. Rewrite consumers to use
call_extension:Before (context plugin):
After:
4. Make core hooks extension points of the agent itself (optional, longer term):
The 11 lifecycle hooks could become extension points defined by a core
lifecycleplugin. ThenHOOK_METHODSin base.py becomes unnecessary — plugins declare what lifecycle events they care about viaimplements, and new lifecycle events can be added by any plugin.What stays the same
PluginMetastructure (extension_points, implements, capabilities, dependencies)ctxdict flowing through chainsabortpattern for short-circuitingMigration path
_registryauto-injection instart_all()→ removes allset_registry()/get_registry()call_extension()helper → reduces boilerplate in context, session, etc.HOOK_METHODSand the fixed hook methods inbase.pybecome optional sugarDiscussion
call_extensionflow ctx through a chain (like hooks) or collect results (like current extension points)? Or support both modes?implementsdict in PluginMeta the right way to declare, or should plugins use decorators?Analysis by @nazim based on reading base.py, registry.py, interfaces.py, agent.py, and the context/session/tools/compaction plugins.
Revised Proposal: Loop Plugin + Minimal Agent
After discussion with @k9ert, a cleaner design emerged:
The hooks are legacy — they belong to the agent loop
The 11 hooks in
base.pyare all called byagent.py. They're not a separate mechanism — they're extension points of the agent loop that were never ported to the extension point system.Solution: Extract the loop into a plugin
agent.py becomes trivially simple:
The agent is a loop runner. Nothing else. One extension point: "start all loops."
The current agent logic moves to a loop plugin:
All 11 current hooks become extension points on the loop plugin. Existing plugins switch from overriding
on_message_received()toimplements: {"loop.on_message": "my_handler"}.Multiple loops run concurrently
Any plugin with
capabilities=["loop"]gets run by the agent. Examples:asyncio.gatherruns them all concurrently. Each loop defines its own extension points for other plugins to hook into.What gets deleted
HOOK_METHODSlist inbase.pyPluginbase classrun_hook()in registry (replaced bycall_extension())run()helper in__init__.pyagent.pyset_registry()/get_registry()patterns (auto-inject instead)Migration path
_registryinto all plugins instart_all()call_extension()helper to basePluginagent-loopplugin with the current agent.py logicimplementsdeclarationsagent.pyto just the loop runnerBenefits