Skip to content

feat(channels): REST adapter for agents/list-conversation-sessions - #559

Merged
chubes4 merged 1 commit into
mainfrom
feat/558-sessions-rest-adapter
Sep 20, 2026
Merged

chubes4 merged 1 commit into
mainfrom
feat/558-sessions-rest-adapter

Conversation

@chubes4

@chubes4 chubes4 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Closes #558

What

Adds a generic sessions REST adapter symmetric to the existing chat adapter:

  • GET /agents-api/v1/sessions dispatches the canonical agents/list-conversation-sessions ability (src/Transcripts/register-agents-conversation-session-abilities.php).
  • apply_filters( 'agents_frontend_chat_rest_session_list_input', $input, $request ) — a host can now scope session lists (by workspace, agent, context, or session owner) once, against the substrate, instead of shipping a client-specific route and filter for this half of the surface.
  • Same input-validation contract as chat: a non-array filter return is rejected as a 400 WP_Error, never a fatal. Input is cached per-request via SplObjectStorage, mirroring agents_frontend_chat_rest_input.
  • The permission callback defers entirely to the canonical agents_conversation_sessions_permission() decision (current-user capability or resolved session owner). A transport-level agents_frontend_chat_rest_session_list_permission filter can only narrow that decision ($allowed && (bool) apply_filters(...)), never widen it — the route adapts authorization, it does not expand it.
  • REST args are derived from the ability's own agents_conversation_sessions_list_input_schema(), so the route's argument contract cannot drift from the ability it adapts.
  • Wired into agents-api.php bootstrap right after the existing chat REST route.

Why this asymmetry mattered

agents/list-conversation-sessions already existed as an ability, but the only registered REST routes were /chat and the JSON-RPC chat route. A client that wanted a session list had to ship its own route and its own filter — Automattic/frontend-agent-chat does exactly that with frontend_agent_chat_session_list_input, and Automattic/intelligence depends on that client-specific filter today for wiki-brain session scoping (Automattic/intelligence#1066). That breaks the moment a different client is used: a consumer that needs both chat and session scoping ends up depending on a client for one and the substrate for the other. This closes that gap so the dependency points one way, at the substrate.

Filter name decision

Kept agents_frontend_chat_rest_session_list_input — symmetric with agents_frontend_chat_rest_input (same agents_frontend_chat_rest_* prefix family as the existing REST adapter's permission filter) rather than inventing a new prefix. Same reasoning for agents_frontend_chat_rest_session_list_permission, added alongside it as the narrowing-only transport permission seam, matching agents_frontend_chat_rest_permission on the chat route.

Design notes

  • The ability's list input schema takes a nested workspace object (workspace_type + workspace_id), but also has a flat-params fallback in agents_conversation_sessions_workspace(). The REST route exposes flat workspace_type / workspace_id query args (simpler for GET) and assembles the nested workspace object internally — filter authors can still override with a full workspace object since the ability checks isset( $input['workspace'] ) first.
  • session_owner is exposed as a REST arg mirroring the ability's own owner schema. It is safe to expose because the ability binds any asserted owner to the authenticated principal's own owner and fails closed on mismatch (agents_conversation_session_owner_from_input()) — the route does not widen what a caller can address.
  • Reused the existing agents_frontend_chat_rest_schema_property() helper from the chat route file (same AgentsAPI\AI\Channels namespace) rather than duplicating it.
  • Reused agents_conversation_sessions_int_value() from the ability's own file for limit/offset coercion instead of a raw (int) cast on a mixed REST param (PHPStan correctly flagged the raw cast; this is also the same coercion the ability applies internally).

Validation

Ran the repo's actual CI commands (.github/workflows/ci.yml runs composer phpstan and composer smoke):

$ composer validate --strict
./composer.json is valid

$ composer phpstan
Note: Using configuration file phpstan.neon.dist.
 [OK] No errors

$ composer test   # (@smoke)
...
frontend-chat-session-list-rest-smoke
  PASS sessions REST route registers
  PASS sessions REST route uses GET
  PASS sessions REST args expose workspace_id
  PASS sessions REST args derive session_owner from the ability input schema
  PASS permission denies without read capability or owner
  PASS permission denial carries forbidden code
  PASS permission denial carries REST status
  PASS permission allows caller with read capability
  PASS transport filter can narrow the canonical permission decision
  PASS transport filter cannot widen the canonical permission decision
  PASS dispatch returns REST response
  PASS dispatch returns ability session list
  PASS dispatch sanitizes agent slug
  PASS dispatch forwards context
  PASS dispatch coerces limit to int
  PASS dispatch coerces offset to int
  PASS dispatch assembles the canonical workspace object
  PASS dispatch preserves the workspace id
  PASS dispatch forwards the caller-supplied session owner
  PASS dispatch reports a missing ability as an error
  PASS missing ability error carries the expected code
  PASS input filter can reshape canonical input
  PASS dispatch rejects a non-array filter return
  PASS invalid input carries the expected error code
  PASS invalid input error carries a 400 status

All 25 frontend conversation session list REST adapter assertions passed.
...
agents-api-no-product-imports-smoke
  ...
All 7 Agents API no-product-imports assertions passed.

Exit code 0 for the full composer test run (every one of the ~140 smoke files in the smoke script, including the new one). No @phpstan-ignore comments, no baseline entries, no type casts added to silence anything — the one PHPStan finding during development (cast.int on (int) $mixed) was fixed by reusing the ability's own coercion helper, not suppressed.


AI-generated by Extra Chill Bot (Claude Sonnet) via Kimaki minion; reviewed by nobody yet.

Adds a generic sessions REST route symmetric to the chat adapter, so a
client that needs to scope session lists no longer has to depend on a
specific chat client's route and filter for this half of the surface.

- GET /agents-api/v1/sessions dispatches the canonical
  agents/list-conversation-sessions ability.
- apply_filters( 'agents_frontend_chat_rest_session_list_input', $input,
  $request ) lets hosts scope by workspace, agent, context, or session
  owner once, against the substrate. Named for symmetry with
  agents_frontend_chat_rest_input.
- A non-array filter return is rejected as a 400 WP_Error, never a fatal,
  matching the chat adapter's contract. Input is cached per-request via
  SplObjectStorage.
- The permission callback defers entirely to the canonical
  agents_conversation_sessions_permission() decision; the transport-level
  agents_frontend_chat_rest_session_list_permission filter can only narrow
  that decision (AND), never widen it, so the route adapts authorization
  instead of expanding it.
- REST args are derived from the ability's own list input schema so the
  route's contract cannot drift from the ability it adapts.
- Wired into agents-api.php bootstrap alongside the existing chat REST
  route; new smoke test added to composer.json's smoke script.

Closes #558
@chubes4
chubes4 merged commit 4597dd4 into main Sep 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(channels): REST adapter for agents/list-conversation-sessions with an input filter

1 participant