One call: run(...).text()
client.run(...) starts a one-shot run and returns a Run; .text() drives
it to completion and returns the run’s transcript — the agent’s answer with
each tool step inline as a ▶ <summary> line, paragraph-separated. Pass
live=True to also print the run while you wait (answer tokens to stdout, tool
steps and [agent] speaker labels to stderr); the return value stays the
authoritative, loss-free version. For a follow-up that builds on a previous
turn, use a session.
SDK runs don’t appear in the app’s chat list or search — they’re managed
entirely through the SDK. The app UI shows only its own conversations.
Multi-turn sessions
client.run(...) is one-shot: each call is an independent conversation. To keep
context across turns — so a follow-up like “now double the load” builds on the
previous run — open a session. Every run on it reuses one conversation, and
the agent remembers the earlier turns.
- Entry agent, then follow the work. Pass
agent=tosession(...)to pick the agent for the first turn. Laterruns omit it and continue with the agent in control — so if the first turn hands off to another agent, the next turn keeps going with that agent (the same behaviour as the app). - Cleanup. Leaving the
withblock (or callingchat.close()) ends the session — the conversation persists server-side (hidden from the app) and can be picked up again later withclient.session(id=chat.id). Callchat.delete()to remove it entirely, or open the session withephemeral=Trueto have it deleted automatically on close. Both are idempotent. The same applies to one-shots:client.run(..., ephemeral=True)deletes the conversation when the run finishes. - Finding old conversations.
client.chats()lists what your scripts left on the server —id,title,updated_at— so a persisted conversation is never orphaned even if you lost the id (cosmon chatsfrom the shell). - Handlers still apply.
on_question/on_confirmon the client work inside a session exactly as for a one-shot run.
AsyncClient mirrors it — async with client.session() as chat: and
await chat.run(...).text().
Attended vs unattended
One rule decides whether the agent can stop to ask you something: the handlers you pass. There’s no mode flag, and each handler controls its own mechanism:
Registering
on_confirm is opting in to being asked: the app forwards every
confirmation gate to it, including ones the app’s own “Confirm tool calls”
toggle would auto-approve for UI users. Omitting a handler never silently
declines — the unwired mechanism falls back to the safe auto-rule.
The handlers are called as the agent needs input, while .text() runs:
- Safe by default. The auto-rule never rubber-stamps a destructive op —
dangerous confirmations are declined unless your
on_confirmapproves them.on_confirm=lambda c: Trueapproves everything, dangerous included — that’s a deliberate, explicit choice (the CLI’s--auto-approve), not something you can fall into by omission. - Handlers may block on a human — for up to ~2 minutes. They run in a
worker thread, so an
input()prompt sitting open doesn’t stall the client’s heartbeats or cancellation. But the agent won’t wait forever: after the window (Question.expires_in/Confirmation.expires_in, ~120s — the same countdown the app draws as a bar) a question auto-dismisses, a confirmation is declined, and the SDK stops waiting on your handler. Show the window to your human like the example does. .text()never stalls. Handlers answer out of band, so a run always drives to completion — it never blocks or errors waiting for input.
Streaming the run
Iterate the run when you need the events as data —.text(live=True)
is just the terminal rendering of this same stream. Route them to a GUI, a
websocket, metrics, or break mid-stream to cancel the run based on what you
see. Each iteration yields a typed event: ToolCall (a tool-call boundary, finished=False on start
and True on finish) or TextChunk (the answer, as it streams). The TextChunks
reconstruct the same answer .text() returns.
Both event types carry agent — which agent produced the event. A run can
span a handoff chain (one agent delegating to another mid-run), and the
agent field is how multi-agent output stays attributable.
.text(live=True) prints exactly this layout for you.)
Observe vs respond: iterating observes what the agent does. Answering what the
agent needs —
ask_user / confirm_action — is the on_question /
on_confirm hooks above, because the agent blocks waiting for a returned value.
The two compose: stream a run and have it interactive at the same time.Images a tool produces
When a tool produces an image for the user — a CAD screenshot, a plotted result — it rides theToolCall’s finish event as event.images, a tuple of image
data: URIs (empty on the start event). Decode and save them as they arrive:
data: image is also embedded in the run’s final result as an MCP image
block, so plain MCP clients see it too — .text() is unaffected (it joins only
text). See the Images from tools example for the full snippet.
Capping a run
Settimeout on the client (seconds) so a stuck run can’t hang forever. The
agent can work for many minutes, so the default is generous (10 minutes), and
there is no server-side cap — a long solve runs as long as you’re willing to
wait. When the timeout fires it doesn’t just give up waiting: it cancels the
run server-side, so nothing keeps working (or holding the one-run-at-a-time
guard) behind your back.
Attaching files
Hand specific files to a run withattachments. Relative paths resolve
against your working directory (the SDK absolutizes them before sending),
and a missing file raises CosmonError immediately — before the agent runs.
The agent reads them alongside your prompt:
Unattended batches
No handlers, atimeout, and decisions baked into the prompt — leave it running:
Triggering runs from your own code
The SDK is a plain library — no daemon of its own — so you call it from whatever fires in your workflow: a file watcher, a webhook, a cron job, a CI step, a queue consumer. Behind an HTTP endpoint, use the async client so one process can drive several runs at once:The one requirement: the Nexus app must be running and signed in on that
machine — the SDK rides it for authentication and the live CAD/CAE connection.
Errors vs. in-run failures
Errors come in two channels: opening the client (CosmonConnectionError,
CosmonAuthError, CosmonProtocolError) versus during a run
(CosmonRunError, CosmonTimeoutError, or a dropped CosmonConnectionError).
All subclass CosmonError, so a single except CosmonError catches everything;
catch the subclasses to branch.
In a batch or pipeline the one worth handling explicitly is CosmonRunError —
in-run failures (including provider errors like out of credits) surface here,
and its code lets you branch without parsing the message:
CosmonRunError.code values.
Next steps
Python reference
The full API surface — clients, runs, interaction handlers, the async twin,
streaming, and errors.
Command line
The
cosmon CLI — doctor, run, and chats from the shell.