Witbitz docs HomeTrustAll docs

The assistant — who it is, when it answers, what it may use

Every Space has an assistant. This page is how you set it up and change it, when it answers, how your code gets its answer — or runs it as a function — which tools it may use, and voice.

Part of the SDK · every call: the reference.


Its name, instructions and model

You name the assistant when you make the room (createSpace({ agent: { name, instructions, model? } })), and change it later, in place, with configure:

javascript
await space.configure({ agent: { instructions: 'Never invent a value. Never create a project unasked.' } })

The model and its reasoning effort

model is a model id — 'kimi-k3' — or an object that also sets how hard the model reasons before it answers:

javascript
const models = await wb.models({ tier: 'enclave' })   // [{ id, label, wire, efforts, defaultEffort, default }]
await space.configure({ agent: { model: { catalog: 'kimi-k3', params: { reasoning_effort: 'high' } } } })

The levels run none · minimal · low · medium · high · xhigh · max, and each model takes only some of them — the list differs by model, and for some models by tier. wb.models() returns each model's efforts, measured against the live provider rather than copied from its documentation; an empty list means the model takes none, so leave the field out.

The table is generated from the live catalog; when they differ, wb.models() is right.

Model Tiers reasoning_effort
gpt-5.5 standard none low medium high xhigh
gpt-5.1 standard none low medium high
gpt-6-sol standard none low medium high xhigh
gpt-6-luna standard none low medium high xhigh
gpt-6-astra standard low medium high xhigh
gpt-5.6-sol standard none low medium high xhigh
gpt-5.6-luna standard none low medium high xhigh
gpt-5.6-terra standard none low medium high xhigh
claude-opus-5 standard none low medium high xhigh max
claude-opus-5.5 standard low medium high xhigh max
claude-sonnet-4.6 standard low medium high max
claude-sonnet-5.5 standard low medium high xhigh max
claude-sonnet-5 standard none low medium high xhigh max
claude-fable-5.1 standard low medium high xhigh max
claude-haiku-4.5 standard low medium high
gemini-2.5-pro standard low medium high
gemini-2.5-flash standard none minimal low medium high
deepseek-v4-pro standard low high xhigh max
deepseek-v4-flash standard low high xhigh max
deepseek-v4.1-flash both low high xhigh max
grok-4.6 standard minimal low medium high xhigh
kimi-k3 both none low high max
glm-5.3-flash both low high max
glm-5.3 both low high max
gpt-oss-120b both low medium high
gemma-4-31b both none minimal low medium high xhigh max
llama-3.3-70b both none — omit the field
mistral-large standard none — omit the field
qwen-3.6-plus standard none minimal low medium high xhigh
nemotron-3-ultra standard none minimal low medium high xhigh max
gemma-4-uncensored standard none — omit the field

When it answers

javascript
await space.setReplyMode('ondemand')
await space.replyMode()                             // → 'ondemand'
await space.send('Thoughts?', { mode: 'always' })   // this turn only
Mode The assistant answers
'auto' (default) when the room is one person and the assistant, or a message is addressed to it; in a group, a small model decides whether a message wants help
'always' every message
'ondemand' only when it is named or @-mentioned

It is the same setting as the Spaces app's reply chip, and a turn re-reads it once it is 20 s old. How the choice is made: how a turn runs.

A turn you can hold

send() behaves like a room: you post, and whatever happens next arrives on on('message') — the assistant may answer or stay quiet, and other people may speak first. When your code needs the answer to this message, use turn():

javascript
const t = space.turn('Plan the trip', { timeoutMs: 120_000 })
t.on('partial', ({ text }) => draft.textContent = text)
const { status, reply, error } = await t.finished
if (status === 'done') show(reply)
else if (status === 'silent') hint('The assistant stayed out of it.')
else if (status === 'failed') warn(error)

What a turn used

send()'s answer and finished carry usage — the turn's model calls added up, from the provider's own figures — so you can measure a turn without waiting for the day's billing:

javascript
const { usage } = await space.turn('Summarise the week').finished
// { tokensIn: 18240, tokensOut: 612, tokensCached: 15872, costMicros: 2140, costUsd: 0.00214,
//   calls: 2, models: ['deepseek/deepseek-v4.1-flash'], source: 'provider' }

The assistant as a function

space.run(input, opts) runs the room's assistant once and gives you its answer. Nothing is written to the room — no message, no reply, no card, no notification. Use it for work that is not a conversation: pull fields out of a text, classify a request, draft something your app shows its own way.

javascript
const r = await space.run('Invoice 2291 from Acme, total 1,240.50 EUR, due 30 Nov', {
  instructions: 'Extract the invoice fields.',
  schema: { vendor: 'string', total: 'number', due: 'string' },
})
if (r.ok) console.log(r.output)   // { vendor: 'Acme', total: 1240.5, due: '30 Nov' }
else console.log(r.error, r.detail)
error Meaning
too_slow the run did not finish within the endpoint's ~30 s, so its answer could not be returned (nothing was kept). Shorten the task or lower the effort.
no_structured_output schema was given and the assistant never produced an answer that fits it (its text comes with the error)
bad_schema the schema is neither form above
run_needs_payer no user token, app key or room grant pays for it
rate_limited more than 4 at once or 60 a minute in this room; wait retryAfter seconds
run_unavailable the server cannot run it here (a confidential room on a server whose enclave does not run them)
run_unsupported the server is older than runs
unavailable (or another transport code, with detail) the request got no answer — the network, or an enclave that could not be reached; a run keeps nothing, so it is safe to try again

Tools

Name the tools the assistant may use when you make the room. The ones in immediateTools it calls directly; the rest become proposals a member approves before they run.

javascript
const space = await wb.createSpace({
  agent: { name: 'Travel', instructions: 'Plan trips; put options on the shared map and list.' },
  tools: ['search_places', 'show_places', 'add_place', 'search_flights', 'show_flights', 'set_itinerary'],
  immediateTools: ['search_places', 'show_places', 'set_itinerary'],
})

The whole catalogue — places, flights, the itinerary, charts, photos, read_file, read_url, write_pdf — and what each produces: How a turn runs. Your page's own functions become tools as a private lane's actions, or for one question in any room (below).

Your page's tools, for one question

send(text, { tools }) offers the assistant your page's own functions for that question only, in the shape a lane's actions take. An await tool runs on this page while the turn waits and its result goes back to the assistant; a propose tool becomes a proposal a member approves.

javascript
await space.send(question, { tools: [
  { name: 'search_guide', description: 'Search the help guide', input: { topic: 'string' }, mode: 'await',
    run: async ({ topic }) => searchGuide(topic) },
  { name: 'where_am_i', description: 'The screen the person is on', mode: 'await', run: async () => currentScreen() },
] })

Choosing the platform tools

Every assistant also gets Witbitz's platform tools: web_search, fetch_url, read_file, write_pdf, look_at_image, show_photo, show_chart, show_diagram, search_places, show_places and add_place; connection_call in a room with an API-key connection; and cross_to_shared in a private lane. Their descriptions go with every model call — about 4k tokens, plus about 1k of guidance for places and photos, paid again on each round of a turn's tool loop — so if your assistant uses only some, name them:

javascript
await wb.createSpace({ agent, platformTools: ['web_search', 'read_file'] })   // only these two
await wb.createSpace({ agent, platformTools: false })                         // none
await space.configure({ platformTools: true })                                // all again (the default)

Builder mode: only your prompt

By default the assistant is framed as a member of a Spaces group chat: a line saying so, rules for formatting and place links, and, in a private lane, how the member's shared room works and how cross_to_shared reaches it. That suits a chat app; in an app of your own it can get in the way. framing: 'builder' leaves it all out: the model gets your instructions, the conversation, the message and the tools you chose, and nothing else.

javascript
const lane = await space.privateLane({
  agent: { name: 'Keep', instructions: 'You are Keepwarden\'s assistant. Change things only through our actions.' },
  framing: 'builder',
  data: { tasks: { title: 'string', done: 'boolean' } },
  dataWrites: false,              // it may read the store (data_query) but proposes no data_put / data_delete
  platformTools: ['web_search'],
})
await space.configure({ framing: null, dataWrites: null })   // back to the defaults

framing works on createSpace, privateLane and configure. Tool descriptions are unchanged — they describe each tool, not your app — and a shared file is still announced to the model when read_file is offered.

See the prompt. send(text, { showPrompt: true }) (and turn() with the same option) answers with prompt: the exact system text, user text, model and tools that turn sent. Nothing extra is stored. A turn longer than about 30 s answers pending without it; send a quick message to read it.

javascript
const { prompt } = await lane.send('explain how to use the app', { showPrompt: true })
console.log(prompt.system, '\n---\n', prompt.user, prompt.tools.map((t) => t.name))

Voice input

wb.dictation() is the press-to-talk the Spaces app uses: words appear as the person speaks, and the final text arrives about half a second after they let go.

javascript
const mic = wb.dictation()
button.onpointerdown = () => mic.start({ onPartial: (text) => (input.value = text) })
button.onpointerup = async () => { input.value = await mic.stop() }

mic.cancel() throws a press away, mic.close() hangs up at once (otherwise after 15 quiet minutes), and mic.listening is true while it records.

Reading aloud

wb.speak(text) reads text aloud with the Spaces app's Play aloud player, and resolves when it has finished:

javascript
button.onclick = () => wb.speak(lastReply.text)
stopButton.onclick = () => wb.speech.stop()

wb.speech.playing is true while it plays, and wb.speech.play(text) is the same call.

Machine-readable source: sdk-assistant.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)