smooth-operator — Python client. A native, fully-async WebSocket client for the smooth-operator protocol, with pydantic v2 models.
The native async Python client for the smooth-operator WebSocket protocol. The pydantic v2 models in smooth_operator._generated are generated from the language-neutral JSON Schemas in ../spec/ (and committed), using pydantic discriminated unions so events deserialize to the right concrete type. The wire is camelCase; you work in idiomatic snake_case.
uv add smooth-operator # PyPI publish pending — install from the local path todayUntil this package is published to PyPI, install it from a sibling checkout
(uv add ../smooth-operator/python, or pip install -e path/to/smooth-operator/python).
The unqualified PyPI name is not this package yet — don't pip install smooth-operator
from the public index until the SmooAI release lands.
import asyncio
from smooth_operator import SmoothAgentClient
async def main():
client = SmoothAgentClient(url="ws://127.0.0.1:8787/ws")
await client.connect()
session = await client.create_conversation_session(agent_id=agent_id, user_name="Alice")
turn = client.send_message(session_id=session.session_id, message="How long is your return window?")
final = await turn # the terminal eventual_response
print(final.data.payload.message_id)
asyncio.run(main())(Point url at your own smooth-operator-server or the hosted endpoint.)
send_message returns a turn you can async for over for live events and await for the authoritative terminal response.
turn = client.send_message(session_id=session.session_id, message="Where is my order?")
async for event in turn:
if event.type == "stream_chunk":
print(f"\n ↳ node: {event.node}") # workflow node boundary
elif event.type == "stream_token":
print(event.token, end="", flush=True) # tokens, live
elif event.type == "write_confirmation_required":
# HITL: approve, and the resumed stream flows back into this same turn.
await client.confirm_tool_action(
session_id=session.session_id, request_id=turn.request_id, approved=True
)
final = await turn # the terminal eventual_response
print("\nmessageId:", final.data.payload.message_id)sequenceDiagram
participant App
participant C as SmoothAgentClient
participant S as Service
App->>C: send_message(...)
C->>S: { action: send_message }
S-->>C: immediate_response (202)
S-->>C: stream_token / stream_chunk …
S-->>C: eventual_response (200)
C-->>App: async-for yields events · await resolves final
The JSON wire form is camelCase (requestId, sessionId); the pydantic models use snake_case attributes with camelCase aliases and populate_by_name = True. So you construct/access with session.session_id, and model_dump(by_alias=True) emits the camelCase wire form.
flowchart LR
SPEC["spec/ (JSON Schema)"] --> PY["Python<br/>smooth_operator"]
SPEC --> TS["TypeScript"]
SPEC --> GO["Go"]
SPEC --> NET[".NET (+ MEAI IChatClient facade)"]
SPEC --> RS["Rust"]
Nothing here is vibe-coded — it's verified against a real LLM gateway.
flowchart TD
J["🎯 LLM-as-judge quality evals (Rust harness)"]
E["🌐 Live cross-language E2E — this client boots the real server + drives a real claude-haiku-4-5 turn"]
C["🧪 Conformance fixtures (shared across all 5 clients)"]
U["⚡ Unit tests (discriminated-union parsing, alias round-trip, correlation)"]
J --> E --> C --> U
26 tests. The live cross-language E2E boots a real smooth-operator-server subprocess (KB seeded) and drives a real claude-haiku-4-5 turn over WebSocket: ≥1 streamed event, a knowledge-grounded "17", per-session memory.
A real bug the live E2E caught (mocks masked it): agentId is UUID-typed in spec/, so pydantic rejected a bare string the lenient Go/TS clients accepted — surfacing a real cross-client string-vs-UUID alignment gap. A mock fixture using a valid UUID would have hidden it.
The proof story: an LLM-as-judge scored a multi-turn answer 1/5 (the runtime forgot turn 1's context); the failing eval drove a per-session-memory fix; it now scores 5/5 — a regression a substring test would have missed. See docs/EVALS.md.
Live tests are gated, never silently skipped — SMOOTH_AGENT_E2E=1 + SMOOAI_GATEWAY_KEY to run; skip cleanly otherwise.
uv run pytest # no creds
SMOOTH_AGENT_E2E=1 uv run pytest -m e2e # live cross-language E2Euv sync
uv run python -c "import smooth_operator"
uv run python scripts/generate.py # regen pydantic models from ../spec via datamodel-code-generatorPoint url at the hosted lom.smoo.ai endpoint, or at your own self-hosted smooth-operator-server — same protocol, same client.
MIT © 2026 Smoo AI