Pawrrtal Extension Boundaries
Use this before changing backend extension code. The goal is a thin generic kernel with optional pieces installed as plugins.
Channel Adapters
A channel adapter talks to one user surface, such as Telegram or Google Chat.
Channel-specific code belongs under backend/app/channels/<channel>/ or
behind a channel plugin capability.
Channel adapters should:
- Normalize inbound/outbound messages through the channel base types.
- Choose a model and call the generic turn runner.
- Keep provider internals, SDK payloads, and tool factories out of channel code.
- Move reusable formatting to channel runtime helpers or a channel adapter.
If a new surface can send a message and receive a response, treat it as a channel unless it is purely a local CLI/operator concern.
Kernel Rules
The kernel owns auth, workspaces, persistence, turn orchestration, plugin discovery, enabled state, env resolution, and stable interfaces.
Directory rules:
- Keep
backend/app/chat/provider-agnostic and channel-agnostic. - Keep turn orchestration generic in
backend/app/turns/pipeline/; do not add provider, channel, or tool special cases to the pipeline. - Tool selection happens in the tool-surface/plugin layer, not inside providers or route handlers.
- Share optional behavior through kernel interfaces, slots, or runtime adapters instead of direct plugin-to-plugin imports.
Smells to fix:
| Smell | Fix |
|---|---|
chat/ imports a provider, MCP adapter, or channel module |
Move the behavior behind a generic interface or plugin capability. |
| Turn orchestration checks for one provider, tool, or channel by name | Emit/handle a generic event or add an adapter hook. |
| A subagent is implemented as a plugin | Move orchestration to the agent runtime and let plugins contribute profiles or invoke it. |
Conversation Boundaries
backend/app/conversations/ owns provider-agnostic conversation data and
workflows. It must not grow provider-specific history helpers, thread stores,
or channel formatting code.
Smells to fix:
| Smell | Fix |
|---|---|
conversations/ contains gemini_*, codex_*, or another provider name |
Move it to the provider package and expose a generic history adapter. |
| Conversation code formats Telegram or Google Chat output | Move it to the channel adapter. |
Plugin Platform
A plugin manifest is backend/plugins/<plugin_id>/plugin.json. It is
declarative metadata: capabilities, env requirements, validation commands,
permissions, slots, and entrypoints.
Plugin runtime code lives in backend/app/plugins/. It is trusted host code
plus bundled Python implementations that a manifest may activate.
A slot is a named extension point where multiple plugins can fit, such as
tasks, web_search, channel:telegram, provider:models, or
conversation_memory.
Before adding optional behavior:
- Name the capability type:
provider,channel,cli_tool,python_tool,turn_context_provider,conversation_memory,agent_runtime,agent_profile,router,settings, orscheduler. - Name the slot if users may choose among multiple implementations.
- Decide whether the code is generic kernel code or a trusted bundled adapter.
- Add or update the manifest first when the behavior is optional.
- Declare env requirements in the manifest. Use
user_workspaceorworkspacescope when values must be overridable per workspace/user. - Keep enabled/disabled behavior explicit. Disabled plugins must not advertise tools, providers, channels, or context providers.
- Add tests at the interface: manifest validation, registry/slot resolution, enabled-state behavior, env gating, and the runtime adapter.
- Verify through Paw when the behavior affects a user-visible flow.
Plugin smells to fix:
| Smell | Fix |
|---|---|
Plugin code reads os.environ directly for user/workspace settings |
Declare env in the manifest and use the plugin env resolver. |
| A plugin is always on because it exists on disk | Add enabled-state tests and make the manifest default intentional. |
| Plugins import each other directly | Share behavior through kernel interfaces, slots, or runtime adapters. |
Provider Adapters
A provider adapter talks to one model provider or provider CLI. It belongs
under backend/app/providers/ or behind a provider plugin capability.
Provider adapters should:
- Translate generic
AgentToolobjects into SDK-specific formats. - Avoid importing tool modules or tool factories directly.
- Keep channel-specific formatting and command output out of providers.
- Keep provider-specific history/session code inside the provider package and expose generic adapters to the rest of the app.
Smells to fix:
| Smell | Fix |
|---|---|
| A provider imports tool factories | Pass generic AgentTool values into the provider and translate there. |
| A channel imports provider internals | Route through the generic turn runner or provider factory. |
Verification
Use Paw as the operator surface for the supported CLI slice. From the repo root,
prefer just paw.
just paw doctor --json
just paw context --json
For runtime changes, add focused tests and then run the relevant gates:
cd backend && uv run pytest tests/test_plugin_discovery.py tests/test_plugin_tools.py
cd backend && uv run pytest tests/test_channel_plugins.py tests/test_provider_plugins.py
Run broader CI gates before claiming the PR is ready.