本文基于 Claude Code Best 和 Open-ClaudeCode,并对照 Anthropic 的 2.1.63 CHANGELOG 及类型定义。核心 CLI 没有完整公开,内部调用链以这两个复刻仓库为准。
选这三个命令,是因为它们分别代表三种执行方式:/compact 把输入交给本地函数,/simplify 把工作步骤写进模型提示词,/goal 把目标保存成跨轮次状态。三者都经过命令发现器,随后进入 local、prompt 和 local-jsx 协议。
一、命令发现器只负责找到入口
复刻实现用 Command 联合类型描述命令的加载方式:
export type LocalCommandResult = | { type: 'text'; value: string } | { type: 'compact'; compactionResult: CompactionResult } | { type: 'skip' }
type LocalCommand = { type: 'local' supportsNonInteractive: boolean load: () => Promise<LocalCommandModule>}
type LocalJSXCommand = { type: 'local-jsx' load: () => Promise<LocalJSXCommandModule>}
export type PromptCommand = { type: 'prompt' source: SettingSource | 'builtin' | 'plugin' | 'bundled' getPromptForCommand( args: string, context: ToolUseContext, ): Promise<ContentBlockParam[]>}类型定义来自复刻实现的 src/types/command.ts。三个命令的注册信息如下:
| 命令 | 类型 | 入口 | 进入执行阶段后做什么 |
|---|---|---|---|
/compact | local | src/commands/compact/index.ts | 调用压缩函数,生成摘要并替换当前上下文 |
/simplify | prompt | src/skills/bundled/simplify.ts | 返回提示词,由主 Agent 按提示词调用工具 |
/goal | local-jsx | src/commands/goal/index.ts | 更新目标状态,并决定是否继续下一轮查询 |
用户输入先进入命令发现器,发现器按 Command.type 把请求交给不同的执行入口:
compact 直接加入 COMMANDS 数组,goal 只有在 GOAL feature flag 打开时才会加入。bundled Skill 通过 registerBundledSkill() 注册,getSkills() 再把它和文件型 Skill、插件 Skill 合并。发现器只决定“这个名字对应哪个入口”,压缩、审查和续跑分别在后面的执行层完成。
二、/compact:模型生成摘要,代码替换历史
2.1 local 命令的懒加载
/compact 的注册对象只包含元数据和动态导入入口:
const compact = { type: 'local', name: 'compact', description: 'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]', isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT), supportsNonInteractive: true, argumentHint: '<optional custom summarization instructions>', load: () => import('./compact.js'),} satisfies Command启动时先加载这份注册信息。用户真正执行 /compact 后,load() 才导入压缩模块。DISABLE_COMPACT 控制手动命令是否可用,自动压缩还要经过自己的配置和 feature flag 判断。
2.2 call() 先确定要压缩的消息
src/commands/compact/compact.ts 的 call() 可以概括成下面的路径:
/compact args -> getMessagesAfterCompactBoundary(messages) -> 没有自定义指令时,尝试 session-memory compaction -> reactive-only 模式走 reactiveCompactOnPromptTooLong -> microcompactMessages(messages) -> compactConversation(messagesForCompact, customInstructions) -> 返回 { type: "compact", compactionResult }getMessagesAfterCompactBoundary() 只取最近一个压缩边界之后的消息。没有可压缩消息时,命令直接返回错误。
microcompactMessages() 会先处理工具结果。启用 cached microcompact 且模型支持时,代码扫描可压缩的 tool_result,按消息分组记录,再通过 cache edit 删除旧结果。消息对象可以保持不变,删除动作延迟到 API 层执行。
2.3 压缩提示词只负责生成交接摘要
compactConversation() 先估算 preCompactTokenCount,执行 executePreCompactHooks(),再把 Hook 返回的指令和用户在 /compact 后输入的指令合并。用户指令排在前面。
getCompactPrompt() 对模型输出有明确约束:只输出文本,不调用工具,并用 <analysis> 和 <summary> 区分内部草稿与最终摘要。摘要要保留用户目标、文件名、函数签名、代码修改、错误修复和安全约束:
<analysis> 为交接摘要整理当前任务状态</analysis><summary> 用户目标 关键文件和函数 已完成的修改与错误修复 未完成步骤 需要继续遵守的约束</summary>formatCompactSummary() 会移除 <analysis> 草稿,只把 <summary> 写回会话。摘要请求复用主会话的工具 Schema 时,模型一旦发起工具调用,这次请求就不会得到可用的摘要文本。
2.4 生成摘要后如何安装回会话
streamCompactSummary() 优先调用 runForkedAgent(),复用主会话的系统提示词、工具和缓存前缀。失败或开关关闭时,代码改用 queryModelWithStreaming(),并做几项输入清理:
- 图片和文档替换成
[image]、[document]标记。 - 移除压缩后会重新注入的 Skill attachment。
- 禁用 thinking,只保留文本摘要。
- 只允许读取类工具,并拒绝工具调用。
历史过长时,摘要请求本身也可能超出上下文窗口。代码按 API round 丢弃最早的一组消息后重试;仍然失败时返回 Conversation too long to summarize,提示用户使用 /clear。
摘要生成后,代码保存 readFileState,清理读取缓存,并构造恢复附件。这些附件可以包含最近读取的少量文件、仍在运行的 Agent、计划文件、plan mode 指令、已经调用过的 Skill,以及工具、Agent、MCP 指令的 delta。复刻实现对单文件、单 Skill、总附件 token 数和恢复文件数量都设有限制。
createCompactBoundaryMessage() 会生成一个 system / compact_boundary 消息,记录触发方式、压缩前 token 数和最后一条旧消息的 UUID。新历史按下面的顺序组装:
boundaryMarkersummaryMessagesmessagesToKeepattachmentshookResultsQueryEngine 和 SDK 层识别这个边界。原始 transcript 仍然存在,当前 API 看到的是摘要、保留的近期消息和恢复附件。
手动压缩、自动压缩和 Reactive Compact 都调用 compactConversation(),触发入口不同:
| 触发方式 | 入口 | 触发条件 |
|---|---|---|
手动 /compact | commands/compact/compact.ts | 用户输入命令,可以附加摘要指令 |
| 自动压缩 | services/compact/autoCompact.ts | token 阈值和配置满足条件 |
| Reactive Compact | reactiveCompactOnPromptTooLong() | API 返回 prompt-too-long,实验开关开启 |
官方 SDK 类型把 session.compact() 的 trigger 暴露为 plugin,并允许 Hook veto。cached microcompact、reactive 模式和具体阈值属于版本实现细节。
三、/simplify:把审查流程放进提示词
3.1 prompt 命令返回一段文本
Anthropic 的 2.1.63 CHANGELOG 将 /simplify 列为 bundled slash command。在复刻实现中,它注册成一个 prompt 命令:
export function registerSimplifySkill(): void { registerBundledSkill({ name: 'simplify', description: 'Review changed code for reuse, quality, and efficiency, then fix any issues found.', userInvocable: true, async getPromptForCommand(args) { let prompt = SIMPLIFY_PROMPT if (args) prompt += `\n\n## Additional Focus\n\n${args}` return [{ type: 'text', text: prompt }] }, })}getPromptForCommand() 返回的是 ContentBlockParam[]。命令本身没有执行审查,也没有独立的 AST 分析器;主 Agent 收到这段文本后,才按照提示词调用 git、Agent、Edit 和 Write 等工具。
提示词的实际拼接方式如下:
SIMPLIFY_PROMPT
## Additional Focus
用户在 /simplify 后提供的参数没有附加参数时,模型只收到 SIMPLIFY_PROMPT。有参数时,参数被追加到 ## Additional Focus 下,用来限定性能、API 兼容性或目录范围。
SIMPLIFY_PROMPT 的主体围绕三个阶段展开。下面保留阶段和审查项,省略了源码中的工具名与补充说明:
# Simplify: Code Review and Cleanup
审查所有变更文件,处理代码复用、质量和效率问题。
## Phase 1: Identify Changes
运行 git diff。存在暂存内容时运行 git diff HEAD。没有 diff 时,审查用户点名或本轮修改过的文件。
## Phase 2: Launch Three Review Agents in Parallel
并行启动三个审查 Agent:- Code Reuse Review- Code Quality Review- Efficiency Review
把完整 diff 交给每个 Agent,并等待三份结果。
## Phase 3: Fix Issues
合并审查结果,跳过误报,修复确认后的问题,并报告跳过的建议。3.2 主 Agent 如何执行审查
SIMPLIFY_PROMPT 要求主 Agent 先确定变更集合:工作树有暂存内容时使用 git diff HEAD,否则使用 git diff;工作树没有变化时,审查用户提到或本轮编辑过的文件。
确定 diff 后,主 Agent 在同一条模型响应中并发调用三个 Agent:
| Agent | 检查内容 |
|---|---|
| Code Reuse Review | 搜索已有 helper,检查重复函数、手写路径处理和重复类型守卫 |
| Code Quality Review | 检查冗余状态、参数膨胀、复制粘贴、泄漏抽象、字符串类型和无意义 JSX 嵌套 |
| Efficiency Review | 检查重复计算、串行执行、热路径膨胀、内存泄漏和过宽读取 |
三个调用返回后,主 Agent 合并发现,选择需要处理的问题并修改代码。误报可以跳过,但需要在最终摘要中说明。整个过程使用普通的权限、工作树和工具调用协议。
这里的提示词决定审查顺序和关注点,代码修改仍由主 Agent 的工具调用完成。是否采纳某条建议,取决于模型对 diff、仓库搜索结果和误报的判断。
四、/goal:状态保存目标,idle hook 负责续跑
4.1 local-jsx 命令处理目标操作
/goal 的注册对象使用 local-jsx:
const goal = { type: 'local-jsx', name: 'goal', description: 'Set or view a persistent goal that drives auto-continuation across turns', argumentHint: '[<objective> | status | clear | pause | resume | complete]', bridgeSafe: false, load: () => import('./goal.js'),} satisfies Commandgoal.tsx 把输入转换成状态操作:
| 输入 | 操作 |
|---|---|
/goal <objective> | 创建或替换目标 |
/goal 或 /goal status | 显示目标、状态、耗时、token 和续跑轮数 |
/goal pause / resume | 暂停或恢复自动续跑 |
/goal continue | 达到最大轮数后,由用户明确继续 |
/goal complete | 手动完成目标 |
/goal clear | 清除目标并写入 tombstone |
命令完成回调可以设置 shouldQuery: true,也可以插入隐藏的 metaMessages。设置新目标时,源码会插入 <goal-objective-updated> 元消息,UI 显示截断后的摘要。
4.2 GoalState 保存跨轮次数据
状态机位于复刻实现的 src/services/goal/goalState.ts,按 session ID 存在 Map<string, GoalState> 中:
type GoalState = { objective: string status: 'active' | 'paused' | 'blocked' | 'budget_limited' | 'usage_limited' | 'max_turns' | 'complete' tokenBudget: number | null tokensUsed: number startTime: number pausedAt: number | null accumulatedActiveMs: number blockedAttempts: number lastBlockReason: string | null turnsExecuted: number}状态更新有几条规则:
pause会把当前 active 时间累加到accumulatedActiveMs,恢复时重新设置startTime。- cost tracker 收到 API usage 后更新 token 计数,达到
tokenBudget就进入budget_limited。 - 相同原因的
blocked报告连续三次后才进入blocked,原因变化会重置计数。 - 自动续跑最多执行
MAX_GOAL_TURNS = 150轮,达到上限后进入max_turns,需要/goal continue才能继续。
goalStorage.ts 每次状态变化都会向当前会话的 JSONL transcript 写入 goal checkpoint。/goal clear 追加 goal-cleared tombstone,恢复会话时,读取器按 session ID 取最后一个状态。
4.3 GoalTool 报告完成和阻塞
模型通过 GoalTool.ts 读取目标或报告状态,输入 schema 是:
z.strictObject({ action: z.enum(['get', 'update']).optional(), status: z.enum(['complete', 'blocked']).optional(), reason: z.string().optional(),})get 返回当前快照。update 只能把目标标记为 complete 或 blocked。完成时工具记录 token、有效耗时和续跑轮数,再调用 completeGoal();阻塞时调用 recordBlockedAttempt()。目标状态由应用代码保存,模型只能通过这个 schema 约束的工具修改它。
4.4 两层提示词把目标带入下一轮
Goal 有两类模型上下文。
第一类是每轮都可以看到的目标摘要。它把状态、耗时、token、预算和轮数放进 XML:
<active-goal status="active" elapsed="4m 12s" tokens="18400" budget="50000" turns="3">为 monorepo 添加可回滚的数据库迁移并补齐测试</active-goal>第二类是 idle hook 注入的 continuation meta message。buildContinuationPrompt() 要求模型保持原始目标范围,完成前逐项寻找测试、文件内容或命令结果等证据;遇到阻塞时继续尝试,只有相同原因连续三轮才标记 blocked;完成或确认阻塞后,通过 GoalTool 更新状态。
续跑提示词的主要内容如下:
<goal-steering type="continuation"> 当前目标:{objective} 已用 token:{tokensUsed} token 预算:{tokenBudget}
保持目标范围。 完成前执行 Completion Audit,寻找可验证证据。 对相同阻塞原因进行连续计数。 完成或确认阻塞后调用 GoalTool 更新状态。</goal-steering>目标文本会经过 escape_xml_text,防止 <、> 和 & 破坏 XML 结构。
4.5 idle hook 决定是否自动发起下一轮
useGoalContinuation() 在 isLoading 从 true 变成 false 时检查续跑条件:
一轮查询结束 -> 检查用户队列、活动 JSX、plan mode 和 abort 状态 -> 读取 GoalState -> budget_limited: 注入一次收尾提示 -> active 且未到 MAX_GOAL_TURNS: turnsExecuted += 1 -> enqueue(<goal-steering type="continuation">...</goal-steering>) -> 下一轮正常 query队列里有 /goal pause 或普通用户消息时,Hook 会跳过本次自动续跑。目标进入 complete、blocked、max_turns 或资源受限状态后,也不会继续创建实质性工作。
五、三个命令分别控制什么
| 维度 | /compact | /simplify | /goal |
|---|---|---|---|
| Command 类型 | local | prompt | local-jsx |
| 处理对象 | 当前会话的消息历史 | 当前任务的审查流程 | 跨轮次目标和生命周期 |
| 主要执行者 | 压缩函数和摘要模型 | 主 Agent 与三个审查 Agent | GoalState、GoalTool、idle hook |
| 提示词作用 | 规定摘要输出格式和保留内容 | 规定审查顺序与检查项 | 规定续跑条件和完成证据 |
| 状态写入 | compact boundary 和新历史 | 普通文件修改 | JSONL goal checkpoint |
| 停止条件 | 摘要失败或上下文仍超限 | 主 Agent 跳过误报或完成修改 | complete、blocked、预算和轮数限制 |
这三个实现展示了三种不同的扩展方式:
local命令把用户输入交给确定的本地函数,函数可以改写会话历史。prompt命令只增加模型上下文,后续动作由模型通过普通工具完成。local-jsx命令维护应用状态,再用提示词和 idle hook 把状态带进下一轮。
命令菜单里的名字不能直接说明运行时行为。要判断一个子命令会做什么,需要继续看它注册的 Command.type、懒加载模块、提示词注入位置和状态写入点。
参考源码
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
