# Talking to a hub

> Connect, ask, list what it understands, and send utterances, actions, and codes.

Source: https://docs.thalovant.com/developers/sdk-functions/talking-to-a-hub/

---
Part of [SDK Functions](/developers/sdk-functions/). Every call below is shown in each language that has it.

## Reuse A Connection And Present Its Skills

All seven managed SDKs expose `HubSession` and `HubSessionPolicy` for a reusable
connection, retained subscriptions, and configurable reconnect/probe timing.
Supply a connected-client factory and close the session when its owner exits.
An uncertain Ask or Emit is not replayed automatically: the application decides
whether retrying an action is safe. Failed cleanup blocks a replacement until
that connection has been retired.

Use `Inventory`, `Skill`, and `Intent` for a presentable view of discovered
skills and locale-aware examples. `InventoryCache` adds optional bounded,
private storage with a default one-hour TTL; unknown catalogue language support
stays unknown. Explicit language order survives JSON serialization. This view
is separate from the native intent-inventory response.

See the managed-session and cache examples for
[Python](/developers/sdks/python/#managed-sessions-and-inventory-caches),
[Node](/developers/sdks/node/#managed-sessions-and-inventory-caches),
[Go](/developers/sdks/go/#managed-sessions-and-inventory-caches),
[Rust](/developers/sdks/rust/#managed-sessions-and-inventory-caches),
[Kotlin](/developers/sdks/kotlin/#managed-sessions-and-inventory-caches),
[.NET](/developers/sdks/dotnet/#managed-sessions-and-inventory-caches), and
[Swift](/developers/sdks/swift/#managed-sessions-and-inventory-caches).
MCP returns the [presentation view](/developers/mcp/#presentable-inventory-output)
within each tool's identity lease. Embedded C leaves sessions and storage to its
caller.

## Create A Runtime Client

Use a runtime client after you have identity material.

Keep the SDK's saved keys in private, persistent storage. Use one identity per client process that runs at the same time. A reconnect authenticates again with the same identity and saved keys; a failed connection is never a reason to delete them. Each [SDK page](/developers/sdks/) names its storage option.

  

```python
from thalovant import ThalovantClient

client = ThalovantClient(identity, protocol="wss")
client.connect()
print(client.healthcheck())
client.close()
```

  
  

```ts

const client = new ThalovantClient(identity, { protocol: "wss" });
await client.connect();
console.log(await client.healthcheck());
await client.close();
```

  
  

```go
client, err := thalovant.NewClientWithOptions(identity, thalovant.ClientOptions{
	Protocol: thalovant.ProtocolWSS,
})
if err != nil {
	log.Fatal(err)
}

if err := client.Connect(ctx); err != nil {
	log.Fatal(err)
}
fmt.Println(client.Healthcheck())
client.Close(ctx)
```

  
  

```rust
use thalovant::{Client, HubProtocol};

let client = Client::with_protocol(identity.clone(), HubProtocol::Wss)?;
client.connect().await?;
println!("{:?}", client.healthcheck().await);
client.close().await?;
```

  

Use a context manager, `try/finally`, or `defer` so connections close reliably. In Rust, close the client before propagating a request error: `?` immediately returns from the current function and does not await connection cleanup. Concurrent callers join authenticated readiness within their own deadlines. A cancelled queued caller must not close the active connection.

## Runtime Helper Names

These helpers cover the same workflows in each high-level SDK's supported transports. Kotlin extension helpers need `import com.thalovant.sdk.*`.

| Workflow | Python | Node.js | Go | Rust | Kotlin / Swift | .NET |
| --- | --- | --- | --- | --- | --- | --- |
| Routed query and cascade replies | `query` | `query` | `Query` | `query` | `query` | `QueryAsync` |
| Reuse a conversational session | `conversation` | `conversation` | `Conversation` | `conversation` | `conversation` | `Conversation` |
| Wait for a correlated event | `wait_for_event` | `waitForEvent` | `WaitForEvent` | `wait_for_event` | `waitForEvent` | `WaitForEventAsync` |
| Observe an event stream | `listen` | `on` | `Listen` | `listen` | `listen` | `ListenAsync` |
| Connect and inspect readiness | `connect_with_info` | `connectWithInfo` | `ConnectWithInfo` | `connect_with_info` | `connectWithInfo` | `ConnectWithInfoAsync` |
| Ask with language, pipeline and location hints | `ask` | `ask` | `AskWithOptions` | `ask_with_hints` | `askWithHints` | `AskWithHintsAsync` |
| Build a request location | `build_location` | `buildLocation` | `BuildLocation` | `build_location` | `buildLocation` | `ThalovantContext.BuildLocation` |
| Render a speakable intent pattern | `speakable` | `speakable` | `Speakable` | `speakable` | `speakable` | `ThalovantContext.Speakable` |
| Decode one embedded audio event | `audio_bytes` | `audioBytes` | `AudioBytes` | `audio_bytes` | `audioBytes` | `AudioBytes` |
| Read answering pipeline IDs | `reply.pipeline_ids` | `reply.pipelineIds` | `reply.PipelineIDs()` | `reply.pipeline_ids()` | `reply.pipelineIds` | `reply.PipelineIds` |
| Read answering skill IDs | `reply.skill_ids` | `reply.skillIds` | `reply.SkillIDs()` | `reply.skill_ids()` | `reply.skillIds` | `reply.SkillIds` |
| Inspect advisory reply claim | `reply.claimed` | `reply.claimed` | `reply.Claimed()` | `reply.claimed()` | `reply.claimed` | `reply.Claimed` |

Kotlin uses a `Flow`, Swift an `AsyncThrowingStream`, and .NET an `IAsyncEnumerable` for listening. Their buffers hold at most 64 events and report overflow explicitly. Cancel or finish the stream to remove its subscription. Swift starts the subscription when `listen` is called; .NET starts when enumerated. Use each language guide for timeout units and cancellation inputs.

Ask allows bounded delayed speech; Query follows its query ID through cascade replies and waits for completion. A soft intent miss can recover when speech arrives. Hard policy denial and explicit query timeout remain failures, with partial speech preserved. Conversation helpers reuse the session while creating a fresh request ID and merged context per request. A reconnect never automatically replays an application request.

Reply IDs are nonempty strings in first-seen order. A fallback-only successful reply retains its text and success status but reports `claimed=false`; a successful reply without stage stamps retains legacy claimed behavior. Failed or unhandled replies are never claimed. Claim metadata does not verify peer identity. See [reply claims](/developers/sdks/python/#reply-claims) for matching and malformed-stamp behavior.

Embedded C exposes `thalovant_ask_event_pipeline_id()`, `thalovant_ask_event_skill_id()` and `thalovant_reply_claimed()` with caller-owned buffers and ID aggregation. [MCP runtime summaries](/developers/mcp/#reply-claims) expose `pipelineIds`, `skillIds` and `claimed` through the Node SDK.

## Ask A Hub

Use `ask` for one request-response interaction.

  

```python
reply = client.ask("What can this hub do?")

print(reply.text)
for item in reply.display_items():
    print(item)
```

  
  

```ts
const reply = await client.ask("What can this hub do?");

console.log(reply.text);
for (const item of reply.displayItems()) {
  console.log(item);
}
```

  
  

```go
reply, err := client.Ask(ctx, "What can this hub do?", thalovant.RequestOptions{})
if err != nil {
	log.Fatal(err)
}

fmt.Println(reply.Text)
for _, item := range reply.DisplayItems(600) {
	fmt.Println(item)
}
```

  
  

```rust
let reply = client
    .ask("What can this hub do?", RequestOptions::default())
    .await?;

println!("{}", reply.text);
for item in reply.display_items(Some(600)) {
    println!("{item:?}");
}
```

  

Expected output shape:

```json
{
  "text": "This hub can answer quick joke and trivia requests.",
  "display_items": []
}
```

## List What A Hub Can Be Asked

Use `intents` when a client should show people what the hub understands, or confirm that a skill registered. The hub answers over the client's own session with every intent, per language, and the sentences that reach it. No control-plane token is involved. The connection must be allowed to publish `ovos.intent.list`, and `ovos.intent.describe` as well when the SDK has to ask for the sentences separately, which a hub that returns them with the listing never makes it do. See [What Can My Hub Be Asked?](/concepts/hub-intents/).

  

```python
from thalovant import ThalovantPolicyDeniedError

try:
    inventory = client.intents(["en-us", "fr-fr"])
except ThalovantPolicyDeniedError as denied:
    print("refused:", denied.denied_type, "allowed:", denied.allowed)
    raise

for intent in inventory.intents:
    print(intent.id, intent.examples("en-us"))
```

  

Expected output shape:

```json
{
  "languages": ["en-us", "fr-fr"],
  "source": "intent-manifest",
  "denied": [],
  "skills": [
    {
      "skill_id": "thalovant-skill-weather.thalovant",
      "languages": ["en-us", "fr-fr"],
      "intents": [
        {
          "id": "thalovant-skill-weather.thalovant:current.weather",
          "skill_id": "thalovant-skill-weather.thalovant",
          "name": "current.weather",
          "engine": "padatious",
          "enabled": true,
          "phrases": {
            "en-us": ["what is the weather", "what is the weather in {location}"],
            "fr-fr": ["quel temps fait-il", "quelle est la météo à {location}"]
          }
        }
      ]
    }
  ]
}
```

A refused query raises the SDK's policy error at once, naming the message type, instead of waiting for a timeout. When `ovos.intent.list` is refused and the fallback is on, which is the default, the result carries names only with `source` set to `engine-manifests`. A hub that accepts a query and answers it with `ok: false` is a different case: a failed listing raises the SDK's runtime error with the hub's error text, because a failed listing is not an empty hub, while a failed describe leaves that one intent without sentences and raises nothing. Every published SDK ships this recipe under its own spelling; each [SDK page](/developers/sdks/) gives a working example, and the [SDKs page](/developers/sdks/) lists the version each one shipped it in.

The current high-level SDKs use the default engine-manifest fallback when `ovos.intent.list` times out. In that case, `denied` names the unanswered query and does not prove a policy refusal. Disable fallback to propagate the listing timeout; silent fallback engines still produce a timeout. See [intent discovery](/concepts/hub-intents/).

## Emit A Raw Event

Use raw events when you already have an event name and a structured payload.

  

```python
client.emit(
    "thalovant.client.status",
    {"state": "ready"},
    {"source": "checkout-kiosk"},
)
```

  
  

```ts
await client.emit(
  "thalovant.client.status",
  { state: "ready" },
  { source: "checkout-kiosk" },
);
```

  
  

```go
err := client.Emit(ctx, "thalovant.client.status", map[string]any{
	"state": "ready",
}, map[string]any{
	"source": "checkout-kiosk",
})
if err != nil {
	log.Fatal(err)
}
```

  
  

```rust
use serde_json::json;

let mut data = serde_json::Map::new();
data.insert("state".into(), json!("ready"));

let mut context = serde_json::Map::new();
context.insert("source".into(), json!("checkout-kiosk"));

client
    .emit("thalovant.client.status", data, context)
    .await?;
```

  

## Send An Utterance

Use an utterance when the input should behave like speech or chat text from a client.

  

```python
client.send_utterance(
    "What is the status?",
    lang="en-us",
    session_id="status-session",
)
```

  
  

```ts
await client.sendUtterance("What is the status?", {
  lang: "en-us",
  sessionId: "status-session",
});
```

  
  

```go
err := client.SendUtterance(ctx, "What is the status?", thalovant.RequestOptions{
	Lang:      "en-us",
	SessionID: "status-session",
})
if err != nil {
	log.Fatal(err)
}
```

  
  

```rust
client
    .send_utterance(
        "What is the status?",
        RequestOptions {
            lang: Some("en-us".into()),
            session_id: Some("status-session".into()),
            ..Default::default()
        },
    )
    .await?;
```

  

## Send An Action

Use actions for buttons, menu picks, confirmations, and tool commands.

  

```python
client.send_action(
    "/approve invoice-42",
    title="Approve invoice",
    session_id="approval-session",
)
```

  
  

```ts
await client.sendAction(
  "/approve invoice-42",
  { title: "Approve invoice", sessionId: "approval-session" },
);
```

  
  

```go
err := client.SendAction(ctx, "/approve invoice-42", thalovant.ActionOptions{
	Title:     "Approve invoice",
	SessionID: "approval-session",
})
if err != nil {
	log.Fatal(err)
}
```

  
  

```rust
client
    .send_action(
        "/approve invoice-42",
        ActionOptions {
            title: Some("Approve invoice".into()),
            session_id: Some("approval-session".into()),
            ..Default::default()
        },
    )
    .await?;
```

  

## Send A Code

Use code input for QR values, barcode scans, serial numbers, short codes, or typed exact values.

  

```python
client.send_code(
    "INV-2026-001",
    kind="invoice",
    session_id="scan-session",
)
```

  
  

```ts
await client.sendCode("INV-2026-001", {
  kind: "invoice",
  sessionId: "scan-session",
});
```

  
  

```go
err := client.SendCode(ctx, "INV-2026-001", thalovant.CodeOptions{
	Kind:      "invoice",
	SessionID: "scan-session",
})
if err != nil {
	log.Fatal(err)
}
```

  
  

```rust
client
    .send_code(
        "INV-2026-001",
        CodeOptions {
            kind: Some("invoice".into()),
            session_id: Some("scan-session".into()),
            ..Default::default()
        },
    )
    .await?;
```

  

## Keep A Conversation

Use a conversation when related turns should share the same session.

  

```python
with client.conversation(lang="en-us") as convo:
    print(convo.ask("Remember that my favorite color is blue.").text)
    print(convo.ask("What color did I mention?").text)
```

  
  

```ts
const convo = client.conversation({ lang: "en-us" });

const first = await convo.ask("Remember that my favorite color is blue.");
const second = await convo.ask("What color did I mention?");
console.log(first.text, second.text);
```

  
  

```go
convo := client.Conversation(thalovant.ConversationOptions{Lang: "en-us"})

first, err := convo.Ask(ctx, "Remember that my favorite color is blue.", thalovant.RequestOptions{})
if err != nil {
	log.Fatal(err)
}
second, err := convo.Ask(ctx, "What color did I mention?", thalovant.RequestOptions{})
if err != nil {
	log.Fatal(err)
}
fmt.Println(first.Text, second.Text)
```

  
  

```rust
let convo = client.conversation(ConversationOptions {
    lang: Some("en-us".into()),
    ..Default::default()
});

let first = convo
    .ask("Remember that my favorite color is blue.", RequestOptions::default())
    .await?;
let second = convo
    .ask("What color did I mention?", RequestOptions::default())
    .await?;
println!("{} {}", first.text, second.text);
```

  

## Render Locale-Aware Intent Examples

With the [current supported SDK versions](/developers/sdks/#supported-versions),
Python uses `sentence=True` with the optional listing extra. Node accepts
`{ sentence: true }` in `intent.examples(lang, limit, options)`.
Go uses `IntentExampleOptions{Sentence: true}` with `ExamplesWithOptions`;
Rust uses `IntentExampleOptions` with `examples_with_listing`.
Kotlin, Swift and .NET expose `examplesWithListing` / `ExamplesWithListing`.
MCP exposes `sentence` on `thalovant_intent_inventory`.

These renderers use locale slot samples before caller overrides, prefer complete
phrases, and count unique nonempty results toward rendered limits. Raw unlimited
examples preserve registration order. The managed SDKs bundle the same data used
by Python's listing extra. See each [SDK guide](/developers/sdks/) for its call
signature and custom-rule options.
