Skip to content

feat(agent-platform-backend): proxy kagent session detail and tasks - #2002

Merged
marians merged 5 commits into
mainfrom
agent-platform-session-detail-proxy
Jul 29, 2026
Merged

feat(agent-platform-backend): proxy kagent session detail and tasks#2002
marians merged 5 commits into
mainfrom
agent-platform-session-detail-proxy

Conversation

@marians

@marians marians commented Jul 29, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds the transport the upcoming Agent Platform session detail page needs. Two new proxy routes, both requiring ?installation= and a forwarded user token, both passing kagent's JSON through verbatim like the existing ones:

Route Purpose
GET /kagent/sessions/:id One session plus its stored events
GET /kagent/sessions/:id/tasks The session's A2A tasks — conversation, state, token usage

Nothing calls them yet; the frontend lands in follow-up PRs.

What is the effect of this change to users?

None yet — no UI consumes these routes in this PR.

Any background context you can provide?

Three decisions worth reviewing:

1. The conversation comes from …/tasks, not from …/sessions/:id's events. This is what kagent's own UI renders from (ui/src/components/chat/ChatInterface.tsxextractMessagesFromTasks), and the two payloads carry genuinely different things:

events[] Task[]
Message content data is a JSON string holding an A2A message (doubly encoded) history already structured
Session state status.state
Token usage per-message {adk,kagent}_usage_metadata
Per-item timestamp created_at only one per task

So the frontend will fetch both: tasks for the timeline and state, events only to recover per-message timestamps — A2A messages carry none of their own. Hence two routes rather than one.

2. No A2A-Version header is sent. kagent's NegotiateA2AWireVersion treats a missing header as the legacy v0 wire on both v0.9.9 (our pin) and v0.10, which is the shape kagent's UI consumes and therefore the best-tested one. There's a test asserting the header stays absent so it isn't added casually; opting into the v1 wire should be a deliberate migration.

3. Session ids stay opaque. Real responses mix 64-character hex strings and UUIDs, so nothing validates a format — only that a path segment is present. Ids are encodeURIComponent-ed before interpolation, with a test that a / or ? in an id can't retarget the request.

One consequence to be aware of: a session belonging to another user answers 404 exactly as a deleted one does, because kagent scopes the lookup by the token's user id. Both are expected outcomes for a stale deep link, so neither returns a 5xx — anything >= 500 is logged at error by MiddlewareFactory.error() and forwarded to Sentry. Both cases are covered by tests asserting < 500.

Backend tests: 54 → 72.

Do the docs need to be updated?

Yes, done in this PR — the plugin README gains the two routes, the events-vs-tasks comparison, the wire-version rationale, and the id-opacity note.

Should this change be mentioned in the release notes?

  • A changeset describing the change and affected packages was added.

Add the transport the upcoming session detail page needs:

- GET /kagent/sessions/:id       — one session plus its stored events
- GET /kagent/sessions/:id/tasks — the session's A2A tasks

Both require ?installation= and a forwarded user token, and pass kagent's
JSON through verbatim like the existing routes.

The conversation comes from .../tasks, not from the events on
.../sessions/:id — that is what kagent's own UI renders from, and the
payloads differ: an event's `data` is a JSON *string* holding an A2A
message and carries no state or token usage, while task history is
already structured and carries both. Events remain useful for
timestamps, which A2A messages lack.

No A2A-Version header is sent: kagent treats a missing header as the
legacy v0 wire on both v0.9.9 and v0.10, the shape its UI consumes.

Session ids stay opaque (real ones mix 64-char hex and UUIDs), so
nothing validates a format and ids are URL-encoded before use.

A session belonging to another user answers 404, as a deleted one does.
Both are expected outcomes for a stale deep link, so neither returns a
5xx, which MiddlewareFactory.error() would forward to Sentry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2M37V7ubYwimkkS7YZ9iz
@marians
marians requested a review from a team as a code owner July 29, 2026 06:09
Comment thread plugins/agent-platform-backend/src/KagentClient.ts
Comment thread plugins/agent-platform-backend/src/router.ts Outdated
Three different things arrive as a 404 and all three reported the same
message, which was wrong for two of them:

- Nothing listening at the host: keeps "kagent is not available here",
  the fleet-wide wording the frontend's silent classification relies on.
- kagent answering "no such resource" (404 with a JSON body, since its
  error middleware always answers JSON): now "that session does not
  exist". Previously a bookmarked link to a deleted session reported an
  outage on a perfectly healthy installation.
- The endpoint not existing (404 with a non-JSON body, because kagent
  registers no custom NotFoundHandler and net/http answers text/plain):
  now "this kagent predates that endpoint". Without this an installation
  on an older kagent would report "session not found" for every session
  on every page load, and with no version probe there is nothing else to
  go on.

kagent's own message is not forwarded: its middleware appends the
underlying error, so a session 404 reads "Session not found: no rows in
result set".

Also drop the session id's trim(). Express hands over the decoded
segment, so an id with surrounding whitespace was trimmed here and a
different id re-encoded upstream — a 404 indistinguishable from a
missing session, on a value the docs declare opaque. The empty-id guard
went with it: :sessionId cannot match an empty segment, so
/kagent/sessions/ reaches the list route and the guard was unreachable.
Both behaviours are now pinned by tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2M37V7ubYwimkkS7YZ9iz
…er read

Ask kagent for `limit=1` on GET /kagent/sessions/:id. The caller wants
the session object and nothing else, but kagent bundles the session's
stored events into that response and they dominate it: on a real 4-turn
gazelle session, 591 KB of events against 261 bytes of session metadata.

The events were originally going to supply per-message timestamps, since
A2A messages carry none. They cannot. kagent's Go type calls each
event's `data` a "JSON-serialized protocol.Message", but the decoded
value is an ADK event — author, content, invocation_id, partial,
timestamp, usage_metadata — with no messageId anywhere, so there is
nothing to correlate with task history. 36 events yielded zero usable
ids. Its invocation_id does correlate, but only per turn, which the
task's own timestamp already gives.

Both v0.9.9 and v0.10 parse `limit` with the same handler code, and a
version that ignored it would return everything — the previous
behaviour — so this can only help.

Also corrects the README and doc comments, which described events as A2A
messages and as the timestamp source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2M37V7ubYwimkkS7YZ9iz
Comment thread plugins/agent-platform-backend/src/KagentClient.ts Outdated
Comment thread plugins/agent-platform-backend/src/KagentClient.ts Outdated
Comment thread .changeset/agent-platform-session-detail-proxy.md Outdated
Three review findings, all about claims that were imprecise rather than
behaviour that was wrong.

limit=0 does NOT mean "no events". kagent gates its LIMIT clause on
opts.Limit > 0 (client_postgres.go), so zero reads as unlimited and the
obvious-looking simplification — "ask for zero, we don't read them" —
would silently restore the full 591 KB payload. Now stated at the call
site, in the README, and in the test that guards the URL, so a failure
there explains itself instead of just reporting a diff.

"Both parse limit with the same handler code" was not literally true.
Semantics match, but v0.9.9 parses inline in HandleGetSession while
v0.10 uses the extracted eventQueryOptionsFromRequest. The claim only
needed "both honour the param", so it now says that.

The changeset said "Two details worth knowing" followed by three, with
the limit paragraph floating between the lead-in and the bullets. Folded
in as the first bullet. Also trims the *response*, not the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2M37V7ubYwimkkS7YZ9iz
@marians
marians merged commit c1b3690 into main Jul 29, 2026
8 of 9 checks passed
@marians
marians deleted the agent-platform-session-detail-proxy branch July 29, 2026 13:35
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.

1 participant