Files

15 KiB

manager/ — Session Management & WebSocket Infrastructure

This package handles the full lifecycle of agent sessions — creating, running, stopping, editing, branching, persisting, restoring, duplicating, and deleting — plus the WebSocket infrastructure that powers real-time communication with the frontend.

Architecture

                    ┌──────────────────────────┐
                    │     agent_manager.py      │  ← singleton facade
                    │       (AgentManager)      │
                    └────────────┬─────────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              ▼                  ▼                   ▼
   agent_manager_ops.py   agent_manager_meta.py   ws_manager.py
   (edit, close, resume,  (title gen, group meta, (WebSocket connections,
    duplicate, invoke)     persist, restore,       HITL futures,
                           delete)                 browser bridge)
              │                  │
              └────────┬─────────┘
                       ▼
               session_store.py
               (JSON persistence,
                history, search)

Design principle: agent_manager.py is a thin coordinator — it holds the state dicts and delegates all complex logic to sibling modules. This keeps every file under ~250 lines.

Files

agent_manager.py — Central AgentManager Singleton

The single entry point consumed by all API routes and WebSocket handlers. Holds two core dicts:

  • sessions: dict[str, AgentSession] — all active in-memory sessions
  • tasks: dict[str, asyncio.Task] — running agent loop tasks

Every method either handles simple logic directly or delegates to a sibling module.

Method Delegates To Purpose
launch_agent(config) Creates session, resolves mode/tools, records analytics, emits WS status
send_message(session_id, prompt, ...) run_agent_loop Validates session, handles model/mode switching, creates Message, spawns agent loop task
stop_agent(session_id) Cancels task, resolves approvals, stops browser children, sets status to stopped
handle_approval(request_id, decision) ws_manager Resolves pending approval Future
edit_message(...) agent_manager_ops Triggers branching and re-run
switch_branch(session_id, branch_id) Sets active_branch_id, emits WS event
generate_title(...) agent_manager_meta LLM-powered title generation
generate_group_meta(...) agent_manager_meta LLM-powered tool group naming + SVG icon
update_session(session_id, **fields) Updates system_prompt or name, emits WS status
close_session(session_id) agent_manager_ops Stops children, persists, fires analytics
delete_session(session_id) agent_manager_meta Permanent deletion from memory and disk
resume_session(session_id) agent_manager_ops Loads from disk, restores to memory
duplicate_session(...) agent_manager_ops Deep-copies messages and branches
invoke_agent(...) agent_manager_ops Forks session, runs agent loop synchronously
get_all_sessions(dashboard_id?) Filters in-memory sessions
get_session(session_id) Dict lookup
get_history(...) session_store Paginated, filterable session history
reconcile_on_startup() session_store Marks stale running sessions as stopped
persist_all_sessions() agent_manager_meta Shutdown persistence
restore_all_sessions() agent_manager_meta Startup restore
get_browser_agent_children(...) session_store Finds child browser sessions

Exported as: agent_manager = AgentManager() (module-level singleton)

Environment:

  • Sets CLAUDE_CODE_STREAM_CLOSE_TIMEOUT to 1 hour (3,600,000ms)

agent_manager_ops.py — Complex Session Operations

Implements operations that involve multiple steps (cancellation, branching, persistence, re-execution).

edit_message_op(sessions, tasks, session_id, message_id, new_content)

  1. Cancels any running agent loop task for the session
  2. Creates a new MessageBranch forking from the edited message's position
  3. Appends a new Message with the edited content on the new branch
  4. Resets sdk_session_id (forces a fresh SDK session)
  5. Spawns a new run_agent_loop with the edited content
  6. Records session.branched analytics

close_session_op(sessions, tasks, session_id)

  1. Stops all child browser-agent sessions (has a known circular import workaround)
  2. Cancels the running task
  3. Resolves any pending approvals with denial
  4. Fires session.completed analytics
  5. Persists the session to disk via save_session
  6. Removes from in-memory dicts

resume_session_op(sessions, session_id)

  1. Checks if session is already in memory (returns it directly if so)
  2. Loads from disk via load_session_data
  3. Records session.resumed analytics (with hours since closed)
  4. Clears closed_at, sets status back to stopped
  5. Deletes the on-disk file (session is now in memory)
  6. Emits WS status

duplicate_session_op(sessions, session_id, dashboard_id?, up_to_message_id?)

  1. Deep-copies all messages and branches via copy_session_messages
  2. Creates a new AgentSession with "(copy)" suffix
  3. Emits WS status for the new session

invoke_agent_op(sessions, source_session_id, message, parent_session_id?, dashboard_id?)

  1. Forks the source session (copies messages/branches)
  2. Creates a new session in "invoked-agent" mode
  3. Appends the new user message
  4. Runs the agent loop synchronously (awaits completion)
  5. Returns the last assistant response text + cost

Known issue: close_session_op has a circular import from agent_manager (flagged with a TODO comment in the code).


agent_manager_meta.py — LLM Metadata, Persistence, Deletion

Handles LLM-powered metadata generation and the full persistence lifecycle.

generate_title_op(sessions, session_id, first_prompt)

  • Calls quick_llm_call with a prompt asking for a 3-6 word session title
  • Falls back to truncating the first prompt on failure
  • Emits agent:name_updated via WebSocket

generate_group_meta_op(sessions, session_id, group_id, tool_calls, ...)

  • Calls quick_llm_json asking for a 2-5 word name and 24x24 SVG icon for a group of tool calls
  • Stores result as ToolGroupMeta on the session
  • Supports refinement (regeneration) via is_refinement flag
  • Emits agent:group_meta_updated via WebSocket

persist_all_sessions_op(sessions, tasks) — Shutdown hook

  1. Iterates all in-memory sessions
  2. Stops running sessions, resolves pending approvals
  3. Fires session.completed analytics for each
  4. Serializes to JSON and saves to disk
  5. Clears both sessions and tasks dicts

restore_all_sessions_op(sessions) — Startup hook

  1. Loads all session data from disk
  2. Skips closed or corrupt sessions
  3. Resets "running" status to "stopped" (since the agent loop is no longer active)
  4. Clears any stale pending approvals
  5. Adds to in-memory sessions dict
  6. Deletes the disk file (session is now managed in memory)

delete_session_op(manager, session_id)

  1. Stops child browser-agent sessions
  2. Cancels the running task
  3. Removes from in-memory dicts
  4. Deletes the on-disk file

ws_manager.py — WebSocket ConnectionManager

Manages all WebSocket connections and provides Future-based async bridges for HITL approval and browser commands.

Zero internal dependencies — only uses fastapi.WebSocket and stdlib. This makes it the lowest-level component in the dependency graph.

Connection Management

Method Purpose
connect_session(session_id, ws) Accept and register a per-session WS connection
connect_global(ws) Accept and register a dashboard-level WS connection
disconnect_session(session_id, ws) Remove a session connection
disconnect_global(ws) Remove a global connection

Message Sending

send_to_session(session_id, event, data)

  • Broadcasts to ALL connections for that session AND all global connections
  • This ensures dashboard-level listeners always see session updates

broadcast_global(event, data)

  • Sends only to global (dashboard) connections

HITL Approval Bridge

send_approval_request(session_id, request_id, tool_name, tool_input, timeout=600)

  • Creates an asyncio.Future
  • Sends the approval request to the frontend
  • Awaits the Future with a 10-minute timeout
  • Returns the user's decision (or auto-deny on timeout)

resolve_approval(request_id, decision)

  • Sets the result on the pending Future, unblocking the waiting agent

Browser Command Bridge

send_browser_command(request_id, action, browser_id, params, tab_id?)

  • Sends a browser command to the frontend via global WS connections
  • Waits up to 30 seconds for the frontend to return a result
  • Returns error if no dashboard is connected

resolve_browser_command(request_id, result)

  • Sets the result on the pending Future

Typed Event Emitters

14 convenience methods that wrap send_to_session with specific event types:

Emitter Event Name Data
emit_status agent:status Status string + optional full session
emit_message agent:message Message object
emit_cost_update agent:cost_update Cost in USD
emit_stream_start agent:stream_start Message ID, role, optional tool name
emit_stream_delta agent:stream_delta Message ID + text delta
emit_stream_end agent:stream_end Message ID
emit_branch_created agent:branch_created Branch object + active branch ID
emit_branch_switched agent:branch_switched Active branch ID
emit_name_updated agent:name_updated New session name
emit_group_meta_updated agent:group_meta_updated Group ID, name, SVG, is_refined
emit_closed agent:closed Full session object

Exported as: ws_manager = ConnectionManager() (module-level singleton)


session_store.py — On-Disk Persistence & History

Wraps the generic SessionStore (from backend.apps.common.json_store) with agent-specific logic.

Persistence (re-exported from SessionStore)

Name Purpose
save_session(id, data) Save session JSON to SESSIONS_DIR/{id}.json
load_session_data(id) Load a session's JSON from disk
delete_session_file(id) Delete a session file
load_all_session_data() Load all session files from disk

Agent-Specific Functions

build_search_text(session, max_len=5000)

  • Builds a search-indexing string from session name + all user/assistant message text
  • Truncated to max_len characters
  • Used by get_history for text search

get_history(q?, limit=20, offset=0, dashboard_id?)

  • Loads all sessions from disk
  • Sorts by closed_at descending (most recent first)
  • Applies text search filter (case-insensitive against build_search_text)
  • Applies optional dashboard_id filter
  • Returns {sessions: [...], total: N, has_more: bool}

reconcile_on_startup()

  • Iterates all on-disk sessions
  • Sets any "running" or "waiting_approval" status to "stopped"
  • Handles crashes/restarts gracefully

get_browser_agent_children(sessions, parent_session_id)

  • Finds all browser-agent sessions belonging to a parent
  • Checks both in-memory sessions and on-disk data
  • Deduplicates by session ID
  • Returns list of session summary dicts

copy_session_messages(source, up_to_message_id?)

  • Deep-copies all messages and branches from a source session
  • Generates fresh UUIDs for each message
  • Re-maps parent_id references to new IDs
  • Updates branch fork_point_message_id to new IDs
  • Returns (new_messages, new_branches, old_to_new_id_map)
  • Used by duplicate and invoke operations

WebSocket Event Flow

Frontend                    ws_manager                     Agent System
   │                            │                              │
   │── connect_session ────────►│                              │
   │                            │                              │
   │                            │◄── emit_status("running") ──│  (agent starts)
   │◄── agent:status ──────────│                              │
   │                            │                              │
   │                            │◄── emit_stream_start ───────│  (LLM streaming)
   │◄── agent:stream_start ────│                              │
   │                            │◄── emit_stream_delta ───────│
   │◄── agent:stream_delta ────│          (repeated)          │
   │                            │◄── emit_stream_end ─────────│
   │◄── agent:stream_end ──────│                              │
   │                            │                              │
   │                            │◄── emit_message ────────────│  (tool call)
   │◄── agent:message ─────────│                              │
   │                            │                              │
   │                            │◄── send_approval_request ───│  (HITL needed)
   │◄── agent:approval_request │                              │
   │                            │         (user decides)       │
   │── approval_response ──────►│                              │
   │                            │── resolve_approval ─────────►│  (unblocks agent)
   │                            │                              │
   │                            │◄── emit_cost_update ────────│  (completion)
   │◄── agent:cost_update ─────│                              │
   │                            │◄── emit_status("completed")─│
   │◄── agent:status ──────────│                              │

Persistence Lifecycle

Server Startup:
  reconcile_on_startup()     →  Fix stale "running" statuses on disk
  restore_all_sessions_op()  →  Load disk sessions into memory, delete disk files

During Operation:
  close_session_op()         →  Stop, persist to disk, remove from memory
  resume_session_op()        →  Load from disk to memory, delete disk file

Server Shutdown:
  persist_all_sessions_op()  →  Stop all, save all to disk, clear memory

Sessions live in memory while active. They move to disk when closed. They move back to memory when resumed. On server restart, disk sessions are loaded back into memory.