Skip to content
Console
Try one

    Go SDK

    Install the Thalovant Go SDK for services, gateways, CLIs, and long-running agents.

    Use the Go SDK when your client needs a compact service, gateway, command-line tool, or agent process.

    The module path is github.com/thalovant/thalovant-go-sdk.

    Current releases of the Go SDK require Go 1.26 or newer. Upgrade the compiler before upgrading from the v0.4.x line.

    Add the module with go get github.com/thalovant/thalovant-go-sdk:

    Terminal window
    go get github.com/thalovant/thalovant-go-sdk

    This is a public module, so you need no GitHub credentials and no GOPRIVATE override. go get takes the latest release; append @<version> when you need a reproducible build, and take the current version from Supported Versions.

    package main
    import (
    "context"
    "fmt"
    "log"
    thalovant "github.com/thalovant/thalovant-go-sdk"
    )
    func main() {
    ctx := context.Background()
    control := thalovant.NewDefaultControlPlane("")
    _, err := control.Login(ctx, "[email protected]", "password", "")
    if err != nil {
    log.Fatal(err)
    }
    result, err := control.CreateClientIdentityForHubID(ctx, "hub-id", thalovant.BootstrapIdentityOptions{
    Name: "go-demo-client",
    PreferredProtocols: []thalovant.HubProtocol{
    thalovant.ProtocolWSS,
    thalovant.ProtocolHTTPS,
    thalovant.ProtocolMQTT,
    },
    })
    if err != nil {
    log.Fatal(err)
    }
    client, err := thalovant.NewClientWithOptions(result.Identity, thalovant.ClientOptions{
    Protocol: thalovant.ProtocolWSS,
    })
    if err != nil {
    log.Fatal(err)
    }
    defer client.Close(ctx)
    reply, err := client.Ask(ctx, "Tell me a short clean joke.", thalovant.RequestOptions{})
    if err != nil {
    log.Fatal(err)
    }
    fmt.Println(reply.Text)
    }

    NewDefaultControlPlane uses https://api.thalovant.com. Use NewControlPlane only for local development or a self-hosted control plane.

    Accounts with multi-factor authentication enabled must include a TOTP code or a one-time recovery code with the login. A plain Login call is rejected with HTTP 401 and code mfa_required. Use LoginWithOptions; the fields are sent only when set. MFA support needs SDK v0.3.2 or newer.

    _, err := control.LoginWithOptions(ctx, "[email protected]", "password", thalovant.LoginOptions{
    OTPCode: "123456",
    })
    // Or use a one-time recovery code instead:
    _, err = control.LoginWithOptions(ctx, "[email protected]", "password", thalovant.LoginOptions{
    RecoveryCode: "abcd-efgh-ijkl",
    })

    Device login asks your browser to approve the sign-in, so CLIs, services, and agents never handle your password. The SDK prints a short code and the address https://dash.thalovant.com/activate, opens that page when it can, and waits while you approve the request in the dashboard with your normal sign-in, including Google sign-in or MFA. On approval the SDK holds a scoped, revocable API token.

    Device login needs SDK v0.3.3 or newer. Approving the request needs a paid workspace plan; a free plan gets HTTP 402.

    token, err := control.LoginWithBrowser(ctx, thalovant.DeviceLoginOptions{
    Scopes: []string{"hubs:read", "clients:write"}, // optional
    ClientName: "my-cli", // optional label in the dashboard
    })
    if err != nil {
    log.Fatal(err)
    }
    fmt.Println("signed in, token id:", token["token_id"])

    Manage the resulting token on the dashboard’s API Tokens page.

    Pass a stored API token when the process should start authenticated, such as CI jobs and services. Mint one on the API Tokens page or with LoginWithBrowser; the page shows scopes, expiry, and last use, and can revoke the token at any time.

    control := thalovant.NewDefaultControlPlane(os.Getenv("THALOVANT_API_TOKEN"))
    page, err := control.ListHubs(ctx, 50, "", "")
    client, err := thalovant.NewClientFromConfig("", "prod")
    if err != nil {
    log.Fatal(err)
    }
    defer client.Close(ctx)

    Raw identity files work too:

    client, err := thalovant.NewClientFromFile("_identity.json")
    if err != nil {
    log.Fatal(err)
    }
    defer client.Close(ctx)
    reply, err := client.Ask(ctx, "What can this hub do?", thalovant.RequestOptions{})
    if err != nil {
    log.Fatal(err)
    }
    fmt.Println(reply.Text)

    Environment variables work too:

    client, err := thalovant.NewClientFromEnv()
    if err != nil {
    log.Fatal(err)
    }

    The SDK connects over WSS, HTTPS, or MQTT over TLS. Every connection is encrypted and authenticated before anything is sent.

    Use the transport’s NoiseStateDir to choose the private, persistent directory where the SDK keeps its keys and the hub identities it has recorded; RemoteStaticKey exposes the authenticated hub’s key. Share the same directory when one identity moves between transports. Use a distinct identity for each client process that runs at the same time, and confirm a changed hub identity with the hub owner before accepting it.

    Set the directory before connecting. For example, when using WSS:

    transport := thalovant.NewWSSTransport(identity)
    transport.NoiseStateDir = "/path/to/private/persistent/sdk-state"
    client := &thalovant.Client{Identity: identity, Transport: transport}

    HTTPS keeps its session across reconnects and cleans up a stale connection before reconnecting after a failed poll. MQTT requires TLS and accepts a custom TLSConfig for private certificate authorities. After the broker connection is lost, call Connect again before sending.

    Use fresh request IDs for Ask and fresh query IDs for Query; generated IDs are the default. An overlapping collector with the same ID is rejected on the same client. The namespaces are independent. Do not reuse an ID for a later logical operation, even after cancellation: a delayed reply can still carry it. Reuse a session ID for conversation context.

    identity := result.Identity
    fmt.Println(identity.EnabledProtocols())
    fmt.Println(identity.EndpointFor(thalovant.ProtocolWSS))
    fmt.Println(identity.EndpointFor(thalovant.ProtocolHTTPS))
    fmt.Println(identity.EndpointFor(thalovant.ProtocolMQTT))
    for _, protocol := range []thalovant.HubProtocol{
    thalovant.ProtocolWSS,
    thalovant.ProtocolHTTPS,
    thalovant.ProtocolMQTT,
    } {
    if !identity.SupportsProtocol(protocol) {
    continue
    }
    if protocol == thalovant.ProtocolMQTT && identity.MQTT == nil {
    continue
    }
    client, err := thalovant.NewClientWithOptions(identity, thalovant.ClientOptions{Protocol: protocol})
    if err != nil {
    log.Fatal(err)
    }
    reply, err := client.Ask(ctx, fmt.Sprintf("Reply over %s.", protocol), thalovant.RequestOptions{})
    _ = client.Close(ctx)
    if err != nil {
    log.Fatal(err)
    }
    fmt.Println(protocol, reply.Text)
    }

    MQTT requires the Identity.MQTT broker credentials returned for that client.

    NewClientWithOptions without an explicit protocol and the file, config, and environment helpers select WSS, then HTTPS, then MQTT. The older NewClient(identity) constructor selects HTTPS; use the options constructor when you want protocol selection.

    For broker details, see MQTT.

    requestContext := thalovant.BuildClientContext(nil, thalovant.ClientContextOptions{
    UserID: "user-42",
    UserName: "Ada",
    AuthProvider: "oidc",
    Source: "checkout-kiosk",
    Platform: "kiosk",
    Locale: "en-US",
    Channel: "chat",
    })
    reply, err := client.Ask(ctx, "Show the next instruction.", thalovant.RequestOptions{
    Context: requestContext,
    })
    events := client.Transport.Events()
    if err := client.SendUtterance(ctx, "Say the current status.", thalovant.RequestOptions{}); err != nil {
    log.Fatal(err)
    }
    select {
    case event := <-events:
    fmt.Println(event.Name, event.Text())
    case <-time.After(12 * time.Second):
    log.Fatal("timed out waiting for a hub event")
    }

    A connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one, over its own session and with no control-plane token. See What Can My Hub Be Asked? for what the hub returns, which message types the connection must be allowed to publish, and what a refusal or a failed listing looks like.

    Since SDK v0.3.13 the calls are client.Intents(ctx, languages, opts...), which returns the inventory grouped by skill, client.ListIntents(ctx, lang, opts...), which returns the registration rows, and client.DescribeIntent(ctx, skillID, intentName, lang, opts...), which returns the definitions behind one intent. The options argument is variadic, so leave it off to take the defaults.

    inventory, err := client.Intents(ctx, []string{"en-us", "fr-fr"})
    if err != nil {
    var denied *thalovant.PolicyDeniedError
    if errors.As(err, &denied) {
    log.Fatalf("refused %s; allowed %v", denied.DeniedType, denied.Allowed)
    }
    log.Fatal(err)
    }
    fmt.Println(inventory.Source, inventory.Languages)
    for _, skill := range inventory.Skills {
    for _, intent := range skill.Intents {
    fmt.Println(intent.ID(), intent.Engine, intent.Examples("fr-fr", 2))
    }
    }

    Intents asks ovos.intent.list once per language, then fills in the sentences the listing did not carry. IntentOptions tunes the call: Timeout bounds each query the hub is sent, five seconds when zero, and Describe set to a false pointer stops at names, engines, and enabled state. Examples(lang, limit) prefers whole sentences to ones with a {slot}, then fuller wording up to eight words; PhrasesFor(lang) returns all of them. Language tags compare without regard to case or _ and -, so fr-fr and fr_FR are one request. Built-in transports give these calls independent subscriptions. A slow subscriber receives an explicit overflow error without consuming another request’s replies. Custom legacy transports that expose only shared channels must serialize callers.

    A hub that refuses a query returns a *PolicyDeniedError at once, which errors.As unwraps for DeniedType, Code, Reason, and Allowed. With the default Fallback, a refused or silent ovos.intent.list returns names only instead: inventory.Source is engine-manifests, inventory.Denied names the unavailable query without proving a policy refusal, and HasPhrases() is false. A hub that accepts the listing and answers it with ok: false returns an error wrapping ErrRuntime and carrying the hub’s own error text, because a failed listing is not an empty hub. Since v0.3.14 that error replaces the empty list the SDK used to return.

    Ask

    Send one request and receive normalized text, speech, and display items.

    Conversation

    Keep related turns in one session.

    SendAction

    Send a button, menu, or tool action.

    SendCode

    Send a scanned value, serial number, QR value, or typed code.

    For the full method list, see SDK Functions. Use control.GetOperation(ctx, operationID) to follow an accepted command; see Operations.

    Since SDK v0.3.6 the control plane can create hubs, runtime groups, and skill installs. Browsing the catalog with ListMarketplaceSkills needs hubs:read and works on any plan. CreateHub, CreateRuntimeGroup, InstallRuntimeGroupSkill, ReleaseHub, and ReleaseRuntimeGroup need hubs:write and a paid plan.

    group, err := api.CreateRuntimeGroup(ctx, map[string]any{"name": "kiosks"})
    hub, err := api.CreateHub(ctx, map[string]any{
    "name": "joke-garden",
    "runtime_group_id": group["id"].(string),
    "spec": map[string]any{},
    }, thalovant.HubCreateOptions{})
    hub, err = api.GetHub(ctx, hub["id"].(string))
    hub, err = api.UpdateHub(ctx, hub["id"].(string), map[string]any{"active": false}, hub["etag"].(string))

    UpdateHub and DeleteHub take the hub’s current etag as a required argument. See Provision Hubs for the full flow, the immutable fields, and the error table.

    SymptomCheck
    Missing access tokenCall control.Login(...) or control.LoginWithBrowser(...) before private API actions, or pass a token to NewDefaultControlPlane.
    HTTP 401 with code mfa_requiredUse control.LoginWithOptions(...) with an OTPCode or RecoveryCode.
    API access requires a paid planUpgrade the workspace before provisioning private resources through the API.
    Unsupported protocolEnable that protocol on the hub and create a fresh identity.
    MQTT fails immediatelyConfirm the identity has Identity.MQTT broker credentials.

    Fallback Capabilities And Request Deadlines

    Section titled “Fallback Capabilities And Request Deadlines”

    Use the additive capability wrapper when deciding whether a hub may answer in a language. The existing HubIntentInventory shape stays compatible:

    capabilities, err := client.IntentsWithCapabilities(ctx, []string{"en-us"}, thalovant.IntentOptions{})
    if err != nil { return err }
    fmt.Println(capabilities.Inventory.Source, capabilities.FallbacksKnown)
    fmt.Println(capabilities.MayAnswer("en-us"))

    ListFallbacks(ctx, timeout) returns nil for unknown support and a non-nil empty slice for a confirmed empty result. Direct calls honor the supplied positive timeout, defaulting to five seconds. The automatic probe in IntentsWithCapabilities includes connect, send, and wait within at most 1.5 seconds. See fallback discovery.

    AskWithOptions takes AskOptions, embedding the existing RequestOptions plus ReplySettle and EmptyReplyWait. Defaults allow 250 ms for adjacent fragments and five seconds for delayed speech after a soft miss, within the overall request deadline. Query waits for its correlated completion; a later speech event can recover an intent miss. A policy denial or explicit query timeout preserves a failed partial reply.

    Connection and cleanup deadlines include time spent queued behind an earlier operation. A timed-out caller cannot steal another operation’s transport. Keep using the same client until retained cleanup finishes; do not create competing clients with the same identity. Do not copy a Client after first use. On Windows, store the SDK’s saved keys in an application-private directory with restrictive inherited ACLs; POSIX mode bits do not establish Windows access control.

    Control-plane redirects are refused. Authenticated API calls and request bodies require HTTPS except explicit loopback development endpoints. See API credential security.

    WaitForEvent(ctx, name, EventOptions) waits for one correlated event with a twelve-second default budget. Listen(ctx, name, ListenOptions) returns a bounded Subscription[Event]. Options select timeout, context, session ID, request ID, and predicate; listen options also select MaxEvents and Capacity.

    Cancel the context or close the subscription to unsubscribe. Overflow and disconnect are explicit errors. A cancelled or slow subscriber does not consume another caller’s replies or close the shared connection. Listen has no lifetime or count cap unless you set one, but its queue remains bounded.

    The SDK can list, install, update, remove and inspect the history of skills on a hub’s runtime group. Every hub sharing that runtime group is affected. A hub UUID selects the group; it does not create an isolated installation for that hub. A restricted token must cover all hubs served by the group.

    Reads require hubs:inspect (hubs:read includes it). Writes require hubs:write, an eligible paid plan and ownership. Use latest or a concrete released version for installation and updates. An accepted write returns an operation_id; acceptance does not mean the runtime has finished installing the skill.

    ActionMethod
    ListListHubSkills
    HistoryListHubSkillHistory
    InstallInstallHubSkill
    UpdateUpdateHubSkill
    RemoveRemoveHubSkill

    History returns the API JSON envelope with newest-first event and operation entries, preserving nullable actor/version fields. The limit is 1–200.

    Use HubSkillWaitOptions to opt into operation waiting (120 seconds, polling every two seconds by default). For cancellation-sensitive calls, first submit without waiting, retain the accepted response, then call WaitForHubSkillOperation. Cancelling the wait does not undo the accepted server operation. Polling failures retain the operation ID in the error; inspect that operation instead of repeating the write. The wait deadline prevents new polls but does not cancel a request already in flight.

    Request Helpers And Safe Configuration Updates

    Section titled “Request Helpers And Safe Configuration Updates”

    Available since 0.7.0.

    AskOptions and Reply gain fields in this release. Use keyed struct literals when upgrading code that constructed these types positionally.

    Request hints carry a recognized language, ordered intent pipeline, and caller location without changing the caller’s context. Empty hints are omitted. The location helper requires a city and omits invalid or zero/zero coordinates. The hub validates language hints against its configured languages.

    Replies expose their reported language, ordered speech/audio events, and a count of dropped media. Embedded skill clips are limited to 4 MiB each and 16 MiB per reply, checked before retention and decoding. Audio does not extend the reply settlement window. Decoding accepts hexadecimal bytes with ASCII whitespace between bytes; it never fetches a skill-supplied URL or file path. The application owns playback. Any play or Play function below belongs to your app.

    location := thalovant.BuildLocation(thalovant.LocationOptions{City: "Montréal", Country: "CA"})
    reply, err := client.AskWithOptions(ctx, "Quel temps fait-il ?", thalovant.AskOptions{
    STTLang: "fr-ca", Location: location,
    })
    // Check err before reading reply. AudioBytes returns ([]byte, error).
    examples := intent.ExamplesWithOptions("en-us", 2, thalovant.IntentExampleOptions{Speakable: true})
    delta := map[string]any{"lang": "en-us"}
    _, err = control.UpdateRuntimeGroupConfig(ctx, groupID, delta, thalovant.RuntimeGroupConfigOptions{})
    // Explicit full replacement:
    fullConfig := map[string]any{"lang": "en-us"}
    _, err = control.ReplaceRuntimeGroupConfig(ctx, groupID, fullConfig, thalovant.RuntimeGroupConfigOptions{})

    Guarded merging requires the hubs:read and hubs:write scopes and a paid plan. Safe merging requires an API whose configuration GET returns a valid revision and whose configuration PUT checks expected_revision. The SDK rereads and reapplies the original delta only after HTTP 412, with at most three attempts. Arrays and scalar values replace; objects merge recursively. Personas replace only when explicitly supplied. Connection failures, redirects, other statuses, and ambiguous write results are never retried. No unsafe PATCH fallback is used. Unconditional replacements must still be coordinated with other writers.

    Use the explicit replacement operation shown above when a complete replacement is intended, including when working with an older API. Existing code relying on replacement must opt into it when upgrading. Raw intent patterns remain the default; speakable examples remove optional parts, choose alternatives, and substitute caller-supplied slots while retaining complete-phrase priority.

    The audio limits use encoded-length upper bounds before decoding, so formatting whitespace consumes budget too. Like Python’s bytes.fromhex, ASCII whitespace alone decodes to zero bytes. Bounded malformed clips remain available as event metadata and fail when decoded; they are never fetched or played automatically. Distinct audio events may intentionally repeat identical sound content. Only repeated delivery of the same event object is suppressed where object identity is available, without counting it as a dropped clip. Rendered example ranking uses the original pattern’s slot presence even when sample values are supplied.

    Available since 0.8.1.

    Sentence listings use bundled thalovant-languages 0.2.1 data and match Python 0.6.8 with its listing extra. Language selection follows OVOS regional distances, so a request for fr-CA can use a registered fr-FR locale.

    examples := intent.ExamplesWithOptions("fr-CA", 2, thalovant.IntentExampleOptions{Sentence: true})
    text := thalovant.AsSentence("what time is it", "en-US")
    sample := thalovant.SpeakableWithLanguage("play {song}", nil, "en-US")

    AsSentence(text, lang string) string renders one phrase. SpeakableWithLanguage(pattern string, slots map[string]string, lang string) string uses locale sample slots before explicit overrides.

    Sentence rendering implies speakable rendering. Explicit slot values override locale samples. Complete phrases rank before prefixes and slot patterns; empty and duplicate rendered examples do not consume the limit. Unlimited raw examples keep registration order. Unknown locales retain bare text and slot names.

    Existing example methods remain available. Use ListingRules to provide a complete custom data snapshot or select bare rendering without locale data. No network request is needed to load the bundled rules. See the repository README for custom-rule construction and regex error handling.

    The listing data now describes 270 languages, including regional overrides. Spanish qué hora es becomes Qué hora es?, and French coupe le son becomes Coupe le son.. Languages without rules, such as tlh, remain bare.

    HubSession owns one reusable hub connection. Supply a factory that returns a connected client and cleans up a failed or cancelled connection attempt. Event subscriptions survive client replacement. Go and Rust expose a persistent event stream; the other managed SDKs expose subscription handles. Close the session when its owner shuts down; close waits for accepted operations and is terminal.

    Background connection attempts back off for 10, 20, 40, 80, then 120 seconds. Foreground calls can try immediately. Your application owns probe scheduling: use the reported probe delay (60 seconds while held, 5 seconds while down). The SDK never replays an accepted Ask or Emit after a lost response, because an Ask can trigger an action. A request timeout applies to the underlying operation; waiting for the session and your connection factory are separate budgets.

    session, err := thalovant.NewHubSession(connectClient, thalovant.DefaultHubSessionPolicy())
    if err != nil { return err }
    defer session.Close(context.Background())
    reply, err := session.Ask(ctx, "What is the weather?", thalovant.AskOptions{})

    Inventory, Skill, and Intent provide a presentable view separate from the runtime’s native intent inventory. Unknown catalogue locales remain unknown; phrases observed for a language do not prove catalogue support. Examples choose the closest supported locale. A nonpositive limit returns the raw phrase pool (Rust uses zero for its unsigned limit). Cache JSON includes explicit intent language order so serialization cannot change the default example language.

    InventoryCache is optional, defaults to a one-hour TTL, and returns a miss for invalid, expired, or unreadable data. Writes use private, unique scratch files and atomic replacement. POSIX cache files are owner-readable/writable; Windows uses the user’s directory ACLs. Cache keys separate mode, identity path, and the full normalized hub hostname. Never use inventory caches to store credentials.

    OriginPreference gives a preferred address its own short connection budget and cools it down after a failure. In non-Python SDKs the factory must implement the address binding on its own transport, retain the public host for TLS/SNI, and finish failed-attempt cleanup before returning. Transport/platform restrictions still apply. This helper does not change global DNS or disable TLS validation.

    A reply exposes reply.PipelineIDs(), reply.SkillIDs() and reply.Claimed().

    Pipeline and skill IDs are nonempty strings in first-seen order, with duplicates removed. A successful reply with only pipeline IDs containing the case-sensitive text fallback reports an unclaimed reply. Any non-fallback stage makes a successful reply claimed. Successful replies from older hubs without stage stamps remain claimed. Failed or unhandled replies are never claimed. Malformed non-string stamps are ignored.

    Use this as a hint when deciding whether to continue a conversation. Fallback text and the existing success status are retained. These stamps are not verified peer identity and must not authorize actions.