Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/19543-list-doors-3-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
'@objectstack/spec': minor
'@objectstack/client': minor
'@objectstack/runtime': minor
---

feat!: retire the `GET /api/v1/automation` flow list in favour of `GET /api/v1/meta/flow`; `ListAiConversationsResponse` declares `hasMore` (#19543)

**BREAKING** — two sibling list doors that declared paging nobody honoured.

**The flow list is retired, with no alias and no transition window** (maintainer
ruling: 「退役,统一走 /meta/flow」). Its contract described a capability no build
ever delivered: the request declared `status`, `type`, `limit` (default 50) and
`cursor`, and the route read none of them; the response declared `FlowSummary`
rows with `total`, `nextCursor` and `hasMore`, and the route answered bare flow
names beside a literal `hasMore: false`. Measured before removal on the main branch
of this repository and cloud, and on objectui at its pinned commit and at main:
zero callers of the route or of `client.automation.list` outside their own tests,
while the Console flow-runs page and the Setup packaged-automation page already
read `GET /api/v1/meta/flow`.

FROM → TO, per surface:

- `GET /api/v1/automation` (and its environment-scoped twin) → no longer mounted
for `GET`. `POST /api/v1/automation` (create a flow) still lives at that path, so
on the default Hono host a `GET` there answers the host's standard method
mismatch — `405 METHOD_NOT_ALLOWED` with `Allow: POST` — the same answer any
POST-only path gets. A transport that forwards every automation path to the
dispatcher (the `@objectstack/hono` catch-all) is told the domain does not handle
it and answers its own not-found `404`. Fix: read `GET /api/v1/meta/flow`;
flows are metadata (ADR-0106), and it answers full definitions, so map each item
to its `name` if you only need names. Per-flow runtime enablement and trigger
binding is `GET /api/v1/automation/_status`, unchanged.
- `client.automation.list` (`@objectstack/client`) → removed; calling it is a
compile error. Fix: `client.meta.getItems('flow')`, or
`client.automation.getRuntimeStatus()` for the enabled/bound state.
- `ListFlowsRequestSchema`, `ListFlowsResponseSchema`, `FlowSummarySchema` and the
types `ListFlowsRequest`, `ListFlowsRequestParsed`, `ListFlowsResponse`,
`ListFlowsResponseParsed`, `FlowSummary` (`@objectstack/spec/api`) → removed,
no replacement export (TS2305 on import). Fix: delete the import; the flow
definition type is `Flow` from `@objectstack/spec/automation`.
- `AutomationApiContracts.listFlows` → removed; the map has eight entries, none of
them a `GET` at the bare path. Every other automation route is unchanged.

**`ListAiConversationsResponseSchema` gains a required `hasMore`** (the spec half
of the same card; the server half is objectstack-ai/cloud#2426). The list is
declared **newest first** and pages by keyset: `cursor` is the `id` of the last
conversation the caller already holds, and `hasMore` says whether another page
follows. `hasMore` is required rather than optional so a server that does not
compute it is off-contract instead of silently spec-valid; no `nextCursor` is
declared, because the next cursor is the last conversation's id, already on the
page. Who notices: code that constructs a `ListAiConversationsResponse` must now
set `hasMore`, and a response parsed with the schema is refused without it.
`client.ai.conversations.list()` is unchanged — it still resolves to the
conversation array.

Breaking ships as `minor` per the launch-window convention
(`scripts/check-changeset-no-major.mjs`).

**Clause-②: yes (narrowing)** — the conversation list's response surface gains a
declared `hasMore`; a route, an SDK method, three published schemas with their five
types and a contract entry are removed, and a conversation-list response without
`hasMore` is now refused.

<!-- adr-0087: registered automation-flow-list-route-retired -->
4 changes: 3 additions & 1 deletion content/docs/api/client-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ await client.ai.models(); // plan-filtered picker list (ADR-0028)

// Conversations — all six routes, scoped to the authenticated user server-side.
const conv = await client.ai.conversations.create({ title: 'Q3 pipeline' });
await client.ai.conversations.list({ limit: 20 });
const page = await client.ai.conversations.list({ limit: 20 }); // newest first
// The next page: the cursor is the id of the last conversation you hold.
await client.ai.conversations.list({ limit: 20, cursor: page.at(-1)?.id });
await client.ai.conversations.get(conv.id);
await client.ai.conversations.addMessage(conv.id, { role: 'user', content: 'hi' });
await client.ai.conversations.update(conv.id, { title: 'Renamed' });
Expand Down
6 changes: 4 additions & 2 deletions content/docs/api/plugin-endpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ Approve/reject were never workflow routes (ADR-0019): approval is a flow node, a
|:-------|:---------|:------------|
| POST | `/automation/:name/trigger` | Trigger an automation flow by name (legacy alias: `/automation/trigger/:name`). Failures answer real status codes, not a `200` wrapping an inner failure: **404** unknown flow, **409** `FLOW_DISABLED`, **422** `FLOW_NO_START_NODE`, **422** `FLOW_INPUT_SCHEMA_INVALID`, **400** `FLOW_FAILED` for a run that ran and was rejected — see [Run a flow via API](/docs/automation/flows#run-a-flow-via-api) |

The automation dispatcher also exposes flow CRUD (`GET`/`POST /automation`, `GET`/`PUT`/`DELETE /automation/:name`) and run observability/resume routes — see [Durable pause & resume](/docs/automation/flows#durable-pause--resume-adr-0019).
The automation dispatcher also exposes flow CRUD (`POST /automation`, `GET`/`PUT`/`DELETE /automation/:name`) and run observability/resume routes — see [Durable pause & resume](/docs/automation/flows#durable-pause--resume-adr-0019).

To **list** flows, read the metadata plane: `GET /meta/flow` (`client.meta.getItems('flow')`) — flows are metadata (ADR-0106). There is no `GET /automation` list route; it was retired, and a `GET` to that path gets the host's method-mismatch answer (`405` with `Allow: POST` on the default server), because only `POST` lives there. Per-flow runtime enablement and trigger binding is `GET /automation/_status` (`client.automation.getRuntimeStatus()`).

### Views (`/ui`) — Plugin Required

Expand Down Expand Up @@ -109,7 +111,7 @@ These are the routes `service-ai` mounts, and the SDK method that reaches each:
| GET | `/ai/models` | `ai.models` | Models this environment offers (ADR-0028) |
| GET | `/ai/status` | — | Active adapter provenance (console diagnostics) |
| GET | `/ai/effective-model` | — | Resolved model ids and their source (console diagnostics) |
| POST / GET | `/ai/conversations` | `ai.conversations.create` / `.list` | Create / list conversations |
| POST / GET | `/ai/conversations` | `ai.conversations.create` / `.list` | Create / list conversations — the list is newest first and pages by keyset: send the `id` of the last conversation you hold as `cursor`, and read `hasMore` to learn whether another page follows |
| GET / PATCH / DELETE | `/ai/conversations/:id` | `ai.conversations.get` / `.update` / `.delete` | Read / update / delete |
| POST | `/ai/conversations/:id/messages` | `ai.conversations.addMessage` | Append a message |

Expand Down
89 changes: 8 additions & 81 deletions content/docs/references/api/automation-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ The wire paths the platform serves: the dispatcher mounts this door at its
(`automation-api-contract-mounts.test.ts`) holds every `path` in
`AutomationApiContracts` to that mount table.

The flow LIST is not on this door. Flows are metadata (ADR-0106), and the
governed read of them is `GET /api/v1/meta/flow` (`client.meta.getItems`);
the former `GET /api/v1/automation` list route, its request/response schemas
and `client.automation.list` are retired (ADR-0087 semantic entry
`automation-flow-list-route-retired`).

**Endpoints**
```
GET /api/v1/automation — List flows
GET /api/v1/automation/:name — Get flow
POST /api/v1/automation — Create flow
PUT /api/v1/automation/:name — Update flow
Expand All @@ -38,8 +43,8 @@ GET /api/v1/automation/:name/runs/:runId — Get single execution run
## TypeScript Usage

```typescript
import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, FlowSummarySchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListFlowsRequestSchema, ListFlowsResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ResumeFailureDetailsSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api';
import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, FlowSummary, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListFlowsRequest, ListFlowsResponse, ListRunsRequest, ListRunsResponse, ResumeFailureDetails, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api';
import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ResumeFailureDetailsSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api';
import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListRunsRequest, ListRunsResponse, ResumeFailureDetails, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api';

// Validate data
const result = AutomationApiErrorCode.parse(data);
Expand Down Expand Up @@ -297,24 +302,6 @@ const result = AutomationApiErrorCode.parse(data);
| **deleted** | `boolean` | ✅ | Whether the flow was deleted |


---

## FlowSummary

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Flow machine name |
| **label** | `string` | ✅ | Flow display label |
| **type** | `string` | ✅ | Flow type |
| **status** | `string` | ✅ | Flow deployment status |
| **version** | `integer` | ✅ | Flow version number |
| **enabled** | `boolean` | ✅ | Whether the flow is enabled for execution |
| **nodeCount** | `integer` | optional | Number of nodes in the flow |
| **lastRunAt** | `string` | optional | Last execution timestamp |


---

## GetFlowRequest
Expand Down Expand Up @@ -459,66 +446,6 @@ const result = AutomationApiErrorCode.parse(data);
| **tenantId** | `string` | optional | Tenant ID for multi-tenant isolation |


---

## ListFlowsRequest

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Filter by flow status |
| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | optional | Filter by flow type |
| **limit** | `integer` | optional (default: `50`) | Maximum number of flows to return |
| **cursor** | `string` | optional | Cursor for pagination |


---

## ListFlowsResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |

### Nested Shape: `ListFlowsResponse.error`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| **message** | `string` | ✅ | Readable error message |
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
| **refusal** | `true` | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose `message` is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose `message` is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — `true` is the only value. |
| **category** | `string` | optional | Error category (e.g. validation, authorization) |
| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error |
| **details** | `any` | optional | Additional error context (e.g. field validation errors) |
| **requestId** | `string` | optional | Request ID for tracking |

### Nested Shape: `ListFlowsResponse.meta`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **timestamp** | `string` | ✅ | |
| **duration** | `integer` | optional | Server-side processing duration in milliseconds |
| **requestId** | `string` | optional | |
| **traceId** | `string` | optional | |

### Nested Shape: `ListFlowsResponse.data`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **flows** | `{ name: string; label: string; type: string; status: string; … }[]` | ✅ | Flow summaries |
| **total** | `integer` | optional | Total matching flows |
| **nextCursor** | `string` | optional | Cursor for the next page |
| **hasMore** | `boolean` | ✅ | Whether more flows are available |


---

## ListRunsRequest
Expand Down
5 changes: 3 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1996,7 +1996,7 @@ Install package response
| :--- | :--- | :--- | :--- |
| **agentId** | `string` | optional | Filter by agent |
| **limit** | `integer` | optional | Maximum conversations to return |
| **cursor** | `string` | optional | Pagination cursor |
| **cursor** | `string` | optional | The `id` of the last conversation on the previous page. The next page starts with the conversation created immediately before it, continuing newest first. Omit it to read the first page. An id that names no conversation of the caller is refused rather than read as the start of the list. |


---
Expand All @@ -2007,7 +2007,8 @@ Install package response

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **conversations** | `{ id: string; title?: string; agentId?: string; userId?: string; … }[]` | ✅ | Matching conversations |
| **conversations** | `{ id: string; title?: string; agentId?: string; userId?: string; … }[]` | ✅ | The caller's conversations, newest first — ordered by creation time, then `id`, both descending |
| **hasMore** | `boolean` | ✅ | Whether at least one more conversation follows this page. When `true`, send the `id` of the last conversation in `conversations` as `cursor` to read the next page. |

### Nested Shape: `ListAiConversationsResponse.conversations[number]`

Expand Down
Loading
Loading