github.com/SmooAI/smooth-operator/go β the idiomatic, race-clean Go client for the smooth-operator service.
Streaming agent turns, HITL resume, transport-agnostic. One of five native SDKs over one schema-driven WebSocket protocol.
What is this?
The native Go client for the smooth-operator WebSocket protocol. It connects to a running smooth-operator service (create a session, send a message, stream the agent's events back) β not the agent engine itself. Wire types are generated from the language-neutral JSON Schemas in spec/ (committed, so you don't need the generator); the ergonomic layer gives you typed As* event accessors and a streaming MessageTurn that's both range-able and awaitable.
30-second quickstart
go get github.com/SmooAI/smooth-operator/go/protocol
package main
import (
"context"
"fmt"
"github.com/SmooAI/smooth-operator/go/protocol"
)
func main() {
ctx := context.Background()
c, _ := protocol.New(protocol.Options{
Transport: protocol.NewWebSocketTransport("ws://127.0.0.1:8787/ws", nil),
})
_ = c.Connect(ctx)
defer c.Close()
sess, _ := c.CreateConversationSession(ctx, protocol.CreateConversationSessionParams{
AgentID: "11111111-1111-1111-1111-111111111111",
UserName: "Alice",
})
turn := c.SendMessage(protocol.SendMessageParams{SessionID: sess.SessionID, Message: "How long is your return window?"})
final, _ := turn.Wait(ctx)
fmt.Println("messageId:", final.Data.Data.MessageID)
}
(Point the transport at your own smooth-operator-server or the hosted endpoint.)
Watch it stream
SendMessage returns a MessageTurn β range over Events() for live tokens, then Wait() for the authoritative terminal response. Go has no sum types, so a ServerEvent carries the common envelope fields plus the raw frame; switch on Type and call the matching As* accessor.
turn := c.SendMessage(protocol.SendMessageParams{SessionID: sess.SessionID, Message: "Where's my order?"})
for ev := range turn.Events() {
switch ev.Type {
case protocol.EventStreamToken:
tok, _ := ev.AsStreamToken()
fmt.Print(tok.Data.Token) // tokens, live
case protocol.EventStreamChunk:
chunk, _ := ev.AsStreamChunk()
fmt.Printf("\n β³ node: %s\n", chunk.Node) // workflow node boundary
case protocol.EventWriteConfirmationRequired:
// HITL: approve and the resumed stream flows back into this same turn.
c.ConfirmToolAction(protocol.ConfirmToolActionParams{
SessionID: sess.SessionID, RequestID: turn.RequestID(), Approved: true,
})
}
}
final, _ := turn.Wait(ctx)
fmt.Println("\nmessageId:", final.Data.Data.MessageID)
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','actorBkg':'#0b1426','actorBorder':'#2b3a52','actorTextColor':'#e6edf6','signalColor':'#7c8aa0','signalTextColor':'#e6edf6','noteBkgColor':'#f49f0a','noteTextColor':'#1a0f00','noteBorderColor':'#ff6b6c','fontFamily':'ui-sans-serif, system-ui, sans-serif'}}}%%
sequenceDiagram
participant App
participant C as protocol.Client
participant S as Service
App->>C: SendMessage(...)
C->>S: { action: send_message }
S-->>C: immediate_response (202)
S-->>C: stream_token / stream_chunk β¦
S-->>C: eventual_response (200)
C-->>App: range Events() Β· Wait() returns final
Layout
| File |
Purpose |
protocol/types_gen.go |
Generated wire types (one struct per schema / $def). Do not edit. |
protocol/events.go |
Ergonomic ServerEvent discrimination + typed As* accessors. |
protocol/transport.go |
Transport interface + default coder/websocket implementation. |
protocol/client.go |
Client with the action methods. |
protocol/turn.go |
MessageTurn (streaming events + awaitable terminal) + ProtocolError. |
protocol/validate.go |
Optional runtime JSON Schema validation against ../spec/. |
The Transport interface is mockable, so the test suite drives real client code (correlation, event discrimination, HITL routing) without a network.
Polyglot β one spec, five clients
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
flowchart LR
SPEC["spec/ (JSON Schema)"] --> GO["Go<br/>github.com/SmooAI/smooth-operator/go"]
SPEC --> TS["TypeScript"]
SPEC --> NET[".NET (+ MEAI IChatClient facade)"]
SPEC --> PY["Python"]
SPEC --> RS["Rust"]
Test-driven by default
Nothing here is vibe-coded β it's verified against a real LLM gateway.
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
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 (event discrimination, transport, correlation) β race-clean"]
J --> E --> C --> U
26 tests, race-clean (go test -race). In the live cross-language E2E (e2e_live_test.go), this client boots a real smooth-operator-server subprocess (KB seeded) and drives a real claude-haiku-4-5 turn over WebSocket β asserting β₯1 streamed event, a knowledge-grounded "17", and per-session memory.
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 β they run with SMOOTH_AGENT_E2E=1 + SMOOAI_GATEWAY_KEY and skip cleanly otherwise.
go test -race ./... # no creds
SMOOTH_AGENT_E2E=1 go test -race ./... -run Live # live cross-language E2E
Regenerating types
protocol/types_gen.go is generated with go-jsonschema (pure Go, offline). Run scripts/generate-go.sh from the repo root after any ../spec change:
go install github.com/atombender/go-jsonschema@latest # once
./scripts/generate-go.sh
The script documents its own flags. The two that matter: --only-models (plain structs, no generated enum validation, so the client tolerates forward-compatible wire values and the conformance fixtures round-trip cleanly) and --struct-name-from-title (every $def in the spec carries a stable title, so feeding all schemas at once doesn't collide on the shared $defs/Request / $defs/Response keys).
Smoo-powered or bring-your-own
Point the transport at the hosted lom.smoo.ai endpoint, or at your own self-hosted smooth-operator-server β same protocol, same client.
Authenticating to a token-gated server? Pass a connection Token β it rides the ?token= query slot of the WS URL (browsers can't set WebSocket headers):
c, _ := protocol.New(protocol.Options{
Transport: protocol.NewWebSocketTransportWithOptions(
"wss://lom.smoo.ai/ws",
protocol.WebSocketOptions{Token: connToken},
),
})
π§© Part of Smoo AI
This Go client is built and open-sourced by Smoo AI β the AI-powered business platform with AI built into every product. It's the Go member of the polyglot SDK set (TypeScript Β· Python Β· Go Β· .NET Β· Rust) for the smooth-operator service.
- π The service β smooth-operator (protocol, server, the five clients, AWS/k8s deploy)
- π§° More open source from Smoo AI β smoo.ai/open-source
- βοΈ Hosted β lom.smoo.ai runs smooth-operator for you, managed and multi-tenant
π Links
π License
MIT Β© 2026 Smoo AI. See LICENSE.
Built by Smoo AI β AI built into every product.