Chapter 4: Session Manager¶
In Chapter 3: ACP, we saw how the TypeScript TUI and the Rust backend speak to each other over a JSON-RPC protocol. The TUI sends a session/new request, the backend sends back a session ID. The TUI sends session/prompt, the backend streams back the agent's response.
But where do those messages actually land inside the Rust backend? Who decides which session gets which prompt? Who creates the session in the first place?
That's the Session Manager.
Motivation: Why Do We Need a Session Manager?¶
Imagine a hotel front desk. Guests check in, get assigned rooms, and the concierge routes messages and room service to the right room. Without the front desk, guests would wander the hallways, deliveries would go to random rooms, and nobody would know who's checked in.
The Session Manager is that front desk. It solves three problems:
- Multi-session coordination — kiro-cli can run multiple conversations at once (a main session plus subagent sessions). Someone needs to track which sessions exist and route messages to the right one.
- Lifecycle management — Sessions are created, loaded from disk, swapped to different agents, and terminated. Each transition has side effects (loading configs, spawning MCP servers, cleaning up processes).
- State isolation — Each session has its own agent config, conversation history, tool permissions, and MCP servers. The Session Manager ensures one session's state never leaks into another.
A Concrete Use Case¶
Let's trace what happens when you open kiro-cli and start typing:
- TUI sends
session/new— "I need a new conversation in/home/user/my-project." - Session Manager receives the request — It loads agent configs, resolves which agent to use, creates a
SessionDb(the on-disk persistence handle), spawns anAcpSessionactor with the right config, and returns the session ID. - TUI sends
session/prompt— "Fix the login bug." - Session Manager routes the prompt — It looks up the session by ID in its internal
HashMap, finds theAcpSessionHandle, and forwards the prompt to that session's Agent Loop.
The TUI never talks to the Agent Loop directly. Every request goes through the Session Manager first.
TUI ──session/new──► SessionManager ──creates──► AcpSession ──► AgentLoop
TUI ──session/prompt──► SessionManager ──routes──► AcpSession ──► AgentLoop
Key Concepts¶
The Session Registry¶
At its core, the Session Manager is a HashMap from session IDs to session handles:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs
pub struct SessionManager {
sessions: HashMap<SessionId, AcpSessionHandle>,
agent_configs: Vec<LoadedAgentConfig>,
// ... orchestration state, code intelligence, etc.
}
Every active session — whether it's the main conversation or a spawned subagent — lives in this map. When a request arrives, the manager looks up the target session by ID and delegates to it.
Session Lifecycle¶
A session moves through these states:
| State | What's happening |
|---|---|
| Create | session/new → Manager builds an AcpSession with agent config, MCP servers, tool permissions, and persistence |
| Load | session/load → Manager reads an existing SessionDb from ~/.kiro/sessions/cli/{id}.json, replays history |
| Active | Session is running — accepting prompts, streaming responses, calling tools |
| Swap | /agent command → Manager hot-swaps the agent config without destroying the session |
| Terminate | Session exits → Manager removes it from the registry, shuts down MCP servers, releases the lock file |
Session Persistence (SessionDb)¶
Each session is backed by three files on disk:
~/.kiro/sessions/cli/
├── {session_id}.json ← metadata (cwd, timestamps, agent name, permissions)
├── {session_id}.jsonl ← append-only event log (prompts, responses, tool calls)
└── {session_id}.lock ← PID lock file (prevents concurrent access)
The SessionDb struct in crates/chat-cli-v2/src/agent/session/mod.rs manages these files. It uses atomic writes (write-to-temp-then-rename) for the metadata file and an RAII lock guard that auto-releases on drop:
// crates/chat-cli-v2/src/agent/session/mod.rs
pub struct SessionDb {
_lock_guard: SessionLockGuard,
session: Mutex<SessionData>,
sessions_dir: PathBuf,
}
The lock file prevents two kiro-cli processes from opening the same session simultaneously. If a process crashes, the stale lock is detected by checking whether the owning PID is still alive.
The Actor Pattern¶
The Session Manager runs as a tokio actor — a long-lived async task that processes messages from a channel one at a time:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs (simplified)
let (tx, mut session_rx) = mpsc::channel(25);
tokio::spawn(async move {
loop {
let Some(request) = session_rx.recv().await else {
break;
};
session_manager.handle_request(request).await;
}
});
External code never touches SessionManager directly. Instead, it holds a SessionManagerHandle — a thin wrapper around the channel sender — and sends typed request messages:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs
#[derive(Clone, Debug)]
pub struct SessionManagerHandle {
tx: mpsc::Sender<SessionManagerRequest>,
}
Each request carries a oneshot::Sender for the response, so the caller can await the result. This pattern serializes all mutations through a single task, eliminating the need for locks on the session registry.
How It Works: Step by Step¶
Let's walk through the session/new flow in detail.
sequenceDiagram
participant TUI as TUI (TypeScript)
participant ACP as ACP Layer
participant SM as SessionManager
participant Session as AcpSession
participant DB as SessionDb (disk)
TUI->>ACP: session/new {cwd: "/home/user/project"}
ACP->>SM: StartSession(config)
SM->>SM: Resolve agent config
SM->>DB: Create session files (.json, .jsonl, .lock)
SM->>Session: AcpSessionBuilder.start_session()
Session-->>SM: (handle, ready_rx)
SM-->>ACP: StartSessionResult {handle, agent_name, ...}
ACP-->>TUI: {sessionId: "abc-123"}
Step 1: Resolve the Agent Config¶
When StartSession arrives, the manager resolves which agent to use through a priority chain:
This happens in handle_request for the StartSession variant (line ~300 of session_manager.rs). If the requested agent isn't found, it falls back to the built-in default and records the original request so the TUI can show a warning.
Step 2: Build the Session¶
The manager constructs an AcpSessionBuilder with everything the session needs:
- Agent config — system prompt, allowed tools, MCP server definitions
- Code intelligence — shared per-CWD, lazily initialized
- Trust settings — from
--trust-toolsCLI flag - Connection context — the JSON-RPC connection to the TUI (cloned for subagents)
- MCP registry — enterprise governance data, if applicable
// crates/chat-cli-v2/src/agent/acp/session_manager.rs (simplified)
let mut builder = AcpSessionBuilder::default()
.os(self.os.clone())
.session_id(config.session_id)
.cwd(config.cwd.clone())
.initial_agent_config(Cow::Owned(agent_config))
.code_intelligence(code_intel)
.trust_all_tools(self.trust_all_tools);
Step 3: Register and Return¶
Once the session starts, the manager stores the handle in its registry and returns the result:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs (simplified)
let (handle, ready_rx, model_id, _) = builder.start_session().await?;
self.sessions.insert(session_id.clone(), handle.clone());
The ready_rx is a oneshot channel that resolves when MCP servers finish initializing — the TUI waits on this before showing the input prompt.
Dispatch: Routing Requests to Sessions¶
Once a session exists, subsequent requests (prompts, mode changes, termination) are routed by session ID. The handle_request method is a large match over SessionManagerRequestData variants:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs (simplified)
match data {
StartSession { .. } => { /* create/load session */ },
GetSessionHandle { .. } => {
let handle = self.sessions.get(&session_id);
// return handle to caller
},
TerminateSession => {
if let Some(handle) = self.sessions.remove(&session_id) {
handle.shutdown().await;
}
},
SetMode { mode_id, .. } => {
// hot-swap agent config on existing session
},
// ... 20+ more variants for orchestration, messaging, etc.
}
This is the central dispatch loop. Every ACP request that touches session state flows through here.
Orchestration: Managing Subagent Trees¶
Beyond single sessions, the Session Manager coordinates orchestrated sessions — subagents spawned by a parent to work on subtasks. This is the "crew" system:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs
orchestrated_sessions: HashMap<String, OrchestratedSession>,
groups: HashMap<String, SessionGroup>,
inbox_store: InboxStore,
permission_store: PermissionStore,
When a parent agent calls spawn_session, the manager:
- Creates a new
AcpSessionmarked as a subagent - Registers parent→child permissions (who can message whom)
- Runs the task asynchronously — when the subagent finishes, its result is delivered to the parent's inbox
- Triggers any pending DAG stages whose dependencies are now satisfied
This enables multi-agent workflows where a coordinator spawns workers, workers report back, and the next wave of workers starts automatically.
Shutdown and Cleanup¶
When kiro-cli exits, the Session Manager gracefully shuts down all sessions:
// crates/chat-cli-v2/src/agent/acp/session_manager.rs (simplified)
SessionManagerRequestData::Shutdown { resp_sender } => {
let sessions: Vec<_> = self.sessions.drain().collect();
for (id, handle) in &sessions {
timeout(Duration::from_secs(4), handle.shutdown()).await;
}
}
Each session gets 4 seconds to clean up its MCP server child processes. If a session hangs, the timeout fires and the process moves on. The SessionDb lock guard is dropped automatically, releasing the .lock file so the session can be reopened later.
Summary of Key Files¶
| File | Role |
|---|---|
crates/chat-cli-v2/src/agent/acp/session_manager.rs |
The actor: session registry, dispatch loop, orchestration |
crates/chat-cli-v2/src/agent/session/mod.rs |
SessionDb: on-disk persistence, lock files, event log |
crates/chat-cli-v2/src/agent/acp/acp_agent.rs |
AcpSessionBuilder / AcpSessionConfig: session construction |
crates/chat-cli-v2/src/agent/acp/schema.rs |
ACP extension types (ListSessionsRequest, TerminateSessionRequest, etc.) |
Recap¶
The Session Manager is the central coordinator of kiro-cli's V2 backend. Like a hotel front desk:
- It checks guests in (creates sessions with the right agent config and persistence)
- It routes messages (dispatches prompts and commands to the correct session by ID)
- It manages checkouts (terminates sessions, cleans up MCP servers, releases locks)
- It coordinates group activities (orchestrates subagent trees with DAG-based scheduling)
It runs as a single tokio actor processing requests from a channel, which means all session state mutations are serialized — no locks needed on the registry itself.
What's Next¶
Before the Session Manager can start an Agent Loop, it needs to know how to configure it — which system prompt to use, which tools to allow, which MCP servers to launch. That configuration comes from agent JSON files.
Next up: Chapter 5: Agent Configuration — the personality blueprint that defines what an agent can do.