docs/plan/2026-05-29-shell-session-design.md
Rama main · réplica de lectura del repositorio original.
# Shell 会话系统技术方案
> Note: The `bash` tool has been removed. `shell` is now the only command execution tool. See `2026-06-03-shell-unification-design.md`.
## 一、背景与目标
### 1.1 当前实现的局限
原有 `BashTool`(已移除)采用无状态设计,每次调用都创建新进程:
```java
ProcessBuilder pb = new ProcessBuilder("bash", "-c", command);
```
这导致:
- **状态丢失**:`cd /tmp` 后再 `ls` 无法感知目录变化
- **环境隔离**:无法维持环境变量、虚拟环境等
- **交互缺失**:不支持 `top`、`python -i` 等交互式程序
- **输出截断**:超过 50000 字符直接丢弃,无法回溯
### 1.2 设计目标
将 Shell 从"一次性命令执行器"升级为"有状态的交互式子系统":
1. **会话持久化**:每个 Agent 对话拥有独立 Shell 会话,状态持续存在
2. **双模式支持**:普通命令(同步)+ 交互式程序(PTY)
3. **输出管理**:截断预览 + 完整落盘,不丢失信息
4. **向后兼容**:原有 BashTool 调用方式已迁移至 ShellSessionTool
---
## 二、架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ AgentLoop │
│ │ │
│ ToolDispatcher │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ │ │
│ BashTool (已移除) ShellSessionTool (当前) │
│ │ │
│ ShellSessionManager │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ShellSession ShellSession ShellSession │
│ (普通模式) (PTY模式) (...) │
│ │ │ │ │
│ Process PTY Process ... │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 核心组件
| 组件 | 职责 |
|------|------|
| `ShellSessionManager` | 会话生命周期管理,会话池 |
| `ShellSession` | 单个会话的状态封装(进程、工作目录、环境变量) |
| `ShellSessionTool` | 新的 Tool 实现,暴露给 LLM |
| `OutputManager` | 输出截断、落盘、格式化 |
| `PathSandbox` | 复用现有路径安全校验 |
---
## 三、核心组件详细设计
### 3.1 ShellSession — 会话实体
```java
public class ShellSession {
private final String sessionId; // 会话唯一标识
private final Long conversationId; // 关联的 Agent 对话 ID
private final boolean ptyMode; // 是否 PTY 模式
private final Process process; // bash 进程
private final BufferedWriter stdin; // 标准输入流
private final BufferedReader stdout; // 标准输出流
private final Path workspaceDir; // 工作目录
private final Path outputFile; // 输出落盘文件
private final Instant createdAt; // 创建时间
private Instant lastActiveAt; // 最后活跃时间
private volatile boolean alive = true; // 会话是否存活
// 状态信息
private String currentWorkdir; // 当前工作目录(通过 pwd 追踪)
private Map<String, String> envVars; // 环境变量快照
}
```
**关键设计点**:
1. **进程复用**:启动 `bash --norc --noprofile` 保持运行,通过 stdin/stdout 交互
2. **工作目录追踪**:每次命令执行后自动注入 `pwd` 获取当前目录
3. **输出落盘**:所有输出 append 到文件,返回时截断预览
### 3.2 ShellSessionManager — 会话管理器
```java
@Component
public class ShellSessionManager {
// 会话池:sessionId -> ShellSession
private final ConcurrentHashMap<String, ShellSession> sessions = new ConcurrentHashMap<>();
// 按 conversationId 分组索引
private final ConcurrentHashMap<Long, Set<String>> conversationSessions = new ConcurrentHashMap<>();
// 配置
private static final int MAX_SESSIONS_PER_CONVERSATION = 5;
private static final Duration SESSION_IDLE_TIMEOUT = Duration.ofMinutes(30);
private static final Duration SESSION_MAX_LIFETIME = Duration.ofHours(2);
/**
* 获取或创建会话
*/
public ShellSession getOrCreate(Long conversationId, String sessionId, boolean ptyMode) {
// 1. 如果 sessionId 已存在且存活,直接返回
// 2. 如果 sessionId 不存在,创建新会话
// 3. 检查会话数限制
// 4. 启动 bash 进程
}
/**
* 关闭指定会话
*/
public void close(String sessionId) {
// 1. 关闭进程
// 2. 从 sessions 移除
// 3. 从 conversationSessions 索引移除
}
/**
* 关闭某个对话的所有会话
*/
public void closeByConversation(Long conversationId) {
// 遍历 conversationSessions.get(conversationId) 逐个关闭
}
/**
* 清理过期会话(定时任务调用)
*/
@Scheduled(fixedRate = 60000)
public void cleanupExpiredSessions() {
// 遍历 sessions,关闭 idle > 30min 或 lifetime > 2h 的会话
}
}
```
### 3.3 OutputManager — 输出管理器
```java
@Component
public class OutputManager {
private static final int MAX_PREVIEW_LINES = 100;
private static final int MAX_PREVIEW_CHARS = 10000;
private static final String OUTPUT_DIR = "shellOutput";
/**
* 读取进程输出,返回截断预览 + 完整落盘
*/
public OutputResult readOutput(BufferedReader reader, Path outputFile, Duration yieldTime) {
// 1. 等待 yieldTime(默认 10s),让命令有时间产生输出
// 2. 非阻塞读取所有可用输出
// 3. 写入 outputFile(append 模式)
// 4. 如果超过阈值,只返回尾部 N 行/字符
// 5. 返回 OutputResult(preview, fullWritten, totalLines)
}
/**
* 格式化工具返回结果
*/
public String formatToolResult(int exitCode, String sessionId, long elapsedMs,
OutputResult output, String currentWorkdir) {
return String.format("""
exit_code: %d
session_id: %s
elapsed_ms: %d
current_workdir: %s
output_lines: %d
truncated: %s
---
%s""",
exitCode, sessionId, elapsedMs, currentWorkdir,
output.totalLines(), output.truncated(), output.preview());
}
}
```
### 3.4 ShellSessionTool — 新工具实现
```java
@Component
public class ShellSessionTool implements Tool {
private final ShellSessionManager sessionManager;
private final OutputManager outputManager;
private final PathSandbox pathSandbox;
private final ObjectMapper objectMapper;
@Override
public String getName() {
return "shell"; // 新工具名,与原有 bash 区分
}
@Override
public String getDescription() {
return """
Interactive shell tool with session persistence.
Actions:
- exec: Execute a command in a shell session
- write_stdin: Write input to an interactive session
- close: Close a shell session
- list: List active sessions for current conversation
""";
}
@Override
public Map<String, Object> getInputSchema() {
// 返回 JSON Schema,包含 action、session_id、command 等参数
}
@Override
public String execute(String arguments, Long sessionId, String workspace) {
JsonNode args = objectMapper.readTree(arguments);
String action = args.get("action").asText();
return switch (action) {
case "exec" -> handleExec(args, sessionId, workspace);
case "write_stdin" -> handleWriteStdin(args, sessionId, workspace);
case "close" -> handleClose(args, sessionId);
case "list" -> handleList(sessionId);
default -> errorJson("Unknown action: " + action);
};
}
private String handleExec(JsonNode args, Long conversationId, String workspace) {
// 1. 解析参数:session_id(可选)、command、workdir(可选)、pty(可选)
// 2. 获取或创建 ShellSession
// 3. 如果有 workdir,先执行 cd
// 4. 写入命令到 stdin
// 5. 读取输出(带 yieldTime)
// 6. 追踪当前工作目录(注入 pwd)
// 7. 格式化返回
}
}
```
---
## 四、交互模式设计
### 4.1 普通模式(pty=false)
适用场景:大多数命令,直接获取结果
```
Agent 调用: shell(action="exec", command="ls -la", session_id="s1")
↓
ShellSession:
stdin.write("ls -la\n")
stdin.write("pwd\n") // 自动注入,追踪目录
stdout.readWithTimeout(10s)
↓
返回: {exit_code: 0, current_workdir: "/tmp", output: "..."}
```
### 4.2 PTY 模式(pty=true)
适用场景:交互式程序(top、python -i、vim 等)
```
Agent 调用: shell(action="exec", command="python3 -i", session_id="s2", pty=true)
↓
ShellSession:
启动 PTY 进程
stdout.readWithTimeout(5s) // 读取初始输出(Python banner)
↓
返回: {session_id: "s2", output: "Python 3.11.0\n>>>"}
Agent 调用: shell(action="write_stdin", session_id="s2", input="print('hello')\n")
↓
ShellSession:
stdin.write("print('hello')\n")
stdout.readWithTimeout(5s)
↓
返回: {session_id: "s2", output: "hello\n>>>"}
```
### 4.3 命令执行流程对比
| 步骤 | BashTool (已移除) | ShellSessionTool (当前) |
|------|--------------|---------------------|
| 1. 接收命令 | `execute(command)` (已移除) | `exec(command, session_id)` |
| 2. 创建进程 | 每次新建 | 复用已有会话 |
| 3. 执行命令 | `bash -c command` | `stdin.write(command)` |
| 4. 读取输出 | `process.getInputStream()` | `stdout.readWithTimeout()` |
| 5. 返回结果 | 截断到 50000 字符 | 预览 + 落盘 |
| 6. 清理 | `process.destroy()` | 保持会话存活 |
---
## 五、输出管理设计
### 5.1 输出截断策略
```
原始输出(可能 100000+ 字符)
↓
┌───┴───┐
│ │
预览 完整输出
(返回给AI) (落盘到文件)
│
├── 尾部 100 行
└── 最多 10000 字符
```
### 5.2 文件落盘结构
```
workspace/
└── shellOutput/
├── {sessionId}_1.out # 第 1 次命令输出
├── {sessionId}_2.out # 第 2 次命令输出
└── {sessionId}_N.out # 第 N 次命令输出
```
Agent 可通过 `read_file` 工具查看完整输出:
```
read_file(path="shellOutput/s1_3.out")
```
### 5.3 返回格式
```json
{
"exit_code": 0,
"session_id": "s1",
"elapsed_ms": 156,
"current_workdir": "/tmp/project",
"output_lines": 245,
"truncated": true,
"output_file": "shellOutput/s1_3.out",
"output": "(尾部 100 行预览)..."
}
```
---
## 六、数据库设计
### 6.1 新增表:shell_session
```sql
CREATE TABLE IF NOT EXISTS `shell_session` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`session_id` VARCHAR(64) NOT NULL UNIQUE COMMENT '会话唯一标识',
`conversation_id` BIGINT NOT NULL COMMENT '关联的会话 ID',
`pty_mode` TINYINT DEFAULT 0 COMMENT '是否 PTY 模式 0-否 1-是',
`pid` INT COMMENT '进程 PID',
`current_workdir` VARCHAR(512) COMMENT '当前工作目录',
`status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/CLOSED/EXPIRED',
`command_count` INT DEFAULT 0 COMMENT '已执行命令数',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`last_active_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`closed_at` DATETIME,
INDEX `idx_conversation` (`conversation_id`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 6.2 新增表:shell_command_log
```sql
CREATE TABLE IF NOT EXISTS `shell_command_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`session_id` VARCHAR(64) NOT NULL COMMENT '会话 ID',
`conversation_id` BIGINT NOT NULL COMMENT '会话 ID',
`command` TEXT NOT NULL COMMENT '执行的命令',
`exit_code` INT COMMENT '退出码',
`output_lines` INT COMMENT '输出行数',
`output_file` VARCHAR(512) COMMENT '输出文件路径',
`truncated` TINYINT DEFAULT 0 COMMENT '是否截断',
`elapsed_ms` INT COMMENT '执行耗时(毫秒)',
`executed_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_session` (`session_id`),
INDEX `idx_conversation` (`conversation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
---
## 七、API 设计
### 7.1 Tool Schema(供 LLM 调用)
```json
{
"name": "shell",
"description": "Interactive shell tool with session persistence. Supports multi-step operations and interactive programs.",
"input_schema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["exec", "write_stdin", "close", "list"],
"description": "Action to perform"
},
"session_id": {
"type": "string",
"description": "Session ID. Omit to create new session, or provide to reuse existing."
},
"command": {
"type": "string",
"description": "Shell command to execute (for exec action)"
},
"input": {
"type": "string",
"description": "Input to write to stdin (for write_stdin action)"
},
"workdir": {
"type": "string",
"description": "Working directory (relative to workspace)"
},
"pty": {
"type": "boolean",
"description": "Use PTY mode for interactive programs (default false)"
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds (default 30)"
},
"yield_time_ms": {
"type": "integer",
"description": "Initial wait time for output (default 10000)"
}
},
"required": ["action"]
}
}
```
### 7.2 调用示例
**普通命令**:
```json
{
"action": "exec",
"command": "npm install",
"workdir": "frontend"
}
```
**交互式程序**:
```json
// 第一次调用:启动 Python
{"action": "exec", "command": "python3 -i", "pty": true}
// 后续调用:发送输入
{"action": "write_stdin", "session_id": "s1", "input": "import pandas as pd\n"}
{"action": "write_stdin", "session_id": "s1", "input": "df = pd.read_csv('data.csv')\n"}
{"action": "write_stdin", "session_id": "s1", "input": "df.head()\n"}
// 关闭会话
{"action": "close", "session_id": "s1"}
```
---
## 八、安全设计
### 8.1 路径安全
复用现有 `PathSandbox`,所有路径操作必须经过校验:
```java
Path resolvedWorkdir = pathSandbox.resolve(workdir, workspace);
```
### 8.2 命令白名单(可选)
可配置允许/禁止的命令模式:
```yaml
app:
harness:
shell:
allowed-commands:
- "npm *"
- "git *"
- "python3 *"
blocked-commands:
- "rm -rf /"
- "sudo *"
```
### 8.3 资源限制
| 资源 | 限制 |
|------|------|
| 每对话最大会话数 | 5 |
| 会话空闲超时 | 30 分钟 |
| 会话最大生命周期 | 2 小时 |
| 单命令最大输出 | 100 MB(落盘) |
| 预览输出大小 | 10000 字符 |
### 8.4 会话清理
```java
@Scheduled(fixedRate = 60000) // 每分钟检查
public void cleanupExpiredSessions() {
sessions.forEach((id, session) -> {
if (session.isExpired() || session.isIdleTimeout()) {
session.close();
sessions.remove(id);
}
});
}
```
---
## 九、与现有系统的集成
### 9.1 ToolDispatcher 路由
```java
public String dispatch(String toolName, String arguments, String executionMode,
Long sessionId, String workspace) {
// 新增 shell 工具路由
if ("shell".equals(toolName)) {
Tool tool = toolRegistry.getTool("shell");
return tool.execute(arguments, sessionId, workspace);
}
// 原有逻辑保持不变
if ("LOCAL".equals(executionMode)) {
return localToolExecutor.execute(sessionId, toolName, arguments, workspace);
}
Tool tool = toolRegistry.getTool(toolName);
return tool.execute(arguments, sessionId, workspace);
}
```
### 9.2 AgentLoop 集成
在 AgentLoop 的会话结束时清理 Shell 会话:
```java
public void execute(AgentExecutionContext context, AgentEventListener listener,
MessagePersistenceCallback persistenceCallback) {
try {
// ... 原有逻辑 ...
} finally {
// 清理该对话的所有 Shell 会话
shellSessionManager.closeByConversation(context.getSessionId());
}
}
```
### 9.3 PromptEngine 集成
在系统提示词中注入 Shell 工具使用指南:
```java
public String buildSystemPrompt(AgentExecutionContext context) {
StringBuilder sb = new StringBuilder();
// ... 原有逻辑 ...
// 如果 Agent 配置了 shell 工具,注入使用指南
if (context.getToolNames().contains("shell")) {
sb.append("\n## Shell Tool Usage\n");
sb.append(shellUsageGuide); // 从 skill 文档加载
}
return sb.toString();
}
```
---
## 十、改造清单
### 10.1 新增文件
| 文件路径 | 说明 |
|---------|------|
| `backend/.../harness/shell/ShellSession.java` | 会话实体类 |
| `backend/.../harness/shell/ShellSessionManager.java` | 会话管理器 |
| `backend/.../harness/shell/OutputManager.java` | 输出管理器 |
| `backend/.../harness/tool/impl/ShellSessionTool.java` | 新 Tool 实现 |
| `backend/.../harness/shell/ShellUsageGuide.md` | 使用指南(Skill 文档) |
| `backend/src/main/resources/db/migration/V011__shell_session.sql` | 数据库迁移 |
### 10.2 修改文件
| 文件路径 | 修改内容 |
|---------|---------|
| `backend/.../harness/tool/ToolRegistry.java` | 注册 ShellSessionTool |
| `backend/.../harness/init/ToolDataInitializer.java` | 初始化 shell 工具数据 |
| `backend/.../harness/core/AgentLoop.java` | 会话结束时清理 Shell 会话 |
| `backend/.../harness/core/PromptEngine.java` | 注入 Shell 使用指南 |
| `backend/.../harness/tool/ToolDispatcher.java` | 添加 shell 工具路由(可选) |
### 10.3 配置文件
```yaml
# application.yml 新增配置
app:
harness:
shell:
enabled: true
max-sessions-per-conversation: 30
session-idle-timeout-minutes: 30
session-max-lifetime-hours: 2
default-yield-time-ms: 10000
output:
max-preview-lines: 100
max-preview-chars: 10000
dir: shellOutput
```
---
## 十一、实施步骤
### Phase 1:基础框架(1-2 天)
1. 创建 `ShellSession` 实体类
2. 实现 `ShellSessionManager` 基础会话管理
3. 实现 `OutputManager` 输出截断和落盘
4. 编写数据库迁移脚本
### Phase 2:工具实现(2-3 天)
5. 实现 `ShellSessionTool` 的 `exec` action(普通模式)
6. 实现 `write_stdin` 和 `close` action
7. 集成到 `ToolRegistry` 和 `ToolDataInitializer`
8. 编写单元测试
### Phase 3:高级特性(2-3 天)
9. 实现 PTY 模式支持
10. 实现会话过期清理定时任务
11. 集成到 `AgentLoop` 会话生命周期
12. 编写 Skill 使用指南文档
### Phase 4:测试与优化(1-2 天)
13. 集成测试:多轮对话、会话复用、输出落盘
14. 性能测试:并发会话、长时间运行
15. 安全测试:路径逃逸、命令注入
16. 文档更新
---
## 十二、风险与注意事项
### 12.1 风险点
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 进程泄漏 | 内存/文件描述符耗尽 | 严格的超时和清理机制 |
| PTY 资源消耗 | 高并发下资源不足 | 限制每对话会话数 |
| 状态不可见 | Agent 忘记当前目录 | 每次返回 current_workdir |
| 输出过大 | 磁盘空间耗尽 | 限制单命令输出大小 |
### 12.2 向后兼容
- `BashTool` 已移除,`shell` 工具是唯一的命令执行工具
- 现有 Agent 已迁移到 `shell` 工具
### 12.3 性能考量
- 普通模式:与原有 BashTool 性能相当
- PTY 模式:每个会话占用一个 PTY 文件描述符
- 输出落盘:异步写入,不阻塞返回
---
## 十三、后续扩展
### 13.1 可能的增强
1. **命令历史**:记录会话内的命令历史,支持上下文感知
2. **环境变量管理**:支持设置/获取环境变量
3. **后台任务集成**:长时间命令自动转为后台任务
4. **会话持久化**:支持跨对话的会话恢复(需评估安全性)
5. **多用户隔离**:不同用户的会话完全隔离
### 13.2 与 LOCAL 模式的集成
ShellSessionTool 也可支持 LOCAL 模式,将命令转发到桌面客户端执行:
```
ShellSessionTool → LocalToolExecutor → WebSocket → Electron
```
这需要在桌面客户端实现对应的 Shell 会话管理逻辑。