From ad0e7ce6eb098939c0e4795e2e16e2f112930266 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sun, 26 Apr 2026 20:57:35 -0700 Subject: [PATCH] docs: mark 1.5.0 legacy URL surface removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG [Unreleased] / Removed (BREAKING — 1.5.0) block calling out the legacy URL family removal with the swap table. Doc passes on api-reference.md (per-endpoint sections rewritten with path parameters and slimmer body shapes), architecture.md (handler-list diagram and console-proxy URL example), console.md (URL-rewriting JS shim docstring + SSE proxy example), and the two PlantUML diagrams (11-console-data-flow, 16-channel-architecture). Also picks up two test-side stragglers from step 5 that referenced the legacy adapters in a docstring + a stale /v1/api/events SSE test: turn into path-keyed equivalents. OpenAPI JSON dump regenerated to reflect the catalog edits from step 3. After this commit: - 4557 tests passing under -m "not live" - ruff + mypy clean on turnstone/ tests/ sdk/ - grep for "/v1/api/send", "/v1/api/approve", "/v1/api/cancel", "/v1/api/workstreams/close" returns zero hits across turnstone/ sdk/ docs/ tests/ (excluding CHANGELOG.md, which intentionally documents the old shape). - grep for make_legacy_body_keyed_adapter, make_legacy_query_keyed_adapter, _make_method_dispatch, close_legacy returns zero hits. --- CHANGELOG.md | 46 ++++++ docs/api-reference.md | 65 +++++--- docs/architecture.md | 24 +-- docs/console.md | 4 +- docs/diagrams/11-console-data-flow.puml | 12 +- docs/diagrams/16-channel-architecture.puml | 18 +-- sdk/typescript/openapi-server.json | 174 +++++++++++++-------- tests/test_sdk_sse.py | 2 +- tests/test_server_authz.py | 13 +- 9 files changed, 232 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e3f6b2..5d53f43d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,52 @@ Three release tracks are maintained: ## [Unreleased] +### Removed (BREAKING — 1.5.0) + +- **Legacy body-keyed and query-keyed URL family for the workstream + interaction verbs.** Pre-1.5 interactive shipped both a path-keyed + and a body-keyed surface for the same five verbs; this release drops + the body-keyed and query-keyed mounts (and the + ``make_legacy_body_keyed_adapter`` / + ``make_legacy_query_keyed_adapter`` shims that backed them). External + SDK consumers on stable 1.0/1.3/1.4 must move to the path-keyed + shape: + + | Removed (1.0/1.3/1.4) | Use instead | + | ---------------------------------------------- | ------------------------------------------------------ | + | ``GET /v1/api/events?ws_id=X`` | ``GET /v1/api/workstreams/{ws_id}/events`` | + | ``POST /v1/api/send`` (body ``ws_id``) | ``POST /v1/api/workstreams/{ws_id}/send`` | + | ``DELETE /v1/api/send`` (body ``ws_id``) | ``DELETE /v1/api/workstreams/{ws_id}/send`` | + | ``POST /v1/api/approve`` (body ``ws_id``) | ``POST /v1/api/workstreams/{ws_id}/approve`` | + | ``POST /v1/api/cancel`` (body ``ws_id``) | ``POST /v1/api/workstreams/{ws_id}/cancel`` | + | ``POST /v1/api/workstreams/close`` (body) | ``POST /v1/api/workstreams/{ws_id}/close`` | + + Calls to the old URLs return **404** on 1.5.0+. Bodies on the new + URLs no longer carry ``ws_id`` (the path provides it); the + ``SendRequest`` / ``ApproveRequest`` / ``CancelRequest`` Pydantic + schemas drop the field, and ``CloseWorkstreamRequest`` is removed + outright (its only field was ``ws_id``). + + ``/v1/api/plan`` and ``/v1/api/command`` are unaffected and remain + body-keyed in this release. The bundled web UI, channel adapters, + Python SDK, TypeScript SDK, and console routing-proxy SDK ship the + new URLs automatically; pinning to ≥ 1.5.0 is enough. + + The console routing proxy's ``/v1/api/route/...`` family is updated + alongside: ``/v1/api/route/workstreams/{ws_id}/`` replaces the + pre-1.5 ``/v1/api/route/{send,approve,cancel,workstreams/close}`` + mounts. ``DELETE`` is now passed through (``client.request(method, + ...)`` instead of ``client.post(...)``) so the new dequeue route + works through the proxy. Audit attribution for ``DELETE`` on + ``/send`` is logged as ``route.workstream.dequeue`` rather than + ``route.workstream.send``. + + Auth scope wiring (``WRITE_PATHS`` / ``APPROVE_PATHS`` literals plus + the path-keyed verb match in ``required_scope``) updated to grant + ``write`` for path-keyed ``send/cancel/close``, ``approve`` for + path-keyed ``approve``, and ``write`` for ``DELETE`` on + path-keyed ``/send``. The ``/node/*`` proxy branch mirrors all four. + ### Changed - **Dashboard row shape: ``id`` → ``ws_id``.** The diff --git a/docs/api-reference.md b/docs/api-reference.md index ac2eef78..6e26a7e6 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -229,12 +229,12 @@ below. --- -### `GET /v1/api/events?ws_id=` +### `GET /v1/api/workstreams/{ws_id}/events` Opens a Server-Sent Events stream scoped to a single workstream. The connection remains open indefinitely; the server pushes events as they occur. -**Query parameters:** +**Path parameters:** | Parameter | Type | Required | Description | |-----------|--------|----------|----------------------------| @@ -346,7 +346,7 @@ action required). ``` **`approve_request`** -- one or more tool calls that require user approval. The -client must respond via `POST /v1/api/approve`. +client must respond via `POST /v1/api/workstreams/{ws_id}/approve`. ```json { @@ -450,7 +450,7 @@ after `/clear` or `/new` commands). ``` **`cancelled`** -- a cancel request was acknowledged (via the Stop button or -`POST /v1/api/cancel`). This signals that cancellation is in progress, not +`POST /v1/api/workstreams/{ws_id}/cancel`). This signals that cancellation is in progress, not that it is complete. The worker thread may still be finishing — wait for `stream_end` before transitioning to a ready state. The client should clear any in-progress assistant rendering but not re-enable the send button until @@ -558,7 +558,7 @@ Possible `state` values: and copies each event to every client queue. If a client queue is full, the event is silently dropped for that client. -**Keepalive:** Same as `/v1/api/events` -- an SSE comment every 5 seconds. +**Keepalive:** Same as `/v1/api/workstreams/{ws_id}/events` -- an SSE comment every 5 seconds. --- @@ -654,21 +654,26 @@ Each skill summary: --- -### `POST /v1/api/send` +### `POST /v1/api/workstreams/{ws_id}/send` Sends a user message to a workstream. Spawns a daemon worker thread that calls `session.send()` and streams results back via the SSE channel. +**Path parameters:** + +| Parameter | Type | Required | Description | +|-----------|--------|----------|----------------------| +| `ws_id` | string | yes | Target workstream ID | + **Request body:** ```json -{"message": "Explain how the server works", "ws_id": "abc123"} +{"message": "Explain how the server works"} ``` | Field | Type | Required | Description | |-----------|--------|----------|-------------------------| | `message` | string | yes | The user's message text | -| `ws_id` | string | yes | Target workstream ID | **Response (success):** @@ -692,15 +697,21 @@ from a previous request. Also pushes a `busy_error` event to the SSE stream. --- -### `POST /v1/api/approve` +### `POST /v1/api/workstreams/{ws_id}/approve` Responds to a tool approval request. The SSE stream must have previously sent an `approve_request` event for the given workstream. +**Path parameters:** + +| Parameter | Type | Required | Description | +|-----------|--------|----------|----------------------| +| `ws_id` | string | yes | Target workstream ID | + **Request body:** ```json -{"approved": true, "feedback": null, "always": false, "ws_id": "abc123"} +{"approved": true, "feedback": null, "always": false} ``` | Field | Type | Required | Description | @@ -708,7 +719,6 @@ an `approve_request` event for the given workstream. | `approved` | bool | yes | `true` to approve, `false` to deny | | `feedback` | string/null | no | Optional feedback text (sent as denial reason) | | `always` | bool | no | If `true` and `approved`, enables auto-approve | -| `ws_id` | string | yes | Target workstream ID | When `always` is `true` and `approved` is `true`, the workstream's WebUI instance sets `auto_approve = True`, causing all subsequent tool calls to be @@ -789,7 +799,7 @@ containing the resumed session's messages. --- -### `POST /v1/api/cancel` +### `POST /v1/api/workstreams/{ws_id}/cancel` Cancels the active generation in a workstream. Sets a cooperative cancellation flag that is checked at multiple points in the generation loop (per streaming @@ -812,15 +822,20 @@ for the orphaned thread. Use force cancel when cooperative cancel has not resolved within a few seconds — the web UI offers this as a "Force Stop" button automatically. +**Path parameters:** + +| Parameter | Type | Required | Description | +|-----------|--------|----------|----------------------| +| `ws_id` | string | yes | Target workstream ID | + **Request body:** ```json -{"ws_id": "abc123", "force": false} +{"force": false} ``` | Field | Type | Required | Description | |--------|--------|----------|----------------------| -| `ws_id`| string | yes | Target workstream ID | | `force`| bool | no | Abandon stuck worker immediately (default: `false`) | **Response:** @@ -893,20 +908,18 @@ Status code: `400` --- -### `POST /v1/api/workstreams/close` +### `POST /v1/api/workstreams/{ws_id}/close` Closes and removes a workstream. The last remaining workstream cannot be closed. -**Request body:** +**Path parameters:** -```json -{"ws_id": "abc123"} -``` +| Parameter | Type | Required | Description | +|-----------|--------|----------|------------------------| +| `ws_id` | string | yes | Workstream ID to close | -| Field | Type | Required | Description | -|---------|--------|----------|---------------------------| -| `ws_id` | string | yes | Workstream ID to close | +The body is empty (or `{}`). **Response (success):** @@ -937,7 +950,7 @@ turn on this workstream. The attachment moves through three states: `pending → reserved → consumed`. Reservation tokens are threaded through -`POST /v1/api/send` so a queued multimodal turn cannot lose its file to +`POST /v1/api/workstreams/{ws_id}/send` so a queued multimodal turn cannot lose its file to an overlapping send. Ownership failures are masked as `404` so non-owners cannot enumerate @@ -1952,7 +1965,7 @@ Status code: `200` with an empty body. | Malformed or unparseable JSON body | Treated as an empty dict `{}`; missing fields use defaults | | Unknown `ws_id` | `404` with `{"error": "Unknown workstream"}` | | Unknown path (GET or POST) | `404` with plain-text body `Not found` | -| Empty `message` on `/v1/api/send` | `400` with `{"error": "Empty message"}` | +| Empty `message` on `/v1/api/workstreams/{ws_id}/send` | `400` with `{"error": "Empty message"}` | | Empty `command` on `/v1/api/command` | `400` with `{"error": "Empty command"}` | | Rate limit exceeded | `429` with `Retry-After` header (see below) | @@ -1995,7 +2008,7 @@ reconnection: On reconnect, the server replays the full conversation history via the `history` event, so the client can rebuild its UI state without data loss. The same reconnection strategy applies to both the per-workstream SSE stream -(`/v1/api/events`) and the global state stream (`/v1/api/events/global`). +(`/v1/api/workstreams/{ws_id}/events`) and the global state stream (`/v1/api/events/global`). --- @@ -2105,7 +2118,7 @@ turnstone_workstreams_active_total 1 # TYPE turnstone_http_requests_total counter turnstone_http_requests_total{method="GET",endpoint="/health",status_code="200"} 42 turnstone_http_requests_total{method="GET",endpoint="/metrics",status_code="200"} 7 -turnstone_http_requests_total{method="POST",endpoint="/v1/api/send",status_code="200"} 18 +turnstone_http_requests_total{method="POST",endpoint="/v1/api/workstreams/{ws_id}/send",status_code="200"} 18 # HELP turnstone_tokens_total Total tokens consumed # TYPE turnstone_tokens_total counter turnstone_tokens_total{type="prompt"} 84320 diff --git a/docs/architecture.md b/docs/architecture.md index db0209df..dd1a8840 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -384,11 +384,11 @@ non-idle background workstreams above the input prompt. (`Ctrl+\`, `Ctrl+Shift+\`). Max 6 panes; no duplicate workstreams across panes. Layout persisted to `localStorage`. - **Per-pane SSE**: `Pane.connectSSE(wsId)` opens - `/v1/api/events?ws_id=` for each pane's event stream independently. + `/v1/api/workstreams/{ws_id}/events` for each pane's event stream independently. - **Global SSE**: `connectGlobalSSE()` opens `/v1/api/events/global` which receives `ws_state` broadcasts from all workstreams, used to update tab indicators and pane headers without switching. -- **New tab / close**: POST `/v1/api/workstreams/new`, POST `/v1/api/workstreams/close`. +- **New tab / close**: POST `/v1/api/workstreams/new`, POST `/v1/api/workstreams/{ws_id}/close`. ### Thread Safety @@ -1198,12 +1198,12 @@ stderr so it does not interfere with readline. Tool execution may use a Starlette ASGI app (served by uvicorn) | +-- Async request handlers (all under /v1/ prefix) - | POST /v1/api/send -> starts worker thread per workstream - | POST /v1/api/approve -> unblocks WebUI._approval_event - | POST /v1/api/plan -> unblocks WebUI._plan_event - | POST /v1/api/workstreams/new -> creates workstream + worker - | GET /v1/api/events -> SSE via EventSourceResponse (per workstream) - | GET /v1/api/events/global -> SSE via EventSourceResponse (fan-out) + | POST /v1/api/workstreams/{ws_id}/send -> starts worker thread per workstream + | POST /v1/api/workstreams/{ws_id}/approve -> unblocks WebUI._approval_event + | POST /v1/api/plan -> unblocks WebUI._plan_event + | POST /v1/api/workstreams/new -> creates workstream + worker + | GET /v1/api/workstreams/{ws_id}/events -> SSE via EventSourceResponse (per workstream) + | GET /v1/api/events/global -> SSE via EventSourceResponse (fan-out) | +-- ASGI middleware stack | MetricsMiddleware -> CORSMiddleware -> AuthMiddleware -> RateLimitMiddleware @@ -1277,10 +1277,10 @@ Monitoring (2 daemon threads) Control + Proxy (async Starlette) | SSE manager | | GET /node/{node_id}/ | | asyncio loop | | → httpx.AsyncClient | | 1 task per node | | proxy to server_url | -| /events/global | | GET /node/{id}/v1/api/events | -| snapshot+deltas | | → SSE stream proxy | -+------------------+ | POST /node/{id}/v1/api/send | - | → forwarded to server | +| /events/global | | GET /node/{id}/v1/api/workstreams/{ws_id}/events | +| snapshot+deltas | | → SSE stream proxy | ++------------------+ | POST /node/{id}/v1/api/workstreams/{ws_id}/send | + | → forwarded to server | +----------------------------+ ``` diff --git a/docs/console.md b/docs/console.md index 96f4c3dd..c54e5d64 100644 --- a/docs/console.md +++ b/docs/console.md @@ -334,7 +334,7 @@ The console reverse-proxies each node's server UI at `/node/{node_id}/`. This al ### URL Rewriting -The server UI uses root-relative URLs (`/v1/api/send`, `/static/app.js`, `/shared/base.css`, etc.). Since `` tags cannot rewrite root-relative URLs, the console uses a JS shim approach: +The server UI uses root-relative URLs (`/v1/api/workstreams/{ws_id}/send`, `/static/app.js`, `/shared/base.css`, etc.). Since `` tags cannot rewrite root-relative URLs, the console uses a JS shim approach: 1. **HTML rewriting** — when serving `index.html`, replaces `href=` and `src=` references to both `/static/` and `/shared/` with the proxy prefix (`/node/{node_id}/static/` and `/node/{node_id}/shared/` respectively). @@ -344,7 +344,7 @@ The server UI uses root-relative URLs (`/v1/api/send`, `/static/app.js`, `/share ### SSE Proxy -SSE streams (`/v1/api/events`, `/v1/api/events/global`) are proxied as raw byte passthrough — the console opens an `httpx.AsyncClient.stream()` to the upstream server (with `read=None` and `pool=None` timeouts since SSE connections are long-lived) and relays every byte via `StreamingResponse`. This preserves server-side ping comments, event framing, and keepalives verbatim without parsing or re-encoding. +SSE streams (`/v1/api/workstreams/{ws_id}/events`, `/v1/api/events/global`) are proxied as raw byte passthrough — the console opens an `httpx.AsyncClient.stream()` to the upstream server (with `read=None` and `pool=None` timeouts since SSE connections are long-lived) and relays every byte via `StreamingResponse`. This preserves server-side ping comments, event framing, and keepalives verbatim without parsing or re-encoding. ### Authentication diff --git a/docs/diagrams/11-console-data-flow.puml b/docs/diagrams/11-console-data-flow.puml index f44fe918..904bce96 100644 --- a/docs/diagrams/11-console-data-flow.puml +++ b/docs/diagrams/11-console-data-flow.puml @@ -170,15 +170,15 @@ Server --> Browser : Shimmed app.js deactivate Server note right of Browser - All fetch("/v1/api/send") calls in the - server UI now become fetch("/node/nodeA/v1/api/send"), + All fetch("/v1/api/workstreams/{ws_id}/send") calls in the + server UI now become fetch("/node/nodeA/v1/api/workstreams/{ws_id}/send"), routed through the console proxy. end note -Browser -> Server : GET /node/nodeA/v1/api/events?ws_id=ws789 +Browser -> Server : GET /node/nodeA/v1/api/workstreams/ws789/events activate Server #FFF9C4 -Server -> NodeA : GET http://10.0.1.1:8080/v1/api/events?ws_id=ws789\n(SSE stream via httpx.AsyncClient timeout=None) +Server -> NodeA : GET http://10.0.1.1:8080/v1/api/workstreams/ws789/events\n(SSE stream via httpx.AsyncClient timeout=None) activate NodeA loop SSE streaming @@ -189,10 +189,10 @@ end deactivate NodeA deactivate Server -Browser -> Server : POST /node/nodeA/v1/api/send\n{message:"hello", ws_id:"ws789"} +Browser -> Server : POST /node/nodeA/v1/api/workstreams/ws789/send\n{message:"hello"} activate Server #FFF9C4 -Server -> NodeA : POST http://10.0.1.1:8080/v1/api/send\n(body forwarded) +Server -> NodeA : POST http://10.0.1.1:8080/v1/api/workstreams/ws789/send\n(body forwarded) activate NodeA NodeA --> Server : {status:"ok"} deactivate NodeA diff --git a/docs/diagrams/16-channel-architecture.puml b/docs/diagrams/16-channel-architecture.puml index eb4280b1..743e5e74 100644 --- a/docs/diagrams/16-channel-architecture.puml +++ b/docs/diagrams/16-channel-architecture.puml @@ -95,10 +95,10 @@ class "ChannelRouter" as Router <> { ' -- Server -- class "turnstone-server" as Server <> { - POST /v1/api/send - POST /v1/api/approve + POST /v1/api/workstreams/{ws_id}/send + POST /v1/api/workstreams/{ws_id}/approve POST /v1/api/workstreams/new - GET /v1/api/events?ws_id= + GET /v1/api/workstreams/{ws_id}/events -- LLM execution + tool use SSE event stream @@ -148,15 +148,15 @@ Bot --> Router : on_message\non_interaction Router --> CU : resolve identity Router --> CR : resolve / register route -Router --> Server : POST /v1/api/send\nPOST /v1/api/approve\nPOST /v1/api/workstreams/new -Bot --> Server : GET /v1/api/events?ws_id=\n(SSE via httpx-sse) +Router --> Server : POST /v1/api/workstreams/{ws_id}/send\nPOST /v1/api/workstreams/{ws_id}/approve\nPOST /v1/api/workstreams/new +Bot --> Server : GET /v1/api/workstreams/{ws_id}/events\n(SSE via httpx-sse) Server --> Bot : SSE event stream Bot --> Discord : reply / embed\nbutton callback Slack --> SlackBot : socket-mode\nevents SlackBot --> Router : on_message / on_action -SlackBot --> Server : POST /v1/api/send\nGET /v1/api/events?ws_id= +SlackBot --> Server : POST /v1/api/workstreams/{ws_id}/send\nGET /v1/api/workstreams/{ws_id}/events SlackBot --> Slack : post / update\nBlock Kit button callbacks Teams .[hidden]. Slack @@ -179,7 +179,7 @@ note right of Bot (or creates new workstream) 4. ChannelRouter resolves platform user -> user_id via channel_users table - 5. Router sends POST /v1/api/send to server + 5. Router sends POST /v1/api/workstreams/{ws_id}/send to server **Workstream Resume (evicted workstreams)** 1. Stale route detected (no active SSE listener) @@ -193,7 +193,7 @@ end note note right of Server **Outbound Flow** 1. Server emits SSE events on - GET /v1/api/events?ws_id= + GET /v1/api/workstreams/{ws_id}/events 2. Bot subscribes via httpx-sse 3. Bot formats and sends to Discord thread end note @@ -204,7 +204,7 @@ note bottom of CR 2. Bot renders Discord buttons (Approve / Deny) 3. User clicks button -> on_interaction() 4. Router builds ApproveMessage - 5. Router sends POST /v1/api/approve to server + 5. Router sends POST /v1/api/workstreams/{ws_id}/approve to server end note note bottom of CU diff --git a/sdk/typescript/openapi-server.json b/sdk/typescript/openapi-server.json index 59be394f..c3859c14 100644 --- a/sdk/typescript/openapi-server.json +++ b/sdk/typescript/openapi-server.json @@ -55,7 +55,7 @@ "tags": [ "Workstreams" ], - "description": "Accepts two content types. Default is `application/json` with a `CreateWorkstreamRequest` body. Alternatively, `multipart/form-data` with one `meta` field (JSON-encoded `CreateWorkstreamRequest` shape) plus zero-or-more `file` parts saves each file as an attachment under the new workstream. When `initial_message` is also set, attachments are reserved onto that turn before the worker thread dispatches; otherwise they remain pending for a follow-up `POST /v1/api/send`.", + "description": "Accepts two content types. Default is `application/json` with a `CreateWorkstreamRequest` body. Alternatively, `multipart/form-data` with one `meta` field (JSON-encoded `CreateWorkstreamRequest` shape) plus zero-or-more `file` parts saves each file as an attachment under the new workstream. When `initial_message` is also set, attachments are reserved onto that turn before the worker thread dispatches; otherwise they remain pending for a follow-up `POST /v1/api/workstreams/{ws_id}/send`.", "requestBody": { "required": true, "content": { @@ -110,23 +110,23 @@ } } }, - "/v1/api/workstreams/close": { + "/v1/api/workstreams/{ws_id}/close": { "post": { "summary": "Close a workstream", - "operationId": "v1_api_workstreams_close_post", + "operationId": "v1_api_workstreams_{ws_id}_close_post", "tags": [ "Workstreams" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloseWorkstreamRequest" - } + "parameters": [ + { + "name": "ws_id", + "in": "path", + "required": true, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", @@ -147,17 +147,37 @@ } } } + }, + "404": { + "description": "Error 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/v1/api/send": { + "/v1/api/workstreams/{ws_id}/send": { "post": { "summary": "Send a user message", - "operationId": "v1_api_send_post", + "operationId": "v1_api_workstreams_{ws_id}_send_post", "tags": [ "Chat" ], + "parameters": [ + { + "name": "ws_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -200,15 +220,68 @@ } } } - } - }, - "/v1/api/approve": { - "post": { - "summary": "Approve or deny a tool call", - "operationId": "v1_api_approve_post", + }, + "delete": { + "summary": "Cancel a queued message", + "operationId": "v1_api_workstreams_{ws_id}_send_delete", "tags": [ "Chat" ], + "description": "Removes a previously-queued message from the workstream's pending queue. Body: ``{\"msg_id\": \"...\"}``. Returns ``status: removed`` when the queue had the entry, ``status: not_found`` otherwise.", + "parameters": [ + { + "name": "ws_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Error 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Error 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v1/api/workstreams/{ws_id}/approve": { + "post": { + "summary": "Approve or deny a tool call", + "operationId": "v1_api_workstreams_{ws_id}_approve_post", + "tags": [ + "Chat" + ], + "parameters": [ + { + "name": "ws_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -335,13 +408,23 @@ } } }, - "/v1/api/cancel": { + "/v1/api/workstreams/{ws_id}/cancel": { "post": { "summary": "Cancel the active generation in a workstream", - "operationId": "v1_api_cancel_post", + "operationId": "v1_api_workstreams_{ws_id}_cancel_post", "tags": [ "Chat" ], + "parameters": [ + { + "name": "ws_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -386,10 +469,10 @@ } } }, - "/v1/api/events": { + "/v1/api/workstreams/{ws_id}/events": { "get": { "summary": "Per-workstream SSE event stream", - "operationId": "v1_api_events_get", + "operationId": "v1_api_workstreams_{ws_id}_events_get", "tags": [ "Streaming" ], @@ -397,12 +480,11 @@ "parameters": [ { "name": "ws_id", - "in": "query", + "in": "path", "required": true, "schema": { "type": "string" - }, - "description": "Workstream identifier" + } } ], "responses": { @@ -1823,11 +1905,6 @@ "title": "Message", "type": "string" }, - "ws_id": { - "description": "Target workstream ID", - "title": "Ws Id", - "type": "string" - }, "attachment_ids": { "anyOf": [ { @@ -1846,8 +1923,7 @@ } }, "required": [ - "message", - "ws_id" + "message" ], "title": "SendRequest", "type": "object" @@ -1939,16 +2015,10 @@ "description": "Auto-approve the tools in this batch going forward", "title": "Always", "type": "boolean" - }, - "ws_id": { - "description": "Target workstream ID", - "title": "Ws Id", - "type": "string" } }, "required": [ - "approved", - "ws_id" + "approved" ], "title": "ApproveRequest", "type": "object" @@ -1995,11 +2065,6 @@ }, "CancelRequest": { "properties": { - "ws_id": { - "description": "Target workstream ID", - "title": "Ws Id", - "type": "string" - }, "force": { "default": false, "description": "Force cancel: abandon the stuck worker thread immediately. Use when cooperative cancel has not resolved within a few seconds.", @@ -2007,9 +2072,6 @@ "type": "boolean" } }, - "required": [ - "ws_id" - ], "title": "CancelRequest", "type": "object" }, @@ -2138,7 +2200,7 @@ "type": "integer" }, "attachment_ids": { - "description": "Ids of attachments saved by this request (multipart variant only). Already reserved onto the initial_message turn when one was provided; otherwise left pending for a follow-up POST /v1/api/send.", + "description": "Ids of attachments saved by this request (multipart variant only). Already reserved onto the initial_message turn when one was provided; otherwise left pending for a follow-up POST /v1/api/workstreams/{ws_id}/send.", "items": { "type": "string" }, @@ -2153,20 +2215,6 @@ "title": "CreateWorkstreamResponse", "type": "object" }, - "CloseWorkstreamRequest": { - "properties": { - "ws_id": { - "description": "Workstream ID to close", - "title": "Ws Id", - "type": "string" - } - }, - "required": [ - "ws_id" - ], - "title": "CloseWorkstreamRequest", - "type": "object" - }, "ListWorkstreamsResponse": { "description": "Response body for ``GET /v1/api/workstreams`` on either kind.\n\nTop-level key is ``workstreams`` regardless of the kind serving\nthe request \u2014 pre-lift coord returned ``{\"coordinators\": [...]}``;\nconvergence lifted both kinds onto the same shape. Coord SDK /\nfrontend consumers branching on ``data.coordinators`` swap to\n``data.workstreams``.", "properties": { diff --git a/tests/test_sdk_sse.py b/tests/test_sdk_sse.py index e83715b3..90c0908d 100644 --- a/tests/test_sdk_sse.py +++ b/tests/test_sdk_sse.py @@ -34,7 +34,7 @@ async def test_stream_sse_yields_json(): async with httpx.AsyncClient(transport=transport, base_url="http://test") as hc: client = _BaseClient(httpx_client=hc) events = [] - async for data in client._stream_sse("/v1/api/events", params={"ws_id": "ws1"}): + async for data in client._stream_sse("/v1/api/workstreams/ws1/events"): events.append(data) assert len(events) == 2 assert events[0]["type"] == "content" diff --git a/tests/test_server_authz.py b/tests/test_server_authz.py index 9ea91862..7e4e9457 100644 --- a/tests/test_server_authz.py +++ b/tests/test_server_authz.py @@ -665,13 +665,12 @@ class TestAuditEventsOnMutations: class TestInteractiveCancelLifted: - """HTTP-level coverage for the post-lift interactive ``/api/cancel`` - handler. The lifted ``make_cancel_handler`` body is shared with - coord but interactive routes through ``make_legacy_body_keyed_adapter`` - (ws_id in body, not path). Pre-lift ``cancel_generation`` was - untested at the HTTP layer; coord exercised the lifted body via - ``test_coordinator_endpoints.py``. This class adds the missing - interactive-side parity.""" + """HTTP-level coverage for the post-lift interactive ``cancel`` + handler at ``POST /v1/api/workstreams/{ws_id}/cancel``. The lifted + ``make_cancel_handler`` body is shared with coord. Pre-lift + ``cancel_generation`` was untested at the HTTP layer; coord + exercised the lifted body via ``test_coordinator_endpoints.py``. + This class adds the missing interactive-side parity.""" def _create_ws(self, client) -> str: resp = client.post(