README
ΒΆ
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)
Stop button
turn.Cancel() is the client-initiated "stop" β it sends a cancel frame for this turn. The server aborts the LLM + tool work and emits a terminal cancelled event (in place of eventual_response); the turn then settles as a user-stop: Wait resolves with no error, the Events() channel closes cleanly, and turn.Cancelled() reports true so the UI tells a stop apart from a failure. Idempotent β a cancel with nothing in flight is a harmless no-op.
turn := c.SendMessage(protocol.SendMessageParams{SessionID: sess.SessionID, Message: "Write me an essayβ¦"})
go func() { <-stopButton; turn.Cancel() }() // user hits stop
if _, err := turn.Wait(ctx); err != nil {
// a real failure β surface it
} else if turn.Cancelled() {
fmt.Println("stopped by user") // resolved, not errored
}
c.Cancel(protocol.CancelParams{RequestID: turn.RequestID(), SessionID: sess.SessionID}) is the lower-level form when you hold the requestId but not the MessageTurn.
%%{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 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://your-operator.example.com/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 by Smoo β smooth-operator runs the Smoo AI platform in production
π Links
- π¦ pkg.go.dev β
github.com/SmooAI/smooth-operator/go - π°οΈ Protocol β
docs/PROTOCOL.md - π§ͺ Evals β
docs/EVALS.md - π¬ Issues β github.com/SmooAI/smooth-operator/issues
π License
MIT Β© 2026 Smoo AI. See LICENSE.
Built by Smoo AI β AI built into every product.
Documentation
ΒΆ
Overview ΒΆ
Package e2e is the module root for the Go implementation of smooth-operator.
Index ΒΆ
Constants ΒΆ
const Version = "1.55.2"
Version is the shared, lockstep release version for all smooth-operator language artifacts. The real Go "publish" is a git tag (go/v<Version>); this constant is the anchor that scripts/sync-versions.mjs keeps in sync with the canonical npm version on every changeset release.
Variables ΒΆ
This section is empty.
Functions ΒΆ
This section is empty.
Types ΒΆ
This section is empty.
