- Worktree:
/Users/ardaerzin/Documents/GitHub/agenta_open_source/.claude/worktrees/big-agents-sessions - Branch:
big-agents-add-sessions=origin/big-agents+ a clean merge of PR #4916 (feat/add-sessions). - Read
web/AGENTS.mdand theagenta-package-practicesskill before writing code. Frontend is a pnpm monorepo (Node ≥ 22.13; use Node 24). Runpnpm lint-fixinweb/before committing.
It is ~95% backend + generated client, almost no FE feature code:
- New API domains under
api/oss/src/.../sessions/{states,streams,transcripts,interactions}+ standalonemounts. - The Fern-generated TS client for them in
@agentaai/api-client— this is your entire FE-facing surface. Nothing consumes it yet. - Design docs in
docs/designs/sessions/(states / streams / transcripts / interactions) anddocs/designs/mounts/. These docs are labelled "draft for discussion / not implemented" — treat them as intent, and verify the actual backend wiring is live before building UI on an endpoint (esp. transcripts ingest worker, interactions beyonduser_approval). v1 of interactions implementsuser_approvalonly;user_inputandtool_callexist in the schema but are not wired.
So your job is integration, not greenfield: connect the existing agent-chat FE (today localStorage-only) to these durable endpoints.
Get the client the standard way (mirror web/packages/agenta-entities/src/trace/api/client.ts):
import {getAgentaSdkClient} from "@agenta/sdk"
const sessions = getAgentaSdkClient().sessions
const mounts = getAgentaSdkClient().mountsProject/app scope ALWAYS rides queryParams, never the body (standing rule, see projectScopedRequest in the trace client): pass {queryParams: {project_id, application_id}, abortSignal} as the per-request options arg. Fern methods throw AgentaApiError on non-2xx — wrap at the boundary.
| Method | Endpoint | Purpose |
|---|---|---|
invokeStream({session_id, prompt?, force?, detached?}) |
POST /sessions/streams/invoke |
Start/resume a live run. force steals the run lock; detached = fire-and-forget (no held connection). |
queryStreams({session_id?, sandbox_live?}) |
POST /sessions/streams/query |
List live stream handles (who's running / attached / sandbox alive). |
getLiveness(...) |
GET /sessions/streams/liveness |
Is a session's run currently alive. |
queryTranscripts({session_id}) |
POST /sessions/transcripts/query |
Durable, append-only event log = the replay source for rendering a conversation. |
getTranscriptEvent({event_id}) |
GET /sessions/transcripts/{event_id} |
One event by id. |
getState({session_id}) / setState({session_id, data, sandbox_id}) |
GET/POST /sessions/states/{session_id} |
Durable SDK SessionRecord (opaque JSON) + sandbox resume pointer. |
setStateSandboxId(...) |
POST /sessions/states/{session_id}/sandbox-id |
Update just the resume pointer. |
queryInteractions({query, windowing}) |
POST /sessions/interactions/query |
List HITL requests (pending approvals etc.). |
fetchInteraction({interaction_id}) |
GET /sessions/interactions/{interaction_id} |
One interaction. |
respondInteraction({interaction_id, answer}) |
POST /sessions/interactions/{interaction_id}/respond |
Resolve a HITL request (approve/deny/input/tool-result). |
querySessionMounts({...}) |
POST /sessions/mounts/query |
Session-scoped view of mounts. |
queryMounts, createMount, fetchMount, editMount, archiveMount/unarchiveMount, plus file ops getMountFiles, uploadMountFile, writeMountFile, deleteMountFile, createMountFolder.
SessionTranscript:{id, session_id, project_id, event_index?, sender?, session_update?, payload?, created_at?}—payloadis the ACP event (text / tool_call / result). This is what you map → chat messages.SessionState:{session_id, data (opaque SessionRecord JSON), sandbox_id?}.SessionStream:{id, session_id, attached?, sandbox_live?, last_seen_at?, status:{code, message}}.StreamStatusCode = Running | Detached | Idle | Ended.SessionInteraction:{id, session_id, run_id?, token, kind, status?, data}.kind = user_approval | user_input | tool_call.status = pending | resolved | denied | cancelled.data = {request, references, selector, resolution}.SessionMount/Mount:{id, session_id, project_id, data: MountData, flags}.
Consistency model to respect: the transcript renders the question (append-only, replayable); the interaction record holds the answer-state (is it still actionable + what was answered). Don't render HITL from the interactions table — render from the transcript, and use interactions to know if it's still pending/resolved.
Today this slice is localStorage-only with an ephemeral runner transport. Files that matter:
state/sessions.ts— the multi-session model. HISTORY (sessionsByAppAtom) vs OPEN TABS (openIdsByAppAtom) vs messages (sessionMessagesAtom), allatomWithStorage(localStorage), scope-keyed (app id, ordrawer:<entityId>for the create/edit drawer — seestate/scope.tsx). Messages keyed by globally-unique session id.assets/loadSession.ts— the hydration seam.loadSessionMessages(sessionId)returnsnulltoday. Its docstring still points at the old/services/agent/v0/load-sessionendpoint — that is now superseded byclient.sessions.queryTranscripts({session_id}). This is the single cleanest place to light up server-backed history.assets/transport.ts+assets/AgentChatTransport.ts— theuseChat(AI SDK v6) transports. Today they POST the agent-protocol envelope to the runner and stream a v6 UI Message Stream;AgentChatTransportalso handles a batch→one-shot replay. The live run currently does not go through/sessions/streams/invoke.assets/toAgentaMessage.ts,assets/trace.ts,assets/rewind.ts,assets/files.ts,assets/attachments.ts— message adaptation, trace stamping, edit/rewind, inlinedata:file attachments.components/SessionHistoryMenu.tsx— the clock-icon history picker (already shipped, localStorage-backed).components/AgentChatConversation.tsx,AgentMessage.tsx,ToolActivity.tsx,QueuedMessages.tsx— render surface.hooks/useAgentChatQueue.ts— send queue.- Pages:
web/oss/src/pages/.../apps/[app_id]/agent-chat/index.tsx(standalone) and the playground panelAgentChatPanel.tsx.
Separately, observability already has a read-only session viewer (web/oss/src/components/SharedDrawers/SessionDrawer/, components/pages/observability/components/SessionsTable/) that lists sessions by aggregating ag.session.id off traces. That's a different surface — don't conflate it; but the new transcripts API could eventually back its message panel too.
- Server-backed history (highest value, smallest change). Implement
loadSessionMessagesagainstclient.sessions.queryTranscripts({session_id}): mapSessionTranscript.payloadevents → v6UIMessage[](reuse the vercel/toAgentaMessageadapters). Caller already writes the result intosessionMessagesAtombefore opening the tab. This makes open-from-deep-link / open-from-trace render conversations this browser never ran. Confirm the transcript ingest worker is actually populating rows first — if empty, this is a no-op. - Session state / resume. On session open,
getStateto restore the SDK record +sandbox_id; persist viasetStateso a run can resume the same sandbox across reloads/devices. Decide the localStorage-vs-server source-of-truth merge (server wins when present; localStorage is the offline fallback — keep the existing fallback semantics). - HITL interactions. When the transcript renders an
interaction_request, cross-checkqueryInteractions/fetchInteractionfor live status, and wire the approve/deny control torespondInteraction({interaction_id, answer}). Scope v1 touser_approval(deny/allow, maybe "always"); leaveuser_input/tool_callstubbed behind a flag. Build it as a derived view over the transcript, not a second source of truth. - Live run via streams (bigger). Move the live send from the current runner transport onto
/sessions/streams/invoke(+queryStreams/getLivenessfor attach/detach/“someone else is running this” state, andforceto steal). This is the multi-replica-correct path; verify the backend stream endpoint is live and returns the v6 chunk stream the transport expects before committing to it. - Mounts / files (optional, last). A file panel over
client.mounts.*for the agent working directory.
- Fern client only (
@agentaai/api-clientviagetAgentaSdkClient()), never axios, for all new API code. - Package vs app placement per
agenta-package-practices. New shared session entity logic likely belongs in a@agenta/entities-style package (mirrortrace/api/), not inweb/oss. OSS lint blocks re-exporting@agenta/*from app barrels. - Text size 12px (
text-xs), nevertext-sm. Nosize="small"on antd controls. Dark mode: use antd semantic tokens (--ag-colorText,--ag-colorBorder, …);dark:/bg-gray-*/ fixed hex are no-ops here. - Thin-references pattern: query caches store IDs, the molecule/atom is the source of truth.
- Don't put
project_id/application_idin request bodies — queryParams only. - Before commit:
pnpm lint-fixinweb/; run the oss tsc manually (relocations can mask new errors — gate on an error-signature diff, not a count). - Commit messages: no "Claude"/Anthropic/
Co-Authored-By.
- The fastest reality check is whether the endpoints return data: hit
queryTranscripts/queryStreams/queryInteractionsfor a real session id and confirm non-empty, correctly-shaped responses. Several of these depend on a runner/worker rebuild that may not be running locally. - Use the
verifyskill / run the app rather than trusting types — the generated client compiles regardless of whether the backend route is wired.