Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AYA integration guide

AYA is a hub that fronts agent sessions and a queue of execution turns. Workers execute those turns on real agent CLIs and services (Claude Code, Codex, and others) running on a machine you control, pulling work rather than having it pushed at them. Dispatchers are the servers a worker pulls that work from — your own service, or the hub’s own built-in review and work-run queues. UIs read session history and steer turns — sending a message, watching it stream back, forking a conversation — over a browser session.

Those are the three ways to integrate. Pick the one that matches what you’re building:

Integrate as a dispatcher

You have your own service that decides what work exists, and you want a worker to pull it from you rather than push a fixed request at the hub. You implement the same small execution-pull contract the hub itself implements — a worker configures your server as one of its own [[execution_sources]] and polls you directly, never the other way around — or, if you don’t want to run a full execution-pull server, submit work runs directly to the hub instead and just serve its one result-callback endpoint. See Integrate as a dispatcher.

Integrate as a worker

You have a machine that can run an agent CLI, and you want it to pull work from AYA rather than have work pushed at it. You poll for a claimable turn, drive it locally, and report events and a terminal outcome back. This is what the reference worker (aya-helper) does. See Integrate as a worker.

Integrate as a UI

You want to show someone their agent sessions — browse history, search across it, send a chat message naming a provider that’s currently being served (or a configured tier), fork a conversation where the harness supports it, and watch the reply arrive — without caring how any of it actually runs. You read a tree-shaped session/message model and drive turns through a session-cookie-gated chat-and-poll surface. See Integrate as a UI.

Five-line quickstart

# 1. Get a credential — an OIDC client-credentials grant (see
#    Authentication). You end up with a bearer token.
export TOKEN="..."

# 2. Call an authenticated agent route with it — this one needs the
#    agent:report scope.
curl -s https://hub.example.test/api/agent/turns \
  -H "Authorization: Bearer $TOKEN"
# {"turns": [...], "total": 0, "limit": 50, "offset": 0}

# 3. Read the full contract — the worker-facing and service-integration
#    surface, not the browser routes a UI uses (see Reference).
#    https://hub.example.test/api/docs        # interactive OpenAPI reference
curl -s https://hub.example.test/api/openapi.json   # the spec itself

See Reference for the complete operation table and Authentication for how to get a credential in the first place.

Your first turn in ten minutes

The fastest way to see the whole loop — enqueue, claim, run, complete, read the result — is to play both sides of it yourself against a real deployment, using the hub’s own built-in review queue as the concrete enqueue point (a real dispatcher’s own enqueue step is private to its own storage, so there’s nothing to curl for it in general — this queue is the one exception that’s fully self-contained and runnable end to end). Get two credentials first: a review:run-scoped token ($REVIEW_TOKEN) and an agent token ($TOKEN) — see Authentication. Step 2 claims WHATEVER’S next in the queue for the given provider, not necessarily the turn you just made — run this against a deployment with no other pending turns for that provider, or you may claim someone else’s. Step 2 also assumes the deployment’s configured review profile uses claude — ask your operator, or check Run your own, and adjust ?providers= if it’s something else. Every step but the first captures what it needs from the previous response with jq rather than hardcoding it, so this is meant to be pasted and actually run, not just read.

# 1. Enqueue — stands in for "your own enqueue" in this walkthrough.
curl -s -X POST https://hub.example.test/api/integrations/turns \
  -H "Authorization: Bearer $REVIEW_TOKEN" -H "Content-Type: application/json" \
  -d '{"request_id": "first-turn-1", "purpose": "idea_review_v1", "input_version": 1,
       "context": {"idea": {"id": "i1", "title": "Try AYA", "description": "A test idea."},
       "messages": [], "omitted_message_count": 0,
       "projects": [{"id": "p1", "name": "Test project", "description": ""}],
       "linked_project_ids": ["p1"]}}'
# 202 {"request_id": "first-turn-1", "state": "queued", "replayed": false}
# (resubmitting this exact body is safe — same request_id, same input,
# replays as "replayed": true rather than queuing twice; changing the
# body under the same request_id instead gets a 409)

# 2. A worker claims it — this is the same call any worker polling this
#    hub already makes on its own; run it yourself to see the shape.
#    204 with no body means nothing is pending for this provider right now.
CLAIM=$(curl -s "https://hub.example.test/api/agent/turns/next?providers=claude" \
  -H "Authorization: Bearer $TOKEN")
echo "$CLAIM"
# {"turn_id": "101", "provider": "claude", "native_id": null, "text": "...",
#  "model": null, "claim_token": "fence-1", "claim_generation": 1, ...}
TURN_ID=$(echo "$CLAIM" | jq -r .turn_id)
CLAIM_TOKEN=$(echo "$CLAIM" | jq -r .claim_token)

# 3. Post one event against that claim — a real review result, so step 5
#    below actually reaches "ready" instead of "failed". The shape is
#    exactly ReviewResult (see Integrate as a dispatcher's review-queue
#    section): summary, questions, evidence, proposals. Built with jq so
#    the nested JSON-as-a-string doesn't need hand escaping.
REVIEW_RESULT='{"summary":"Looks reasonable.","questions":[],"evidence":[],"proposals":[]}'
BODY=$(jq -n --arg token "$CLAIM_TOKEN" --arg delta "$REVIEW_RESULT" \
  '{claim_token: $token, batch_id: "b1", events: [{type: "text", delta: $delta}]}')
curl -s -X POST "https://hub.example.test/api/agent/turns/$TURN_ID/events" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "$BODY"
# 200 {"ok": true}

# 4. Complete it.
curl -s -X POST "https://hub.example.test/api/agent/turns/$TURN_ID/done" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"claim_token\": \"$CLAIM_TOKEN\", \"ok\": true}"
# 200 {"ok": true}

# 5. Read the result back through the review queue you enqueued into.
curl -s https://hub.example.test/api/integrations/turns/first-turn-1 \
  -H "Authorization: Bearer $REVIEW_TOKEN"
# {"request_id": "first-turn-1", "state": "ready",
#  "result": {"summary": "Looks reasonable.", "questions": [], "evidence": [], "proposals": []}}

Steps 2-4 are exactly what Integrate as a worker documents in full — a real worker just does them in a loop, automatically, against whatever’s actually pending. Step 1 and step 5 are the one hub-hosted shortcut Integrate as a dispatcher describes; building your own is the rest of that page.

The two hosts

A deployment normally answers on two hostnames:

  • The app host serves the product itself: the browser UI, OIDC login, and every /api/* route — the agent-facing surface this guide covers, and the browser-facing surface a UI integrates against.
  • The docs host (optional) serves this site at its own root, in addition to — not instead of — /docs on the app host, which keeps working exactly as before whether or not a docs host is configured. Either way, /api/openapi.json and /api/docs (the interactive reference) are reachable from both hostnames, so links in this guide use root-relative paths that work no matter which host served the page you’re reading. See Run your own for how an operator configures this split.