Command, API, SDK, and WebUI extension boundaries

Corresponding source

Runtime surfaceSource
Slash commandsnanobot/command/router.py::CommandRouter, nanobot/command/builtin.py
HTTP APInanobot/api/server.py
Python SDK and streamingnanobot/sdk/clients.py, runtime.py, streaming.py, types.py
WebSocket and WebUI backendnanobot/channels/websocket.py, nanobot/webui/*_api.py, *_routes.py, ws_http.py
Runtime events and hooksnanobot/bus/runtime_events.py, nanobot/agent/hook.py
Frontend implementationwebui/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

1. Choose a surface by consumer

ConsumerPreferred surface
Deterministic control by a chat userslash command
An action the model decides to takeTool
Code in the same Python processSDK
An OpenAI client or external HTTP program/v1 API
nanobot’s browser interfaceWebUI HTTP + WebSocket
General progress needed by every runtime surfaceRuntime 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.py
  • nanobot/command/builtin.py

CommandRouter has three dispatch layers:

LayerBehaviorAppropriate use
priorityExact match before the Session dispatch lockControl that must interrupt or inspect a running Turn
exactFull match while holding the lockCommands without arguments
prefixLongest prefix winsCommands with arguments

Adding a built-in command usually requires:

  1. writing cmd_xxx(ctx);
  2. registering it in register_builtin_commands();
  3. adding palette and help metadata to BUILTIN_COMMAND_SPECS;
  4. adding handler and router tests;
  5. checking WebUI /api/commands and help on other Channels.

priority expresses concurrency semantics, not a performance optimization

A 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/completions
  • GET /v1/models
  • GET /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__.py
  • nanobot/sdk/clients.py
  • nanobot/sdk/runtime.py
  • nanobot/sdk/streaming.py
  • nanobot/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.py
  • nanobot/webui/ws_http.py
  • nanobot/webui/*_routes.py
  • nanobot/webui/*_api.py
  • nanobot/session/webui_turns.py
  • nanobot/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.ts
  • webui/src/lib/api.ts
  • webui/src/lib/types.ts
  • webui/src/hooks/
  • webui/src/components/
  • webui/src/i18n/locales/
  • webui/src/tests/

A visible feature commonly requires synchronized changes to:

  1. wire or HTTP types;
  2. a client method;
  3. a hook or state store;
  4. a component;
  5. loading, empty, and error states;
  6. i18n;
  7. a frontend test;
  8. a Python contract test;
  9. the Vite build output.

7. Decide between a Runtime Event and a hook

When several runtime surfaces need a new high-level runtime state:

  1. define general semantics in nanobot/bus/runtime_events.py;
  2. publish it at the core’s actual state transition;
  3. let the WebSocket coordinator and other subscribers translate it into their own protocols;
  4. 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

ChangeCommonly requiredRequired when applicable
built-in commandhandler, router, spec, testsdocs and i18n
/v1 APIhandler, schema, contract tests, docsSDK, if parity is promised
SDK methodclient/runtime, types, exports, tests, docsexamples
WebUI HTTP routeroute, API helper, authentication, Python testsfrontend client
WebSocket eventevent type, coordinator, TypeScript type, hook, teststranscript replay
visible settingconfiguration schema, settings API, UI, i18n, testsmigration 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.py
  • tests/command/test_model_command.py
  • tests/command/test_skill_command.py

API:

  • tests/test_openai_api.py
  • tests/test_api_stream.py
  • tests/test_api_attachment.py

SDK:

  • tests/test_nanobot_facade.py

WebUI and WebSocket:

  • tests/channels/test_websocket_http_routes.py
  • tests/channels/test_websocket_channel.py
  • tests/webui/
  • webui/src/tests/

Check your understanding

  1. Which commands should be registered as priority commands?
  2. Why should the SDK not return a mutable internal Session object directly?
  3. Why should a private WebUI route not casually be added under /v1?
  4. When does a new UI state require a Runtime Event, and when is local frontend state enough?

Next section: Source practice