6.6 KiB
6.6 KiB
Agent Orchestration + Session UI Data Flow
This document describes how a task session is created, how agentctl/agent lifecycle events flow through the backend, and how the session details page consumes those events.
Overview (Actors)
- Client UI: Next.js app with session details page and panels (chat, terminal, git, files).
- Backend: Orchestrator + WebSocket gateway + task/session store.
- agentctl: Sidecar HTTP/WS server inside agent container (or standalone process) that manages the agent subprocess and exposes ACP + workspace streams.
- Agent: ACP-speaking process (JSON-RPC over stdin/stdout) that performs work.
Orchestration Flow (Task Session Creation)
Client UI
| HTTP: POST /api/v1/tasks { start_agent: true, agent_profile_id }
v
Backend Task Service
| create task + first session
| start agentctl orchestration
| create task_session (STARTING)
| launch agentctl (container or standalone)
| wait for /health
| POST /api/v1/start
| POST /api/v1/acp/call initialize
| POST /api/v1/acp/call session/new
| POST /api/v1/acp/call session/prompt
v
agentctl
| spawns agent process
| streams ACP updates + workspace streams
v
Agent
| ACP notifications (session/update)
v
agentctl -> Backend
| ACP update stream -> backend
| backend updates task_session state + messages
| backend publishes WS notifications
v
Client UI
| session.subscribe
| receive session-scoped events
Backend Event Routing (Session Scoped)
Events are now routed primarily by session_id. The WS gateway maintains session subscriber lists. It broadcasts session events only to clients subscribed to that session.
agentctl WS /api/v1/acp/stream
-> backend ACP handler
-> task_session state update
-> message persisted
-> WS broadcast: session.channel
agentctl WS /api/v1/workspace/git-status/stream
-> backend git-status handler
-> attach to session
-> WS broadcast: session.channel
agentctl WS /api/v1/output/stream
-> backend shell handler
-> session resolve (session_id -> task_id)
-> WS broadcast: session.channel
Session notifications:
session.agentctl_startingsession.agentctl_readysession.agentctl_errorsession.state_changedsession.message.added,session.message.updatedsession.shell.output,session.shell.statussession.git.statussession.workspace.file.changes
Frontend Session Page Flow (SSR + WS)
Server (SSR)
- fetch session by id
- fetch parent task + workspace context
- hydrate store (tasks, sessions, repos, agent profiles)
Client (Hydrated)
- read sessionId from URL (/s/:sessionId)
- set activeSessionId in store
- subscribe to session events
- child components read from store + hooks
Component Subscriptions
Each component subscribes to the data it needs via hooks. The WS client deduplicates subscriptions so multiple components can safely request the same channel.
TaskSessionPage (/s/:sessionId)
-> useSession(sessionId)
-> subscribe: session.{sessionId}
TaskChatPanel
-> useSessionMessages(sessionId)
-> subscribe: session.{sessionId}
ShellTerminal
-> useSessionAgentctl(sessionId)
-> subscribe: session.{sessionId}
-> useShellOutput(sessionId)
-> subscribe: session.{sessionId}
TaskChangesPanel (Git)
-> useSessionGitStatus(sessionId)
-> subscribe: session.{sessionId}
TaskFilesPanel
-> useSessionAgentctl(sessionId)
-> wait for readiness
-> useSessionWorktrees(sessionId)
-> subscribe: session.{sessionId}
Event + State Flow Diagram
Agent
| ACP session/update
v
agentctl
| WS /api/v1/acp/stream
v
Backend ACP handler
| update session state/messages
| emit session WS events
v
WS Gateway (session subscribers)
| broadcast to session.{sessionId}
v
WebSocket Client (browser)
| dedupe subscription
| route to store slice
v
Zustand Store
| sessions, messages, shell, git, worktrees
v
Session UI Panels
| hook selectors
| render
Concrete Example: New Session -> Message Appears In Chat
Scenario: user starts a task session and the agent replies with a completion message that appears in the chat panel.
Sequence (Calls + Events)
- Client -> Backend (HTTP request)
POST /api/v1/tasks
body:
title: "Example task"
workspace_id: "workspace-1"
repository_id: "repo-9"
agent_profile_id: "profile-abc"
start_agent: true
- Backend (internal)
- creates task
- creates first task session
- starts agentctl orchestration for the new session
- returns session_id in the HTTP response
- Backend -> agentctl (HTTP)
POST /api/v1/startPOST /api/v1/acp/callinitializePOST /api/v1/acp/callsession/newPOST /api/v1/acp/callsession/prompt
- Backend -> Client (WS notification)
action: session.agentctl_starting
payload:
task_id: "task-123"
session_id: "session-789"
- Client subscribes to session
action: session.subscribe
payload:
session_id: "session-789"
- agentctl -> Backend (ACP notification)
jsonrpc: "2.0"
method: "session/update"
params:
type: "content"
data:
text: "Working on your request..."
session_id: "session-789"
- Backend -> Client (WS notification)
action: session.message.added
payload:
session_id: "session-789"
message:
id: "msg-1"
author_type: "agent"
content: "Working on your request..."
- Client store update (message handler)
messages.bySession["session-789"] += message
- Chat panel render
TaskChatPanel -> useSessionMessages("session-789") -> messages list -> render
Full Event List (Typical Session Start)
- HTTP request:
POST /api/v1/taskswithstart_agent: true - WS notification:
session.agentctl_starting - WS request:
session.subscribe - WS notification:
session.agentctl_ready - ACP notifications from agent:
session/update(content/toolCall/complete/error)
- WS notifications:
session.message.addedsession.message.updatedsession.state_changedsession.shell.status/session.shell.output(if shell active)session.git.status(if git watcher active)session.workspace.file.changes(if watcher active)
Notes / Guardrails
- Session is the source of truth; task id is derived from session when needed.
- No fallback to “active task” or “first message” for session ownership.
- Agentctl readiness is explicit; panels that require a live agent should wait for
agentctl_ready. - Store slices are keyed by session id (messages, shell output, git status, worktrees).
- WS subscriptions are session-scoped and deduplicated at the client level.