Skip to content

Agent endpoints

Publish a saved local agent profile to call its configured model and tools through OpenAI-compatible APIs or A2A. Publishing is disabled by default.

Publish a profile

Open the local profile editor, save profile changes, and select Publish. Enable publishing, choose a public slug, and apply the publication. Configure the allowed tools, approval mode, workspace, iteration limit, execution time and session retention there. A tool must be enabled in the profile before it can be published. Remove selected tools that became unavailable before applying changes. Remote profiles cannot be republished as local agents.

The API backend list separates Model APIs from Agent APIs. GO manages the aigo-agents backend, whose models are named agent:<slug>. Do not create that backend manually. Enabling publication starts the embedded Management API where applicable; the router also needs its TCP listener. Copy connection details only after the panel verifies the listener and the exact served agent model. A saved port or a Management API SSH tunnel does not establish access to the router port. On the desktop app the panel shows this computer's own addresses. A loopback address (127.0.0.1, [::1], localhost) is marked as reachable from this computer only; clients on your network replace that host with this computer's network address.

Connections and approval

Interface Address Approval behavior
Chat Completions Router base URL plus /chat/completions Tools requiring approval are denied
Native Responses Router base URL plus /responses Saved ask mode can ask the calling application
Converted Responses Same /responses address Text conversation conversion; tools requiring approval are denied
A2A 0.3 Management API card's url Saved ask mode can ask the calling application

The connection panel reports the running router's applied mode. Native capability alone is insufficient until the managed backend configuration is verified. Pending or unknown status is not proof of native availability. Approval belongs to the calling application, not a desktop approval dialog. Saved deny mode denies approval requests on every interface.

Use separate client keys for the router and Management API. Router examples use an inference access key. A2A requires agent_invoke; agent_read and agent_write do not grant it. Even when general Management API authentication is disabled, A2A still requires an authenticated identity. The internal router-to-GO credential is not a client key.

Closing a native Responses stream while the upstream was silent used to leave the agent running (router issue #2003). The fix (router PR #2004) ships in continuum-router v1.32.0, the version GO bundles. Two related gaps are still open. The router's Chat Completions relay can miss a client disconnect while the upstream is silent (router issue #2008). Cancelling a published run, by cancel request, disconnect or tasks/cancel, stops the run but not the model request it has in flight, so a local model that serves one request at a time stays busy until that generation ends (#5461). Use the explicit cancel endpoint when stopping work. Older routers, such as v1.29.0, provide only converted Responses; the connection panel reports the mode the running router applies.

Shell setup

Examples require Bash, curl, jq and Python 3. Load keys privately rather than placing literal secrets in shell history. Set the variables below from the connection panel. ROUTER_BASE includes /v1; MANAGEMENT_BASE is the Management API origin. Local defaults are ports 39080 and 8001, but use the configured addresses. OUT contains private response and conversation capabilities; do not publish these files.

Approval examples need a separate saved ask publication, ASK_SLUG, and ASK_INPUT that reliably requests one of its approval-required allowed tools. A text-only profile will not produce an approval request. Each decision below starts a fresh invocation.

set -eu
umask 077
: "${ROUTER_BASE:?Set the copied OpenAI base URL, including /v1}"
: "${MANAGEMENT_BASE:?Set the Management API origin, without a trailing slash}"
: "${ROUTER_API_KEY:?Load a router access key}"
: "${MANAGEMENT_API_KEY:?Load a key with agent_invoke}"
: "${SLUG:?Set an enabled deny publication slug}"
MODEL="agent:$SLUG"
OUT=$(mktemp -d)
# For approval examples, also set ASK_SLUG and ASK_INPUT.
ASK_MODEL="agent:${ASK_SLUG:-}"

Discover the published model

curl --fail-with-body -sS "$ROUTER_BASE/models" \
  -H "Authorization: Bearer $ROUTER_API_KEY" > "$OUT/models.json"
jq -e --arg model "$MODEL" '.data[] | select(.id == $model)' "$OUT/models.json"

Chat and session continuation

jq -n --arg model "$MODEL" '{model:$model,messages:[{role:"user",content:"Remember the word apricot."}]}' |
  curl --fail-with-body -sS "$ROUTER_BASE/chat/completions" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/chat.json"
CHAT_SESSION=$(jq -er '.agent_session' "$OUT/chat.json")
jq -n --arg model "$MODEL" '{model:$model,messages:[{role:"user",content:"Which word did I ask you to remember?"}]}' |
  curl --fail-with-body -sS "$ROUTER_BASE/chat/completions" -H "Authorization: Bearer $ROUTER_API_KEY" -H "X-Agent-Session: $CHAT_SESSION" -H 'Content-Type: application/json' --data-binary @- > "$OUT/chat-next.json"
jq -e '.choices[0].message.content | type == "string"' "$OUT/chat-next.json"

Stream Chat output

jq -n --arg model "$MODEL" '{model:$model,messages:[{role:"user",content:"Say hello."}],stream:true}' |
  curl --fail-with-body -sS -N "$ROUTER_BASE/chat/completions" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/chat.sse"

Read agent_session from the buffered Chat JSON body or from a streaming SSE chunk. Send it as X-Agent-Session on the next request. Proxies may omit response headers, so do not rely on receiving the capability in a header. Treat it as a secret in every location.

Responses text and streaming

jq -n --arg model "$MODEL" '{model:$model,input:"Say hello."}' |
  curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/response.json"
jq -e '.id and .status == "completed"' "$OUT/response.json"
jq -n --arg model "$MODEL" '{model:$model,input:"Say hello.",stream:true}' |
  curl --fail-with-body -sS -N "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/response.sse"

The remaining Responses examples require the connection panel to report Native with the managed backend configuration verified. Converted mode supports the text and streaming calls above with approval-required tools denied. With router v1.29.0 in converted mode, GET /v1/responses/{id} returned 200, a previous_response_id continuation returned 200, and cancel returned 404. That retrieval and continuation are served from the router's own Responses state, not from the agent's retained history, so the native history, approval, lifecycle and store:false behavior below does not apply to them.

Native response and conversation history

PREVIOUS=$(jq -er '.id' "$OUT/response.json")
jq -n --arg model "$MODEL" --arg previous "$PREVIOUS" '{model:$model,previous_response_id:$previous,input:"Explain your answer."}' |
  curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/response-next.json"
CONVERSATION=$(jq -er '.conversation.id' "$OUT/response-next.json")
jq -n --arg model "$MODEL" --arg conversation "$CONVERSATION" '{model:$model,conversation:$conversation,input:"Summarize this conversation."}' |
  curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/response-third.json"

Approve or deny a native tool request

for DECISION in true false; do
  jq -n --arg model "$ASK_MODEL" --arg input "$ASK_INPUT" '{model:$model,input:$input}' |
    curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/pending-$DECISION.json"
  PENDING_ID=$(jq -er '.id' "$OUT/pending-$DECISION.json")
  APPROVAL_ID=$(jq -er '.output[] | select(.type == "mcp_approval_request") | .id' "$OUT/pending-$DECISION.json")
  jq -n --arg model "$ASK_MODEL" --arg previous "$PENDING_ID" --arg approval "$APPROVAL_ID" --argjson approve "$DECISION" '{model:$model,previous_response_id:$previous,input:[{type:"mcp_approval_response",approval_request_id:$approval,approve:$approve}]}' |
    curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/resumed-$DECISION.json"
  jq -e --arg previous "$PENDING_ID" '.id != $previous' "$OUT/resumed-$DECISION.json"
done

Retrieve, cancel a pending approval, and delete

jq -n --arg model "$ASK_MODEL" --arg input "$ASK_INPUT" '{model:$model,input:$input}' |
  curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/cancel-pending.json"
RESPONSE_ID=$(jq -er '.id' "$OUT/cancel-pending.json")
jq -e '.output[] | select(.type == "mcp_approval_request")' "$OUT/cancel-pending.json" > /dev/null
curl --fail-with-body -sS "$ROUTER_BASE/responses/$RESPONSE_ID" -H "Authorization: Bearer $ROUTER_API_KEY" > "$OUT/retrieved.json"
curl --fail-with-body -sS -X POST "$ROUTER_BASE/responses/$RESPONSE_ID/cancel" -H "Authorization: Bearer $ROUTER_API_KEY" > "$OUT/cancelled-approval.json"
curl --fail-with-body -sS -X DELETE "$ROUTER_BASE/responses/$RESPONSE_ID" -H "Authorization: Bearer $ROUTER_API_KEY" > "$OUT/deleted.json"
jq -e '.deleted == true' "$OUT/deleted.json"

Run without retained response history

jq -n --arg model "$MODEL" '{model:$model,input:"Say hello.",store:false}' |
  curl --fail-with-body -sS "$ROUTER_BASE/responses" -H "Authorization: Bearer $ROUTER_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/ephemeral.json"
EPHEMERAL_ID=$(jq -er '.id' "$OUT/ephemeral.json")
HTTP_STATUS=$(curl -sS -o "$OUT/ephemeral-get.json" -w '%{http_code}' "$ROUTER_BASE/responses/$EPHEMERAL_ID" -H "Authorization: Bearer $ROUTER_API_KEY")
test "$HTTP_STATUS" = 404

Discover the authenticated A2A card

curl --fail-with-body -sS "$MANAGEMENT_BASE/api/v1/a2a/$SLUG/.well-known/agent-card.json" -H "Authorization: Bearer $MANAGEMENT_API_KEY" > "$OUT/card.json"
A2A_URL=$(jq -er '.url' "$OUT/card.json")
jq -e '.protocolVersion == "0.3"' "$OUT/card.json"

Send an A2A task and poll

MESSAGE_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
jq -n --arg message "$MESSAGE_ID" '{jsonrpc:"2.0",id:1,method:"message/send",params:{message:{kind:"message",messageId:$message,role:"user",parts:[{kind:"text",text:"Say hello."}]}}}' |
  curl --fail-with-body -sS "$A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/task.json"
TASK_ID=$(jq -er '.result.id' "$OUT/task.json")
for ATTEMPT in $(seq 1 120); do
  jq -n --arg id "$TASK_ID" '{jsonrpc:"2.0",id:2,method:"tasks/get",params:{id:$id}}' |
    curl --fail-with-body -sS "$A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/task-status.json"
  STATE=$(jq -er '.result.status.state' "$OUT/task-status.json")
  case "$STATE" in completed|failed|canceled|rejected|input-required) break;; esac
  sleep 1
done
jq -e '.result.status.state == "completed"' "$OUT/task-status.json"

Stream an A2A task

MESSAGE_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
jq -n --arg message "$MESSAGE_ID" '{jsonrpc:"2.0",id:3,method:"message/stream",params:{message:{kind:"message",messageId:$message,role:"user",parts:[{kind:"text",text:"Say hello."}]}}}' |
  curl --fail-with-body -sS -N "$A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/task.sse"

Approve, deny, or cancel a pending A2A task

curl --fail-with-body -sS "$MANAGEMENT_BASE/api/v1/a2a/$ASK_SLUG/.well-known/agent-card.json" -H "Authorization: Bearer $MANAGEMENT_API_KEY" > "$OUT/ask-card.json"
ASK_A2A_URL=$(jq -er '.url' "$OUT/ask-card.json")
for DECISION in approve deny cancel; do
  MESSAGE_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
  jq -n --arg message "$MESSAGE_ID" --arg input "$ASK_INPUT" '{jsonrpc:"2.0",id:4,method:"message/send",params:{message:{kind:"message",messageId:$message,role:"user",parts:[{kind:"text",text:$input}]}}}' |
    curl --fail-with-body -sS "$ASK_A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/ask-task.json"
  TASK_ID=$(jq -er '.result.id' "$OUT/ask-task.json")
  for ATTEMPT in $(seq 1 120); do
    jq -n --arg id "$TASK_ID" '{jsonrpc:"2.0",id:5,method:"tasks/get",params:{id:$id}}' |
      curl --fail-with-body -sS "$ASK_A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/ask-status.json"
    STATE=$(jq -er '.result.status.state' "$OUT/ask-status.json")
    case "$STATE" in input-required|completed|failed|canceled|rejected) break;; esac
    sleep 1
  done
  jq -e '.result.status.state == "input-required"' "$OUT/ask-status.json" > /dev/null
  CONTEXT_ID=$(jq -er '.result.contextId' "$OUT/ask-status.json")
  CALL_ID=$(jq -er '.result.status.message.metadata.aigoApproval.callId' "$OUT/ask-status.json")
  if [ "$DECISION" = cancel ]; then
    jq -n --arg id "$TASK_ID" '{jsonrpc:"2.0",id:6,method:"tasks/cancel",params:{id:$id}}' > "$OUT/decision.json"
  else
    MESSAGE_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
    jq -n --arg task "$TASK_ID" --arg context "$CONTEXT_ID" --arg call "$CALL_ID" --arg decision "$DECISION" --arg message "$MESSAGE_ID" '{jsonrpc:"2.0",id:6,method:"message/send",params:{message:{kind:"message",messageId:$message,role:"user",taskId:$task,contextId:$context,parts:[{kind:"text",text:$decision}],metadata:{aigoApproval:{callId:$call,response:$decision}}}}}' > "$OUT/decision.json"
  fi
  curl --fail-with-body -sS "$ASK_A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @"$OUT/decision.json" > "$OUT/a2a-$DECISION.json"
  jq -e '.result and (has("error") | not)' "$OUT/a2a-$DECISION.json"
  for ATTEMPT in $(seq 1 120); do
    jq -n --arg id "$TASK_ID" '{jsonrpc:"2.0",id:7,method:"tasks/get",params:{id:$id}}' |
      curl --fail-with-body -sS "$ASK_A2A_URL" -H "Authorization: Bearer $MANAGEMENT_API_KEY" -H 'Content-Type: application/json' --data-binary @- > "$OUT/final-$DECISION.json"
    STATE=$(jq -er '.result.status.state' "$OUT/final-$DECISION.json")
    case "$STATE" in completed|failed|canceled|rejected|input-required) break;; esac
    sleep 1
  done
  if [ "$DECISION" = cancel ]; then
    jq -e '.result.status.state == "canceled"' "$OUT/final-$DECISION.json"
  else
    jq -e '.result.status.state | . == "completed" or . == "failed" or . == "input-required"' "$OUT/final-$DECISION.json"
  fi
done

History, lifecycle and limits

Each request without X-Agent-Session starts a new conversation. Send the returned agent_session as X-Agent-Session to continue that server-held history. A client may instead resend prior user and assistant messages in a fresh request. Native Responses accepts either the latest previous_response_id or a returned conversation.id, never both. Send only the new user input when continuing retained history. These handles grant access to the conversation. They are in-memory capabilities, not durable OpenAI Conversations API objects. Sessions are revoked by publication changes/removal, restart or expiry.

A client that sends a stale or revoked handle receives one of these responses. GO answers a stale Chat session with HTTP 404 and error.code: agent_session_not_found, but the router reports every backend 4xx other than 429 as HTTP 400 and keeps the body's error.code, so match Chat errors on the code rather than the status.

Interface Stale or revoked handle Publication disabled
Chat Completions, X-Agent-Session HTTP 400, error.code: agent_session_not_found HTTP 404, error.code: model_not_found from the router once its catalog drops the model; HTTP 400, error.code: agent_error in the seconds before that
Native Responses, GET /responses/{id}, previous_response_id or conversation HTTP 404 HTTP 404
A2A message/send with contextId HTTP 200, JSON-RPC error -32001, Context not found or expired HTTP 404 with a REST error body
A2A tasks/get HTTP 200, JSON-RPC error -32001, Task not found HTTP 404 with a REST error body

An approval-bearing native response completes one response phase while execution waits. Approving or denying creates a new response ID and resumes the same execution. Always inspect the newest output for another approval request. A nonce can be used only once. Cancelling the latest completed approval response stops the paused execution but leaves that completed response snapshot unchanged. An older phase cannot cancel a newer turn. Expired or revoked response state returns 404.

Active native cancellation emits the standard error event with code: response_cancelled; GET reports cancelled. It does not emit response.cancelled. Execution failure emits response.failed. The official SDK convenience get_final_response() expects response.completed; inspect raw failure/error events for unsuccessful runs. A buffered Chat Completions run that fails after it starts returns error.code: agent_error and no choices. GO sends it with HTTP 422, which the router neither retries nor falls back on, so the run is not repeated; the client receives HTTP 400. A streaming Chat request has already returned HTTP 200, so it reports the failure as an error event followed by [DONE]. Disconnecting an active Chat request cancels execution when the router passes the disconnect on (see router issue #2008 above). With the bundled router, closing a native stream also cancels execution; with an older router, use explicit cancellation. After an approval phase completes, its bounded waiting execution survives closing that response connection. A2A tasks survive stream disconnection; cancel them with tasks/cancel.

A2A message/send returns before completion. Poll tasks/get and inspect JSON-RPC errors even when HTTP status is 200. A new turn uses contextId; taskId targets pending approvals or task operations. A terminal task cannot be restarted. Only text parts are accepted. Blocking requests, push notifications and task history retrieval are unsupported. A stream gap requires task retrieval, not treating partial output as success. Denial reaches the agent as a tool error. After either decision, the model may complete, fail, or request another approval. If the state is input-required, inspect the new approval and reply with its current callId, task and context IDs. Do not reuse a consumed approval ID. Use the authenticated card URL to register this agent on another GO instance; publishing a local profile and registering a remote profile are separate operations.

Chat and both Responses modes have a 510-second execution ceiling, further restricted by saved execution/session limits. Native approval waiting and continuation share the original deadline. A2A uses the saved limits. The service allows 1,024 sessions and 128 turns per session. Native retention is bounded by 4,096 responses, 1,024 conversations, 128 phases per conversation, 1 MiB encoded output per response and a shared 64 MiB response budget. A2A retains at most 2,048 tasks with 1 MiB output per task. Capacity exhaustion is an error.

store:false is a fresh native invocation using a deny profile. It does not retain a response/conversation or private run record, and rejects approval waiting, previous IDs and background operation. Tool-written files and audit records remain. The saved profile owns instructions and tools: client system/tool overrides, non-null instructions, custom tool definitions and multimodal input are rejected.

Troubleshooting

A macOS client reports No route to host

On macOS, a client process can fail to reach the router or Management API on another LAN host with No route to host, while /usr/bin/curl on the same Mac reaches the same address. macOS requires Local Network permission for connections to LAN addresses and attributes it to the app that started the process. A client started from an SSH login or a background service, such as a Python SDK script, has no app that can be asked, so the connection is refused. Open System Settings > Privacy & Security > Local Network and turn on access for the terminal or app that runs the client, then start the client from that app.

The agent is listed but its model is not running

A restart of GO, including headless aigo-server, restores publications, so agent:<slug> appears in /v1/models again. It does not run the local model the agent's profile uses again. Run that model before calling the agent after each restart.

A republished agent is missing from /v1/models

After a publish or republish, agent:<slug> can take several seconds (3 to 8 seconds in testing) to reappear in the router's /v1/models. GO updates the router's catalog on a one-second cycle, and the router then applies the change. Poll /v1/models until the model is listed before the first call.

Access and side effects

Expose router and Management API listeners separately when connecting over a LAN. Use the server's reachable address and the correct key for each listener. Check connectivity from the calling device. Restrict published tools and workspaces to what callers need. Delegation and skill loading do not become available merely because they exist in the local profile.

Retry protection is separate from native Responses support. When GO verifies applied protection, the router disables automatic replay, trace-keyed request coalescing and response caching for agent requests. An unsupported-protection warning means a connection failure may repeat tool actions; use tools that are safe to repeat and distinct trace IDs for distinct requests. Client retries can repeat side effects even with router protection. No published-agent protocol provides transaction rollback or application-level exactly-once execution.