docs: mark 1.5.0 legacy URL surface removal

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.
This commit is contained in:
Patrick Buckley
2026-04-26 20:57:35 -07:00
committed by Patrick Buckley
parent 1358121d52
commit ad0e7ce6eb
9 changed files with 232 additions and 126 deletions
+46
View File
@@ -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}/<verb>`` 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
+39 -26
View File
@@ -229,12 +229,12 @@ below.
---
### `GET /v1/api/events?ws_id=<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
+12 -12
View File
@@ -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=<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 |
+----------------------------+
```
+2 -2
View File
@@ -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 `<base>` 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 `<base>` 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
+6 -6
View File
@@ -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
+9 -9
View File
@@ -95,10 +95,10 @@ class "ChannelRouter" as Router <<service>> {
' -- Server --
class "turnstone-server" as Server <<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
+111 -63
View File
@@ -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": {
+1 -1
View File
@@ -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"
+6 -7
View File
@@ -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(