Command, API, SDK, and WebUI extension boundaries
Corresponding source
| Runtime surface | Source |
|---|---|
| Slash commands | nanobot/command/router.py::CommandRouter, nanobot/command/builtin.py |
| HTTP API | nanobot/api/server.py |
| Python SDK and streaming | nanobot/sdk/clients.py, runtime.py, streaming.py, types.py |
| WebSocket and WebUI backend | nanobot/channels/websocket.py, nanobot/webui/*_api.py, *_routes.py, ws_http.py |
| Runtime events and hooks | nanobot/bus/runtime_events.py, nanobot/agent/hook.py |
| Frontend implementation | webui/src/lib/, webui/src/hooks/, webui/src/components/ |
Learning objectives
After reading this chapter, you should be able to:
- choose among a slash command, Tool, SDK, HTTP API, and WebUI;
- keep a command handler, router, and command palette in sync;
- distinguish the public OpenAI-compatible API from private WebUI HTTP routes;
- let the UI consume general Runtime Events without coupling the Agent core to a component.
Prerequisites
- Understand Five entry points and runtime boundaries.
- Understand HTTP API and Python SDK.
- Understand WebSocket and WebUI.
1. Choose a surface by consumer
| Consumer | Preferred surface |
|---|---|
| Deterministic control by a chat user | slash command |
| An action the model decides to take | Tool |
| Code in the same Python process | SDK |
| An OpenAI client or external HTTP program | /v1 API |
| nanobot’s browser interface | WebUI HTTP + WebSocket |
| General progress needed by every runtime surface | Runtime Event |
Do not add a Tool for the model merely because a capability appears in WebUI. Conversely, do not force the model to infer when to invoke an action that must be explicitly initiated by the user.
2. Add a slash command
The command system lives in:
nanobot/command/router.pynanobot/command/builtin.py
CommandRouter has three dispatch layers:
| Layer | Behavior | Appropriate use |
|---|---|---|
| priority | Exact match before the Session dispatch lock | Control that must interrupt or inspect a running Turn |
| exact | Full match while holding the lock | Commands without arguments |
| prefix | Longest prefix wins | Commands with arguments |
Adding a built-in command usually requires:
- writing
cmd_xxx(ctx); - registering it in
register_builtin_commands(); - adding palette and help metadata to
BUILTIN_COMMAND_SPECS; - adding handler and router tests;
- checking WebUI
/api/commandsand help on other Channels.
priorityexpresses concurrency semantics, not a performance optimizationA priority command bypasses the current Session lock. Use it only when the command truly must take effect during a running Turn and can read or mutate that state safely.
3. Public HTTP API
nanobot/api/server.py is an aiohttp application. It currently exposes:
POST /v1/chat/completionsGET /v1/modelsGET /health
The /v1 routes are intended for OpenAI-compatible interaction. Before adding a field or endpoint, evaluate:
- whether it still follows OpenAI-compatible semantics;
- compatibility of streaming SSE;
- multipart and base64 media limits;
- timeout and cancellation behavior;
- Session keys and concurrency;
- error status codes;
- documentation and compatibility tests.
Settings, workspace, or Session-management endpoints used only by WebUI should not casually be placed under /v1.
4. Python SDK
The SDK lives in:
nanobot/sdk/__init__.pynanobot/sdk/clients.pynanobot/sdk/runtime.pynanobot/sdk/streaming.pynanobot/sdk/types.py
It is an in-process facade over AgentLoop, not an HTTP API client. SessionClient, MemoryClient, and RuntimeClient operate directly on loop services.
When adding an SDK capability:
- call an existing service instead of duplicating business logic;
- return snapshots or dataclasses rather than exposing mutable internal objects;
- update public exports;
- preserve the resource semantics of the async context manager;
- account for the reader/writer gate around model overrides;
- add focused SDK tests.
Do not turn internal state such as _cache, _pending_queues, or Provider credentials into a public SDK contract.
5. WebUI backend
The Python boundary for WebUI is distributed across:
nanobot/channels/websocket.pynanobot/webui/ws_http.pynanobot/webui/*_routes.pynanobot/webui/*_api.pynanobot/session/webui_turns.pynanobot/bus/runtime_events.py
Responsibilities are separated as follows:
- Agent core: publish general Runtime Events;
- WebSocket coordinator: sequence, replay, subscription, and wire envelopes;
- HTTP route: authentication, parsing, and status codes;
*_api.py: configuration, Session, usage, and related business behavior;- frontend: fetch, cache, display, and interaction.
Before adding an endpoint, look for an existing route dispatcher and API helper. Do not accumulate all business logic in ws_http.py.
6. WebUI frontend
The main entry points are:
webui/src/lib/nanobot-client.tswebui/src/lib/api.tswebui/src/lib/types.tswebui/src/hooks/webui/src/components/webui/src/i18n/locales/webui/src/tests/
A visible feature commonly requires synchronized changes to:
- wire or HTTP types;
- a client method;
- a hook or state store;
- a component;
- loading, empty, and error states;
- i18n;
- a frontend test;
- a Python contract test;
- the Vite build output.
7. Decide between a Runtime Event and a hook
When several runtime surfaces need a new high-level runtime state:
- define general semantics in
nanobot/bus/runtime_events.py; - publish it at the core’s actual state transition;
- let the WebSocket coordinator and other subscribers translate it into their own protocols;
- test publication order and subscriber-failure isolation at each layer.
An event name should describe a runtime fact, such as “session turn started” or “goal state changed,” not a component action such as “open the right drawer.”
For fine-grained progress such as text, reasoning, Tool activity, or file edits, first inspect nanobot/agent/hook.py, progress_hook.py, and the SDK streaming adapter. Current SDK events such as text.delta and tool.started are not produced by directly converting RuntimeEventBus dataclasses.
flowchart TD A["High-level Turn or Goal state"] --> B["RuntimeEventBus"] B --> C["WebSocket coordinator or other subscriber"] C --> D["Runtime-surface protocol"] E["Text, reasoning, or Tool progress"] --> F["Agent hooks and callbacks"] F --> G["SDK, Channel, or WebSocket adapter"] G --> H["Surface-specific streaming events"]
8. Synchronized-change matrix
| Change | Commonly required | Required when applicable |
|---|---|---|
| built-in command | handler, router, spec, tests | docs and i18n |
/v1 API | handler, schema, contract tests, docs | SDK, if parity is promised |
| SDK method | client/runtime, types, exports, tests, docs | examples |
| WebUI HTTP route | route, API helper, authentication, Python tests | frontend client |
| WebSocket event | event type, coordinator, TypeScript type, hook, tests | transcript replay |
| visible setting | configuration schema, settings API, UI, i18n, tests | migration and docs |
This table is a checklist, not a requirement that every extension modify every layer.
9. Reuse before adding
Before creating a new surface, check:
- whether the Session service already provides the operation;
- whether RuntimeEventBus already has a close semantic match;
- whether WebUI already has an API client and route dispatcher;
- whether the SDK can reuse a core service directly;
- whether a command is merely a deterministic entry point to an existing Tool;
- whether the public API truly needs a new protocol instead of existing metadata.
10. Test references
Commands:
tests/command/test_router_dispatchable.pytests/command/test_model_command.pytests/command/test_skill_command.py
API:
tests/test_openai_api.pytests/test_api_stream.pytests/test_api_attachment.py
SDK:
tests/test_nanobot_facade.py
WebUI and WebSocket:
tests/channels/test_websocket_http_routes.pytests/channels/test_websocket_channel.pytests/webui/webui/src/tests/
Check your understanding
- Which commands should be registered as priority commands?
- Why should the SDK not return a mutable internal Session object directly?
- Why should a private WebUI route not casually be added under
/v1? - When does a new UI state require a Runtime Event, and when is local frontend state enough?
Next section: Source practice