> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beeos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a message to an agent and receive a reply (sync / SSE).

> Sends a `chat_message` to the specified agent via Message Service
channel-primitives and blocks until the agent replies (or times out).

For streaming responses, set `Accept: text/event-stream` — the
response body will be Server-Sent Events with `delta` and `done`
event types. For async fire-and-forget invocations, use the `tasks`
API instead.




## OpenAPI

````yaml /openapi/beeos-platform-v1.yaml post /api/v1/agents/{agentId}/invoke
openapi: 3.1.0
info:
  title: BeeOS OpenAPI (user contract)
  version: 1.1.0
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  description: |
    The **BeeOS OpenAPI** user contract. The sole implementer is
    **[`services/openapi-gateway`](../services/openapi-gateway)** (`openapi-gw`,
    local `:8095`). SDKs (Go / TypeScript) published under
    [`sdks/beeos-ai-sdk-go`](../../sdks/beeos-ai-sdk-go) and
    [`sdks/beeos-ai-sdk`](../../sdks/beeos-ai-sdk) are generated directly from
    this document.

    The main user Gateway (`services/gateway`, `:9080`) serves web / mobile /
    desktop clients and is **not** described by this document. It maintains its
    own route set independent of the SDK contract.

    **Auth:** every operation requires either a user **JWT** or a **`oag_`**
    User API Key, both on `Authorization: Bearer`. Agent-level protocol
    endpoints (A2A JSON-RPC with `bak_`, MCP) live in
    [beeos-agent-integration-v1.yaml](beeos-agent-integration-v1.yaml) and are
    hosted by A2A Gateway / MCP Gateway — not by `openapi-gateway`.

    ## Changelog

    ### 1.1.0 (ADR-0022 — dual-layer message storage)

    BREAKING for clients that depended on full streaming chunk replay
    via the persistent message log:

    * `GET /api/v1/agents/{agentId}/conversations/{convId}/messages`
      and `GET /api/v1/agents/{agentId}/tasks/{taskId}/messages` now
      **default-filter out** ephemeral streaming chunks
      (`agent_reply_delta`, `agent_thought_chunk`,
      `agent_message_chunk`). Add `?include_deltas=true` to opt back in.
    * `latest_offset` continues to reflect the TRUE server-side max
      offset across both filtered and unfiltered rows, so `since=` /
      cursor-style pagination is unaffected.
    * SSE `/events` is **NOT** affected — live consumers continue to
      receive every envelope as it happens.
    * Backwards-compatible recovery path: SSE `/events` may now emit
      a `backfill_truncated` event frame when the client reconnects
      with `Last-Event-ID` before the oldest retained chunk. SDKs
      that don't recognise the frame should treat it as a hint to
      resume from `oldest_redis_offset` instead (the field carrying
      the smallest offset still readable on the per-channel stream).
    * **Clarified contract**: SSE event `offset` is strictly monotonic
      per channel but **NOT guaranteed contiguous** (ADR-0022 §1.2).
      Producer-side write failures may leave small holes; clients MUST
      use `offset > since`, never `offset == since + 1`.

    BREAKING for clients that depended on the `oag_` API key scope
    vocabulary:

    * The per-route scope gate (`agents:read`, `agents:write`,
      `tasks:read`, `tasks:write`, `files:read`, `files:write`,
      `instances:read`, `instances:write`, plus the `admin:*` wildcard)
      has been **removed entirely**. The 37 routes that previously
      required scopes are now governed exclusively by owner-ACL — an
      `oag_` User API Key inherits full access to its owner's resources.
    * The `403 insufficient_scope` error code is no longer emitted.
      Existing callers that branched on it should fold the case into
      their generic 403 / `forbidden` handler.
    * SDK clients that explicitly passed `scopes` to `createAPIKey`
      should drop that argument. The Web / Desktop UIs no longer render
      scope badges on existing keys.
    * Existing `oag_` keys continue to work unchanged — the owner_id
      binding is preserved, and no key needs to be re-issued.

    BREAKING for clients of `GET /api/v1/providers` that read the
    `capabilities` object on the wire:

    * 7 of the 8 `capabilities` field names are now **camelCase**
      (`longRunning`, `browserUse`, `codeExec`, `fileSystem`,
      `customImage`, `maxDurationSec`, `costModel`) — matching this
      OpenAPI contract. Previous wire emitted **snake_case**
      (`long_running`, `browser_use`, …) because the server DTO bound
      `capabilities` to the raw protobuf message
      (`*pb.ProviderCapabilities`) whose auto-generated `json` tags
      use snake_case. SDK consumers (`@beeos-ai/sdk`,
      `github.com/beeos-ai/sdk-go`) generated from this spec were
      **already unable** to read any `capabilities` field — the
      camelCase property names produced by the generator never
      matched the snake_case the server actually sent. Raw HTTP
      callers that hand-coded against the old snake_case names must
      migrate; SDK callers gain access to the field set for the first
      time without code changes.
    * Note: the `device` boolean was always a single word — its wire
      name (`device`) is unchanged. Only the seven multi-word
      capability fields are affected by the rename.
    * `meta`, `id`, `name`, `description`, `version` are unchanged.
    * A new drift-guard (`internal/dto/contract_test.go`) now locks
      the server DTO to the spec property names, so the two cannot
      silently diverge again.

    ### 1.0.0

    Initial OpenAPI contract (ADR-001).
servers:
  - url: https://openapi.beeos.ai
    description: Production
  - url: https://openapi-staging.beeos.ai
    description: Staging
  - url: http://localhost:8095
    description: Local dev (goreman)
security:
  - bearerAuth: []
tags:
  - name: deploy
    description: Provider / region / model catalog.
  - name: instances
    description: Agent instance lifecycle (deploy, start, stop, restart, destroy).
  - name: computer
    description: |
      Synchronous desktop control (linux / windows / mac instances):
      screenshot, click, move, scroll, type, key. Coordinates are
      absolute device pixels (origin top-left); query
      `GET /instances/{id}/computer` for the live screen geometry and
      supported action set. Android instances are controlled via the
      `mobile` tag instead (calling `computer` on an android instance
      returns 404). Aligns with Anthropic / OpenAI "computer use".
  - name: mobile
    description: |
      Synchronous mobile (android) control: screenshot, tap, swipe,
      scroll, type, key, press_button. Coordinates are absolute device
      pixels. Query `GET /instances/{id}/mobile` for live geometry and
      supported actions. Desktop instances are controlled via the
      `computer` tag (calling `mobile` on a desktop instance returns 404).
  - name: agents
    description: Agent listing, details, and synchronous invocation.
  - name: tasks
    description: |
      Async agent tasks (the canonical task core — see ADR-0017
      `0017-unified-task-core`). `tasks` differs from `agents/invoke` in three
      ways: it returns 202 immediately with a `task_id` (no blocking on the
      reply), exposes a polling endpoint plus an SSE event stream, and supports
      cancel/continue lifecycle controls.
  - name: conversations
    description: >
      Long-lived multi-turn dialogs. Unlike tasks (single-shot, finite, terminal

      status), conversations are open until the caller closes them — messages

      are individually persisted and addressable by offset. Use the conversation

      API when you need to interleave caller and agent turns over an extended

      session; use `tasks` when a single prompt has a bounded reply with
      optional

      pause-and-resume.
  - name: files
    description: |
      Presigned URLs for caller uploads (`POST /files/presign-upload`) and
      result downloads (`GET /files/presign-download`) — the canonical
      attachment mechanism shared by `agents/{id}/invoke` and
      `agents/{id}/tasks`. Callers obtain a short-lived signed URL, upload the
      bytes directly to object storage (S3 / MinIO), and pass the resulting
      `file_id` in subsequent invoke / task requests via `attachments`.
paths:
  /api/v1/agents/{agentId}/invoke:
    parameters:
      - name: agentId
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
    post:
      tags:
        - agents
      summary: Send a message to an agent and receive a reply (sync / SSE).
      description: |
        Sends a `chat_message` to the specified agent via Message Service
        channel-primitives and blocks until the agent replies (or times out).

        For streaming responses, set `Accept: text/event-stream` — the
        response body will be Server-Sent Events with `delta` and `done`
        event types. For async fire-and-forget invocations, use the `tasks`
        API instead.
      operationId: invokeAgent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvokeAgentRequest'
      responses:
        '200':
          description: |
            Blocking: JSON envelope with agent reply.
            Streaming (`Accept: text/event-stream`): SSE stream — see
            `InvokeEventStream` for the per-frame schema (`delta`,
            `error`, `done` types).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvokeAgentResponse'
              examples:
                shortReply:
                  summary: Synchronous reply
                  value:
                    success: true
                    data:
                      text: >-
                        BeeOS ships unified task core in 1.0.x. The release adds
                        A2A v1.0 compatibility, slim A2A task schema, and
                        chatinvoke as the canonical primitive.
                      context_id: ch-uuid
                      is_error: false
                agentReplyError:
                  summary: Agent in-band error (transport succeeded)
                  value:
                    success: true
                    data:
                      text: >-
                        I can't access that file — please re-upload it or check
                        that you own it.
                      context_id: ch-uuid
                      is_error: true
            text/event-stream:
              schema:
                $ref: '#/components/schemas/InvokeEventStream'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        5XX:
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    InvokeAgentRequest:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          description: The message to send to the agent.
        context_id:
          type: string
          description: Optional conversation context ID for multi-turn conversations.
        timeout_ms:
          type: integer
          format: int64
          minimum: 0
          maximum: 115000
          description: |
            Per-request timeout in milliseconds. Default 120000 (2 minutes).
            Hard-capped at 115000 server-side so the response always has
            time to flush before the HTTP `WriteTimeout` (120s) fires.
            Values above 115000 are silently clamped; if the agent does not
            reply within the effective window the server returns
            `service_timeout` (HTTP 504) — see
            [docs/reference/errors.md](../../docs/reference/errors.md).
        idempotency_key:
          type: string
          description: |
            Optional caller-generated idempotency key forwarded to Message
            Service as the chat_message's `idempotency_key`. Retries that race
            the same key dedup at MS (UNIQUE index on `channel_messages`).
            When omitted, the gateway generates a fresh UUID. The same key
            also doubles as the `message_id` an agent must echo back as
            `in_reply_to` on its reply.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: |
            Opaque caller-controlled key/value pairs merged into the channel
            metadata (e.g. `trace_id`, `user_id`, business tags). Reserved
            routing keys (`protocol`, `caller_owner_id`, `target_agent_id`,
            `delivery_principal`) are silently scrubbed server-side and
            cannot be overridden by the caller.
        attachments:
          type: array
          maxItems: 16
          items:
            $ref: '#/components/schemas/AttachmentRef'
          description: |
            Optional list of files previously uploaded via
            [`POST /api/v1/files/presign-upload`](#operation/presignFileUpload).
            Each `file_id` is resolved server-side to a presigned download
            URL and embedded in the chat_message envelope so the receiving
            agent can fetch the bytes without further BeeOS auth.
    InvokeAgentResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          required:
            - data
          properties:
            data:
              type: object
              required:
                - text
                - context_id
                - is_error
              properties:
                text:
                  type: string
                  description: Agent reply text.
                context_id:
                  type: string
                  description: Conversation context ID (channel ID).
                is_error:
                  type: boolean
                  description: Whether the agent's reply indicated an error.
    InvokeEventStream:
      type: string
      description: >
        Server-Sent Events stream for the streaming `invoke` path.


        Frame format:

        ```

        data: {"type":"reasoning","text":"Let me think… "}

        data: {"type":"delta","text":"Hello "}

        data: {"type":"delta","text":"world"}

        data: {"type":"done","text":"Hello
        world","context_id":"ch-1","is_error":false}

        ```


        `reasoning` frames carry the agent's live chain-of-thought

        ("thinking"), delivered on a parallel stream and interleaved with

        `delta` (answer) frames. They are independent: concatenate only

        `delta` text for the answer, and only `reasoning` text for the

        thinking trace. Clients that don't render reasoning can ignore

        `reasoning` frames entirely.


        On agent error (audit-v4 P1-1 unified error frame):

        ```

        data:
        {"type":"error","code":"agent_unavailable","status_code":503,"message":"agent
        is offline"}

        data: {"type":"done","is_error":true,"code":"agent_unavailable"}

        ```


        Frame schemas:

        - `reasoning` — see
        [`InvokeAgentSSEReasoning`](#/components/schemas/InvokeAgentSSEReasoning)

        - `delta` — see
        [`InvokeAgentSSEDelta`](#/components/schemas/InvokeAgentSSEDelta)

        - `error` — see
        [`InvokeAgentSSEError`](#/components/schemas/InvokeAgentSSEError)

        - `done`  — see
        [`InvokeAgentSSEDone`](#/components/schemas/InvokeAgentSSEDone)
    AttachmentRef:
      type: object
      required:
        - file_id
      properties:
        file_id:
          type: string
          maxLength: 128
          description: |
            File identifier returned from
            [`POST /api/v1/files/presign-upload`](#operation/presignFileUpload).
        filename:
          type: string
          description: Optional override for the file_record's filename.
        content_type:
          type: string
          description: Optional override for the file_record's MIME type.
    SuccessEnvelope:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          const: true
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: '#/components/schemas/Error'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        type:
          type: string
          description: |
            RFC 9457 problem-type slug. Most SDK consumers key on
            `code` rather than `type`; this field is kept for
            structural parity with the wider apierror catalogue.
          enum:
            - api_error
            - invalid_request_error
            - authentication_error
            - permission_error
            - rate_limit_error
            - not_found_error
            - conflict_error
            - validation_error
        code:
          type: string
          description: >
            Stable machine-readable error code. See

            [docs/reference/errors.md](https://github.com/beeos-ai/beeos/blob/main/docs/reference/errors.md)

            for the complete catalogue.
          examples:
            - agent_not_found
            - agent_service_unavailable
            - service_timeout
            - conflict
            - forbidden
            - invalid_param
            - rate_limited
            - internal_error
        message:
          type: string
          description: Human-readable explanation; presentation-only, never key on this.
        request_id:
          type: string
          description: |
            Per-request correlation id propagated end-to-end (also returned
            in the `X-Request-Id` response header). Include this value when
            reporting errors so the server-side trace can be located.
        param:
          type: string
          description: |
            Field path of the offending parameter for validation-style
            errors (`code` in `missing_param` / `invalid_param`). Empty for
            errors that don't reference a specific field.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: |
            Optional structured key-value hints for the client (retry
            strategy, related resource ids, etc.). Always a string-to-string
            map.
  responses:
    BadRequest:
      description: >-
        Malformed request body, query parameter, or path parameter (`code` is
        one of `invalid_json` / `invalid_body` / `invalid_param` /
        `missing_param`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: >-
        Missing or invalid `Authorization` header (`code` is one of
        `unauthorized` / `invalid_token` / `missing_token`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Caller doesn't own the resource. `code` is `forbidden`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: >-
        The agent / task / instance / conversation / webhook referenced doesn't
        exist or isn't visible to the caller. `code` is typically
        `agent_not_found`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: >
        Resource is in a state that doesn't allow this operation. `code` is
        `conflict`.


        Two distinct cases for agent refusal:

        - **Legacy pre-output refused** (e.g. `agent_busy` from the agent
        runtime BEFORE any output is streamed): the gateway maps this to **409
        Conflict** with `code = task_rejected`. No task body is produced.

        - **v3 in-band refused** (agent started a streaming reply, then
        transitioned the v3 envelope to `state = refused` with a refusal
        `body`): the invoke endpoint returns **200 OK** with `is_error = true`
        and the body carrying the refusal text. The terminal `stop_reason =
        refused` lets clients distinguish this from a successful completion.
        **NOT** a 409.


        Other 409 triggers: task already terminal, duplicate idempotency key on
        a changed payload.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PayloadTooLarge:
      description: >-
        Request body exceeds the per-route size limit (catalog/instances 64 KiB;
        invoke/tasks 1 MiB; attachments must use file presign). `code` is
        `payload_too_large`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: >-
        Per-caller × endpoint quota exceeded. `code` is `rate_limited`. Honour
        `Retry-After` if present.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServerError:
      description: >
        Server-side fault. `code` is one of:


        - `internal_error` (500) — unexpected

        - `agent_offline` / `agent_service_unavailable` (503) — agent or
        messaging
          subsystem unreachable
        - `auth_unavailable` / `auth_transient` (503) — Auth gRPC transient

        - `service_timeout` (504) — gateway gave up waiting for the agent


        See
        [docs/reference/errors.md](https://github.com/beeos-ai/beeos/blob/main/docs/reference/errors.md).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Pass a user **JWT** or a **`oag_`** User API Key on the
        `Authorization: Bearer <token>` header. Both are validated by
        `openapi-gateway` against the Auth service.

        Both credential types are **user-scoped**: every key (and every
        JWT) is bound to exactly one owner, and every route grants the
        caller full access to that owner's own resources. Cross-tenant
        access is denied by owner-ACL inside the handlers — there is no
        per-route scope vocabulary on this API.

        > **Removed in v1.1.0:** the legacy `agents:*` / `tasks:*` /
        > `files:*` / `instances:*` scope set has been dropped together
        > with the `403 insufficient_scope` error. Existing `oag_` keys
        > automatically gain full owner-level access and do **not** need
        > to be re-issued. SDK calls that previously passed `scopes` to
        > `createAPIKey` should drop the argument. See the changelog at
        > the bottom of this spec for the full migration note.

````