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 {
|
入口变量#
| 变量 | 来源 | 默认值 |
|---|
expandPromptTemplates | options.expandPromptTemplates | true — 默认展开模板 |
preflightResult | options.preflightResult | undefined — 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
} // ───┘
|
expandPromptTemplates 为 true 时才会进入此分支。sendUserMessage() 传入 expandPromptTemplates: false,所以扩展发送的消息不会被当作命令拦截。
另一点:这里即使 isStreaming === true 也会立即执行扩展命令。注释说得很清楚 — 扩展命令自己管理 LLM 交互(通过 pi.sendMessage()),所以不需要排队。
preflightResult?.(true) 在此处调用:命令找到并被执行,prompt() 提前返回,不需要后续的模型检查。
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,按顺序调用它们,并处理返回的 handled 或 transform 结果。
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 message → pendingNextTurnMessages → extension 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 — 扩展命令找到并执行 | true | return |
| 段 2 — input 事件 handled | true | return |
| 段 4 — 消息已排队 | true | return |
catch 块 — try 块内任何错误 | false | throw |
try 成功后,_runAgentPrompt 前 | true | 执行 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 的结构:
| 字段 | 类型 | 作用 |
|---|
messages | Array<{customType, content, display, details}> | 扩展注入的 custom messages,与 user message 一起发送 |
systemPrompt | string | 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.steeringMode 和 settings.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 在 _handleAgentEvent 的 message_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
关键状态字段#
| 字段 | 类型 | 作用 |
|---|
_steeringMessages | string[] | 等待 UI 展示的 steer 队列(双向追踪) |
_followUpMessages | string[] | 等待 UI 展示的 followUp 队列(双向追踪) |
_pendingNextTurnMessages | CustomMessage[] | 扩展注入的 aside 消息,缓存在此等待下轮发送 |
_pendingBashMessages | BashExecutionMessage[] | 流式中的 bash 结果,排队至此等待统一刷新 |
_lastAssistantMessage | AssistantMessage | undefined | 上一条 assistant 响应,用于后处理链 |
_overflowRecoveryAttempted | boolean | 防止 overflow 压缩-重试死循环 |
_retryAttempt | number | 当前轮次的重试计数,正常响应时重置为 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 排队 |
| 防死循环 | _overflowRecoveryAttempted | overflow 压缩-重试只尝试一次,防止反复触发 |