AgentSession — Bash Execution(Bash 执行)

文件位置:F:\Pi\packages\coding-agent\src\core\agent-session.ts

Bash 结果定义:F:\Pi\packages\coding-agent\src\core\bash-executor.tsBashResultexecuteBashWithOperations()

Bash 操作接口:F:\Pi\packages\coding-agent\src\core\tools\bash.tsBashOperationscreateLocalBashOperations()

Bash 消息类型:F:\Pi\packages\coding-agent\src\core\messages.tsBashExecutionMessage

UI 消费:F:\Pi\packages\coding-agent\src\modes/interactive/interactive-mode.tshandleBashCommand()addMessageToChat()

编写目的:详细拆解 AgentSession 中 Bash Execution(Bash 执行)机制的完整实现,涵盖从用户输入 ! 命令到执行、流式输出、结果持久化的全链路。


一、Bash Execution 概述

Bash Execution 是 AgentSession 中负责执行用户和 LLM 的 bash 命令的模块。Pi 中 bash 执行有三个入口:

  1. 用户 !命令 — 用户在交互模式下输入 !ls 直接执行 bash,结果发给 LLM
  2. 用户 !!命令 — 同 ! 但结果也发给 LLM(标记 excludeFromContext: true
  3. LLM 的 bash 工具调用 — Agent 在思考过程中调用 bash 工具

核心消息类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export interface BashExecutionMessage {
  role: "bashExecution";                // 自定义角色
  command: string;                      // 执行的命令
  output: string;                       // 输出内容(可能被截断)
  exitCode: number | undefined;         // 退出码(被取消时为 undefined)
  cancelled: boolean;                   // 是否被取消
  truncated: boolean;                   // 输出是否被截断
  fullOutputPath?: string;              // 完整输出的临时文件路径(截断时)
  timestamp: number;                    // 时间戳
  excludeFromContext?: boolean;         // !! 前缀时不发给 LLM
}

Bash 执行结果

1
2
3
4
5
6
7
export interface BashResult {
  output: string;                       // 合并的 stdout + stderr
  exitCode: number | undefined;         // 退出码
  cancelled: boolean;                   // 是否被取消
  truncated: boolean;                   // 是否被截断
  fullOutputPath?: string;              // 完整输出文件路径
}

完整执行全景

用户输入 !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 命令

1
2
3
4
5
async executeBash(
  command: string,
  onChunk?: (chunk: string) => void,
  options?: { excludeFromContext?: boolean; operations?: BashOperations },
): Promise<BashResult>

位置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

参数说明

参数类型说明
commandstring要执行的 bash 命令
onChunk(chunk: string) => void可选,流式输出回调,每收到一块数据就调用,用于 UI 实时显示
options.excludeFromContextboolean | undefinedtrue 时结果不发送给 LLM(! 前缀行为)
options.operationsBashOperations | undefined自定义执行后端,如 SSH、Docker 远程执行

返回BashResult 包含输出文本、退出码、取消/截断状态。

设计意图operations 参数是实现远程执行的关键抽象。默认使用 createLocalBashOperations() 在本地 shell 中执行。Extension 可以通过 user_bash 事件拦截命令并返回自定义的 BashOperations,实现 SSH 远程执行、Docker 容器内执行等场景。


2.2 recordBashResult(command, result, options?) — 记录执行结果

1
2
3
4
5
recordBashResult(
  command: string,
  result: BashResult,
  options?: { excludeFromContext?: boolean }
): void

位置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() — 刷新待处理队列

1
private _flushPendingBashMessages(): void

位置agent-session.ts 第 2668 行

功能:将 _pendingBashMessages 队列中的所有消息一次性写入 agent state 和 session。

调用时机(3 处):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. _runAgentPrompt() 的 finally 块
private async _runAgentPrompt(messages): Promise<void> {
  try {
    await this.agent.prompt(messages);
    while (await this._handlePostAgentRun()) {
      await this.agent.continue();
    }
  } finally {
    this._flushPendingBashMessages();  // ★ 每次 prompt 结束后刷新
  }
}

// 2. prompt() 中发送新消息前(处理压缩后的重试)
await this._runAgentPrompt(messages);
// ...
this._flushPendingBashMessages();  // ★ 明确刷新

// 3. prompt() 中压缩后的继续循环
await this.agent.continue();
while (await this._handlePostAgentRun()) {
  await this.agent.continue();
}
this._flushPendingBashMessages();  // ★ 确保刷新

设计意图:多条 bash 消息可以积累在队列中,一次批量 flush。这避免了多次循环调用 agent.state.messages.push()sessionManager.appendMessage() 带来的性能开销和事件风暴。


2.4 abortBash() — 取消正在执行的命令

1
abortBash(): void

位置agent-session.ts 第 2659 行

1
2
3
abortBash(): void {
  this._bashAbortController?.abort();
}

调用时机

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// dispose() 时清理
dispose(): void {
  this.abortRetry();
  this.abortCompaction();
  this.abortBranchSummary();
  this.abortBash();      // ★ 关闭 session 时取消 bash
  this.agent.abort();
}

// interactive-mode 中用户中断
if (this.session.isBashRunning) {
  this.session.abortBash();
}

2.5 isBashRunning / hasPendingBashMessages — 状态查询

1
2
3
4
5
6
7
get isBashRunning(): boolean {
  return this._bashAbortController !== undefined;
}

get hasPendingBashMessages(): boolean {
  return this._pendingBashMessages.length > 0;
}

用于 UI 层判断:

1
2
3
4
5
6
// interactive-mode.ts 中,用户按 Escape 时的处理
if (this.session.isBashRunning) {
  this.session.abortBash();          // 取消正在运行的 bash
} else if (this.isBashMode) {
  this.isBashMode = false;           // 退出 bash 模式
}

三、BashOperations 接口与执行后端

3.1 BashOperations 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export interface BashOperations {
  exec: (
    command: string,
    cwd: string,
    options: {
      onData: (data: Buffer) => void;      // 输出数据回调
      signal?: AbortSignal;                  // 取消信号
      timeout?: number;                      // 超时时间
      env?: NodeJS.ProcessEnv;               // 环境变量
    },
  ) => Promise<{ exitCode: number | null }>;
}

这是一个极其精简的接口——只有一个 exec 方法。这种设计使得任何执行后端都可以通过实现这 4 个参数集成进来

3.2 createLocalBashOperations() — 本地执行

1
2
3
export function createLocalBashOperations(
  options?: { shellPath?: string }
): BashOperations

位置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 用户输入触发

1
2
3
4
5
6
7
// 检测是否进入 bash 模式
this.isBashMode = text.trimStart().startsWith("!");   // ! 或 !! 开头

// bash 模式下的回车处理
private async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
  // ...
}

4.2 Extension 拦截点 — user_bash 事件

1
2
3
4
5
6
7
// 发送 user_bash 事件 → Extension 可拦截
const eventResult = await extensionRunner.emitUserBash({
  type: "user_bash",
  command,
  excludeFromContext,
  cwd: this.sessionManager.getCwd(),
});

Extension 可以通过三种方式响应该事件:

返回含义示例
result?: BashResultExtension 全权处理了执行,直接使用结果用于 SSH 执行后返回远程结果
operations?: BashOperations替换执行后端,仍由 AgentSession 执行用于 Docker 容器内执行
不返回使用默认的本地执行标准行为
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Extension 返回完整结果 → 跳过 executeBash()
if (eventResult?.result) {
  // 直接显示结果 + recordBashResult()
  this.session.recordBashResult(command, result, { excludeFromContext });
  return;
}

// 使用自定义 operations(或默认的本地执行)
const result = await this.session.executeBash(
  command,
  (chunk) => { /* 流式更新 UI */ },
  { excludeFromContext, operations: eventResult?.operations },
);

4.3 UI 实时展示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 创建 UI 组件
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);

// Streaming 中 → 放入"待处理"区域
if (isDeferred) {
  this.pendingMessagesContainer.addChild(this.bashComponent);
} else {
  // 空闲时 → 直接加入聊天区域
  this.chatContainer.addChild(this.bashComponent);
}

// 流式更新:每收到一块数据就追加到 UI
(chunk) => {
  this.bashComponent.appendOutput(chunk);
  this.ui.requestRender();
}

// 执行完成:显示退出码和截断状态
this.bashComponent.setComplete(result.exitCode, result.cancelled, ...);

4.4 历史消息渲染

当从会话文件恢复消息时,bashExecution 角色的消息会被渲染为相同的 UI 组件:

1
2
3
4
5
6
7
case "bashExecution": {
  const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
  if (message.output) component.appendOutput(message.output);
  component.setComplete(message.exitCode, message.cancelled, ...);
  this.chatContainer.addChild(component);
  break;
}

五、输出截断机制

Bash 执行输出的大小受 bash-executor.ts 中的 DEFAULT_MAX_BYTES 控制(默认 2MB)。

情况行为
输出 < 2MBoutput 字段包含完整内容,truncated = false
输出 > 2MBoutput 包含前 2MB,truncated = truefullOutputPath 指向临时文件
输出 > 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.tsgetShellCommandPrefix()获取 shell 命令前缀(如 shopt -s expand_aliases
settings-manager.tsgetShellPath()获取 shell 路径(如 /bin/bashpowershell.exe
bash-executor.tsexecuteBashWithOperations()实际执行 bash 的核心函数
bash-executor.tsBashResult执行结果结构体
tools/bash.tsBashOperations执行后端的抽象接口
tools/bash.tscreateLocalBashOperations()默认的本地执行后端
messages.tsBashExecutionMessage存储在 session 中的消息格式
extensions/index.tsUserBashEvent / UserBashEventResultExtension 拦截事件
extensions/runner.tsemitUserBash()发送 user_bash 事件
session-manager.tsgetCwd()获取当前工作目录
session-manager.tsappendMessage()持久化 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() 取消
流式 UIonChunk 回调使 UI 可以实时显示命令输出(类似终端效果)

一句话总结:Bash Execution 是 AgentSession 中连接用户 shell 的桥梁——通过 BashOperations 接口解耦执行后端,通过 _pendingBashMessages 队列解决 streaming 中的消息顺序问题,通过 user_bash 事件让 Extension 可以完全接管执行流程。