# Python

The Python client: install, authenticate, make the first call, and reach the four verbs from your own code. It is the counterpart of the [TypeScript client](./typescript.md) and behaves the same way.

## Install

The distribution is not published yet. Through the beta it runs from source, out of `sdk/python` in the repository:

```
cd sdk/python
uv sync
uv run python generate.py
```

`generate.py` writes the typed message code from the contracts. It needs no network. Run it again after pulling a change to the contracts.

When the distribution is published, this section becomes one install line and nothing else on this page changes.

## Authenticate

```python
import os

from rudrite_automaton import AutomatonClient

atmon = AutomatonClient("https://api.atmon.ai", os.environ["ATMON_API_KEY"])
```

The key rides on every request on every call. It resolves to one project, and that project scopes everything you can read or write.

Construction raises `ValueError` on an empty key or on an address that is not an http or https URL, so a missing environment variable is a startup failure rather than a mystery on the first call. A key the server rejects comes back as `AutomatonError` with the code `unauthenticated`.

The client owns its HTTP connection unless you pass your own. Close it with `atmon.close()`, or use it as a context manager.

If you run atmon on your own machines, the first argument is your own node's address instead.

## The first call

Find the tool by describing the task:

```python
resolution = atmon.search_tools(
    "file a bug about the failing build",
    entity_id="user-42",
    limit=5,
)
```

`entity_id` names the person you are acting for. What comes back is scoped to what that person has connected. A `limit` of 0 leaves the size of the list to the server. `context_messages` takes recent turns of a conversation, most recent last, when the intent alone is thin.

Read the tool, when the compact schema in the match is not enough:

```python
tool = atmon.describe_tool(resolution.matches[0].tool_slug)
```

Then act:

```python
call = atmon.call_tool(
    "github.create_issue",
    {"owner": "rudrite", "repo": "automaton", "title": "build is red"},
    entity_id="user-42",
    resolution_id=resolution.resolution_id,
    idempotency_key="issue-build-red-1",
)
```

`args` is a plain mapping; the helper encodes it. `entity_id` is required. `idempotency_key` makes a retry return the original call rather than acting twice. `confirm=True` clears the gate on a destructive tool and means the person was told what would change.

## Read the answer

What comes back is the receipt, not a bare result. Check the status before you use it:

```python
if call.status == ToolCallStatus.SUCCEEDED:
    result = json.loads(call.result_json)
else:
    # error_code is one of the stable codes; error_detail says more
    ...
```

A destructive call with no confirmation comes back refused, with the reason in the detail, and nothing reached the app: the gate runs before any credential is unlocked.

Close the loop when you can. Reporting whether the tool you chose was the right one is what improves the next ranking:

```python
atmon.router.report_outcome(resolution_id=resolution.resolution_id)
```

Each resolution is reportable once.

## Hand over a whole job

More than about twenty rows, or work that outlives the request you are serving, belongs in a job rather than a loop:

```python
job = atmon.jobs.submit_job(program=program, idempotency_key="quarterly-invites")
state = atmon.jobs.get_job(id=job.id)
```

See [Jobs](../using/jobs.md) for what a plan is made of and how to dry-run one first.

## Everything else

Every call in the [API reference](../reference/api/index.md) is on the same object: `atmon.catalog`, `atmon.connections`, `atmon.router`, `atmon.execution`, `atmon.jobs`, `atmon.traces`, `atmon.triggers`, `atmon.usage`, and `atmon.keys`. Method names are snake_case and fields follow the contract.

```python
flow = atmon.connections.initiate_connection(entity_id="user-42", toolkit_slug="github")
```
