---
slug: agentsky-developer-guide
title: AgentSky Developer API
category: capability
status: published
tags: [agentsky, api, developer, integration, agent, session, sse, 接入, 开发者, 接口]
aliases: []
lastEditAt: 2026-08-22
---
AgentSky is a general-purpose agent cloud: it runs configurable AI agents
(engines like Hermes, Claude Code, Codex, and others) in managed sandboxes and
streams their output back over a single event stream. This guide is the
complete reference for integrating with it as a third party — how to
authenticate, every endpoint you will call, and the event model your receiver
must handle.

Base URL:

```text
https://agentsky.dev
```

The authoritative, machine-readable contract for everything below is the
OpenAPI document at:

```text
https://agentsky.dev/api/v1/openapi.json
```

If a field or endpoint is not described here, the OpenAPI document is the
source of truth — not this prose.

## Core model

AgentSky has a small, fixed object model:

| Object | What it is | Lifecycle |
|---|---|---|
| **Universe** | An isolated tenancy: its own agents, sessions, secrets, and model subscriptions. | Created once; everything else lives inside one. |
| **Agent** | A reusable configuration: engine (`agentType`), model (`llm`), prompt, capabilities, secrets, install steps. | Created, patched, archived, or deleted. Runs zero or more sessions. |
| **Session** | One running conversation with a working directory. The pod materializes lazily on the first turn. | `provisioning` → `idle` / `running` → `terminated`. |
| **Turn** | One user message plus the agent's response. | Started by `POST /sessions/{id}/messages`; boundaries read from the stream. |

Nothing is provisioned when you create an agent — the pod and its working
directory exist only after you start a session and send a turn.

## Authentication

Every request is authorized with a bearer token that carries an `ast_` prefix:

```http
Authorization: Bearer ast_...
```

Tokens are scoped to a single **universe** and carry one or more **scopes**.
Each endpoint in the OpenAPI document declares the scope it requires:
`read` for reads, `write` for writes, `admin` for destructive or
universe-level actions (for example deleting an agent requires `admin`).

Scopes are hierarchical — `read ⊂ write ⊂ admin` — so a token that can
`admin` also implies `write` and `read`. You mint a token in the AgentSky
console (**Settings → API tokens**) or from the `sky` CLI with
`sky auth login`.

Send `GET /api/v1/whoami` first — it returns the resolved identity, universe,
and effective scopes for the token you hold, and is the cheapest way to
confirm a credential before building on it:

```bash
curl -sS https://agentsky.dev/api/v1/whoami \
  -H "Authorization: Bearer $AGENTSKY_TOKEN"
```

```json
{
  "user":    { "id": "u_...", "email": "you@example.com", "name": "You" },
  "universe": { "slug": "acme", "name": "Acme", "isPersonal": false },
  "scopes":  ["read", "write", "admin"],
  "auth":    "token"
}
```

Notes:

- `universe.isPersonal` is `true` for a personal token. Only personal tokens
  can create universes (`POST /api/v1/universes`).
- Secret values and subscription credentials are write-only: the API accepts
  them but never returns them.
- Rate limit: **120 requests per minute per token**; exceed it and you get
  HTTP `429` with a `Retry-After` header.
- The human-readable documentation lives at `https://agentsky.dev/docs`.

## Agents

### Create an agent

`POST /api/v1/agents` — every field is optional; an empty body `{}` creates a
default `hermes` agent. `agentType` selects the engine:

`hermes` (default) · `claude_code` · `codex` · `openclaw` · `pi` · `dsh` ·
`kimi_code` · `opencode`

```bash
curl -sS https://agentsky.dev/api/v1/agents \
  -H "Authorization: Bearer $AGENTSKY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-bot",
    "displayName": "Support Bot",
    "agentType": "hermes",
    "llm": "claude-fable-5",
    "prompt": "You are a concise support assistant.",
    "capabilities": ["exa.search", "gptimage.generate"]
  }'
```

The response returns the full `agent` object including its generated `slug`
(a globally unique handle) and `version` (bumped on every update). You address
the agent by `slug` from here on.

Full `CreateAgent` fields:

| Field | Meaning |
|---|---|
| `name` / `displayName` | Internal name (≤60 chars) and human-facing label (≤60 chars). |
| `description` | ≤500 chars. |
| `agentType` | Engine, one of the eight values above. |
| `llm` | Model id, passed through to the engine; engine↔model mismatches are rejected rather than silently replaced. |
| `prompt` | The user prompt layer (≤100000 chars). |
| `capabilities` | Built-in tool grants — see the table below. |
| `instructions` | Array of `{name: "*.md", content}` markdown files injected at startup. |
| `skills` | Array of `{name, url, source, version, config?, secretRefs?}` packages loaded into the agent. |
| `customInstalls` | Array of `{command, description?, timeoutSeconds? (≤1800), allowNetwork?}` shell steps run before the engine starts. |
| `customData` | Array of `{id, name, kind, scope?, description?, uri?, config?}` data attachments. |
| `metadata` | Your key-merged client metadata, echoed back on reads. |

Capabilities are **not** LLM tools — they are platform grants surfaced through
the in-pod `actl` CLI. The full enum:

`exa.search` · `exa.contents` · `tinyfish.fetch` · `tinyfish.browser` ·
`dataforseo.serp` · `gptimage.generate` · `rembg.remove-background` ·
`seedance.generate` · `mm.i2v` · `fish-audio.transcribe`

### Read, update, archive, delete

```bash
GET    /api/v1/agents                # list agents in the resolved universe
GET    /api/v1/agents/{slug}         # agent detail (includes prompt, capabilities, metadata)
PATCH  /api/v1/agents/{slug}         # update displayName / capabilities / metadata / harnessVersion
POST   /api/v1/agents/{slug}/archive # archive: read-only, sessions keep running, new sessions rejected
DELETE /api/v1/agents/{slug}         # delete — only with zero sessions (409 agent_has_sessions otherwise)
```

`PATCH` supports `displayName`, `capabilities`, `metadata`, `harnessVersion`,
and `expectedVersion` (an optimistic-concurrency guard: pass the `version` you
last read, and the write fails with `version_conflict` if the agent has moved
on).

### Prompt

```bash
PUT /api/v1/agents/{slug}/prompt          # save + hot-reload the user prompt layer
GET /api/v1/agents/{slug}/prompt/versions # version history
```

`PUT` returns `{version, unchanged, applied, sessions[]}`. `applied` is
`live`, `on-restart`, or `unreachable` — it tells you, per session, whether the
new prompt hot-reloaded or will apply on the next restart.

### Secrets

```bash
GET    /api/v1/agents/{slug}/secrets        # declared secret keys + descriptions — never values
PUT    /api/v1/agents/{slug}/secrets/{key}  # set a declared secret's value (write-only)
DELETE /api/v1/agents/{slug}/secrets/{key}  # unset a secret
```

Secrets are injected into the agent process environment. Values are write-only:
you can set and unset them, but the API never echoes them back.

## Model subscriptions

A connected consumer AI subscription (Claude Pro/Max or a ChatGPT plan) can
power eligible agents at $0 model usage.

```bash
GET    /api/v1/model-subscriptions               # list — metadata only, never credentials
PUT    /api/v1/model-subscriptions/{provider}    # connect/replace a credential (provider: anthropic | openai)
PATCH  /api/v1/model-subscriptions/{provider}    # update useForNewAgents / label
DELETE /api/v1/model-subscriptions/{provider}    # disconnect — affected agents fail until reconnected or switched
```

A session opts in to account billing by setting `modelBilling: "account"`
(see Sessions below); the platform meters the turn otherwise.

## Sessions

### Create a session

`POST /api/v1/sessions` — cheap and lazy. Only `agent` is required; the pod
materializes on the first turn, so creation is effectively free.

```bash
curl -sS https://agentsky.dev/api/v1/sessions \
  -H "Authorization: Bearer $AGENTSKY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"agent": "support-bot", "title": "First session"}'
```

A non-empty `initial_events` list (`user.message` only, all-or-nothing, ≤50)
starts the first turn in the same call:

```bash
curl -sS https://agentsky.dev/api/v1/sessions \
  -H "Authorization: Bearer $AGENTSKY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "support-bot",
    "initial_events": [
      { "type": "user.message", "parts": [ { "index": 0, "type": "text", "text": "Hello" } ] }
    ]
  }'
```

`CreateSession` fields: `agent` (required), `eager`, `title` (≤120 chars),
`metadata`, `instructions` (≤20 markdown files), `initial_events`, `vcpus`,
`memoryMb`, and `modelBilling` (`platform` | `account`). Machine shape
(`vcpus` / `memoryMb`) is per-session.

### Read, update, delete

```bash
GET    /api/v1/sessions          # list sessions; ?agent= filters to one spec
GET    /api/v1/sessions/{id}     # detail — status is provisioning | idle | running | terminated
PATCH  /api/v1/sessions/{id}     # update title / metadata / modelBilling
DELETE /api/v1/sessions/{id}     # end the session — tears down the pod, emits session.deleted
```

The session has a **persistent working directory**: files you write in one
turn survive to the next. That directory is destroyed when the session is
deleted.

## Sending a turn

`POST /api/v1/sessions/{id}/messages` accepts a message and returns `202` with
an empty body — the result never comes back in this response. Output arrives
on the event stream (next section).

```bash
curl -sS -X POST https://agentsky.dev/api/v1/sessions/$SESSION_ID/messages \
  -H "Authorization: Bearer $AGENTSKY_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: turn-7f3a" \
  -d '{
    "parts": [ { "index": 0, "type": "text", "text": "Summarize this quarter." } ]
  }'
```

The request body requires `parts` (≥1). The optional `Idempotency-Key` header
dedupes retries — reuse the same key and a repeated send does not create a
second turn.

A turn is bounded by two `turn.status_idle` events on the stream: one at the
start (or from history) and one carrying the final `stop_reason`. The
`stop_reason.type` is `end_turn` when the turn finished normally, or
`interrupted` when it was aborted.

### Interrupt

`POST /api/v1/sessions/{id}/interrupt` aborts the in-flight turn:

```json
{ "status": "interrupting" }
```

`status` is `interrupting` when a turn is aborted, or `no_turn` when nothing
was in flight. The stream then emits `turn.interrupted`.

### Logs

`GET /api/v1/sessions/{id}/logs` returns pod logs (`{lines: [{at, severity, line}]}`).
Pass `follow=true` to stream them as SSE.

## Events

AgentSky has **no HTTP webhooks**. The event model is an SSE stream plus a
pollable event history — the same event objects appear in both, so you can
implement either a long-lived streaming client or a polling client on one
contract.

### The stream

`GET /api/v1/sessions/{id}/stream` is `text/event-stream`, live-only, and stays
open across turns. Event types:

| Event | Meaning |
|---|---|
| `user.message` | A user message. **History only** — not sent on the live stream. |
| `agent.message` | A completed agent message (`messageId`, `parts`, `text`). |
| `agent.reasoning` | A reasoning delta (`part`). |
| `agent.tool_use` | A tool invocation (`part` with `call_id`, `tool_name`, `args`). |
| `agent.tool_result` | A tool result (`part` with `call_id`, `tool_name`, `status`, `result`). |
| `agent.status` | A status update (`part` with `level`: `thinking`/`working`/`waiting`/`idle`/`done`). |
| `turn.status_idle` | A turn boundary. `stop_reason.type` is `end_turn` or `interrupted`. **Never break on a bare idle** — read the `stop_reason`. |
| `turn.interrupted` | The in-flight turn was aborted. |
| `session.deleted` | Terminal — the session is gone. Close your client. |
| `error` | An error (`code`, `message`, `retryable`). |

### The history

`GET /api/v1/sessions/{id}/events` returns the session history, oldest-first,
with an opaque numeric cursor. Only `user.message`, `agent.message`, and
`turn.status_idle` are persisted; the live-only types (`agent.reasoning`,
`agent.tool_use`, `agent.tool_result`, `agent.status`, `error`) appear on the
stream but not in history.

```bash
curl -sS "https://agentsky.dev/api/v1/sessions/$SESSION_ID/events?limit=100" \
  -H "Authorization: Bearer $AGENTSKY_TOKEN"
```

```json
{
  "events": [ { "id": "evt_...", "type": "agent.message", "sessionId": "sess_...", "parts": [...] } ],
  "cursor": "evt_...",
  "hasMore": false
}
```

Query parameters: `cursor` (resume), `limit` (≤500), and `types[]` filters —
`types[]=user.message&types[]=agent.message` is the transcript view.

### Reconnect and dedupe

Every event carries a stable `id`. The reconnect contract is:

1. Reopen the stream.
2. List events (`GET .../events`) from where you left off.
3. Dedupe by `id` — an event you already processed is safe to ignore.

The same `id` is the dedupe key across the stream and the history, so a
streaming client and a polling fallback can share the same state.

## Message parts

A message is a sequence of typed `parts`. Every part has an integer `index`
and a `type`:

| `type` | Required extra fields |
|---|---|
| `text` | `text` |
| `reasoning` | `text`, `redacted` |
| `tool_call` | `call_id`, `tool_name`, `args`, `args_partial` |
| `tool_result` | `call_id`, `tool_name`, `status` (`ok`/`error`), `result` |
| `file` | `name`, `media_type`, `uri`/`data`, `size_bytes` |
| `image` | `media_type`, `uri`/`data`, `alt`, `width`, `height` |
| `video` | `media_type`, `uri`/`data`, `alt`, `width`, `height`, `duration_ms`, `size_bytes`, `thumbnail_uri` |
| `status` | `level` (`thinking`/`working`/`waiting`/`idle`/`done`), `text` |
| `error` | `code`, `message`, `retryable` |

Media parts carry either a `uri` (a signed URL) or inline `data`, plus
metadata. Indexing is zero-based and defines display order.

## Errors

Every error, on every endpoint, has one shape:

```json
{ "error": { "code": "...", "message": "..." } }
```

The HTTP status distinguishes the class; `code` is the stable, machine-readable
value. Common ones:

| Status | `code` | Meaning |
|---|---|---|
| 401 | `invalid_token` | Missing, malformed, or unknown token. |
| 402 | `insufficient_credits` | The universe wallet cannot pay for the turn. |
| 409 | `agent_has_sessions` | Deleting an agent that still has sessions. |
| 409 | `version_conflict` | An optimistic `expectedVersion` no longer matches the agent. |
| 422 | `invalid_spec` | The payload is well-formed but invalid (for example an engine↔model mismatch). |

The OpenAPI document names `agent_has_sessions` and `version_conflict`
explicitly; the remaining codes above are returned at runtime under the same
`{ error: { code, message } }` shape. Treat `retryable: true` on an `error`
event as safe to retry; a bare `error` without that flag should be surfaced
to a human.

## End-to-end example

A complete integration in five calls:

```bash
BASE=https://agentsky.dev
AUTH="Authorization: Bearer $AGENTSKY_TOKEN"

# 1. Confirm identity and scopes.
curl -sS $BASE/api/v1/whoami -H "$AUTH"

# 2. Create an agent.
curl -sS $BASE/api/v1/agents -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name":"demo","agentType":"hermes","prompt":"Be concise."}'

# 3. Create a session and start the first turn in one call.
curl -sS $BASE/api/v1/sessions -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"agent":"demo","initial_events":[{"type":"user.message","parts":[{"index":0,"type":"text","text":"Say hello."}]}]}'

# 4. Open the event stream and read until turn.status_idle with stop_reason.type = "end_turn".
curl -sS -N $BASE/api/v1/sessions/$SESSION_ID/stream -H "$AUTH"

# 5. Tear down.
curl -sS -X DELETE $BASE/api/v1/sessions/$SESSION_ID -H "$AUTH"
```

## Verification checklist

To confirm your integration before shipping, walk these in order and check the
result of each:

1. `GET /whoami` returns your universe and scopes without error.
2. `POST /agents` returns a `201` with an `agent.slug` and `version`.
3. `GET /agents/{slug}` returns the configuration you created.
4. `POST /sessions` returns a `201` with a `session.id` and `status`.
5. `POST /sessions/{id}/messages` returns `202` with an empty body.
6. `GET /sessions/{id}/stream` delivers `agent.message` and a terminal
   `turn.status_idle` whose `stop_reason.type` is `end_turn`.
7. `GET /sessions/{id}/events` returns the same events, dedupable by `id`.
8. `POST /sessions/{id}/interrupt` returns `interrupting` during a turn.
9. `DELETE /sessions/{id}` returns success and the stream emits
   `session.deleted`.

## Endpoints at a glance

```
GET    /api/v1/whoami
GET    /api/v1/universes
POST   /api/v1/universes
GET    /api/v1/universes/{slug}
GET    /api/v1/agents
POST   /api/v1/agents
GET    /api/v1/agents/{slug}
PATCH  /api/v1/agents/{slug}
DELETE /api/v1/agents/{slug}
POST   /api/v1/agents/{slug}/archive
PUT    /api/v1/agents/{slug}/prompt
GET    /api/v1/agents/{slug}/prompt/versions
GET    /api/v1/agents/{slug}/secrets
PUT    /api/v1/agents/{slug}/secrets/{key}
DELETE /api/v1/agents/{slug}/secrets/{key}
GET    /api/v1/model-subscriptions
PUT    /api/v1/model-subscriptions/{provider}
PATCH  /api/v1/model-subscriptions/{provider}
DELETE /api/v1/model-subscriptions/{provider}
GET    /api/v1/sessions
POST   /api/v1/sessions
GET    /api/v1/sessions/{id}
PATCH  /api/v1/sessions/{id}
DELETE /api/v1/sessions/{id}
POST   /api/v1/sessions/{id}/messages
GET    /api/v1/sessions/{id}/events
GET    /api/v1/sessions/{id}/stream
POST   /api/v1/sessions/{id}/interrupt
GET    /api/v1/sessions/{id}/logs
```
