Session¶
Purpose¶
Enable the agent to restore state across requests, process restarts, and multi-user scenarios. After each call() ends, two outputs are automatically persisted on parallel tracks:
StateModule snapshot (
Memory,ToolExecutionContext, and other serializable state) — defaults toWorkspaceSessionConversation JSONL (LLM context + full history) — goes through
SessionTree, triggered byMemoryFlushManager.offloadMessages
The two are parallel, independent paths.
Trigger Points¶
When |
Action |
|---|---|
|
|
|
|
Compaction / |
|
End of session |
|
Key Logic¶
Dual-Track Storage Layout¶
graph LR
Call[agent.call] --> Hook[SessionPersistenceHook]
Hook -->|saveTo / loadIfExists| WS[(WorkspaceSession<br/>StateModule snapshot)]
Call --> Compact[CompactionHook / MemoryFlushHook]
Compact -->|offloadMessages| ST[(SessionTree<br/>JSONL dual files)]
WSWrite[WorkspaceManager<br/>updateSessionIndex] --> Idx[(sessions.json<br/>session index)]
Compact --> WSWrite
workspace/agents/<agentId>/
├── context/ ← managed by WorkspaceSession
│ └── <sessionId>/
│ ├── memory.json ← ReActAgent.memory snapshot
│ └── *.json ← other StateModule serialization artifacts
└── sessions/ ← managed by SessionTree + WorkspaceManager
├── sessions.json ← session index (sessionId / summary / updatedAt)
├── <sessionId>.jsonl ← LLM-visible compacted context
└── <sessionId>.log.jsonl ← full conversation log (append-only, never compacted)
context/:WorkspaceSessionextendsJsonSession, base atagents/<agentId>/context/; eachStateModuleis stored perSessionKey → {key}.jsonin the sessionId subdirectory.sessions/:SessionTreeorganizes a JSONL file as aid/parentIdtree; the paired<sessionId>.log.jsonlis never compacted, used for auditing andsession_search.
How RuntimeContext Aligns the Two Tracks¶
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("sess-001")
.userId("alice")
.build();
agent.call(msg, ctx).block();
HarnessAgent.bindRuntimeContext does several things:
Fill defaults: if
sessionis null, use thedefaultSessionfrom build time (defaults toWorkspaceSession(workspace, agentId)); ifsessionKeyis null, trySimpleSessionKey.of(sessionId)→SimpleSessionKey.of(agentName)in order.Distribute to hooks:
workspaceContextHook,memoryFlushHook,sessionPersistenceHook,compactionHookall sync to this ctx — they can readsessionIdduring offload / saveTo.Link
userIdRef: anAtomicReference<String>is updated touserId; the defaultNamespaceFactory → List.of(userId)uses this as a path prefix, enabling transparent multi-tenant isolation.Pre-load state: if both
sessionandsessionKeyare present, callsdelegate.loadIfExiststo overwrite current Memory. Does nothing if not found.
Default vs Custom Session¶
// 1. Default: nothing passed → WorkspaceSession(workspace, agentId)
HarnessAgent.builder()
.name("MyAgent").model(model).workspace(workspace).build();
// 2. Use a JsonSession at a specific path
HarnessAgent.builder()
...
.session(new JsonSession(Path.of("/custom/sessions")))
.build();
// 3. Override per call
agent.call(msg, RuntimeContext.builder()
.sessionId("sess-001")
.session(customSession)
.sessionKey(SimpleSessionKey.of("sess-001"))
.build())
.block();
Multi-User Isolation at Two Levels¶
Session level:
sessionIddetermines thatcontext/<sessionId>/andsessions/<sessionId>.jsonlare independent.File level:
userId+NamespaceFactorydetermines the file operation path prefix (the defaultLocalFilesystemWithShellreadsuserIdRef).
// Serve alice and bob from the same agent instance
agent.call(msg, RuntimeContext.builder().sessionId("alice-1").userId("alice").build()).block();
agent.call(msg, RuntimeContext.builder().sessionId("bob-1").userId("bob").build()).block();
// Session state and file paths for the two users do not interfere with each other
Session Index¶
After MemoryFlushManager.offloadMessages completes, WorkspaceManager.updateSessionIndex(agentId, sessionId, summary) merges a write into sessions/sessions.json. In another turn, the agent can use the session_list tool to see “what conversations this agent has had historically”.