mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-14 05:37:40 +02:00
314 lines
15 KiB
Markdown
314 lines
15 KiB
Markdown
# 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.
|