# Session-style coding-agent API pattern

Treat these paths and JSON keys as an **example contract to map onto your provider's actual documentation**, not a promise that all coding agents expose them. Configure your own authorized client and API base URL privately.

| Phase | Example route | Request or result to verify |
|---|---|---|
| Discover | `GET /sessions` | Confirm the supported repository identifiers and current jobs. |
| Create | `POST /sessions` | `{taskDescription, repoSlug}` returns a slug or ID. This may create an idle session only. |
| Dispatch | `POST /sessions/{slug}/messages` | `{content, conversationIndex:0}` or a provider-documented conversation ID. Check response and read back the message. |
| Observe | `GET /sessions/{slug}/status` | Session status, activity, question flag, CI checks, mergeability, and PR status. |
| Inspect plan | `GET /sessions/{slug}` | Full plan content and current conversation. |
| Approve | `POST /sessions/{slug}/plan/approve` | One conversation target. Require appropriate user authorization first. |
| Unblock | `GET /sessions/{slug}/questions` then `POST /sessions/{slug}/questions/{requestId}/answer` | Submit the actual option label(s) or required free text. |
| Follow up | `POST /sessions/{slug}/messages` | Clarify within the same job, with a target conversation. |
| Finish | `GET /sessions/{slug}/status` | Inspect `complete`, PR `ready`, CI, and the actual PR URL separately. |

A common status progression is `planning` -> `awaiting_approval` -> `building` -> `complete`, with `error` possible. Activity can be `working`, `waiting`, or `idle`. PR state can differ from session state: `wip`, `ready`, `merged`, or `closed`. These exact enums are provider-specific.

## Minimal TypeScript dispatch example

This snippet assumes an `authorizedFetch` function supplied by your own authenticated client. It creates and sends the task, then checks the message list. It **does not** approve a plan or merge a PR.

```ts
type AuthorizedFetch = typeof fetch;

export async function createAndDispatch(
  authorizedFetch: AuthorizedFetch,
  baseUrl: string,
  repoSlug: string,
  task: string
) {
  const api = baseUrl.replace(/\/$/, "");
  async function call(path: string, method = "GET", body?: unknown) {
    const response = await authorizedFetch(`${api}${path}`, {
      method,
      headers: { "Content-Type": "application/json" },
      ...(body === undefined ? {} : { body: JSON.stringify(body) })
    });
    const text = await response.text();
    if (!response.ok) throw new Error(`${method} ${path}: ${response.status} ${text}`);
    return text ? JSON.parse(text) : null;
  }

  const created = await call("/sessions", "POST", {
    taskDescription: task,
    repoSlug
  });
  const slug = created?.slug ?? created?.session?.slug;
  if (!slug) throw new Error("Session creation returned no slug");
  console.log(JSON.stringify({ phase: "created", slug }));

  const path = `/sessions/${encodeURIComponent(slug)}/messages`;
  console.log(JSON.stringify({ phase: "dispatching", slug }));
  await call(path, "POST", { content: task, conversationIndex: 0 });
  const listed = await call(`${path}?conversationIndex=0`);
  const messages = Array.isArray(listed) ? listed : listed?.messages;
  if (!Array.isArray(messages) || !messages.some((message) => message.content === task)) {
    throw new Error("Dispatch isn't verified; inspect the session before retrying");
  }
  console.log(JSON.stringify({ phase: "dispatched", slug }));
  return slug;
}
```

If the provider returns paginated or transformed message content, use its documented message receipt or search rather than relying on exact text equality. For long polling, use the cheap status endpoint with bounded backoff and stop on questions or errors. Don't retry an ambiguous dispatch until the remote session has been checked.

**Outside Sauna:** The `authorizedFetch` argument is your own authenticated fetch adapter. Keep credentials out of code, logs, and published skill files.
