概述

prompt() 及相关方法是 AgentSession核心入口,位于 agent-session.ts 第 759-1200 行。它处理从用户输入到 agent 调用的完整链路,包括扩展拦截、技能展开、模板替换、流式排队、后处理链(重试/压缩/队列消费)等。

一句话定位:prompting 系统是 AgentSession 的请求前端 — 连接用户输入与 agent-core 的 LLM 循环,并在此过程中提供扩展集成、技能注入、消息排队的枢纽。

核心职责

职责领域说明
消息入口接收用户文本,执行七步预处理管道
扩展命令路由拦截 /command 并路由到 ExtensionRunner
技能/模板展开/skill:name 和提示模板展开为完整内容
流式排队streaming 状态下将消息排队为 steer(中断)或 followUp(等待)
后处理链agent 停止后按序检查:重试 → 压缩 → 队列消费
自定义消息扩展通过 sendCustomMessage 发送非 LLM 消息
Bash 结果延迟刷新流式处理中 bash 结果排队等待,在 turn 结束时统一注入
队列消费跟踪_handleAgentEvent 中检测 steer/followUp 队列的消息何时被 agent 消费

架构位置

用户输入 text
    │
    ▼
AgentSession.prompt()  ←── 本文件重点
    │
    ├── 扩展命令?→ _tryExecuteExtensionCommand() (直接执行)
    ├── input 事件 → ExtensionRunner (拦截/转换)
    ├── 技能展开 → _expandSkillCommand()
    ├── 模板展开 → expandPromptTemplate()
    ├── 流式中?→ steer/followUp 排队
    ├── 认证检查 → ModelRegistry
    ├── 预压缩检查 → _checkCompaction()
    ├── before_agent_start 事件 → 扩展注入 systemPrompt/messages
    │
    ▼
_runAgentPrompt()  ←── 运行循环
    │
    ▼
agent.prompt() / agent.continue()  (agent-core 内部循环)
    │
    ▼
_handlePostAgentRun()  ←── 后处理链
    ├── 重试?→ _prepareRetry() → 指数退避
    ├── 压缩?→ _runAutoCompaction() → LLM 摘要
    └── 队列?→ agent.hasQueuedMessages() → continue

逐段解读

prompt() 方法

1
async prompt(text: string, options?: PromptOptions): Promise<void> {

这是 AgentSession 的主入口方法,覆盖了从纯文本到可发给 agent-core 的消息数组的完整转换过程。

完整的 try 块控制流

prompt() 方法的主体是一个大 try 块,内部包含 4 个 early return 点3 个 preflightResult?.(true) 调用1 个 preflightResult?.(false)1 个嵌套的 try/finally。以下是逐段分析:

1
2
3
4
5
6
async prompt(text: string, options?: PromptOptions): Promise<void> {
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
  const preflightResult = options?.preflightResult;
  let messages: AgentMessage[] | undefined;

  try {

入口变量

变量来源默认值
expandPromptTemplatesoptions.expandPromptTemplatestrue — 默认展开模板
preflightResultoptions.preflightResultundefined — RPC 模式才提供
messages稍后构建undefined — try 块内被赋值

段 1:扩展命令拦截(Step 1)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    // Handle extension commands first (execute immediately, even during streaming)
    // Extension commands manage their own LLM interaction via pi.sendMessage()
    if (expandPromptTemplates && text.startsWith("/")) {
      const handled = await this._tryExecuteExtensionCommand(text);
      if (handled) {
        // Extension command executed, no prompt to send
        preflightResult?.(true);    // ───┐
        return;                     //    │ [Return 1]
      }                             //    │ 扩展命令已处理,无需发送 prompt
    }                               // ───┘

expandPromptTemplatestrue 时才会进入此分支。sendUserMessage() 传入 expandPromptTemplates: false,所以扩展发送的消息不会被当作命令拦截。

另一点:这里即使 isStreaming === true 也会立即执行扩展命令。注释说得很清楚 — 扩展命令自己管理 LLM 交互(通过 pi.sendMessage()),所以不需要排队。

preflightResult?.(true) 在此处调用:命令找到并被执行,prompt() 提前返回,不需要后续的模型检查。


段 2:input 事件拦截(Step 2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    let currentText = text;
    let currentImages = options?.images;
    if (this._extensionRunner.hasHandlers("input")) {
      const inputResult = await this._extensionRunner.emitInput(
        currentText,
        currentImages,
        options?.source ?? "interactive",   // source 参数:区分交互/扩展来源
        this.isStreaming ? options?.streamingBehavior : undefined,
      );
      if (inputResult.action === "handled") {
        preflightResult?.(true);    // ───┐
        return;                     //    │ [Return 2]
      }                             //    │ 扩展在 input 事件中处理了消息
      if (inputResult.action === "transform") {
        currentText = inputResult.text;
        currentImages = inputResult.images ?? currentImages;
      }
    }

设计:input 事件发生在技能/模板展开之前,所以扩展看到的是原始的 text。扩展可以返回 handled(消耗消息)、transform(修改内容)、或不处理。

source 参数传给 input 事件 handler。sendUserMessage() 传入 "extension",而用户交互默认用 "interactive"。这样扩展可以根据消息来源决定行为。

streamingBehavior 也在 input 事件中传递,让扩展知道消息会被如何排队。

ExtensionRunner 参见 ./packages/coding-agent/src/core/extensions/runner.ts,它管理扩展的事件注册和触发。它的 emitInput() 方法会遍历所有注册的 input handler,按顺序调用它们,并处理返回的 handledtransform 结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	/** Emit input event. Transforms chain, "handled" short-circuits. */
	async emitInput(
		text: string,
		images: ImageContent[] | undefined,
		source: InputSource,
		streamingBehavior?: "steer" | "followUp",
	): Promise<InputEventResult> {
		const ctx = this.createContext();
		let currentText = text;
		let currentImages = images;

		for (const ext of this.extensions) {
			for (const handler of ext.handlers.get("input") ?? []) {
				try {
					const event: InputEvent = {
						type: "input",
						text: currentText,
						images: currentImages,
						source,
						streamingBehavior,
					};
					const result = (await handler(event, ctx)) as InputEventResult | undefined;
					if (result?.action === "handled") return result;
					if (result?.action === "transform") {
						currentText = result.text;
						currentImages = result.images ?? currentImages;
					}
				} catch (err) {
					this.emitError({
						extensionPath: ext.path,
						event: "input",
						error: err instanceof Error ? err.message : String(err),
						stack: err instanceof Error ? err.stack : undefined,
					});
				}
			}
		}
		return currentText !== text || currentImages !== images
			? { action: "transform", text: currentText, images: currentImages }
			: { action: "continue" };
	}

段 3:技能/模板展开(Step 3)

1
2
3
4
5
    let expandedText = currentText;
    if (expandPromptTemplates) {
      expandedText = this._expandSkillCommand(expandedText);
      expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
    }

顺序:先展开 /skill:name_expandSkillCommand),再展开 {{template}}expandPromptTemplate)。技能展开可能引入新的文本,其中可能包含模板变量,所以顺序不能颠倒。


段 4:流式排队(Step 4)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    if (this.isStreaming) {
      if (!options?.streamingBehavior) {
        throw new Error(
          "Agent is already processing. Specify streamingBehavior " +
          "('steer' or 'followUp') to queue the message.",
        );
      }
      if (options.streamingBehavior === "followUp") {
        await this._queueFollowUp(expandedText, currentImages);
      } else {
        await this._queueSteer(expandedText, currentImages);
      }
      preflightResult?.(true);    // ───┐
      return;                     //    │ [Return 3]
    }                             // ───┘ 消息已排队,无需发送

必须指定 streamingBehavior:如果 isStreaming 但未提供 streamingBehavior,直接抛错。这防止了用户在流式处理中发送消息时意外丢失。


段 5:认证检查(Step 5)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    // Flush any pending bash messages before the new prompt
    this._flushPendingBashMessages();

    if (!this.model) {
      throw new Error(formatNoModelSelectedMessage());
    }

    if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
      const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
      if (isOAuth) {
        throw new Error(
          `Authentication failed for "${this.model.provider}". ` +
          `Credentials may have expired or network is unavailable. ` +
          `Run '/login ${this.model.provider}' to re-authenticate.`,
        );
      }
      throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
    }

_flushPendingBashMessages() 的位置:在认证检查之前,确保所有 pending 的 bash 结果都已经注入 agent state。

两种认证失败:无模型(formatNoModelSelectedMessage)和无 auth(hasConfiguredAuth)。OAuth 和 API key 有各自的错误提示路径。


段 6:预压缩(Step 6)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    const lastAssistant = this._findLastAssistantMessage();
    if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
      try {
        await this.agent.continue();
        while (await this._handlePostAgentRun()) {
          await this.agent.continue();
        }
      } finally {
        this._flushPendingBashMessages();
      }
    }

嵌套的 try/finally:这里用了一个内部 try/finally,与外面的 try/catch 独立。内部 finally 确保预压缩过程中产生的 bash 结果被刷新,即使压缩过程失败。

_checkCompaction(lastAssistant, false) — 第二个参数 false 表示不跳过 aborted 消息,这是与后处理链中调用的关键区别。


段 7:消息构建 + before_agent_start(Step 7)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    messages = [];

    // 1. user message
    const userContent = [{ type: "text", text: expandedText }];
    if (currentImages) userContent.push(...currentImages);
    messages.push({ role: "user", content: userContent, timestamp: Date.now() });

    // 2. pending "nextTurn" asides
    for (const msg of this._pendingNextTurnMessages) {
      messages.push(msg);
    }
    this._pendingNextTurnMessages = [];

    // 3. before_agent_start 事件
    const result = await this._extensionRunner.emitBeforeAgentStart(
      expandedText, currentImages,
      this._baseSystemPrompt, this._baseSystemPromptOptions,
    );

    // 4. 扩展注入 custom messages
    if (result?.messages) {
      for (const msg of result.messages) {
        messages.push({ role: "custom", customType: msg.customType, content: msg.content, ... });
      }
    }
    // 5. systemPrompt 覆盖或回退
    if (result?.systemPrompt) {
      this.agent.state.systemPrompt = result.systemPrompt;
    } else {
      this.agent.state.systemPrompt = this._baseSystemPrompt;
    }

消息顺序user messagependingNextTurnMessagesextension custom messages。扩展注入在用户消息之后,但仍在同一次 LLM 调用中。


catch 块 — 统一的错误路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  } catch (error) {
    preflightResult?.(false);    // 任何 try 块中的错误
    throw error;
  }

  if (!messages) {
    return;                      // 防呆
  }

  preflightResult?.(true);       // 通过所有检查,确认可发送
  await this._runAgentPrompt(messages);

preflightResult 的 5 个潜在调用点

调用点后续
段 1 — 扩展命令找到并执行truereturn
段 2 — input 事件 handledtruereturn
段 4 — 消息已排队truereturn
catch 块 — try 块内任何错误falsethrow
try 成功后,_runAgentPrompttrue执行 LLM 调用

注意 if (!messages) return 防呆:理论上 try 块成功必定会赋值 messages,但如果引入新的 return 路径,这个检查防止了 undefined 流入 _runAgentPrompt

完整的执行流总结

try {
  ├─ 扩展命令 → 执行 + preflightResult(true) + return      [Return 1]
  ├─ input事件 → handled → preflightResult(true) + return   [Return 2]
  │              transform → 更新 text/images
  ├─ 技能/模板展开
  ├─ 流式中? → 排队 + preflightResult(true) + return        [Return 3]
  ├─ 认证检查 → 失败 → throw → catch → preflightResult(false)
  ├─ 预压缩 → 嵌套 try/finally
  ├─ 消息构建 + before_agent_start
  └─ end try
} catch {
  └─ preflightResult(false) + throw
}

if (!messages) return                             // 防呆
preflightResult(true)                             // 最终确认
await _runAgentPrompt(messages)                   // 执行 LLM 调用

PromptOptions — 参数详解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export interface PromptOptions {
  /** Whether to expand file-based prompt templates (default: true) */
  expandPromptTemplates?: boolean;
  /** Image attachments */
  images?: ImageContent[];
  /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait) */
  streamingBehavior?: "steer" | "followUp";
  /** Source of input for extension input event handlers. Defaults to "interactive". */
  source?: InputSource;
  /** Internal hook used by RPC mode to observe prompt preflight acceptance or rejection */
  preflightResult?: (success: boolean) => void;
}

source 参数:区分消息来源是用户交互("interactive")还是扩展程序("extension")。扩展通过 sendUserMessage() 调用 prompt() 时会传入 source: "extension",允许 input 事件 handler 区分处理。UI 可以根据 source 决定是否跳过提示、不做确认对话框等。

preflightResult 回调:用于 RPC 模式的 preflight 检查。prompt() 在执行前的 try 代码块中,任何时候遇到错误都会调用 preflightResult(false) 并 throw。如果通过所有检查并确认可以发送,在 _runAgentPrompt() 之前调用 preflightResult(true)。这样 RPC 模式可以在不实际执行 LLM 调用的情况下获知 prompt 是否被接受。

七步处理管道

用户输入 text
    │
Step 1: 扩展命令?(/command)
    ├── 是 → _tryExecuteExtensionCommand(text),返回
    └── 否 → 继续
    │
Step 2: input 事件给扩展
    ├── "handled" → 扩展已处理,返回
    ├── "transform" → 用修改后的 text/images 替换
    └── 无 handler → 继续
    │
Step 3: 展开 /skill:name 和提示模板
    ├── _expandSkillCommand() → 读技能文件,生成 <skill> XML 块
    └── expandPromptTemplate() → 替换 {{template}} 变量
    │
Step 4: 流式中?
    ├── 是 → 检查 streamingBehavior
    │   ├── "steer" → _queueSteer()
    │   ├── "followUp" → _queueFollowUp()
    │   └── 无 → 抛出 Error
    ├── 否 → 继续
    │
Step 5: 认证检查(模型 + API key)
    ├── 无模型 → formatNoModelSelectedMessage()
    ├── 无 auth → formatNoApiKeyFoundMessage()
    ├── OAuth 过期 → re-login 提示
    └── 通过 → 继续
    │
Step 6: 预 prompt compaction
    ├── 有 aborted assistant → _checkCompaction() + agent.continue()
    └── 无 → 继续
    │
Step 7: 构建消息 → before_agent_start 事件 → _runAgentPrompt()
    ├── user message(含 text + images)
    ├── pendingNextTurnMessages(注入 asides)
    ├── 扩展注入的 custom messages
    └── 扩展修改的 systemPrompt

Step 5 — 认证检查的两种错误分支

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 场景 1: 模型从未选择
if (!this.model) {
  throw new Error(formatNoModelSelectedMessage());
}

// 场景 2: 模型有但缺乏认证凭据
if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
  const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
  if (isOAuth) {
    throw new Error(
      `Authentication failed for "${this.model.provider}". ` +
      `Credentials may have expired or network is unavailable. ` +
      `Run '/login ${this.model.provider}' to re-authenticate.`,
    );
  }
  throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
}
错误条件错误消息处理方式
模型从未选择formatNoModelSelectedMessage()抛出,提示用户选择模型
API key 缺失formatNoApiKeyFoundMessage(provider)抛出,提示用户配置 key
OAuth 过期直接构造,含 re-login 引导抛出,提示用户重新登录

设计要点formatNoModelSelectedMessage()formatNoApiKeyFoundMessage() 来自 ./auth-guidance.ts,提供用户友好的引导文案,不会暴露敏感信息。isUsingOAuth 区分了 OAuth 和 API key 两种认证方式,各自的错误提示不同。

Step 6 — _findLastAssistantMessage() 预压缩检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const lastAssistant = this._findLastAssistantMessage();
if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
  try {
    await this.agent.continue();
    while (await this._handlePostAgentRun()) {
      await this.agent.continue();
    }
  } finally {
    this._flushPendingBashMessages();
  }
}

_findLastAssistantMessage() 在 agent state 的 messages 数组中从后往前遍历,找到最后一条 role === "assistant" 的消息(包括被中止的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private _findLastAssistantMessage(): AssistantMessage | undefined {
  const messages = this.agent.state.messages;
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];
    if (msg.role === "assistant") {
      return msg as AssistantMessage;
    }
  }
  return undefined;
}

关键细节_checkCompaction 的第二个参数 skipAbortedCheck = false,意味着这次检查包含被中止的消息。而在 _handlePostAgentRun() 中调用的 _checkCompaction 默认 skipAbortedCheck = true,会跳过被中止的消息。区别在于:后处理链只关心正常完成的 assistant,而 pre-prompt 检查需要处理上次被 abort 的残留上下文。

Step 7 — before_agent_start 事件的扩展注入

1
2
3
4
5
6
const result = await this._extensionRunner.emitBeforeAgentStart(
  expandedText,
  currentImages,
  this._baseSystemPrompt,
  this._baseSystemPromptOptions,
);

返回值 result 的结构:

字段类型作用
messagesArray<{customType, content, display, details}>扩展注入的 custom messages,与 user message 一起发送
systemPromptstring | undefined扩展提供的 system prompt 覆盖
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 扩展注入的 custom messages 加入消息数组
if (result?.messages) {
  for (const msg of result.messages) {
    messages.push({
      role: "custom",
      customType: msg.customType,
      content: msg.content,
      display: msg.display,
      details: msg.details,
      timestamp: Date.now(),
    });
  }
}
// systemPrompt 回退逻辑
if (result?.systemPrompt) {
  this.agent.state.systemPrompt = result.systemPrompt;
} else {
  this.agent.state.systemPrompt = this._baseSystemPrompt;
}

设计:扩展可以选择提供自定义 systemPrompt 覆盖 base prompt。如果扩展没有返回 systemPrompt,则回退到 _baseSystemPrompt,确保上一轮扩展的修改不会污染本轮。

preflightResult 的错误路径

1
2
3
4
5
6
7
8
9
try {
  // ... 所有检查步骤(命令/input/技能展开/streaming/auth/预压缩/消息构建)...
} catch (error) {
  preflightResult?.(false);  // 任何检查失败 → 通知 RPC 模式
  throw error;
}

preflightResult?.(true);  // 通过所有检查 → 确认可发送
await this._runAgentPrompt(messages);

设计preflightResult 在整个 try 块外部被保护。try 块内任何步骤出错都会调用 preflightResult(false) 并 throw。只有 try 块完全成功后才调用 preflightResult(true)。这保证了 RPC 模式获取的 preflight 结果是最终确定的。


Step 4 — _queueSteer / _queueFollowUp 内部方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {
  this._steeringMessages.push(text);
  this._emitQueueUpdate();
  this.agent.steer({
    role: "user",
    content: [{ type: "text", text }, ...(images ?? [])],
    timestamp: Date.now(),
  });
}

private async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
  this._followUpMessages.push(text);
  this._emitQueueUpdate();
  this.agent.followUp({
    role: "user",
    content: [{ type: "text", text }, ...(images ?? [])],
    timestamp: Date.now(),
  });
}

两种策略:steer 中断当前 LLM 流式输出立即处理;followUp 等当前工具调用链完成后才处理。模式由 settings.steeringModesettings.followUpMode 控制,支持 "all"(全排)和 "one-at-a-time"(一次一个)。


_runAgentPrompt() — 运行循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
  try {
    await this.agent.prompt(messages);
    while (await this._handlePostAgentRun()) {
      await this.agent.continue();
    }
  } finally {
    this._flushPendingBashMessages();
  }
}

关键设计agent.prompt() 执行首次 LLM 调用及其工具链。完成后进入 _handlePostAgentRun() 后处理链 — 只要后处理返回 true,就继续调用 agent.continue()。循环直到后处理链返回 false 为止。

finally 块 — _flushPendingBashMessages()

_runAgentPrompt 的 finally 块调用 _flushPendingBashMessages()。这是因为 bash 结果在流式处理中不能立即加入 agent state(会破坏 tool_use/tool_result 顺序),所以 recordBashResult()isStreaming 时将 bash 消息缓存到 _pendingBashMessages 数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// agent-session.ts:2636
recordBashResult(command: string, result: BashResult, options?): void {
  const bashMessage: BashExecutionMessage = {
    role: "bashExecution",
    command,
    output: result.output,
    exitCode: result.exitCode,
    // ... 更多字段
  };

  // 流式 → 排队;非流式 → 立即加入
  if (this.isStreaming) {
    this._pendingBashMessages.push(bashMessage);
  } else {
    this.agent.state.messages.push(bashMessage);
    this.sessionManager.appendMessage(bashMessage);
  }
}

_flushPendingBashMessages() 将排队消息统一注入 agent state 和 session:

1
2
3
4
5
6
7
8
private _flushPendingBashMessages(): void {
  if (this._pendingBashMessages.length === 0) return;
  for (const bashMessage of this._pendingBashMessages) {
    this.agent.state.messages.push(bashMessage);
    this.sessionManager.appendMessage(bashMessage);
  }
  this._pendingBashMessages = [];
}

执行时机_flushPendingBashMessages() 在三个地方被调用:prompt() 的 Step 4(streaming 退出后立即刷新)、prompt() 的 Step 6(预压缩完成后刷新)、以及 _runAgentPrompt() 的 finally 块(每次 agent 运行结束后刷新)。


_handlePostAgentRun() — 后处理链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private async _handlePostAgentRun(): Promise<boolean> {
  const msg = this._lastAssistantMessage;
  this._lastAssistantMessage = undefined;
  if (!msg) return false;

  // Check 1: 重试
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) return true;

  // 重试失败时,发送 auto_retry_end 事件并重置计数器
  if (msg.stopReason === "error" && this._retryAttempt > 0) { ... }

  // Check 2: 压缩
  if (await this._checkCompaction(msg)) return true;

  // Check 3: 扩展在 agent_end 中排队了消息
  return this.agent.hasQueuedMessages();
}

三阶段责任链::

agent.prompt() / agent.continue() 完成
    │
    ▼
_handlePostAgentRun()
    │
    ├── 1. 重试检查
    │    ├── _isRetryableError() 匹配错误模式
    │    │   └── 正则覆盖: 429/5xx/WebSocket 断开/超时/terminated
    │    ├── _prepareRetry() → 指数退避 (baseDelay * 2^(attempt-1))
    │    └── agent.continue() → 重回后处理链
    │
    ├── 2. 压缩检查
    │    ├── Overflow?→ 移除错误消息 → _runAutoCompaction() → agent.continue()
    │    ├── Threshold?→ _runAutoCompaction() → 不自动继续
    │    └── 不需要 → 继续
    │
    └── 3. 队列检查
         ├── agent_end handler 排队了消息?→ agent.continue()
         └── 无 → 返回 false,循环结束

设计要点:每次 agent.continue() 后都重新跑整个后处理链。重试 → 压缩 → 队列消费,直到三者都不满足才结束循环。

_lastAssistantMessage_handleAgentEventmessage_end 事件中被设置,后处理链读取后立即重置为 undefined,防止重复处理。


_tryExecuteExtensionCommand() — 扩展命令路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private async _tryExecuteExtensionCommand(text: string): Promise<boolean> {
  const spaceIndex = text.indexOf(" ");
  const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
  const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);

  const command = this._extensionRunner.getCommand(commandName);
  if (!command) return false;

  const ctx = this._extensionRunner.createCommandContext();
  try {
    await command.handler(args, ctx);
    return true;
  } catch (err) {
    this._extensionRunner.emitError({
      extensionPath: `command:${commandName}`,
      event: "command",
      error: err instanceof Error ? err.message : String(err),
    });
    return true;
  }
}

触发时机prompt() 的 Step 1 中,文本以 / 开头时优先尝试。即使正在 streaming 也会立即执行,因为扩展命令自己管理 LLM 交互(通过 pi.sendMessage())。

错误处理:执行出错不 throw,而是 emitError 到扩展系统,保证 prompt() 不会被命令错误阻塞。注意返回值始终是 true(找到了就执行了),不会把带 / 的内容漏给后续步骤。


_expandSkillCommand() — 技能展开

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private _expandSkillCommand(text: string): string {
  if (!text.startsWith("/skill:")) return text;

  const spaceIndex = text.indexOf(" ");
  const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
  const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();

  const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName);
  if (!skill) return text;

  const content = readFileSync(skill.filePath, "utf-8");
  const body = stripFrontmatter(content).trim();
  const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
  return args ? `${skillBlock}\n\n${args}` : skillBlock;
}

展开格式:

/skill:debugging crash in login
  |
  v
<skill name="debugging" location="/path/to/skill.md">
References are relative to /base/dir.

## Systematic Debugging ...

</skill>

crash in login

设计:使用 <skill> XML 块格式包装技能文件,与 parseSkillBlock() 解析格式一致。这种格式让 LLM 能区分技能内容与用户的问题。stripFrontmatter() 去除技能的 frontmatter,只保留正文内容。


steer() / followUp() — 流式消息排队

          prompt("text")   isStreaming=true
                |
          ------+-------
          |            |
          v            v
     steer()       followUp()
        |               |
        |               |
agent.steer()    agent.followUp()
        |               |
        v               v
 中断当前 LLM     等待工具链完成
 立即处理          LLM 空闲时消费
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async steer(text: string, images?: ImageContent[]): Promise<void> {
  // 扩展命令不能排队
  if (text.startsWith("/")) this._throwIfExtensionCommand(text);
  // 展开技能和模板
  let expandedText = this._expandSkillCommand(text);
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
  await this._queueSteer(expandedText, images);
}

async followUp(text: string, images?: ImageContent[]): Promise<void> {
  if (text.startsWith("/")) this._throwIfExtensionCommand(text);
  let expandedText = this._expandSkillCommand(text);
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
  await this._queueFollowUp(expandedText, images);
}

_throwIfExtensionCommand() — 排队守卫

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private _throwIfExtensionCommand(text: string): void {
  const spaceIndex = text.indexOf(" ");
  const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
  const command = this._extensionRunner.getCommand(commandName);
  if (command) {
    throw new Error(
      `Extension command "/${commandName}" cannot be queued. ` +
      `Use prompt() or execute the command when not streaming.`,
    );
  }
}

设计:扩展命令不允许排队,因为命令通常需要即时返回结果或管理自己的 LLM 交互。若用户在 steering/followUp 中使用了扩展命令,直接抛错,防止命令被延迟执行导致行为异常。


sendCustomMessage() — 三种投递模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async sendCustomMessage<T = unknown>(
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): Promise<void> {
  const appMessage = {
    role: "custom" as const,
    customType: message.customType,
    content: message.content,
    display: message.display,
    details: message.details,
    timestamp: Date.now(),
  } satisfies CustomMessage<T>;

  if (options?.deliverAs === "nextTurn") {
    this._pendingNextTurnMessages.push(appMessage);
  } else if (this.isStreaming) {
    if (options?.deliverAs === "followUp") {
      this.agent.followUp(appMessage);
    } else {
      this.agent.steer(appMessage);  // 默认 steering 入队
    }
  } else if (options?.triggerTurn) {
    await this._runAgentPrompt(appMessage);  // 立即触发新 turn
  } else {
    this.agent.state.messages.push(appMessage);
    this.sessionManager.appendCustomMessageEntry(...);
    this._emit({ type: "message_start", message: appMessage });
    this._emit({ type: "message_end", message: appMessage });
  }
}

五种投递路径:

条件行为
deliverAs === "nextTurn"缓存到 _pendingNextTurnMessages,下次 prompt() 时作为 asides 注入
streaming + deliverAs === "followUp"通过 agent.followUp() 插入等待队列
streaming + 默认(或 steer)通过 agent.steer() 立即插入队列
非 streaming + triggerTurn === true直接 _runAgentPrompt(appMessage) 触发新 turn
非 streaming + 不 trigger追加到 state.messages + 持久化,不触发 LLM

“nextTurn” 模式:消息不会立即进入 agent state,而是缓存在 _pendingNextTurnMessages 数组中,等待下一次 prompt() 调用时作为 alongside 上下文注入。这实现了"aside"效果 — 扩展塞入的上下文信息(如搜索结果、代码分析结果)会伴随用户的下一条消息一起发送,而不会单独触发一次 LLM 调用。


sendUserMessage() — 扩展用户消息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async sendUserMessage(
  content: string | (TextContent | ImageContent)[],
  options?: { deliverAs?: "steer" | "followUp" },
): Promise<void> {
  // 规范化: content 数组 → text + images
  let text: string;
  let images: ImageContent[] | undefined;
  if (typeof content === "string") {
    text = content;
  } else {
    const textParts: string[] = [];
    images = [];
    for (const part of content) {
      if (part.type === "text") textParts.push(part.text);
      else images.push(part);
    }
    text = textParts.join("\n");
    if (images.length === 0) images = undefined;
  }

  await this.prompt(text, {
    expandPromptTemplates: false,   // 跳过命令处理和模板展开
    streamingBehavior: options?.deliverAs,
    images,
    source: "extension",            // 标记来源为扩展
  });
}

设计expandPromptTemplates: false 避免扩展消息被二次处理(命令拦截、技能展开、模板替换)。source: "extension" 让 input 事件 handler 可以区分消息来源。这是给扩展用的 API,用户交互应走 prompt() 入口。


clearQueue() — 队列清理

1
2
3
4
5
6
7
8
9
clearQueue(): { steering: string[]; followUp: string[] } {
  const steering = [...this._steeringMessages];
  const followUp = [...this._followUpMessages];
  this._steeringMessages = [];
  this._followUpMessages = [];
  this.agent.clearAllQueues();
  this._emitQueueUpdate();
  return { steering, followUp };
}

用途:用户中止操作时,将排队消息恢复到编辑器。返回的数组让 UI 层可以回填到输入框。


abort() — 中止操作

1
2
3
4
5
async abort(): Promise<void> {
  this.abortRetry();
  this.agent.abort();
  await this.agent.waitForIdle();
}

先终止重试,再终止 agent,然后等待 agent 变为空闲。waitForIdle() 确保后续操作(如 compaction、switch session)不会和正在关闭的 agent 循环产生竞争。三步顺序不可颠倒。


_getUserMessageText() — 队列消费检测的辅助方法

_handleAgentEvent 中,当收到 message_start 事件时,需要检测该消息是否来自 steer/followUp 队列。_getUserMessageText() 负责从消息中提取纯文本用于匹配:

1
2
3
4
5
6
7
private _getUserMessageText(message: Message): string {
  if (message.role !== "user") return "";
  const content = message.content;
  if (typeof content === "string") return content;
  const textBlocks = content.filter((c) => c.type === "text");
  return textBlocks.map((c) => (c as TextContent).text).join("");
}

消费者检测逻辑(位于 _handleAgentEvent 中):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (event.type === "message_start" && event.message.role === "user") {
  this._overflowRecoveryAttempted = false;  // 用户新输入 → 重置 overflow 恢复标志
  const messageText = this._getUserMessageText(event.message);
  if (messageText) {
    // 先检查 steering 队列
    const steeringIndex = this._steeringMessages.indexOf(messageText);
    if (steeringIndex !== -1) {
      this._steeringMessages.splice(steeringIndex, 1);
      this._emitQueueUpdate();
    } else {
      // 再检查 followUp 队列
      const followUpIndex = this._followUpMessages.indexOf(messageText);
      if (followUpIndex !== -1) {
        this._followUpMessages.splice(followUpIndex, 1);
        this._emitQueueUpdate();
      }
    }
  }
}

设计要点::

  • 清除顺序:先 steering 队列,再 followUp 队列。steer 具有更高优先级。
  • 基于消息文本内容匹配,而非消息对象引用。这在异步排队场景下是必要的,因为 queue 中的文本字符串与最终构建的 message 对象不是同一个引用。
  • _emitQueueUpdate() 在每次移除后立即调用,确保 UI 层实时看到队列状态变化。

队列管理与 queue_update 事件

1
2
3
4
5
6
7
private _emitQueueUpdate(): void {
  this._emit({
    type: "queue_update",
    steering: [...this._steeringMessages],
    followUp: [...this._followUpMessages],
  });
}

触发场景::

  • _queueSteer() / _queueFollowUp() — 添加消息
  • _handleAgentEvent 消费检测 — 移除消息
  • clearQueue() — 批量清理

其他与队列相关的 getter::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
get pendingMessageCount(): number {
  return this._steeringMessages.length + this._followUpMessages.length;
}

getSteeringMessages(): readonly string[] {
  return this._steeringMessages;
}

getFollowUpMessages(): readonly string[] {
  return this._followUpMessages;
}

设计_steeringMessages_followUpMessages 只是用于 UI 显示的引用追踪,实际的消息排队由 agent.steer() / agent.followUp() 在 agent-core 中处理。AgentSession 维护这两个数组只是为了知道哪些消息还没有被消费。


关系总结

方法调用关系

prompt()
├── _tryExecuteExtensionCommand()          — 扩展命令
├── _extensionRunner.emitInput()           — input 事件
├── _expandSkillCommand()                  — 技能展开
├── expandPromptTemplate()                 — 模板展开
├── _queueSteer() / _queueFollowUp()       — 流式排队
├── _findLastAssistantMessage()            — 查找上次 assistant
├── _checkCompaction()                     — 预压缩检查
├── _extensionRunner.emitBeforeAgentStart() — 扩展注入
└── _runAgentPrompt()                      — 运行循环

_runAgentPrompt()
└── _handlePostAgentRun()  (循环)
    ├── _prepareRetry()                    — 重试检查
    ├── _checkCompaction()                 — 压缩检查
    └── agent.hasQueuedMessages()          — 队列检查

steer() / followUp()
├── _expandSkillCommand()                  — 技能展开
├── expandPromptTemplate()                 — 模板展开
├── _throwIfExtensionCommand()             — 命令守卫
└── _queueSteer() / _queueFollowUp()       — 实际入队

sendCustomMessage()
├── agent.steer() / agent.followUp()       — 流式排队
├── _runAgentPrompt()                      — 直接触发 turn
└── sessionManager.appendCustomMessageEntry() — 持久化

abort()
├── abortRetry()                           — 先终止重试
├── agent.abort()                          — 终止 agent
└── agent.waitForIdle()                    — 等待空闲

数据流全景

用户输入: /command      → _tryExecuteExtensionCommand() → 扩展处理
用户输入: /skill:name   → _expandSkillCommand() → <skill> XML 块
用户输入: {{template}}  → expandPromptTemplate() → 展开为预设文本
用户输入: 普通文本       → 直接作为 user message
扩展自定义消息            → sendCustomMessage() → 五种投递路径
扩展用户消息              → sendUserMessage() → prompt() 的简化入口

流式中的后续输入
├── steer()   → agent.steer()   → 中断当前 LLM,立即处理
└── followUp() → agent.followUp() → 等工具链完成,空闲时处理

后处理链 (每轮 continue 后检查)
├── 重试 → _prepareRetry() → 指数退避 sleep → continue
├── 压缩 → _runAutoCompaction() → 生成摘要 → continue
└── 队列 → 扩展在 agent_end 中排队了消息 → continue

关键状态字段

字段类型作用
_steeringMessagesstring[]等待 UI 展示的 steer 队列(双向追踪)
_followUpMessagesstring[]等待 UI 展示的 followUp 队列(双向追踪)
_pendingNextTurnMessagesCustomMessage[]扩展注入的 aside 消息,缓存在此等待下轮发送
_pendingBashMessagesBashExecutionMessage[]流式中的 bash 结果,排队至此等待统一刷新
_lastAssistantMessageAssistantMessage | undefined上一条 assistant 响应,用于后处理链
_overflowRecoveryAttemptedboolean防止 overflow 压缩-重试死循环
_retryAttemptnumber当前轮次的重试计数,正常响应时重置为 0

关键设计模式总结

模式位置说明
责任链模式_handlePostAgentRun()重试 → 压缩 → 队列消费的链式检查
策略模式prompt() streaming 分支steer/followUp 两种排队策略
模板方法prompt() 七步管道固定步骤序列,每步可被扩展拦截/修改
观察者模式_emitQueueUpdate()队列变化 → UI 层实时更新显示
命令模式_tryExecuteExtensionCommand()/command 路由到注册的 handler
中间件模式input 事件链扩展链式拦截/转换用户输入
sidecar 注入_pendingNextTurnMessages不触发 LLM 调用,随下条消息一起发送
延迟刷新_flushPendingBashMessages()finally 块中统一刷新 bash 结果,避免破坏 tool 顺序
双向追踪_steeringMessages 数组AgentSession 维护镜像队列,仅用于 UI 展示但非实际的 agent-core 排队
防死循环_overflowRecoveryAttemptedoverflow 压缩-重试只尝试一次,防止反复触发