04 · extend
Five surfaces. One rule.
Tools, the wire, subagents, transports, models — none of them are closed. Every one is a file: drop it, decorate or register it, restart the session. No registry to edit, no client to rebuild.
Tools — 32 shipped, ten families.
What ships today, not what's possible. A new family shows up the same way as the 32nd tool did.
The wire — 12 commands, 27 events.
A tool is a bet the model takes alone. A command or event is a promise every client depends on — it stays reviewable on purpose.
for the model
Tools
A bet the model takes alone. Drop a file, decorate a function, restart. Cheap to add, cheap to be wrong.
for the client
Commands & events
A promise every client depends on. Reviewed before it ships — the wire only grows by agreement.
Same rule, in code.
A tool and a command look almost nothing alike on the page — one is a decorator, the other a reviewable dataclass pair. The ceremony to ship either is still one file and a restart.
tools/echo.py · for the model
Drop a module under tools/. Decorate the function. Restart the session — the engine imports the package tree and registers anything with @tool. No catalog list to edit, no client to rebuild. AgentLoop sends those schemas on each model call and, if the model returns tool_calls, runs them and calls again (capped at 16 turns).
If the agent should read git, add a tool.
from tools.base import ToolContext, tool@tool(description="Echo text back.")def echo(ctx: ToolContext, text: str) -> str:return text# name = function name# schema from type hints, or parameters=# ctx.workspace is the project root
model --tool calls--> tools/*.py
client --JSON cmd----> protocol/commands.py --> @handles
engine --JSON event--> protocol/events.py
Three more surfaces.
The orchestrator, the transport, and the model behind it all extend the same way.
A subagent
A module in agents/profiles/. The orchestrator sees it as a spawn tool automatically.
# agents/profiles/auditor.py
PROFILE = AgentProfile(
name="auditor",
system_prompt=AUDITOR_SYSTEM,
tool_names=NAV + SITTER,
)A transport
EngineServer only ever calls three methods on a session. WebSocket or HTTP needs no core changes.
session.subscribe(client) await session.handle(command) session.unsubscribe(client)
A model
AgentLoop needs one method. Implement it and swap out OpenRouter.
class MyProvider:
async def complete(self, messages, tools=None) -> LLMResult:
...The personas themselves — what each one can and can't touch — are on agents.