mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
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:
committed by
Patrick Buckley
parent
1358121d52
commit
ad0e7ce6eb
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user