---
title: "Open a plan; stream SSE of the orchestration."
method: POST
path: "/plan"
tags: ["Plan"]
---

# Open a plan; stream SSE of the orchestration.

`POST /plan`

Accepts a user question + agent catalog, starts (or resumes) a
session, and streams Server-Sent Events as the planner runs.

### Session continuation

Pass `session_id` to resume an existing session — history persists,
the planner sees prior turns. Omit to start a fresh session. The
server returns the resolved `session_id` in the first SSE frame
(`event: session`), even for new sessions, so clients can cache it.

### Catalog immutability per session

The `agents` catalog is stored on first plan and refreshed on each
subsequent call; agents added or removed between plans take effect
immediately but don't retroactively change prior turns' tool sets.

### Streaming & abort

Closing the HTTP connection aborts the plan — in-flight A2A calls
receive an `AbortSignal` and the planner loop terminates. Clients
that want a partial result should buffer `text.delta` frames
client-side rather than relying on `final`.

## Request body

- PlanRequest
  - `question` string, required — The user's natural-language question. Non-empty — an empty string is rejected upstream because some LLM providers (Anthropic) reject empty user messages with a 400 mid-stream, surfacing as a vague "Provider returned error". Validating here gives a clean 400 with `invalid_request` instead.
  - `agents` AgentRequest[] — Catalog of A2A peers the planner may call. Empty array = planner runs with no tools (useful for questions the configured planner LLM can answer on its own, e.g., general knowledge).
    - `name` string, required — Display name of the peer. Used to derive the tool id exposed to the planner LLM (`call_<name>_<skillId>`) and to correlate SSE events back to the catalog entry. Operator-chosen and potentially collision-prone — use `trust.pinnedDID` for a cryptographically stable identifier.
    - `endpoint` string, uri, required — Absolute HTTP(S) URL where the peer's A2A endpoint is reachable. The gateway POSTs JSON-RPC envelopes here for `message/send` and `tasks/get`.
    - `auth` union — How the gateway authenticates its outbound calls to this peer. Discriminated on `type`: - `none` — anonymous; peer must accept unauthenticated calls. - `bearer` — static token passed literally in `Authorization`. Caller includes the secret in the request, so only use over TLS. - `bearer_env` — gateway reads the token from the named env var. Keeps secrets out of the wire; rotation = restart. - `did_signed` — gateway signs the request body with its configured Ed25519 identity and attaches an OAuth2 token. By default uses the gateway's own auto-acquired Hydra token; pass `tokenEnvVar` to use a per-peer federated token.
      - PeerAuthNone
        - `type` 'none', required
      - PeerAuthBearer
        - `type` 'bearer', required
        - `token` string, required — Literal bearer token to include in `Authorization: Bearer <token>`.
      - PeerAuthBearerEnv
        - `type` 'bearer_env', required
        - `envVar` string, required — Name of the env var on the gateway process whose value is the bearer token.
      - PeerAuthDidSigned
        - `type` 'did_signed', required
        - `tokenEnvVar` string — Optional. Env var name for a pre-acquired OAuth2 token to pair with the DID signature. Omit to use the gateway's own Hydra auto-acquired token (requires `BINDU_GATEWAY_HYDRA_*` env).
    - `trust` PeerTrust — Per-peer trust policy. Both fields are optional; omitting both means "trust the peer's identity at face value — don't verify."
      - `verifyDID` boolean — When true, the gateway verifies every Ed25519 signature on artifacts returned by this peer. Mismatched signatures fail the task. Requires a resolvable DID on the peer.
      - `pinnedDID` string — DID the peer is expected to present. Used both for correlation (SSE `agent_did`) and, when `verifyDID` is true, to reject responses signed by a different key.
    - `skills` SkillRequest[] — Peer capabilities the planner may invoke. Each becomes one dynamic tool scoped to this request. The gateway does NOT discover skills from the peer's `AgentCard` — the caller declares them, ensuring the planner sees only capabilities the caller vouches for.
      - `id` string, required — The skill id the A2A peer recognizes. Passed back to the peer inside `message/send` so it can route to the right internal handler.
      - `description` string — Human-readable description. The planner LLM relies heavily on this to decide whether to invoke the skill — write 3–4 sentences covering intent, inputs, outputs, and when to use it. Descriptions under 120 chars are auto-padded server-side with agent/skill context so the LLM still gets enough signal.
      - `inputSchema` object — Optional JSON Schema for structured inputs. When present, the planner LLM emits a JSON object matching this shape and the gateway forwards it as the message text (serialized). When omitted, the planner sends a plain-text `input` string.
      - `outputModes` string[] — Advisory list of output MIME-like hints the peer may return (e.g., `text/plain`, `application/json`). Surfaced in the tool description so the planner knows what to expect back.
      - `tags` string[] — Free-form tags — helps the planner disambiguate when multiple peers expose similarly-named skills.
  - `preferences` PlanPreferences — Caps and shaping hints. All keys are **snake_case**; an earlier draft declared them camelCase, which caused docs-compliant clients to silently lose the caps — the schema is now strict on casing and unknown keys pass through via `additionalProperties: true` for forward compatibility.
    - `response_format` string — Advisory hint for the planner's final-message format (`"markdown"`, `"plain"`, `"json"`, etc.). Not enforced by the gateway; the planner may honor or ignore it.
    - `max_hops` integer — Maximum number of A2A hops (recursive peer-to-peer calls) the gateway allows. Phase 2+ enforced; currently informational.
    - `timeout_ms` integer — Hard timeout for the whole plan, in milliseconds.
    - `max_steps` integer — Maximum agentic loop steps. Overrides the planner agent's default (`agent.steps`). A "step" is one LLM call — tool calls inside a step don't count.
  - `session_id` string — Opaque external session identifier. If provided AND a session row exists with the matching `external_session_id`, that session is resumed (history persists). If omitted or unmatched, a new session is created and its server-assigned id is surfaced in the first SSE `session` event.

## Response `200`

SSE stream of the plan. Each event is one of the types
documented under `SSEEvent` below. The stream closes after
`event: done`.

## Other responses

- `400` — Malformed JSON, missing required fields, schema validation failure, or a catalog that would produce colliding tool ids (two entries whose `<agent>_<skill>` combination normalizes to the same value — silently swallowed before this guard, which let one peer mask another).
- `401` — Missing or invalid bearer token.
- `500` — Session creation failed (database unreachable, Supabase row insertion error, etc.). Only emitted **before** the SSE stream opens — once streaming starts, errors surface as `event: error` on the stream.

---

[API](https://skmtc.dev/getbindu/apis/bindu-gateway-api.md) · [All operations](https://skmtc.dev/getbindu/apis/bindu-gateway-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/getbindu/bindu-gateway-api/revisions/a99d23cfaf07/schema)
