会话(Session)¶
作用¶
让 agent 能在跨请求、跨进程、多用户场景下恢复状态。一次 call() 结束后自动落盘两路产出:
StateModule 快照(
Memory、ToolExecutionContext等可序列化状态)——默认走WorkspaceSession对话 JSONL(LLM 上下文 + 完整历史)——走
SessionTree,由MemoryFlushManager.offloadMessages触发
两者是**两个并行路径
触发¶
时机 |
动作 |
|---|---|
|
|
|
|
压缩 / |
|
会话结束 |
|
关键逻辑¶
双轨存储布局¶
graph LR
Call[agent.call] --> Hook[SessionPersistenceHook]
Hook -->|saveTo / loadIfExists| WS[(WorkspaceSession<br/>StateModule 快照)]
Call --> Compact[CompactionHook / MemoryFlushHook]
Compact -->|offloadMessages| ST[(SessionTree<br/>JSONL 双文件)]
WSWrite[WorkspaceManager<br/>updateSessionIndex] --> Idx[(sessions.json<br/>会话索引)]
Compact --> WSWrite
workspace/agents/<agentId>/
├── context/ ← WorkspaceSession 负责
│ └── <sessionId>/
│ ├── memory.json ← ReActAgent.memory 快照
│ └── *.json ← 其他 StateModule 序列化产物
└── sessions/ ← SessionTree + WorkspaceManager 负责
├── sessions.json ← 会话索引 (sessionId / summary / updatedAt)
├── <sessionId>.jsonl ← LLM 可见的压缩上下文
└── <sessionId>.log.jsonl ← 完整对话日志(append-only,不被压缩)
context/:WorkspaceSession继承JsonSession,base 在agents/<agentId>/context/;sessionId 子目录里按SessionKey → {key}.json存每个StateModule。sessions/:SessionTree在一个 JSONL 里按id/parentId组成一棵树;另一份同名的<sessionId>.log.jsonl从不被压缩,供审计和session_search使用。
RuntimeContext 怎么让二者对齐¶
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("sess-001")
.userId("alice")
.build();
agent.call(msg, ctx).block();
HarnessAgent.bindRuntimeContext 会做几件事:
补默认:
session为空时使用构建时的defaultSession(默认是WorkspaceSession(workspace, agentId));sessionKey为空时依次试SimpleSessionKey.of(sessionId)→SimpleSessionKey.of(agentName)。传递到 hooks:
workspaceContextHook、memoryFlushHook、sessionPersistenceHook、compactionHook都会同步该 ctx,他们在 offload / saveTo 时都能读到sessionId。联动
userIdRef:AtomicReference<String>被顶下userId,默认NamespaceFactory → List.of(userId)会以该 userId 作为路径前缀,从而多租户透明隔离。预加载状态:若
session && sessionKey都有,调用delegate.loadIfExists覆盖当前 Memory。不存在则什么都不动。
默认与自定义 Session¶
// 1. 默认:什么都不传 → WorkspaceSession(workspace, agentId)
HarnessAgent.builder()
.name("MyAgent").model(model).workspace(workspace).build();
// 2. 使用任意指定路径的 JsonSession
HarnessAgent.builder()
...
.session(new JsonSession(Path.of("/custom/sessions")))
.build();
// 3. 调用时临时覆盖
agent.call(msg, RuntimeContext.builder()
.sessionId("sess-001")
.session(customSession)
.sessionKey(SimpleSessionKey.of("sess-001"))
.build())
.block();
多用户隔离的两个层面¶
会话层:
sessionId决定context/<sessionId>/与sessions/<sessionId>.jsonl独立。文件层:
userId+NamespaceFactory决定文件操作路径前缀(默认LocalFilesystemWithShell会读userIdRef)。
// 同一 agent 实例服务 alice / bob
agent.call(msg, RuntimeContext.builder().sessionId("alice-1").userId("alice").build()).block();
agent.call(msg, RuntimeContext.builder().sessionId("bob-1").userId("bob").build()).block();
// 两个会话状态、文件路径都互不干扰
会话索引¶
MemoryFlushManager.offloadMessages 调完后,WorkspaceManager.updateSessionIndex(agentId, sessionId, summary) 会合并写 sessions/sessions.json,agent 在另一轮可以走 session_list 工具查看“该 agent 历史上都跟谁聊过”。