Overview
Agent Sessions enable persistent, multi-turn conversations with CLI agents. Think of them like HttpSession — the session maintains state across prompts so you can have iterative dialogues with an agent instead of fire-and-forget single tasks.
Without sessions, each AgentClient.goal().run() call starts a fresh conversation. With sessions, you can:
- Send follow-up prompts that build on previous context
- Resume conversations after transport failures
- Manage session lifecycle with automatic stale cleanup
Available since version 0.10.0. Currently implemented for the Claude agent provider. Other providers will follow.
Core Interfaces
AgentSession
A single persistent conversation. Created via AgentSessionRegistry.create() — never instantiated directly.
public interface AgentSession extends AutoCloseable {
String getSessionId();
Path getWorkingDirectory();
AgentSessionStatus getStatus();
AgentResponse prompt(String message);
AgentSession resume();
AgentSession fork();
void close();
}
| Method | Description |
|---|
getSessionId() | Unique session ID, assigned eagerly at creation |
getWorkingDirectory() | Immutable working directory the session operates in |
getStatus() | Current lifecycle status (ACTIVE, DEAD, or RESUMED) |
prompt(message) | Send a follow-up in the same conversation context |
resume() | Resurrect a DEAD session — restores conversation history |
fork() | Branch the conversation (not yet implemented) |
close() | Close the session and release resources |
AgentSessionRegistry
Factory and lifecycle manager for sessions. Analogous to SessionRepository in Spring Session.
public interface AgentSessionRegistry {
AgentSession create(Path workingDirectory);
Optional<AgentSession> find(String sessionId);
void evict(String sessionId);
void evictStale(Duration inactiveSince);
}
| Method | Description |
|---|
create(path) | Start a new session — eagerly connects to CLI and captures session ID |
find(id) | Look up an existing session |
evict(id) | Remove and close a session |
evictStale(duration) | Evict sessions inactive longer than the threshold |
AgentSessionStatus
ACTIVE → Session is connected and ready for prompts
DEAD → Transport died; call resume() to resurrect
RESUMED → Was dead, now active again after resume()
Usage
Create a Registry and Session
import org.springaicommunity.agents.claude.ClaudeAgentSessionRegistry;
import org.springaicommunity.agents.model.AgentSession;
import org.springaicommunity.agents.model.AgentResponse;
// Build a registry (configure once, create many sessions)
ClaudeAgentSessionRegistry registry = ClaudeAgentSessionRegistry.builder()
.timeout(Duration.ofMinutes(5))
.build();
// Create a session — eagerly establishes CLI connection
AgentSession session = registry.create(Path.of("/my/project"));
System.out.println("Session ID: " + session.getSessionId());
Multi-Turn Conversation
// First prompt
AgentResponse r1 = session.prompt("Create a Spring Boot REST controller for /api/users");
// Follow-up — Claude remembers the previous context
AgentResponse r2 = session.prompt("Add input validation with @Valid");
// Another follow-up
AgentResponse r3 = session.prompt("Now write tests for the controller");
// Clean up
session.close();
registry.evict(session.getSessionId());
Each prompt() call continues the same conversation — the agent sees the full history of what it built in earlier turns.
Resuming a Dead Session
If the CLI process dies (crash, timeout, network issue), the session transitions to DEAD. You can resurrect it:
AgentSession session = registry.create(Path.of("/my/project"));
String savedId = session.getSessionId();
try {
session.prompt("Refactor the service layer");
} catch (IllegalStateException e) {
// Transport died — session is now DEAD
if (session.getStatus() == AgentSessionStatus.DEAD) {
session.resume(); // Spawns fresh process, restores history
// Status is now RESUMED — ready for prompts again
session.prompt("Continue the refactoring");
}
}
Finding an Existing Session
// Later in the application lifecycle
Optional<AgentSession> found = registry.find(savedId);
found.ifPresent(s -> {
AgentResponse response = s.prompt("What files did you modify?");
System.out.println(response.getGenerations().get(0).getText());
});
Session Lifecycle
create()
│
▼
┌────────┐
│ ACTIVE │◄─────────────┐
└───┬────┘ │
│ │
prompt() works resume()
transport dies │
│ │
▼ │
┌────────┐ ┌────────┐
│ DEAD │────────►│RESUMED │
└───┬────┘ └────────┘
│
close() /
evict()
│
▼
[removed]
Spring Integration
Bean Configuration
@Configuration
@EnableScheduling
public class AgentSessionConfig {
@Bean
public ClaudeAgentSessionRegistry agentSessionRegistry() {
return ClaudeAgentSessionRegistry.builder()
.timeout(Duration.ofMinutes(10))
.build();
}
}
Stale Session Cleanup
Use @Scheduled to periodically evict inactive sessions and prevent resource leaks:
@Component
public class SessionCleanup {
private final ClaudeAgentSessionRegistry registry;
public SessionCleanup(ClaudeAgentSessionRegistry registry) {
this.registry = registry;
}
@Scheduled(fixedRate = 300_000) // Every 5 minutes
public void cleanupStaleSessions() {
registry.evictStale(Duration.ofMinutes(30));
}
}
Startup Health Probe
Use a disposable session to verify CLI availability at startup:
@Component
public class AgentHealthCheck {
private final ClaudeAgentSessionRegistry registry;
public AgentHealthCheck(ClaudeAgentSessionRegistry registry) {
this.registry = registry;
}
@EventListener(ApplicationReadyEvent.class)
public void verifyCliAvailable() {
try {
AgentSession probe = registry.create(Path.of("/tmp"));
probe.close();
registry.evict(probe.getSessionId());
// CLI is installed, authenticated, and responsive
} catch (IllegalStateException e) {
throw new IllegalStateException("Claude CLI not available: " + e.getMessage(), e);
}
}
}
Limitations
fork() is not yet implemented — calling it throws UnsupportedOperationException. This will be added in a future release.
- Claude-only: Sessions are currently only implemented for the Claude agent provider. Other providers will be added as their CLIs support persistent sessions.
- In-memory registry:
ClaudeAgentSessionRegistry stores sessions in a ConcurrentHashMap. Sessions do not survive application restarts — use resume() with a persisted session ID if you need cross-restart continuity.
- One working directory per session: A session is anchored to the working directory specified at creation time. It cannot be changed.
- No context pruning: Resumed sessions include the full conversation history. You cannot trim earlier turns to reduce token usage.