> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cosmon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python reference

> Clients, runs, interaction handlers, the async twin, streaming, and errors.

The SDK is an MCP client for the local server the Nexus app exposes. Everything
has a sync and an async form: `Client` / `AsyncClient` and `Run` / `AsyncRun`.
The async methods are `await`ed; both share the same handlers and error types.

## Client

```python theme={null}
from cosmon_agent_sdk import Client

with Client() as client:            # discovers the running app, connects, releases on exit
    ...
```

Use it as a context manager. Interactivity is set here, by whether you pass
handlers — supplying `on_question` / `on_confirm` makes runs interactive; omitting
both runs the agent unattended. Use one client per thread (one per event loop for
`AsyncClient`).

<ParamField path="on_question" type="Callable[[Question], str]">
  Answer an `ask_user`. Passing it declares the elicitation capability. Omit to
  run unattended (the agent is never offered questions). Answer within \~2
  minutes (`Question.expires_in` carries the exact window) — after that the
  question auto-dismisses, the agent proceeds on its own judgment, and the SDK
  stops waiting on your handler. Return a
  **non-empty** string: an empty answer reads as "no answer" and the agent
  proceeds immediately — re-prompt your human instead (the app disables its
  submit button on empty text for the same reason).
</ParamField>

<ParamField path="on_confirm" type="Callable[[Confirmation], bool]">
  Approve or deny a `confirm_action`. Omit to auto-resolve (approve safe, decline
  dangerous). Undecided for \~2 minutes counts as a decline.
</ParamField>

<ParamField path="timeout" type="float" default="600.0">
  Seconds a single run may take. Expiry cancels the run server-side (nothing
  keeps working behind your back) and raises `CosmonTimeoutError`. There is no
  server-side cap — set it as high as your longest solve.
</ParamField>

<ParamField path="descriptor_path" type="str">
  Override where the app's descriptor is read from (defaults to the per-OS path;
  or the `COSMON_SDK_DESCRIPTOR` environment variable).
</ParamField>

<ParamField path="endpoint / token" type="str">
  Connect explicitly instead of discovering the app (mainly for tests).
</ParamField>

### Methods

<ParamField path="run(prompt, *, agent=None, attachments=None, ephemeral=False)" type="Run">
  Start a one-shot agent run; returns a `Run`.
</ParamField>

<ParamField path="session(*, agent=None, ephemeral=False, id=None)" type="Session">
  Open a multi-turn `Session` (see below). `agent` picks the entry agent for the
  first turn; `ephemeral=True` deletes the conversation when the session closes;
  `id` resumes an earlier session's conversation (a previous session's `.id`).
</ParamField>

<ParamField path="discover()" type="DiscoverInfo">
  Login state, account, available agents, default agent, active model, and
  runtime readiness.
</ParamField>

<ParamField path="chats()" type="list[ChatInfo]">
  The conversations your scripts left on the server (SDK-owned only — never the
  app user's own chats), newest first. Each `ChatInfo` has `id` (resume with
  `session(id=...)`), `title` (from the first prompt), and `updated_at`.
</ParamField>

<ParamField path="close()" type="None">
  Disconnect. Done automatically by the context manager.
</ParamField>

### `run()` options

<ParamField path="agent" type="str">
  Target platform agent, e.g. `"solidworks"`, `"abaqus"`, `"fluent"`. Omit for the default.
</ParamField>

<ParamField path="attachments" type="list[str]">
  File paths to attach to the prompt (read on the machine running the app).
  Relative paths are resolved against your current working directory.
</ParamField>

<ParamField path="ephemeral" type="bool" default="False">
  Delete the conversation (transcript + agent history) when the run finishes,
  instead of persisting it server-side.
</ParamField>

## Run

The handle for one run — **single-use**: iterate it *or* drain it once.

<ParamField path="text(live=False)" type="str">
  Drive the run to completion and return its transcript — the agent's answer
  with each tool step inline as a `▶ <summary>` line. `live=True` also prints
  the run while it works (tokens to stdout, steps and `[agent]` labels to
  stderr); the returned string stays the authoritative version. On `AsyncRun`,
  `await run.text()`. Raises `CosmonRunError` if the run failed.
</ParamField>

<ParamField path="for event in run" type="Iterator[RunEvent]">
  Stream the run: yields `ToolCall` / `TextChunk` events as they happen (`async for`
  on `AsyncRun`). Raises `CosmonRunError` if the run fails mid-stream.
</ParamField>

## Session

A multi-turn conversation over one stable id — open it with `client.session(...)`.
Every `run` on it reuses the id, so the runtime **resumes the same thread** (it
remembers earlier turns). Use it as a context manager so it's ended on exit.

```python theme={null}
with client.session(agent="solidworks") as chat:
    chat.run("Mesh bracket.sldprt").text()
    chat.run("Now double the load").text()   # remembers the mesh
    saved = chat.id                          # conversation persists after close

with client.session(id=saved) as chat:       # pick it up again later
    chat.run("What load did we end up with?").text()
    chat.delete()                            # done with it — remove entirely
```

<ParamField path="run(prompt, *, attachments=None)" type="Run">
  Run the next turn. The entry `agent` is sent only on the first turn; later turns
  follow the agent in control (so a handoff carries into the next turn). Returns a
  `Run` — same handle as `client.run`.
</ParamField>

<ParamField path="id" type="str">
  The session id shared across the conversation's turns.
</ParamField>

<ParamField path="close()" type="None">
  End the session. The conversation **persists** server-side and can be picked
  up again with `client.session(id=chat.id)` — an `ephemeral` session is deleted
  here instead. Idempotent; also called on `with`-block exit. On `AsyncSession`,
  `await chat.close()`.
</ParamField>

<ParamField path="delete()" type="None">
  Delete the conversation entirely (transcript + agent history), then end the
  session. The explicit destructive act — `close()` never deletes. Idempotent.
  On `AsyncSession`, `await chat.delete()`.
</ParamField>

A one-shot `client.run(...)` (no session) leaves its history server-side unless
run with `ephemeral=True`; SDK runs don't appear in the app's chat list.
`AsyncClient.session(...)` returns an `AsyncSession` — `async with` it and
`await chat.run(...).text()`.

## Events (`RunEvent`)

What iterating a run yields. A small discriminated union — `isinstance`-match it.

<ParamField path="ToolCall" type="pydantic model">
  The agent invoked a tool. `name: str`, `summary: str` (human-readable),
  `finished: bool` (emitted `False` on start, `True` on finish), `agent: str` —
  which agent ran it (a run can span a handoff chain; `""` from older apps).
  `images: tuple[str, ...]` — image `data:` URIs the tool produced for the user
  (e.g. a CAD screenshot), carried on the `finished=True` event; `()` when the
  tool produced none, and from apps that predate the field. See
  the [Images from tools example](/sdk/examples/images).
</ParamField>

<ParamField path="TextChunk" type="pydantic model">
  `text: str` — a piece of the agent's answer, as it streams; `agent: str` —
  who said it. Concatenate the chunks to rebuild the answer text.
</ParamField>

## Interaction handlers

Passed to the client; called as the agent needs input (the *respond* side — kept
out of the event stream because the agent blocks on the returned value).

<ParamField path="Question" type="pydantic model">
  `prompt: str` — the question. `options: list[str]` — the offered choices, or
  `[]` for a free-form answer (custom answers outside the options are fine).
  `expires_in: float` — seconds before the prompt auto-dismisses and the agent
  proceeds without you (\~120; render it if a human is answering). `on_question`
  returns the chosen/typed string.
</ParamField>

<ParamField path="Confirmation" type="pydantic model">
  `summary: str` — what the agent wants to do. `expires_in: float` — seconds
  before an undecided gate counts as declined (\~120). `on_confirm` returns
  `True` to approve, `False` to deny.
</ParamField>

The two handlers are independent: with only `on_confirm`, the run is
question-free (the agent is never offered `ask_user`) while every confirmation
gate reaches your handler; with only `on_question`, questions reach you while
confirmations auto-resolve (approve safe, decline dangerous).

## ChatInfo

Returned by `chats()`: `id: str`, `title: str`, `updated_at: str` (ISO
timestamp of the last activity). Resume one with `client.session(id=info.id)`;
remove one with that session's `delete()`.

## DiscoverInfo

Returned by `discover()`: `logged_in: bool`, `account: str | None`,
`agents: list[str]`, `default_agent: str`, `model: str`, `runtime_ready: bool`,
`tool_server_ready: bool` (reported `True` by older apps that predate the field).

## Async

`AsyncClient` mirrors `Client` for pipelines, FastAPI, notebooks, etc. It's the
native form (the sync `Client` wraps it on a background event loop).

```python theme={null}
import asyncio
from cosmon_agent_sdk import AsyncClient, TextChunk

async def main() -> None:
    async with AsyncClient() as client:
        print(await client.run("what can you do?", agent="nexus").text())

        # or stream while it runs:
        async for event in client.run("mesh and solve", agent="abaqus"):
            if isinstance(event, TextChunk):
                print(event.text, end="")

asyncio.run(main())
```

## Errors

All inherit from **`CosmonError`** — catch that to handle any SDK failure in one
place. There are two channels: errors *opening the client* (connection, auth,
protocol) and failures *during a run* (run failure, timeout, a dropped
connection). Each concrete error below.

### CosmonError

The base class every other SDK error subclasses. Catch it as a catch-all; catch
the specific subclasses below when you want to branch on the cause.

### CosmonConnectionError

The app can't be reached. **Raised opening the client** when the app isn't
running or Developer access is off — and **during a run** if the connection
drops mid-flight (the app quit, or Developer access was turned off). Fix: make
sure Nexus is running, signed in, with Developer access on.

### CosmonAuthError

The server rejected the bearer token — **raised opening the client**. Happens
when Developer access is off, or the per-launch token rotated (the app
restarted). Fix: toggle Developer access off and back on to reissue the token.

### CosmonProtocolError

The app speaks a gateway protocol version the SDK can't drive — **raised opening
the client**. Fix: update the SDK and the app to compatible versions.

### CosmonTimeoutError

A run exceeded the client's `timeout` — **raised during the run**. The run is
cancelled server-side (nothing keeps working behind your back). Fix: raise
`timeout` on the client for long solves, or shorten the task.

### CosmonRunError

The run itself failed — **raised during the run** — including provider errors
like being out of credits. Its **`code`** attribute lets you branch without
parsing the message:

| `code`               | Meaning                                                                |
| -------------------- | ---------------------------------------------------------------------- |
| `"busy"`             | Another run already owns the single CAD instance — retry later.        |
| `"not_ready"`        | The agent runtime isn't ready yet (app still starting / signed out).   |
| `"cancelled"`        | The run was cancelled (timeout, or the client went away).              |
| `"invalid_request"`  | The request was malformed (e.g. an unknown agent).                     |
| `"agent_error"`      | The agent run failed while working — the message has the reason.       |
| `None` / `"timeout"` | From an older app that predates `code` (or reports timeouts this way). |

```python theme={null}
try:
    client.run(prompt, agent="solidworks").text()
except CosmonRunError as err:
    if err.code == "busy":
        time.sleep(30)   # another run owns the CAD instance — retry later
    else:
        raise
```

## Command line

The SDK also installs the `cosmon` command — `doctor`, `run`, and `chats`. It
has its own page: **[Command line](/sdk/cli)**.
