Skip to content

Commit 6fdc5c6

Browse files
os-zhuangclaude
andauthored
feat(client,spec): ai.agents.* and ai.pendingActions.* — the AI routes the SDK could not reach (#3718) (#3888)
* feat(client,spec): `ai.agents.*` and `ai.pendingActions.*` — the AI routes the SDK could not reach (#3718) #3718 deleted three `client.ai.*` methods whose URLs no route had ever mounted, then expressed the surface that does exist. It expressed ONE builder's worth of it. `service-ai` mounts seven; widening its ledger (objectstack-ai/cloud#903) counted ten routes the SDK cannot reach, nine of which had never been counted. This closes the six with the strongest evidence: objectui already ships product on them, over URLs it hand-builds because there is nothing to call. ai.agents — `/ai/chat` talks to the default agent; these talk to a named one. list() — agents this CALLER may chat with; the route filters by permission (ADR-0049), so empty is a legitimate answer, not an error to retry chat(name, req) — forces `stream: false`, same reason `ai.chat` does: the route streams by default chatStream(name, req) — same route, streaming mode. One route, two methods, mirroring chat/chatStream rather than inventing a third shape ai.pendingActions — the HITL approval queue an embedding app must render. list(options?) — status/conversationId/limit ONLY. The service also accepts objectName; the route never forwards it, so typing it would offer a filter that silently does nothing get(id) approve(id) — approves AND executes. `{status:'failed'}` comes back on HTTP 200: the approval succeeded, the execution did not. Reading only `res.ok` reports a failed write as a success reject(id, reason?) — executes nothing Typed from what the routes RETURN, not from what a client might like them to — the failure #3718 exists to punish. Pending actions are the persisted row, snake_case on the wire because that is what it is; agent rows require `capabilities` because that object is what tells a UI what to render. The capstone's `/api/v1/ai/` prefix exemption says the evidence lives across the repo boundary. It does, and it now reaches these: cloud's ledger drives every `ai.*` method against the tables its builders really return, and since #903 that means all seven. Comment updated there — it still described the one-builder version, under which `buildAgentRoutes()` and `buildPendingActionRoutes()` were invisible. Verification: client 184/184, spec 6836/6836 (24 in protocol.test.ts, extended with the new shapes incl. the negative cases — an agent row without capabilities, a non-enum status filter, `approve` yielding "rejected"). Mutation-checked: pointing `ai.pendingActions.get` off the AI prefix fails the capstone by name, proving the new methods are really in its sweep rather than silently absent. Generated artifacts regenerated: api-surface.json (+20 exports, 0 breaking) and json-schema.manifest.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJX6GnuNix7HisBc92THMN * docs(spec,client): regenerate the protocol reference and document the new ai methods `check:docs` regenerates `content/docs/references` from the schemas and fails when the committed copy drifts. The seven new AI schemas produce new reference entries, so that check went red on the first push — the artifact half of the same commit, not a separate defect. Also updates the hand-written SDK doc, which enumerates the whole `client.ai.*` surface and carried the #3718 history note. Leaving it at ten methods while shipping seventeen would be the exact drift this line of work keeps closing — and the docs-drift check flagged `content/docs/api/client-sdk.mdx` as affected, which on inspection it genuinely was (the other 107 files it lists are package-level fan-out, unrelated to this diff). The added block documents the two things a caller gets wrong by default: an access-filtered agent catalog is legitimately EMPTY for a seat-less user, and `pendingActions.approve` returns `{ status: 'failed' }` on HTTP 200 when the tool fails after approval — reading only `res.ok` calls that a success. check:docs / check:api-surface / check:spec-changes / check:upgrade-guide all pass; 250 generated files in sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJX6GnuNix7HisBc92THMN --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5602211 commit 6fdc5c6

9 files changed

Lines changed: 577 additions & 9 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/client": minor
4+
---
5+
6+
feat(client,spec): `ai.agents.*` and `ai.pendingActions.*` — the AI routes the SDK could not reach (#3718)
7+
8+
#3718 deleted three `client.ai.*` methods whose URLs no route had ever mounted,
9+
then expressed the surface that does exist. It expressed **one** builder's worth
10+
of it. `service-ai` mounts seven; the audit that widened its ledger
11+
(objectstack-ai/cloud#903) counted **ten** routes the SDK cannot reach, nine of
12+
which had simply never been counted.
13+
14+
This closes the six with the strongest evidence: `objectui` already ships
15+
product on them, over URLs it builds by hand because there was nothing to call.
16+
17+
**`ai.agents`**`/ai/chat` talks to the environment's default agent; these
18+
talk to one you name.
19+
20+
- `agents.list()` — the agents this CALLER may chat with. The route filters by
21+
the caller's permissions (ADR-0049), so an empty list is a legitimate answer
22+
for a seat-less user, not an error to retry.
23+
- `agents.chat(name, request)` / `agents.chatStream(name, request)` — one route,
24+
two methods, mirroring `ai.chat` / `ai.chatStream` rather than inventing a
25+
third shape for the same endpoint. `chat` forces `stream: false` for the same
26+
reason `ai.chat` does: the route streams by default, so leaving the flag to
27+
the caller means the JSON path is the one you have to remember.
28+
29+
**`ai.pendingActions`** — the human-in-the-loop approval queue. When a tool call
30+
needs a human decision the turn parks an action instead of executing it, and an
31+
app embedding the chat has to render and resolve that queue.
32+
33+
- `pendingActions.list(options?)``status`, `conversationId` and `limit` only.
34+
`AIService.listPendingActions` also accepts `objectName`, but the route never
35+
forwards it; typing it here would offer a filter that silently does nothing.
36+
- `pendingActions.get(id)`
37+
- `pendingActions.approve(id)` — approves **and executes**. Check the returned
38+
`status`: a tool that fails after approval comes back
39+
`{ status: 'failed', error }` with HTTP 200, because the approval succeeded
40+
even though the execution did not. Code that reads only `res.ok` reports a
41+
failed write as a success.
42+
- `pendingActions.reject(id, reason?)` — executes nothing.
43+
44+
Reads and decisions are separately permissioned server-side (`ai:read` vs
45+
`ai:approve`), so a caller that can list the queue may still be refused on
46+
approve. Handle the 403; one does not imply the other.
47+
48+
**Typed from what the routes return**, not from what a client might like them
49+
to — the failure #3718 exists to punish. The pending-action shape is the
50+
persisted row, `snake_case` on the wire because that is what it is. Agent rows
51+
require `capabilities`, because that object is what tells a UI which
52+
affordances to render.
53+
54+
The capstone (#3642) exempts `/api/v1/ai/` by prefix and says the evidence lives
55+
on the other side of the repo boundary. It does: cloud's ledger drives every
56+
`ai.*` method against the tables its builders really return — and since #903
57+
that means all seven builders, which is what makes these six routes checkable
58+
at all. Their routes come from `buildAgentRoutes()` and
59+
`buildPendingActionRoutes()`, neither of which the ledger could see when the
60+
exemption was written.

content/docs/api/client-sdk.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,28 @@ await client.ai.conversations.addMessage(conv.id, { role: 'user', content: 'hi'
337337
await client.ai.conversations.update(conv.id, { title: 'Renamed' });
338338
await client.ai.conversations.delete(conv.id);
339339

340+
// Named agents — `ai.chat` talks to the environment default; these talk to one
341+
// you name. The catalog is access-filtered server-side (ADR-0049), so an empty
342+
// list is a legitimate answer for a seat-less user, not an error to retry.
343+
const agents = await client.ai.agents.list();
344+
await client.ai.agents.chat('build', { messages, context: { appId: 'crm' } });
345+
for await (const frame of await client.ai.agents.chatStream('build', { messages })) {
346+
if (frame.type === 'text-delta') process.stdout.write(frame.delta as string);
347+
}
348+
349+
// Pending actions — the human-in-the-loop queue. A tool call needing a human
350+
// decision parks here instead of executing.
351+
await client.ai.pendingActions.list({ status: 'pending', limit: 20 });
352+
await client.ai.pendingActions.get('pa_1');
353+
const outcome = await client.ai.pendingActions.approve('pa_1');
354+
// CHECK `outcome.status`: approve runs the tool, and a tool that fails comes
355+
// back { status: 'failed', error } on HTTP 200 — the approval succeeded, the
356+
// execution did not. Code that reads only `res.ok` calls that a success.
357+
if (outcome.status === 'failed') console.error(outcome.error);
358+
await client.ai.pendingActions.reject('pa_1', 'wrong account');
359+
// Listing takes `ai:read`, deciding takes `ai:approve` — a caller that can see
360+
// the queue may still be refused on approve. Handle the 403.
361+
340362
// In a React chat UI prefer `useChat()` (`@ai-sdk/react`) over `ai.chatStream`:
341363
// it speaks the same protocol and owns message state. These methods are for
342364
// everything that is not a component — server code, jobs, CLIs, tests.

0 commit comments

Comments
 (0)