# Agent Sessions API guide

## What the Agent Sessions API is

The Agent Sessions API gives your application a persistent conversation with
the NOLGIA Agent. Create a session, send a message, follow its reply, and collect
the Outputs it produces. Sessions keep the transcript and its generated media
together; your Library remains the place to find assets across sessions.

Use the [API reference](../api/) for the full contract and the
[getting started guide](./getting-started.html) for client installation.
The base URL is `https://api.nolgia.ai/v1`.

## Authenticate

Create a Personal Access Token at
[nolgia.ai/settings/api-tokens](https://nolgia.ai/settings/api-tokens).
Send it in the `Authorization` header on every request. The token inherits
its account's permissions and organization role.

```bash
export NOLGIA_TOKEN=nol_...
curl https://api.nolgia.ai/v1/agent/sessions \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

## Create a session

`POST /agent/sessions` creates a session and returns `201` with its `id`.
A `project_id` optionally links it to a project you own.

```bash
SESSION_ID=$(curl -sS https://api.nolgia.ai/v1/agent/sessions \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Product launch"}' | jq -r .id)
```

You can also create a session with its first message atomically by sending
`new_session: {}` to `POST /agent/messages` instead of `session_id`.
The accepted response includes the new `session_id` and `session`.

## Send a message

`POST /agent/messages` returns `202` with `user_message_id`,
`agent_message_id`, and `session_id`. Save the agent reply id to follow this
specific turn. The reply starts as `pending`.

```bash
ACCEPTED=$(curl -sS https://api.nolgia.ai/v1/agent/messages \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"session_id\":\"$SESSION_ID\",\"content\":\"Plan a short product launch video.\"}")
MESSAGE_ID=$(printf '%s' "$ACCEPTED" | jq -r .agent_message_id)
```

## Wait for the reply

Poll `GET /agent/messages/{id}` with backoff, starting at one second,
multiplying by 1.5 and capping at ten seconds. This reads the same transcript
row as `GET /agent/messages`, including feedback on rated agent replies,
without repeatedly paging the transcript.

```bash
curl -sS "https://api.nolgia.ai/v1/agent/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

| Reply `status` | Meaning |
| --- | --- |
| `pending` | Waiting or working. `queued: true` means it is waiting for an execution slot. |
| `complete` | Read `content`; check `question` before treating the conversation as finished. |
| `failed` | Read `error` and its machine-readable `error_code`. |
| `interrupted` | You stopped the turn. Read `error`; its remaining credit hold was returned. |

Stop polling when the reply leaves `pending`. Apply an overall timeout in
your application; a polling timeout does not stop the turn. The SDK helpers
below implement this loop.

## Stream events

For a live transcript, open `GET /agent/sessions/{id}/events`. Use `fetch`
so you can send the bearer header; browser `EventSource` cannot send it.

```javascript
const response = await fetch(
  `https://api.nolgia.ai/v1/agent/sessions/${sessionId}/events`,
  { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" } },
);
if (!response.ok) throw new Error(`Stream failed: ${response.status}`);
// Feed response.body to an SSE parser. Chunks may split lines or frames.
```

The server sends a `retry:` hint and periodic comment heartbeats. Retain the
last SSE `id:` and send it as the `Last-Event-ID` header when reconnecting.
The server replays missed transcript writes before live delivery. Merge
messages by id because replay can include duplicates.

- `message`: the full transcript `message`, or only `message_id` when the
  payload is too large. Fetch that id when the row is omitted.
- `step`: a step summary; fetch `GET /agent/steps` with the `session_id` and
  a `since` cursor for the detailed steps.
- `session_status`: a session moved between `running`, `queued`, and `idle`.
  These frames may describe any session owned by the caller. Their states
  remain these three values; read the session or reply for questions.
- `ready`: replay is finished. Its `replay` value is `complete`, `none`, or
  `truncated`. For `none` or `truncated`, hydrate the transcript with
  `GET /agent/messages?session_id={id}`. Retain its watermark for reconnects.

Ignore event types you do not recognize.

## Steer a running turn

Send another message with `steering: true` in the same session to add direction
without cancelling its current turn.

```bash
curl -sS https://api.nolgia.ai/v1/agent/messages \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"session_id\":\"$SESSION_ID\",\"content\":\"Use a warmer palette.\",\"steering\":true}"
```

When that session already has a pending turn, `202` carries only
`user_message_id`. The direction can reach its running turn live; steering
messages still queued when it finishes are delivered as a follow-up turn.
If no turn is pending, the send creates a normal turn and returns both ids.

## Interrupt a turn

`POST /agent/sessions/{id}/interrupt` stops only that session's newest
running or queued turn. It needs no request body.

```bash
curl -sS -X POST "https://api.nolgia.ai/v1/agent/sessions/$SESSION_ID/interrupt" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

The response contains `session_id`, `interrupted`, `run_stopped`, and
`credits_released`, plus `agent_message_id` and `status` when a turn exists.
`interrupted: true` means that reply is now interrupted, including on repeat
calls. `run_stopped: true` means the running agent acknowledged the stop.
It is false for queued turns, turns whose run has not started, repeat calls,
and when the agent could not be reached. Even then, the reply is marked
`interrupted` and any later result is discarded.

The turn's held reasoning credits are returned; `credits_released` is the
amount returned by this call. Generations already submitted keep running
and bill as usual. Steering messages queued behind the stopped turn remain
in the transcript but do not run. Send a new message to continue.

Interrupt is idempotent. Repeating it, or calling it on an idle session,
returns `200` and changes nothing; repeat calls release zero credits.

## Answer the agent's questions

A `complete` agent reply whose last line is a question can include:

```json
{
  "question": {
    "text": "Which length feels right?",
    "options": [
      {"label": "5 seconds", "description": "one beat, the product and one move."},
      {"label": "15 seconds", "description": "room for a short story."},
      {"label": "30 seconds", "description": "a fuller product introduction."}
    ]
  }
}
```

The session's `turn_status` becomes `awaiting_input`, and the session exposes
the same `question`. Its `options` is an ordered list and may be empty.
Show the text and choices, then answer with a normal `POST /agent/messages`
in the same session. A choice is an ordinary message, not a special action.
The reply itself remains `complete`; `awaiting_input` is the session state.

## Queue semantics

One turn is in flight per session. Another normal submit to that session
returns `409` with `code: session_busy` and `pending_agent_message_id`.
Wait for that reply or send a steering message instead of retrying the same
normal submit immediately.

Turns in other sessions are accepted and queue FIFO when your execution
slots are full. At most ten turns may be pending across your sessions,
including running and queued turns; the next submit returns `429` with
`code: session_busy`. Back off before trying again. A pending reply's
`queued` flag distinguishes waiting for a slot from active work.

Steering stays within its target session. Undelivered steering messages
are combined in order into a follow-up turn after the current turn settles.
Interrupt consumes those queued directions into the stopped turn so they
cannot unexpectedly start another one.

## Assets

`GET /agent/sessions/{id}/assets` returns the session's generated assets,
newest first and de-duplicated by id. Outputs can appear while the reply is
still pending, and remain available if the turn fails or is interrupted.

```bash
curl -sS "https://api.nolgia.ai/v1/agent/sessions/$SESSION_ID/assets" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

Use these assets to build an Outputs view for the session or link into your
Library. The SDK helpers collect asset ids created since the first reply
row in that helper run.

## Credits

Each turn has a flat rate minimum for its selected brain, with metering on
actual model cost above that minimum. Credits are held when the turn
starts running, then settled when it completes. Failed and interrupted
turns have their remaining holds refunded. Waiting in the queue does not
start the turn's time budget.

Generations the NOLGIA Agent submits are billed separately under the usual
generation rules. Stopping a reasoning turn does not cancel or refund those
generations. See the [getting started credit guide](./getting-started.html)
for generation billing and refunds.

## Errors

Agent endpoint failures use RFC 7807 problem objects with `status`, `title`,
and `detail`, and a machine-readable `code` when the failure has one.
The submit endpoint's `409` instead returns a conflict object carrying
`code: session_busy` and `pending_agent_message_id`.

| `code` | HTTP statuses and meaning |
| --- | --- |
| `session_busy` | `409`, `429`: a session already has a turn in flight or too many turns are pending; wait or steer. |
| `access_denied` | `403`: the credential or organization role cannot perform the action. Failed replies include a read-only organization role. |
| `insufficient_credits` | `402`: the wallet cannot fund the turn. Failed replies include insufficient personal or organization funds and a turn credit budget refusal. |
| `backend_error` | `502`, `503`: the agent could not be reached or failed; the default class for other failed replies. |
| `timeout` | `504`: the turn exhausted its time budget or was declared stuck. |

A failed transcript reply exposes this classification as `error_code` next
to its human-readable `error`. Interrupted replies carry `error` without an
`error_code`. Other HTTP errors may omit `code`, including `401` for missing
authentication and `404` for a missing message or session, or one you do not
own. Router rejections can be plain text; check the response content type
before decoding a problem object.

## SDK run helpers

Each helper submits, polls with backoff, and returns a result containing
`status`, `text`, `asset_ids`, `session_id`, and `agent_message_id`, plus
`question` or `error_code` when present. Status is `complete`,
`awaiting_input`, `failed`, `interrupted`, or `timeout`.
For a failed or interrupted reply, `text` is its served error message.

Pass no session id to create a new session. An optional question callback
receives the question and options: return a non-empty answer to continue in
the same session, or an empty answer to return `awaiting_input`. Without a
callback, a question also returns `awaiting_input`.

Python:

```python
import os
from nolgia import AuthenticatedClient
from nolgia.agent_run import run

client = AuthenticatedClient(
    base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"]
)
result = run(client, None, "Plan a product launch video.", timeout=600.0)
print(result.status, result.text, result.asset_ids)
```

TypeScript:

```typescript
import { createNolgiaClient, runAgent } from "@nolgia/sdk";

const client = createNolgiaClient(process.env.NOLGIA_TOKEN ?? "");
const result = await runAgent(client, {
  message: "Plan a product launch video.",
  timeoutMs: 600_000,
  onQuestion: async (question) => {
    console.log(question.text, question.options);
    return null; // Display the choices and collect an answer in your UI.
  },
});
console.log(result.status, result.text, result.asset_ids);
```

Go:

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    nolgia "github.com/nolgiainc/nolgia-api/sdk/go"
)

func main() {
    client, err := nolgia.New(os.Getenv("NOLGIA_TOKEN"))
    if err != nil {
        log.Fatal(err)
    }
    result, err := nolgia.RunAgent(context.Background(), client, "",
        "Plan a product launch video.", &nolgia.RunAgentOptions{
            Timeout: 10 * time.Minute,
        })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result.Status, result.Text, result.AssetIDs)
}
```

All helpers share one overall timeout across questions and follow-up turns.
A timeout returns `timeout` without fetching assets and does not interrupt
the agent. To stop it, call `POST /agent/sessions/{id}/interrupt` using the
returned session id, as shown above. A `409` raises or returns an error with
`code: session_busy` and the pending reply id; it is never automatically
retried. Other HTTP failures carry the status and problem `code`/`detail`.

The final asset lookup happens once, includes only assets whose `created_at`
is at or after the first reply row's `created_at`, preserves server order,
and de-duplicates ids. Poll interval, maximum interval, and sleep are
injectable for tests.
