乐于分享
好东西不私藏

Cline 源码深度解析:ReAct 循环、Plan/Act、上下文管理与记忆系统

Cline 源码深度解析:ReAct 循环、Plan/Act、上下文管理与记忆系统

本文基于 Cline 开源仓库(2026 年 6 月版本)的实际源码分析,所有代码引用均来自 sdk/packages/ 目录下的真实文件。带你从源码层面理解 Cline 到底是怎么"自己写代码"的。

目录

  1. ReAct 主循环:AgentRuntime.execute() 的完整实现
  2. Plan Mode 的实现:不是"AI 判断",而是"工具集裁剪"
  3. 工具审批机制:beforeTool Hook 链
  4. Context Window 管理:三层防线
  5. 记忆系统:ConversationStore + Checkpoint
  6. 错误自愈:MistakeTracker + LoopDetectionTracker
  7. 工具定义:实际的工具列表
  8. 源码分析小结

一、ReAct 主循环:AgentRuntime.execute() 的完整实现

Cline 的核心引擎是 @cline/agents 包中的 AgentRuntime 类(文件:sdk/packages/agents/src/agent-runtime.ts)。其 execute() 方法实现了一个经典的 while 循环驱动的 ReAct 决策环

// 简化后的核心循环(agent-runtime.ts)
asyncexecute(input?: AgentRunInput): Promise<AgentRunResult> {
// 1. 初始化:注册 hooks、工具、插件
awaitthis.ensureInitialized();
this.state.status = "running";
this.state.iteration = 0;
// 2. 注入用户输入消息
for (const message of input ? normalizeInput(input) : []) {
this.state.messages.push(message);
    }
// 3. 核心 ReAct 循环
while (this.state.iteration < this.config.maxIterations) {
this.state.iteration += 1;
// === Reason + Act:调用 LLM 生成助手消息 ===
const { message, finishReason } = awaitthis.generateAssistantMessage();
this.state.messages.push(message);
// 提取模型输出中的工具调用
const toolCalls = message.content.filter(
            (part) => part.type === "tool-call"
        );
// 如果没有工具调用 → 任务可能完成
if (toolCalls.length === 0) {
// 检查是否需要 completion tool(如 submit_and_exit)
const completionReminder = this.getCompletionReminderMessages();
if (completionReminder.length > 0) {
// 提醒模型使用完成工具,继续循环
continue;
            }
// 无 completion 要求 → 正常结束
returnthis.finishRun("completed", message);
        }
// === Observe:执行工具并收集结果 ===
const toolMessages = awaitthis.executeToolCalls(toolCalls);
for (const toolMessage of toolMessages) {
this.state.messages.push(toolMessage);
        }
// 检查是否有终止工具被调用(如 submit_and_exit)
const terminalTool = this.findCompletingToolMessage(toolCalls, toolMessages);
if (terminalTool) {
returnthis.finishRun("completed", message);
        }
// 否则继续下一轮循环...
    }
}

循环流程图

关键设计要点

源码机制说明
maxIterations
 硬上限
防止无限循环,超出后抛出错误
completionPolicy.requireCompletionTool
强制模型必须调用 submit_and_exit 才能结束,避免"自以为完成"
completionPolicy.completionGuard
团队模式下,检查是否还有未完成的任务,未做完则注入提醒消息
AbortController
支持外部中断(用户点击停止),通过 throwIfAborted() 在每轮检查
pendingToolCalls
 追踪
记录当前待处理的工具调用 ID,用于状态快照和恢复

工具调用的并行与串行

源码中 executeToolCalls() 支持两种执行模式:

// agent-runtime.ts
privateasyncexecuteToolCalls(toolCalls): Promise<AgentMessage[]> {
constpreparedPreparedToolExecution[] = [];
for (const toolCall of toolCalls) {
        prepared.push(awaitthis.prepareToolExecution(toolCall));
    }
if (this.config.toolExecution === "parallel") {
// 并行执行所有工具
returnPromise.all(
            prepared.map((execution) => this.executePreparedTool(execution))
        );
    }
// 默认:串行执行
constresultsAgentMessage[] = [];
for (const execution of prepared) {
        results.push(awaitthis.executePreparedTool(execution));
    }
return results;
}

toolExecution 配置为 "sequential"(默认)或 "parallel"。当模型在一轮中输出多个工具调用时,串行模式更安全(后一个工具可以看到前一个的结果),并行模式更快但需要工具之间无依赖。


二、Plan Mode 的实现:不是"AI 判断",而是"工具集裁剪"

很多人以为 Plan Mode 是通过提示词告诉 AI "你现在只能读不能写"。实际上,源码中的实现更加彻底——直接从工具集中移除写入类工具,模型连写文件的工具都看不到。

2.1 ToolPresets:模式即工具集

文件:sdk/packages/core/src/extensions/tools/presets.ts

exportconstToolPresets = {
act: {
enableReadFilestrue,
enableSearchtrue,
enableBashtrue,       // ← 可执行命令
enableEditortrue,     // ← 可编辑文件
enableWebFetchtrue,
enableSubmitAndExitfalse,
enableSpawnAgenttrue,
enableAgentTeamstrue,
    },
plan: {
enableReadFilestrue,
enableSearchtrue,
enableBashtrue,       // ← 仍可执行命令(用于探索)
enableEditorfalse,    // ← 编辑器被禁用!无法写文件
enableWebFetchtrue,
enableSubmitAndExitfalse,
enableSpawnAgenttrue,
enableAgentTeamstrue,
    },
yolo: {
enableReadFilestrue,
enableSearchfalse,
enableBashtrue,
enableWebFetchfalse,
enableEditortrue,
enableSkillsfalse,
enableAskQuestionfalse,
enableSubmitAndExittrue,  // ← 只有 YOLO 有完成工具
enableSpawnAgentfalse,
enableAgentTeamsfalse,
    },
};

2.2 核心发现

发现说明
Plan Mode 的"只读"是物理级别的
enableEditor: false
 意味着 editor(写文件/改文件)工具根本不会出现在模型可见的工具列表中
Plan Mode 仍可执行命令
enableBash: true
 保留了——模型可以执行 lsgrepcat 等只读命令来探索代码
不是"AI 自律",是"物理不可能"
模型连 write_to_file 工具都看不到,想写也写不了

2.3 模式切换机制

functionresolveToolPresetName(options: { mode?: AgentMode }): ToolPresetName {
if (options.mode === "plan"return"plan";
if (options.mode === "yolo"return"yolo";
return"act";
}

在 DefaultRuntimeBuilder.build() 中,模式决定了最终注册到 Agent 的工具列表:

// runtime-builder.ts
const preset = ToolPresets[resolveToolPresetName({ mode: config.mode })];
// 根据 preset 配置创建工具
tools.push(
    ...createBuiltinToolsList(
        config.cwd,
        config.providerId,
        normalized.mode,  // ← 模式传入
        config.modelId,
// ...
    )
);

2.4 YOLO Mode 的另一面

YOLO Mode 不仅启用工具,还通过 createToolPoliciesWithPreset("yolo") 将所有工具的 autoApprove 设为 true

exportfunctioncreateToolPoliciesWithPreset(presetName) {
if (presetName !== "yolo"return {};
constyoloPolicyToolPolicy = { enabledtrueautoApprovetrue };
constpoliciesRecord<stringToolPolicy> = { "*": yoloPolicy };
for (const toolName ofALL_DEFAULT_TOOL_NAMES) {
        policies[toolName] = yoloPolicy;
    }
return policies;
}

这意味着 YOLO = 全工具 + 全自动审批,是真正的"无人值守"模式。


三、工具审批机制:beforeTool Hook 链

每次工具调用前,AgentRuntime 会经过一条完整的审批链。这不是硬编码的弹窗逻辑,而是一个可扩展的 策略模式

3.1 审批流程源码

// agent-runtime.ts → prepareToolExecution()
privateasyncprepareToolExecution(toolCall): Promise<PreparedToolExecution> {
const tool = this.tools.get(toolCall.toolName);
let input = toolCall.input;
letskipReasonstring | undefined;
// ========== 第 1 步:检查工具输入是否解析成功 ==========
const metadata = toolCall.metadata;
if (metadata?.inputParseError) {
        skipReason = metadata.inputParseError;
    }
// ========== 第 2 步:执行 beforeTool hooks ==========
// Hook 可以:修改输入 / 跳过工具 / 甚至终止整个运行
if (tool && !skipReason) {
for (const hook ofthis.hooks.beforeTool) {
const result = awaithook({ snapshot, tool, toolCall, input });
if (result?.input) input = result.input;   // Hook 可修改工具输入
if (result?.skip) {
                skipReason = result.reason;
break;
            }
this.applyStopControl(result);  // Hook 可终止整个运行
        }
    }
// ========== 第 3 步:检查 ToolPolicy ==========
if (tool && !skipReason) {
const policy = resolveToolPolicy(toolCall.toolName, this.config.toolPolicies);
if (policy.enabled === false) {
            skipReason = `Tool "${toolCall.toolName}" is disabled by policy`;
        } elseif (policy.autoApprove === false) {
// 弹窗请求用户审批(由宿主应用实现 UI)
const approval = awaitthis.requestToolApproval(toolCall, input, policy);
if (!approval.approved) {
                skipReason = approval.reason;
            }
        }
    }
return { toolCall, tool, input, skipReason };
}

3.2 审批链的三层结构

工具调用请求
     │
     ▼
┌─────────────────────────────────────────────┐
│ 第 1 层:输入验证                             │
│   JSON 解析失败?→ 跳过执行,返回错误给模型     │
└─────────────────────────────────────────────┘
     │ 通过
     ▼
┌─────────────────────────────────────────────┐
│ 第 2 层:beforeTool Hooks(可插拔)           │
│   LoopDetection → 发现重复调用?注入警告       │
│   自定义 Hook  → 可修改输入/跳过/终止          │
└─────────────────────────────────────────────┘
     │ 通过
     ▼
┌─────────────────────────────────────────────┐
│ 第 3 层:ToolPolicy 策略                      │
│   enabled === false  → 工具被禁用             │
│   autoApprove === false → 请求用户审批        │
│   autoApprove === true  → 自动通过(YOLO)    │
└─────────────────────────────────────────────┘
     │ 通过
     ▼
工具执行

3.3 宿主应用的角色

requestToolApproval 是一个回调函数,由宿主应用注入:

  • VSCode 扩展:弹出 Diff 面板,用户点击 "Approve" 或 "Reject"
  • CLI:终端输出工具调用详情,等待用户输入 y/n
  • CI/CD:可配置为自动批准所有(配合 .clinerules 约束)

这就是 Cline 的"Human-in-the-loop"设计的底层实现——审批逻辑与 Agent 引擎完全解耦


四、Context Window 管理:三层防线

源码中 Context Window 管理由三个独立但协作的机制组成,形成层层递进的防线。

4.1 第一层:MessageBuilder — 消息构建时的实时截断

文件:sdk/packages/core/src/session/services/message-builder.ts

MessageBuilder 在每次构建发给 API 的消息时执行即时截断,不修改原始对话历史:

// 关键常量
DEFAULT_MAX_TOOL_RESULT_CHARS = 8_000;     // 单个工具结果最大 8K 字符
DEFAULT_MAX_FILE_CONTENT_CHARS = 50_000;   // 文件内容最大 50K 字符
DEFAULT_MAX_TOTAL_TEXT_BYTES = 6_000_000;  // 总文本预算 6MB
DEFAULT_MAX_ASSISTANT_TEXT_CHARS = 200_000// 助手消息最大 200K 字符

处理流程

buildForApi(messagesMessage[]): Message[] {
// 1. 修复缺失的 tool_result(防止 API 报错)
//    如果 assistant 消息中有 tool_use 但没有对应的 tool_result,
//    自动补一个 "Tool execution was interrupted" 的占位结果
const repaired = this.addMissingToolResults(messages);
// 2. 逐块截断
const prepared = repaired.map(msg => {
// - 工具结果截断到 8K 字符(中间截断,保留首尾)
// - 过期的文件读取内容替换为 "[outdated]"
// - 助手文本截断到 200K 字符
// - 重复工具调用标记截断到 12K 字符
    });
// 3. 图片媒体预算控制(限制 base64 图片总量)
const mediaLimited = this.applyMediaBudget(prepared);
// 4. 总文本预算控制(6MB 上限,按大小排序逐步截断)
returnthis.truncateToTotalTextBudget(mediaLimited);
}

亮点机制 — 过期文件内容标记

这是一个非常聪明的优化:如果文件被重新读取过,旧的读取结果会被标记为过期,替换为占位符:

// 追踪每个文件的"最新读取者"
private latestFullContentOwnerByPathCache = newMap<stringstring>();
// 判断某次读取是否已过期
privateisOutdatedReadLocator(locator, toolUseId): boolean {
const fullOwner = this.latestFullContentOwnerByPathCache.get(locator.path);
if (fullOwner && fullOwner !== toolUseId) returntrue;  // 有更新的读取
returnthis.latestReadToolUseByLocatorCache.get(key) !== toolUseId;
}
// 过期内容替换为:
constOUTDATED_FILE_CONTENT = "[outdated - see the latest file content]";

效果:如果 Cline 读了 user.ts(500 行),后来修改了它,又读了一次,那么第一次的 500 行内容会被替换为一行占位符,大幅节省 Token。

工具结果截断策略

// 中间截断:保留头部和尾部,中间用标记替代
functiontruncateMiddleByChars(text, maxChars, makeMarker) {
if (text.length <= maxChars) return text;
const keep = Math.floor((maxChars - marker.length) / 2);
const start = text.slice(0, keep);
const end = text.slice(-keep);
return`${start}\n\n...[truncated ${removed} chars]...\n\n${end}`;
}

为什么是中间截断而不是尾部截断?因为头部通常包含错误信息的关键线索(如编译错误的第一行),尾部通常包含最终状态(如命令的退出码),中间部分往往是重复的日志输出。

4.2 第二层:Context Compaction — 上下文压缩

文件:sdk/packages/core/src/extensions/context/compaction.ts

当 Token 总量接近模型上限时,触发压缩。源码支持两种策略:

constBUILTIN_COMPACTION_STRATEGIES = {
basic: (options) => runBasicCompaction(options),    // 策略 1:截断式
agentic: (options) => runAgenticCompaction(options), // 策略 2:AI 摘要式
};

触发条件

// 默认阈值配置
DEFAULT_THRESHOLD_RATIO = 0.9;    // 达到 context window 的 90%
DEFAULT_RESERVE_TOKENS = 16_384;  // 或距离上限不到 16K tokens
// 判断逻辑
functionresolveTriggerState({ inputTokens, maxInputTokens, config }) {
const triggerTokens = Math.max(
0,
Math.min(
            maxInputTokens - DEFAULT_RESERVE_TOKENS,
            maxInputTokens * DEFAULT_THRESHOLD_RATIO
        )
    );
return {
shouldCompact: inputTokens > triggerTokens,
        triggerTokens,
    };
}

Basic 策略 — 纯算法压缩(不依赖 LLM)

文件:basic-compaction.ts

压缩优先级(从先移除):
1. 中间的 assistant 消息(非首尾)
2. 中间的 user 消息(非首尾)
3. 最后的 assistant 消息
4. 最后的 user 消息(非第一条)
5. 如果还不够,逐条截断消息内容(从后往前)
6. 最后才截断第一条用户消息

关键约束:工具调用对(tool_use + tool_result)必须原子性移除——不能只删一半,否则 API 会报错。

// 通过 tool_use_id 关联,BFS 找到所有关联消息一起移除
functioncollectAtomicRemovalIndexes(candidates, startIndex): Set<number> {
const pairIndex = buildToolPairIndex(candidates);
const removalIndexes = newSet();
const queue = [startIndex];
while (queue.length > 0) {
const index = queue.shift();
        removalIndexes.add(index);
// 找到这条消息中所有 tool_use_id / tool_result_id
for (const id ofcollectToolPairIds(candidates[index])) {
// 找到引用了相同 id 的其他消息,一起移除
for (const linkedIndex of pairIndex.get(id) ?? []) {
if (!removalIndexes.has(linkedIndex)) {
                    queue.push(linkedIndex);
                }
            }
        }
    }
return removalIndexes;
}

Agentic 策略 — 调用 LLM 生成摘要

文件:agentic-compaction.ts

压缩流程:
1. 找到切割点(保留最近 N tokens 的消息不动)
2. 切割点之前的消息 → 序列化为文本
3. 调用 LLM 生成结构化摘要
4. 用摘要消息 + 保留的近期消息 替代原始历史

摘要请求模板:

const summaryRequest = `Summarize this session for continuation. Be concise and factual.## GoalOne sentence: what is being built or fixed.## State- Done: completed steps- In Progress: current work- Blocked: blockers or open questions## HighlightsKey technical choices or notable findings.## NextImmediate next steps.## FilesRead: ${fileOps.readFiles.join(", ")}Edited: ${fileOps.modifiedFiles.join(", ")}`;

压缩后生成一条特殊的 compaction_summary 消息:

functionbuildSummaryMessage({ summary, fileOps, tokensBefore }) {
return {
role"user",
content`Context summary:\n\n${summary}`,
metadata: {
kind"compaction_summary",
            summary,
details: fileOps,      // 记录读/改了哪些文件
            tokensBefore,
generatedAtDate.now(),
        },
    };
}

增量摘要:如果之前已经有过压缩(存在旧的 compaction_summary),新的摘要会包含旧摘要的内容,形成递归式摘要链

const previousSummary = findLatestSummaryIndex(messagesToSummarize) >= 0
    ? getCompactionSummaryMetadata(messages[latestSummaryIndex]).summary
    : undefined;
// 摘要请求中包含 previousSummary
const summaryRequest = buildSummaryRequest({
    previousSummary,    // ← 上一次摘要的内容
    conversationText,   // ← 新增的对话内容
    fileOps,
});

4.3 第三层:prepareTurn 钩子 — 每轮请求前的拦截点

// agent-runtime.ts → generateAssistantMessage()
request = awaitthis.prepareTurnForModelRequest(request);

prepareTurn 是一个通用钩子接口,压缩只是它的用途之一:

privateasyncprepareTurnForModelRequest(request) {
if (!this.config.prepareTurn) return request;
const result = awaitthis.config.prepareTurn({
        agentId, conversationId, parentAgentId,
        iteration,
messages: request.messages,
systemPrompt: request.systemPrompt,
tools: request.tools,
model: { id, provider },
signal: request.signal,
emitStatusNotice: (message, metadata) => { ... },
    });
// 可以返回修改后的 messages 和 systemPrompt
if (result?.messages) {
this.state.messages = result.messages;  // 替换整个消息列表
    }
if (result?.systemPrompt !== undefined) {
        next = { ...next, systemPrompt: result.systemPrompt };
    }
return next;
}

插件系统也可以通过注册 prepareTurn 回调来在每轮请求前修改消息、系统提示词等。这为外部扩展提供了极大的灵活性。

4.4 三层防线的协作关系

每轮 API 调用前:
  state.messages(原始完整历史)
       │
       ▼
  ┌─ prepareTurn 钩子 ──────────────────────────┐
  │  检查 Token 总量                              │
  │  ├── 未超阈值 → 不处理                        │
  │  └── 超过阈值 → 触发 Compaction               │
  │        ├── Basic:算法裁剪(快,无成本)        │
  │        └── Agentic:LLM 摘要(慢,花费 Token) │
  │  修改 state.messages                          │
  └───────────────────────────────────────────────┘
       │
       ▼
  ┌─ MessageBuilder.buildForApi() ──────────────┐
  │  对每条消息做即时截断                          │
  │  ├── 工具结果 → 8K 字符上限                   │
  │  ├── 过期文件内容 → "[outdated]" 占位          │
  │  ├── 助手文本 → 200K 字符上限                  │
  │  ├── 图片预算 → 限制 base64 总量               │
  │  └── 总文本预算 → 6MB 上限                     │
  └───────────────────────────────────────────────┘
       │
       ▼
  最终发给 API 的消息(安全、精简、不超限)

五、记忆系统:ConversationStore + Checkpoint

5.1 会话记忆:ConversationStore

文件:sdk/packages/core/src/session/stores/conversation-store.ts

exportclassConversationStore {
privatemessagesMessageWithMetadata[] = [];
private conversationId = createConversationId();
private sessionStarted = false;
// === 核心 API ===
getConversationId(): string
getMessages(): MessageWithMetadata[]
appendMessage(message): void
appendMessages(messages): void
replaceMessages(messages): void// 压缩后整体替换
resetForRun(): void// 清空 + 新 conversationId
clearHistory(): void// 同上
restore(messages): void// 从持久化恢复
// === Session 生命周期 ===
isSessionStarted(): boolean
markSessionStarted(): void
}

设计特点

  • 纯内存消息列表:没有向量数据库,没有 embedding 索引,所有"记忆"就是完整的对话历史
  • sessionStarted 门控:用于判断是否应该触发 session_start hooks(只在新会话首次调用模型时触发一次)
  • conversationId 隔离:每次 resetForRun() 生成新的 ID,确保不同任务的历史不会混淆

5.2 持久化:SQLite 存储

会话元数据存储在 SQLite 数据库中(CoreSessionService):

// session/models/session-row.ts
// sessions 表结构
CREATETABLEsessions (
    session_id TEXTPRIMARYKEY,
    source TEXT,           // "cli" | "vscode" | "jetbrains" | "sdk"
    status TEXT,           // "running" | "stopped" | "failed"
    provider TEXT,         // "anthropic" | "openai" | ...
    model TEXT,
    cwd TEXT,
    workspace_root TEXT,
    team_name TEXT,
    prompt TEXT,
    metadata_json TEXT,    // checkpoint 历史等
    messages_path TEXT,    // 消息文件路径
    started_at TEXT,
    ended_at TEXT,
    updated_at TEXT,
// ...
);

消息内容以 JSONL 格式存储在独立文件中(messages_path),与 SQLite 元数据分离,便于大消息量的高效读写。

5.3 Checkpoint 回滚:基于 Git 的物理文件快照

文件:sdk/packages/core/src/session/checkpoint-restore.ts

Checkpoint 数据结构

interfaceCheckpointEntry {
refstring;        // Git commit hash 或 stash ref
createdAtnumber;  // 创建时间戳
runCountnumber;   // 对应的用户轮次(第几次用户输入)
    kind?: "commit" | "stash";  // 存储方式
}

Checkpoint 历史存储在 session 的 metadata.checkpoint.history 中。

恢复流程

exportasyncfunctionapplyCheckpointToWorktree(cwd, checkpoint) {
// 1. 验证是 Git 仓库
awaitexecFile("git", ["-C", cwd, "rev-parse""--is-inside-work-tree"]);
// 2. 验证 checkpoint ref 存在且有效
awaitexecFile("git", ["-C", cwd, "cat-file""-e"`${checkpoint.ref}^{commit}`]);
// 3. 清理当前工作区
awaitexecFile("git", ["-C", cwd, "reset""--hard"]);
awaitexecFile("git", ["-C", cwd, "clean""-fd"]);
// 4. 恢复到目标 checkpoint
if (checkpoint.kind === "commit") {
awaitexecFile("git", ["-C", cwd, "reset""--hard", checkpoint.ref]);
    } else {
// stash 方式:apply 暂存的内容
awaitexecFile("git", ["-C", cwd, "stash""apply", checkpoint.ref]);
    }
}

消息回滚

恢复时不仅回滚文件,还要回滚对话历史到对应的用户轮次:

exportfunctiontrimMessagesToCheckpoint(messages, runCount) {
const index = findCheckpointMessageIndex(messages, runCount);
return messages.slice(0, index + 1);  // 保留到该轮次
}
functionfindCheckpointMessageIndex(messages, runCount) {
let userRunCount = 0;
for (let index = 0; index < messages.length; index++) {
const message = messages[index];
if (message?.role !== "user"continue;
// 跳过 recovery_notice 类型的系统注入消息
if (metadata?.kind === "recovery_notice"continue;
        userRunCount += 1;
if (userRunCount === runCount) return index;
    }
thrownewError(`Could not find user message for checkpoint run ${runCount}`);
}

两种 Checkpoint 恢复模式

模式文件操作消息操作使用场景
Restore Workspace OnlyapplyCheckpointToWorktree()不截断代码写错但还想继续对话
Restore Task + WorkspaceapplyCheckpointToWorktree()trimMessagesToCheckpoint()整个方向走错,需要全部重来

六、错误自愈:MistakeTracker + LoopDetectionTracker

Cline 有两套独立的错误追踪机制,分别应对不同类型的"AI 犯错"。

6.1 LoopDetectionTracker — 重复工具调用检测

文件:sdk/packages/core/src/runtime/safety/loop-detection.ts

核心思路

检测模型是否在用相同的参数反复调用同一个工具(死循环的典型特征)。

// 配置
constDEFAULT_CONFIG = {
softThreshold3,   // 连续 3 次相同调用 → 软警告
hardThreshold5,   // 连续 5 次相同调用 → 强制停止
};
// 检测逻辑
inspect(call): LoopDetectionVerdict {
// 对工具输入进行规范化(排序 key 后 JSON 序列化)
const signature = toolCallSignature(call.input);
if (toolName === lastToolName && signature === lastToolSignature) {
        consecutiveIdenticalCount++;
    } else {
        consecutiveIdenticalCount = 1;  // 不同调用,重置计数
    }
if (consecutiveIdenticalCount >= hardThreshold) {
return { kind"hard"message`Detected ${count} consecutive identical calls...` };
    }
if (consecutiveIdenticalCount === softThreshold) {
return { kind"soft"message`Consider trying a different approach.` };
    }
return { kind"ok" };
}

签名计算

functiontoolCallSignature(inputunknown): string {
if (input == nullreturn"null";
if (typeof input === "string"return input;
if (typeof input !== "object"returnString(input);
// 递归排序 key,确保 {a:1, b:2} 和 {b:2, a:1} 产生相同签名
returnJSON.stringify(sortKeys(input));
}

集成方式

LoopDetectionTracker 作为 beforeTool hook 安装到 AgentRuntime 中:

工具调用 → beforeTool hook → LoopDetectionTracker.inspect()
  ├── "ok"   → 正常执行
  ├── "soft" → 注入建议消息到对话中("考虑换个方法"),继续执行
  └── "hard" → 跳过执行,返回错误给模型("检测到死循环,已停止")

6.2 MistakeTracker — 连续错误计数

文件:sdk/packages/core/src/runtime/safety/mistake-tracker.ts

核心思路

追踪连续失败的次数,达到上限时可选择停止或注入恢复引导。

classMistakeTracker {
private consecutiveMistakes = 0;
asyncrecord(inputRecordMistakeInput): Promise<MistakeOutcome> {
const max = this.options.maxConsecutiveMistakes;
const next = input.forceAtLimit ? max : this.consecutiveMistakes + 1;
this.consecutiveMistakes = next;
// 未达上限 → 继续
if (next < max) {
return { action"continue" };
        }
// 达到上限 → 调用决策回调
const decision = awaitresolveConsecutiveMistakeDecision({
            iteration, consecutiveMistakes: next,
maxConsecutiveMistakes: max, reason, details
        }, this.options.onLimitReached);
if (decision.action === "continue") {
// 注入恢复引导消息到对话中
const guidance = decision.guidance?.trim();
if (guidance) {
this.options.appendRecoveryNotice(guidance, reason);
            }
this.consecutiveMistakes = 0;  // 重置计数,给模型一次机会
return { action"continue", guidance };
        }
// 停止运行
return {
action"stop",
messagebuildMistakeLimitStopMessage({ ... })
        };
    }
reset(): void {
this.consecutiveMistakes = 0;
    }
}

错误类型

类型触发场景典型原因
api_errorLLM API 调用失败网络超时、限流、密钥过期
invalid_tool_call模型生成无法解析的工具调用
非法 JSON 参数、缺失字段
tool_execution_failed工具执行过程中抛出异常
文件不存在、权限不足、命令超时

停止消息格式

functionbuildMistakeLimitStopMessage(input) {
const parts = [
`Stopped after ${consecutiveMistakes}/${maxConsecutiveMistakes} consecutive mistakes (${reason}) at iteration ${iteration}.`,
    ];
if (details) parts.push(`Error: ${details}`);
if (stopReason) parts.push(`Decision: ${stopReason}`);
    parts.push("Session state was preserved. Send a new prompt to resume from the latest state.");
return parts.join(" ");
}

6.3 两套机制的协作

工具调用请求
     │
     ▼
LoopDetectionTracker(beforeTool hook)
  ├── 相同调用 3 次 → 软警告(注入建议消息)
  └── 相同调用 5 次 → 硬停止(跳过执行)
     │
     ▼ (如果通过)
工具执行
  ├── 成功 → MistakeTracker.reset()
  └── 失败 → MistakeTracker.record()
                ├── 未达上限 → 继续(错误作为 Observe 回传给模型)
                └── 达到上限 → 停止或注入恢复引导

七、工具定义:实际的工具列表

从源码 definitions.ts 中提取的 Cline 实际工具列表(新版命名):

工具名功能Plan ModeAct ModeYOLO Mode
read_files
读取文件内容(支持行范围、多文件)
search_codebase
正则搜索代码库(ripgrep)
run_commands
执行 Shell 命令(支持超时、结构化输入)
fetch_web_content抓取网页内容并分析
editor
创建/修改文件(合并了 write + replace)
skills
加载 Skill 指令(按需注入规则)
ask_question
向用户提问(带 2-5 个可选项)
submit_and_exit声明任务完成(终止工具)

工具名兼容映射

旧版工具名通过 CONFIGURED_AGENT_TOOL_NAME_ALIASES 映射到新版:

constCONFIGURED_AGENT_TOOL_NAME_ALIASES = {
apply_diff"editor",
attempt_completion"submit_and_exit",
bash"run_commands",
execute_command"run_commands",
list_code_definition_names"search_codebase",
list_files"run_commands",      // list_files 实际通过 run_commands 执行 ls
read_file"read_files",
replace_in_file"editor",
search_files"search_codebase",
use_skill"skills",
write_to_file"editor",
};

工具的策略过滤

在 DefaultRuntimeBuilder.build() 中,工具还要经过策略过滤:

// 1. 按 ToolPolicy 过滤
functionfilterToolsByPolicies(tools, toolPolicies) {
return tools.filter(tool => {
const globalPolicy = toolPolicies?.["*"] ?? {};
const toolPolicy = toolPolicies?.[tool.name] ?? {};
return { ...globalPolicy, ...toolPolicy }.enabled !== false;
    });
}
// 2. 按全局禁用列表过滤
functionfilterDisabledTools(tools) {
// 通过环境变量 CLINE_DISABLED_TOOLS 全局禁用某些工具
}

八、源码分析小结

核心机制速查表

机制源码位置核心思路
ReAct 循环agents/src/agent-runtime.ts
 → execute()
while 循环 + 工具调用驱动,maxIterations 防无限
Plan Modeextensions/tools/presets.ts
 → ToolPresets.plan
物理移除 editor 工具,模型看不到写入类工具
工具审批agent-runtime.ts
 → prepareToolExecution()
3 层链:输入验证 → beforeTool hooks → ToolPolicy
消息截断session/services/message-builder.ts
单结果 8K / 总量 6MB / 过期文件标记
上下文压缩extensions/context/compaction.ts
Basic(算法裁剪)+ Agentic(LLM 摘要)
会话记忆session/stores/conversation-store.ts
纯内存消息列表 + SQLite 元数据
Checkpointsession/checkpoint-restore.ts
Git commit/stash + 消息截断到对应轮次
循环检测runtime/safety/loop-detection.ts
签名比对,3 次警告 / 5 次停止
错误追踪runtime/safety/mistake-tracker.ts
连续错误计数 + 可配置恢复/停止策略
完成策略agent-runtime.ts
 → completionPolicy
强制 submit_and_exit + 团队任务守卫

架构全景图


数据来源:本文所有代码引用均来自 Cline GitHub 仓库 的 sdk/packages/ 目录,克隆时间为 2026 年 6 月 24 日。具体文件路径已在各节中标注。

参考资料:

  • Cline GitHub 仓库:https://github.com/cline/cline
  • Cline SDK 文档:https://docs.cline.bot/cline-sdk/overview
  • Cline 核心源码目录:sdk/packages/core/src/
  • Agent 引擎源码:sdk/packages/agents/src/agent-runtime.ts