AgentSession — Bash Execution(Bash 执行)
文件位置:
F:\Pi\packages\coding-agent\src\core\agent-session.tsBash 结果定义:
F:\Pi\packages\coding-agent\src\core\bash-executor.ts—BashResult、executeBashWithOperations()Bash 操作接口:
F:\Pi\packages\coding-agent\src\core\tools\bash.ts—BashOperations、createLocalBashOperations()Bash 消息类型:
F:\Pi\packages\coding-agent\src\core\messages.ts—BashExecutionMessageUI 消费:
F:\Pi\packages\coding-agent\src\modes/interactive/interactive-mode.ts—handleBashCommand()、addMessageToChat()编写目的:详细拆解 AgentSession 中 Bash Execution(Bash 执行)机制的完整实现,涵盖从用户输入
!命令到执行、流式输出、结果持久化的全链路。
一、Bash Execution 概述
Bash Execution 是 AgentSession 中负责执行用户和 LLM 的 bash 命令的模块。Pi 中 bash 执行有三个入口:
- 用户
!命令— 用户在交互模式下输入!ls直接执行 bash,结果不发给 LLM - 用户
!!命令— 同!但结果也发给 LLM(标记excludeFromContext: true) - LLM 的
bash工具调用 — Agent 在思考过程中调用 bash 工具
核心消息类型
| |
Bash 执行结果
| |
完整执行全景
用户输入 !ls
│
▼
interactive-mode.handleBashCommand()
│
├─ 发送 user_bash 事件 → Extension 可劫持
│
├─ Extension 返回了完整结果?──是──→ recordBashResult() → 结束
│
├─ 创建 BashExecutionComponent(UI 组件)
│
├─ isStreaming?
│ ├─ 是 → 加入 pendingMessagesContainer(排队)
│ └─ 否 → 加入 chatContainer(立即显示)
│
└─ 调用 session.executeBash()
│
▼
agent-session.executeBash()
│
├─ 创建 _bashAbortController
├─ 应用 shell 命令前缀(如 alias 支持)
├─ 调用 executeBashWithOperations()
│ ├─ 本地执行 → createLocalBashOperations()
│ └─ 远程执行 → 自定义 operations(SSH/Docker)
│
├─ recordBashResult()
│ ├─ 构建 BashExecutionMessage
│ ├─ isStreaming? → 加入 _pendingBashMessages 队列
│ └─ 非 streaming → 立即追加到 agent.state + session
│
└─ 返回 BashResult
│
▼
UI 更新 → BashExecutionComponent.setComplete()
二、AgentSession 中的 Bash Execution 方法
2.1 executeBash(command, onChunk?, options?) — 执行 Bash 命令
| |
位置:agent-session.ts 第 2602 行
功能:执行 bash 命令,将结果添加到 agent 上下文和会话历史。支持流式回调、命令前缀(如 alias)、自定义 BashOperations(远程执行)。
执行流程:
executeBash(command, onChunk?, options?)
│
├─ 创建 _bashAbortController(用于取消)
│
├─ 获取 shell 命令前缀
│ settingsManager.getShellCommandPrefix()
│ 如 "shopt -s expand_aliases" 用于 alias 支持
│
├─ 拼接完整命令(prefix + \n + command)
│
├─ 获取 shell 路径
│ settingsManager.getShellPath()
│
├─ 调用 executeBashWithOperations()
│ ├─ command: 拼接后的命令
│ ├─ cwd: sessionManager.getCwd()
│ ├─ operations:
│ │ ├─ options?.operations(自定义,如 SSH 远程)
│ │ └─ createLocalBashOperations({ shellPath })(本地默认)
│ └─ options: { onChunk, signal: _bashAbortController.signal }
│
├─ 执行成功 → recordBashResult(command, result, options)
│
└─ finally → _bashAbortController = undefined
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
command | string | 要执行的 bash 命令 |
onChunk | (chunk: string) => void | 可选,流式输出回调,每收到一块数据就调用,用于 UI 实时显示 |
options.excludeFromContext | boolean | undefined | true 时结果不发送给 LLM(! 前缀行为) |
options.operations | BashOperations | undefined | 自定义执行后端,如 SSH、Docker 远程执行 |
返回:BashResult 包含输出文本、退出码、取消/截断状态。
设计意图:
operations参数是实现远程执行的关键抽象。默认使用createLocalBashOperations()在本地 shell 中执行。Extension 可以通过user_bash事件拦截命令并返回自定义的BashOperations,实现 SSH 远程执行、Docker 容器内执行等场景。
2.2 recordBashResult(command, result, options?) — 记录执行结果
| |
位置:agent-session.ts 第 2636 行
功能:将 bash 执行结果封装为 BashExecutionMessage 并写入会话历史。根据 agent 是否正在 streaming 决定立即追加还是排队等待。
执行逻辑:
recordBashResult(command, result, options)
│
├─ 构建 BashExecutionMessage
│ {
│ role: "bashExecution",
│ command, output, exitCode,
│ cancelled, truncated, fullOutputPath,
│ timestamp, excludeFromContext
│ }
│
├─ isStreaming?
│ ├─ 是 → _pendingBashMessages.push(bashMessage)
│ │ 排队,稍后在 agent_end 时刷新
│ │
│ └─ 否 → agent.state.messages.push(bashMessage)
│ sessionManager.appendMessage(bashMessage)
│ 立即追加到 agent 状态 + 会话文件
│
└─ 完成
为什么需要排队:当 agent 正在进行 streaming 时,消息顺序是严格保证的——tool_call 后面必须紧跟对应的 tool_result。如果在 streaming 中间插入一个 bashExecution 消息,会破坏 tool_use/tool_result 的配对关系。所以必须等 agent 的一个完整 turn 结束后再 flush。
2.3 _flushPendingBashMessages() — 刷新待处理队列
| |
位置:agent-session.ts 第 2668 行
功能:将 _pendingBashMessages 队列中的所有消息一次性写入 agent state 和 session。
调用时机(3 处):
| |
设计意图:多条 bash 消息可以积累在队列中,一次批量 flush。这避免了多次循环调用
agent.state.messages.push()和sessionManager.appendMessage()带来的性能开销和事件风暴。
2.4 abortBash() — 取消正在执行的命令
| |
位置:agent-session.ts 第 2659 行
| |
调用时机:
| |
2.5 isBashRunning / hasPendingBashMessages — 状态查询
| |
用于 UI 层判断:
| |
三、BashOperations 接口与执行后端
3.1 BashOperations 接口
| |
这是一个极其精简的接口——只有一个 exec 方法。这种设计使得任何执行后端都可以通过实现这 4 个参数集成进来。
3.2 createLocalBashOperations() — 本地执行
| |
位置:F:\Pi\packages\coding-agent\src\core\tools\bash.ts 第 66 行
功能:创建使用 Pi 内置本地 shell 的 BashOperations。这是默认的执行后端。
执行细节:
createLocalBashOperations()
│
├─ 解析 shell 路径(来自 settings 或系统默认)
│
├─ 检查 cwd 是否存在(不存在则抛错)
│
├─ 根据 commandTransport 选择执行方式:
│ ├─ "stdin" → 通过子进程 stdin 传入命令
│ └─ "arg" → 通过 -c 参数传入命令
│
└─ 返回 { exec: async (command, cwd, options) => { ... } }
四、Interactive Mode 中的 Bash 执行
4.1 用户输入触发
| |
4.2 Extension 拦截点 — user_bash 事件
| |
Extension 可以通过三种方式响应该事件:
| 返回 | 含义 | 示例 |
|---|---|---|
result?: BashResult | Extension 全权处理了执行,直接使用结果 | 用于 SSH 执行后返回远程结果 |
operations?: BashOperations | 替换执行后端,仍由 AgentSession 执行 | 用于 Docker 容器内执行 |
| 不返回 | 使用默认的本地执行 | 标准行为 |
| |
4.3 UI 实时展示
| |
4.4 历史消息渲染
当从会话文件恢复消息时,bashExecution 角色的消息会被渲染为相同的 UI 组件:
| |
五、输出截断机制
Bash 执行输出的大小受 bash-executor.ts 中的 DEFAULT_MAX_BYTES 控制(默认 2MB)。
| 情况 | 行为 |
|---|---|
| 输出 < 2MB | output 字段包含完整内容,truncated = false |
| 输出 > 2MB | output 包含前 2MB,truncated = true,fullOutputPath 指向临时文件 |
| 输出 > 4MB | 超过 DEFAULT_MAX_BYTES * 2 后完全丢弃写入(只保留截断部分) |
这种设计确保:
- LLM 上下文不会爆炸:超长输出不会占用 LLM 的 context window
- 用户不丢数据:完整输出保存在临时文件中,用户随时可以查看
六、与 Stream 模式的交互
Bash Execution 与 Agent Streaming 的交互是最容易出错的地方。AgentSession 用消息队列解决这个问题:
Agent Streaming 中:
┌─────────────────────────────────────────┐
│ assistant message (正在生成) │
│ ├─ toolCall: bash("ls") │
│ ├─ toolCall: read("file.ts") │
│ └─ toolCall: edit("file.ts") │
├─────────────────────────────────────────┤
│ 用户输入 !git status │
│ └─ bashExecution → _pendingBashMessages│ ← 排队
├─────────────────────────────────────────┤
│ tool_result: bash (来自 LLM 调用) │
│ tool_result: read │
│ tool_result: edit │
├─────────────────────────────────────────┤
│ agent_end │
│ → _flushPendingBashMessages() │ ← 刷新队列
│ → bashExecution 消息追加到 state │
│ → 显示在 UI 中 │
└─────────────────────────────────────────┘
为什么不直接用 steer/followUp 处理用户 bash?:因为 bash 不是 LLM 消息,不需要经过 agent 的 LLM 调用。它是用户直接与 shell 交互的通道。放入队列的机制只是保证消息顺序正确,不干扰 LLM 的 tool_use/tool_result 配对。
七、外部依赖接口
| 模块 | 方法/类型 | 在 Bash Execution 中的角色 |
|---|---|---|
settings-manager.ts | getShellCommandPrefix() | 获取 shell 命令前缀(如 shopt -s expand_aliases) |
settings-manager.ts | getShellPath() | 获取 shell 路径(如 /bin/bash、powershell.exe) |
bash-executor.ts | executeBashWithOperations() | 实际执行 bash 的核心函数 |
bash-executor.ts | BashResult | 执行结果结构体 |
tools/bash.ts | BashOperations | 执行后端的抽象接口 |
tools/bash.ts | createLocalBashOperations() | 默认的本地执行后端 |
messages.ts | BashExecutionMessage | 存储在 session 中的消息格式 |
extensions/index.ts | UserBashEvent / UserBashEventResult | Extension 拦截事件 |
extensions/runner.ts | emitUserBash() | 发送 user_bash 事件 |
session-manager.ts | getCwd() | 获取当前工作目录 |
session-manager.ts | appendMessage() | 持久化 bash 消息到会话文件 |
八、设计总结
| 方面 | 设计 |
|---|---|
| 双通道分离 | 用户 !命令 走 executeBash() + recordBashResult();LLM bash 工具调用走 Agent 的 tool_execution 机制,两者互不干扰 |
| 消息顺序保证 | Streaming 中的用户 bash 通过 _pendingBashMessages 排队,agent_end 时一次性 flush |
| 可插拔后端 | BashOperations 接口使远程执行(SSH、Docker)成为一等公民 |
| Extension 可劫持 | user_bash 事件让 Extension 可以完全接管 bash 执行或替换执行后端 |
| 输出截断保护 | 超过 2MB 自动截断,完整输出存临时文件,兼顾 LLM 上下文窗口和用户数据完整性 |
| 命令前缀支持 | 通过 getShellCommandPrefix() 在命令前注入 shell 配置(如 alias 展开) |
| 取消能力 | AbortController 使正在执行的 bash 命令可以被 abortBash() 取消 |
| 流式 UI | onChunk 回调使 UI 可以实时显示命令输出(类似终端效果) |
一句话总结:Bash Execution 是 AgentSession 中连接用户 shell 的桥梁——通过 BashOperations 接口解耦执行后端,通过 _pendingBashMessages 队列解决 streaming 中的消息顺序问题,通过 user_bash 事件让 Extension 可以完全接管执行流程。