HTTP API and Python SDK

Corresponding source

SubjectSource
HTTP routes and OpenAI-compatible responsenanobot/api/server.py::handle_chat_completions, create_app
Python facadenanobot/nanobot.py::Nanobot
SDK clientsnanobot/sdk/clients.py, nanobot/sdk/runtime.py
SDK streamingnanobot/sdk/streaming.py::RunStream, SDKStreamingHook
SDK typesnanobot/sdk/types.py

Learning objectives

After this lesson, you should be able to:

  • explain how the HTTP API and SDK reuse process_direct();
  • describe the API Session lock and SSE lifecycle;
  • distinguish SDK run, run_streamed, and stream;
  • decide whether a problem belongs to an entry adapter or the Agent core.

Prerequisites

  • Understand the AgentLoop direct path.
  • Know the basic ideas of HTTP JSON, SSE, and Python async iterators.

1. HTTP API

Implementation:

  • nanobot/api/server.py
  • built with aiohttp.web.

Primary endpoints:

EndpointPurpose
POST /v1/chat/completionsnormal or streaming conversation
GET /v1/modelscurrent model information
GET /healthhealth check

2. Request parsing

Chat Completions supports:

  • JSON;
  • multipart/form-data;
  • text;
  • media;
  • session_id;
  • model override;
  • stream.

The input layer converts an OpenAI-compatible request into nanobot direct-call arguments. It does not call the Provider SDK directly.

3. API Session

Default:

api:default

With a session_id:

api:<session_id>

The API server maintains:

session_locks: dict[str, asyncio.Lock]

Requests within one API Session are serialized, while different Sessions can run concurrently. AgentLoop also has a Session lock; the API-layer lock protects the complete HTTP request and SSE-adapter lifecycle.

4. Non-streaming API

sequenceDiagram
    participant C as HTTP client
    participant A as API server
    participant L as AgentLoop

    C->>A: POST JSON
    A->>A: Parse + Session lock
    A->>L: process_direct
    L-->>A: response
    A->>A: Convert to chat.completion
    A-->>C: JSON

The API normalizes the process_direct() result into assistant text and wraps it in an OpenAI-compatible response with usage.

5. SSE

Streaming mode:

  1. creates an SSE response;
  2. registers a delta callback as on_stream;
  3. runs process_direct() in the background;
  4. writes a chat.completion.chunk for each delta;
  5. writes the finish chunk and [DONE] after the run completes;
  6. closes the connection.

The key fact is:

HTTP SSE cannot close when only on_stream_end fires, because the Agent may continue with a Tool Call after a stream segment. It must wait for the complete process_direct() call to return.

6. API empty-response retry

The non-streaming path has entry-layer recovery for an empty response. When debugging one, distinguish:

  • Runner’s empty-response recovery;
  • AgentLoop’s normalized empty-response text;
  • an additional direct call from the API layer;
  • response-text normalization.

Do not add an unbounded retry at the outermost layer.

7. Python SDK

High-level class:

  • nanobot/nanobot.py::Nanobot

Construction:

bot = Nanobot.from_config(...)

Internally:

Config
  → AgentLoop.from_config
  → Nanobot(loop)

8. run()

run():

  • acquires an SDK-side Session lock;
  • constructs process_direct keyword arguments;
  • accepts Session, Channel, chat, sender, and media;
  • supports ephemeral execution;
  • supports Hooks;
  • supports per-run model/preset overrides;
  • returns a normalized RunResult.

Use it when the caller only needs the final result.

9. run_streamed()

It returns RunStream:

  • a background Task runs the full Turn;
  • a Queue receives StreamEvent;
  • events include run started, text/reasoning/tool stream events, and run completed or failed;
  • the caller can consume events while separately awaiting the final result;
  • aclose() cancels an unfinished run.

10. stream()

stream() is the simpler async iterator:

async for event in bot.stream("Hello"):
    ...

It still calls run_streamed() internally and closes an unfinished run in finally.

11. Resource cleanup

After using the SDK, call:

await bot.aclose()

or:

async with Nanobot.from_config(...) as bot:
    ...

This releases MCP and other resources owned by AgentLoop.

API and SDK comparison

FeatureHTTP APIPython SDK
TransportHTTP JSON/SSEPython call/async iterator
Core entryprocess_directprocess_direct
Sessionapi:<id>caller-specified; SDK default otherwise
StreamingSSE chunkstyped StreamEvent
ErrorsHTTP/JSONPython exception + failed event
Lifecyclemanaged by servermanaged by host program
Best forcross-language callsPython embedding

Debugging boundary

If CLI direct works:

  • API 400: inspect the body parser;
  • API stream hangs: inspect the SSE writer and full-Turn wait;
  • SDK event is missing: inspect the emitter/adapter;
  • both produce the wrong final answer: return to AgentLoop/Runner;
  • Sessions cross: inspect entry-point keys and locks.

Source and tests

  • nanobot/api/server.py
  • nanobot/nanobot.py
  • nanobot/sdk/streaming.py
  • nanobot/sdk/types.py
  • nanobot/sdk/runtime.py
  • tests/test_openai_api.py
  • tests/test_api_stream.py
  • tests/test_api_attachment.py
  • use rg -n "Nanobot\\(" tests to find SDK behavior tests

Self-check

  1. Why does the API not call the Provider directly?
  2. Why must SSE wait for process_direct() to return?
  3. What do the API Session lock and AgentLoop lock protect separately?
  4. How are run_streamed() and stream() related?
  5. Why does the SDK need aclose()?

Next step

Read WebSocket and WebUI.