跳到主要内容

规划与执行主循环(Tell)

30 秒导读: 你在终端里对 Plandex 说一句"帮我加个登录功能",服务端不会一口气把代码全吐出来。它先进入规划,把这句话拆成一张带文件的子任务清单;然后进入实现,一个子任务一次模型调用地往下做;每一次调用都是一条,流里的文字被边收边解析,认出哪一段是"要写进某个文件的代码"。本章讲的就是这台状态机 + 主循环:一个 prompt 怎么变成一串子任务、又怎么被一步步落地。

本章覆盖 server/model/plantell_* 系列。不覆盖两件事:①<PlandexBlock> 里的代码真正变成 diff 的构建过程(见 02-build-apply);②上下文/项目地图怎么被选出来加载(见 03-context-maps)。模型角色(architect / planner / coder)的分工见 04-models-roles


1. 这是什么(零基础也能懂)

一句话定义: "Tell" 是 Plandex 服务端把用户一句话 prompt 变成一串真实文件改动的那条主流程。

它要解决的问题。 让 AI 改一个大项目的代码,不能靠一次对话就吐完——上下文放不下、模型也容易跑偏。得像人一样:先想清楚"要动哪些文件、分几步",再一步一步做。

Plandex 的做法,一句话: 把任务拆成阶段。

阶段白话模型在干嘛
规划 Planning"先列个待办清单"只输出一个 ### Tasks 子任务列表,绝不写代码
实现 Implementation"照着清单一条一条做"每次只做当前一条子任务,输出代码块

用起来什么样。 用户在 CLI 敲:

plandex tell "add a /health endpoint that returns 200 OK"

服务端随后可能先回一段规划:

### Tasks
1. Add health handler
Uses: `server/handlers/health.go`

2. Register the route
Uses: `server/routes.go`
<PlandexFinish/>

然后自动继续,进入实现,逐个子任务地输出真正的代码块(用 <PlandexBlock> 包住),直到清单全部标记为 done。

一句话直觉。 把它想成一个带 checklist 的施工队:工头(planner)先写施工清单,工人(coder)照单逐项施工,每干完一项在清单上打勾;打完最后一个勾,收工。本章讲的就是"清单怎么来、工人怎么被一次次叫回来、以及工人说的话怎么被翻译成实际动作"。


2. 顶层全景(它大概怎么转)

2.1 一次 Tell 的骨架

从 HTTP handler 到第一段模型输出,主干只有几跳。怎么读这张图:从上往下是控制流,execTellPlan 是会被反复回到的那个"迭代入口"。

HTTP handler (plans_exec.go)
│ modelPlan.Tell(TellParams{...})

Tell() ──activatePlan──► go execTellPlan(iteration:0) ← 每一"步"都从这里进
│ │
│ (本step:决定阶段→组装system prompt→发起模型流)
▼ ▼
立即返回(异步) doTellRequest ──► listenStream(一条 SSE 流)
│ 逐 chunk: processChunk

handleStreamFinished
│ 存回复/判定子任务是否完成

willContinuePlan? ──yes──► execTellPlan(iteration+1) ↺

no ──► 收尾(build/finish)

关键在那条回边:execTellPlan → …流… → handleStreamFinished →(若该继续)execTellPlan(iteration+1)。一次 Tell 不是一次模型调用,而是一串,每串一个 iteration

2.2 各部件一句话职责

部件干什么文件
Tell / execTellPlan一"步"的总入口:装配状态、决定阶段、发请求tell_exec.go
resolveCurrentStage两阶段状态机:这一步该规划还是实现tell_stage.go
getTellSysPrompt按阶段拼 system prompt(规划/实现用不同 prompt)tell_sys_prompt.go
formatSubtasks / ParseSubtasks把子任务列表写进 prompt / 从回复里读出来tell_subtasks.goparse/subtasks.go
listenStream / processChunk收流、把文本解析成文件操作tell_stream_main.gotell_stream_processor.go
execStatusShouldContinue判"当前子任务做完没"exec_status.go
willContinuePlan判"整个计划要不要再来一步"tell_stream_status.go
storeOnFinished把回复、子任务状态、flags 落库+提交 gittell_stream_store.go
handleMissingFile模型要改一个不在上下文里的文件时,停流问用户tell_stream_processor.go / tell_missing_file.go

2.3 主线走一遍(高层)

  1. 入口。 handler 调 modelPlan.Tell(...)(handlers/plans_exec.go:98),Tell 激活 plan 后 go execTellPlan,HTTP 立刻返回,后面全在后台 goroutine 里跑(tell_exec.go:58)。
  2. 定阶段。 execTellPlan 每次先问 resolveCurrentStage:根据"上一条消息是什么、有没有做过 plan"决定这一步是 Planning 还是 Implementation
  3. 拼 prompt。 按阶段选不同 system prompt,并把当前子任务清单塞进去。
  4. 发流、解析。 doTellRequest 开一条流,listenStream 逐 chunk 交给 processChunk,后者认出 <PlandexBlock> 代码块 → 变成"文件操作"排队去构建。
  5. 收尾判定。 流结束后 handleStreamFinished 存回复、判子任务是否完成;willContinuePlan 决定要不要 execTellPlan(iteration+1) 再来一步,还是收工。

3. 核心原理(逐个机制,由浅入深)

3.1 两阶段状态机:这一步该规划还是实现?

要解决的小问题。 每次 execTellPlan 被调用(无论是用户第一次说话,还是自动继续的第 N 步),都要先回答一个问题:这一步,模型该"想"还是该"做"? 答案不是存在某个变量里,而是每步现算——依据是上一条成功的会话消息

思路。 状态藏在对话历史里。resolveCurrentStage(tell_stage.go:22)先拿到"上一条没出错、没被中断的 assistant/user 消息"(lastSuccessfulConvoMessage,tell_stage.go:11),再据此推断。

推断规则,从上到下:

情况判定的阶段依据(tell_stage.go)
没有历史,或上一条是用户 promptPlanningisUserPrompttell_stage.go:56
上一条 assistant 消息 Flags.DidMakePlan == trueImplementationtell_stage.go:60
上一条本身就在 TellStageImplementationImplementationtell_stage.go:63
其它Planningtell_stage.go:66

关键 flag:DidMakePlan 这是连接两阶段的开关。它不在这里设,而是在上一步存回复时设:只要那一步的回复里解析出了新子任务(hasExplicitTasks)或删了子任务,storeOnFinished 就把 flags.DidMakePlan = true(tell_stream_store.go:111-119)。于是——"上一步刚做完 plan"→ 这一步自动切到 Implementation。状态机就是这样靠一个落库的 flag 完成阶段跃迁的。

规划阶段还分两个 phase。tellStage == Planning 时,还要再选一个 PlanningPhase(tell_stage.go:85):

  • PlanningPhaseContext —— 先自动挑上下文。仅当开了 auto-context、有项目地图、且这一步不是"刚做完 context"时进入(tell_stage.go:86)。这一 phase 用 architect 模型,让它根据项目地图挑出该加载哪些文件。细节在 03-context-maps
  • PlanningPhaseTasks —— 真正拆子任务。用 planner 模型,输出 ### Tasks

三态合起来构成 CurrentStage{TellStage, PlanningPhase},存进 state.currentStage(tell_stage.go:97)。后面选模型、选 prompt、判是否继续,全看这个结构。

这一步用哪个模型,由阶段决定(tell_exec.go:181-204,再在 397-409 按 token 量二次细化):

阶段 / phase模型角色
Planning · Contextarchitect
Planning · Tasksplanner
Implementationcoder

3.2 子任务列表:清单如何驱动一步步实现

要解决的小问题。 "规划"产出的清单,得能回写进后续每一步的 prompt,让 coder 知道"总共几步、做到哪了、这步做哪个"。所以子任务要能双向流动:从回复里读出来写回进 prompt

读出来:ParseSubtasks 纯文本解析,不靠模型 function-calling。它在回复里找 ### Tasks 段,然后逐行扫(parse/subtasks.go:10):

  • ^\d+\.\s(如 1. )开头 = 一个新子任务的标题;
  • Uses: 开头 = 这个子任务需要的文件(去掉反引号,逗号分隔);
  • 其余行 = 追加到当前子任务的描述

对应地还有 ParseRemoveSubtasks(parse/subtasks.go:91),扫 ### Remove Tasks 段,按精确标题删任务。

合并:checkNewSubtasks 解析出的新任务不是直接覆盖,而是增量合并(tell_subtasks.go:85):已完成的任务保留;按标题去重,只加新的;然后把 currentSubtask 指向第一个未完成的子任务(tell_subtasks.go:156-163)。删除逻辑同理在 checkRemoveSubtasks(tell_subtasks.go:179)。

写回去:formatSubtasks 每一步拼 prompt 时,把当前清单渲染成一段文本塞进 system prompt(tell_subtasks.go:14)。渲染出来大概长这样(带每项的 Done: yes/no 和当前项标记):

### LATEST PLAN TASKS ###

1. Add health handler
Uses: `server/handlers/health.go`
Done: yes

2. Register the route
Uses: `server/routes.go`
Done: no
Current subtask: yes

最妙的一处:同一段清单文本,尾部会按当前阶段追加一句"你现在不许做什么"的强约束(tell_subtasks.go:61-75)。在 Planning·Tasks 阶段追加"你在规划阶段,绝对不许写任何代码,只能加/删子任务";在 Context 阶段追加"你在 context 阶段,不许写代码也不许改子任务"。这是防止模型在规划时手痒直接开写的护栏。

清单如何"驱动"实现——闭环:

planner 输出 ### Tasks
│ ParseSubtasks / checkNewSubtasks

state.subtasks = [t1(no), t2(no), ...] ; currentSubtask = 第一个 no
│ formatSubtasks 写进 coder 的 system prompt

coder 实现 currentSubtask,输出 "**t1** has been completed"
│ execStatusShouldContinue 认出完成标记

storeOnFinished: t1.IsFinished=true ; currentSubtask ← 下一个 no
│ willContinuePlan: 还有未完成 → 继续

execTellPlan(iteration+1) ↺ 直到没有 no

3.3 流解析:一条 SSE 流怎么变成文件操作

这是本章工程含量最高的一节。模型的回复是一个字一个字流回来的,里面既有给人看的解释文字,又夹着"要写进某文件的代码块"。难点是:边流边认,认出代码块的边界,并且在还没确定边界时别把半个标签直接喷给用户。

代码块长什么样。 coder 被要求用自定义标签而非 markdown 反引号来包代码(prompts/implement.go):文件路径标签 + <PlandexBlock lang="..." path="..."></PlandexBlock>。例如:

- server/handlers/health.go:
<PlandexBlock lang="go" path="server/handlers/health.go">
func Health(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
</PlandexBlock>

主循环:listenStream 它用一个独立 goroutine 收 stream.Recv()(因为 Recv 会永久阻塞),主 goroutine 用 select 同时盯三件事(tell_stream_main.go:75-88):流被取消、超时(firstTokenTimeout 按 token 量放大首字节等待,tell_stream_main.go:286)、以及新 chunk 到达。每收到一个 chunk 就重置计时器,交给 processChunk

逐块:processChunk 每个 chunk 做三件事(tell_stream_processor.go:29):

  1. 喂给 reply parser(replyParser.AddChunkRead,tell_stream_processor.go:78),parser 维护"当前是否在某个文件的代码块里"(CurrentFilePath),据此翻转 processor.fileOpen(:81-101)。
  2. bufferOrStream 决定这段文字现在能不能喷给用户(见下)。
  3. 若解析出的操作数变多handleNewOperations,把新操作排队去构建(:149)。文件操作真正落成 diff 的过程不在本章,见 02-build-apply

核心难点:bufferOrStream(tell_stream_processor.go:165)。 它解决"标签可能被切成两半"的问题。想象模型这一 chunk 只发到 <PlandexBl,下一 chunk 才发 ock lang=...>——你既不能把 <PlandexBl 原样喷给用户,也不能当它是正文。所以引入一组 awaiting* 状态位(定义在 chunkProcessor,tell_state.go:71):

状态位含义
awaitingBlockOpeningTag疑似正在拼 <PlandexBlock ...> 开标签
awaitingBlockClosingTag疑似正在拼 </PlandexBlock> 闭标签
awaitingOpClosingTag疑似正在拼 <EndPlandexFileOps/>
awaitingBackticks文件内容里出现了 ``` 需要转义

规则:只要"疑似半个标签",就把这段缓存contentBuffer、暂不 stream;等后续 chunk 把标签补全,再一次性处理(tell_stream_processor.go:242-256)。补全后做一次翻译:把内部标签换成给用户看的 markdown——<PlandexBlock lang="go" ...> 变成 ```go,</PlandexBlock> 变成 ```(:287-347)。判定"是否可能是标签前缀"靠对 < 后的片段做 strings.HasPrefix 双向匹配(:308-333)。

两处特例值得记:

  • 规划阶段不缓存。 Planning 阶段没有代码块要认,除了手动 stop 序列外一律直接 stream(tell_stream_processor.go:229-234)。缓存机制只在实现阶段这种"要认代码块"的场景才启用。
  • stop 序列 <PlandexFinish/> 请求里把它设为 stop(tell_exec.go:502)。有的模型不支持服务端 stop,于是走"手动 stop":bufferOrStream 在文本里找到该序列,把它之前的内容 stream 出去、并置 shouldStop(:166-205),让主循环据此结束这一步(tell_stream_main.go:242)。

3.4 自动推进:一步做完了,还要不要再来一步?

要解决的小问题。 每条流结束时,得回答两个独立的问题:①当前子任务做完没?②整个计划要不要再自动来一步?

① 子任务做完没:execStatusShouldContinue(exec_status.go:30)。 判据首先是一个字符串标记:coder 被要求在做完时明确输出 **<子任务标题>** has been completed(prompts/implement.go:285)。这里就 strings.Contains 找这个 marker(exec_status.go:50-58),命中即 subtaskFinished = true

  • 代码注释里坦白了一段设计教训:曾经用一次额外的 LLM 调用去"复核"可疑的完成,结果导致太多额外调用角色间反复扯皮("做完了!""没有!"),于是退回到信任完成标记(exec_status.go:60-99 的大段注释)。倾向于"宁可判它做完"。
  • 兜底防死循环:同一子任务的历史消息 ≥ MaxPreviousMessages(=4)就强制标记完成、往下走(exec_status.go:24:121-126)。没命中 marker 又没到上限时,才落到一次真正的模型判断(GetExecStatusFinishedSubtask,用 ExecStatus 角色模型)。

② 整个计划继续吗:willContinuePlan(tell_stream_status.go:138)。 这是自动推进的总闸,分阶段判:

当前阶段是否继续
Planning · Context基本总是继续(去到 Tasks/回答);chat-only 且没加载文件才停(:154-170)
Planning · Taskschat-only 停;AutoContinue 关则停;有新增/删除子任务且没全完成则继续;否则看"是否还有未完成子任务"(:172-200)
Implementation全部子任务完成 → 停;iteration >= MaxAutoContinueIterations(=200)→ 停;否则继续(:202-219)

两道硬护栏:

  • MaxAutoContinueIterations = 200(tell_stream_finish.go:18)——单次 Tell 最多自动推进 200 步,防止无限烧钱。
  • MaxPreviousMessages = 4(exec_status.go:24)——单个子任务最多纠缠 4 条消息,防止卡在一个任务上出不来。

继续的动作。 willContinue == true 时,handleStreamFinished 直接递归execTellPlan(iteration+1)(tell_stream_finish.go:212-223)——注意这不是外层 for 循环,而是"函数尾部再进一次入口",于是又回到 §3.1 重新定阶段。不继续时走收尾:若 build 也完了就 active.Finish(),否则状态置 Building 等构建(:224-273)。

3.5 缺失文件交互:要改一个不在上下文里的文件

要解决的小问题。 模型想改 foo.go,但这个文件在项目里存在、却没被加载进上下文——直接让它凭空重写很危险(可能整段覆盖掉它没看过的内容)。所以要停下来问用户

触发点在 processChunk(tell_stream_processor.go:115-121): 当解析出的 currentFile 满足"不是 chat-only、不在上下文、在项目路径里、且没被允许覆盖"时,调 handleMissingFile

handleMissingFile 的动作(tell_stream_processor.go:595):

  1. 置 plan 状态 MissingFile,把已生成的回复裁剪到"当前文件之前"(GetReplyForMissingFile);
  2. 向客户端推 PromptMissingFile 消息,取消当前模型流(CancelModelStreamFn,:679);
  3. 阻塞等用户选择(MissingFileResponseCh,30 分钟超时,:684-700);
  4. 拿到选择后 ResetModelCtx,再递归 execTellPlan(...),但带上 missingFileResponse保持同一 iteration(:714-723)。

用户的三个选择,由 handleMissingFileResponse 落实(tell_missing_file.go:13):

选择行为(tell_missing_file.go)
Skip丢弃当前文件那段,标记 SkippedPaths,追加"跳过并继续"的 user prompt(:39-51:71-99)
Overwrite标记 AllowOverwritePaths[file]=true,让它可以覆盖(:54-58)
(Load)客户端会加载该文件进上下文,再续写

这样,当 execTellPlan 带着 missingFileResponse != "" 重进时,getTellSysPrompt 之后会调 handleMissingFileResponse 把"之前那半段回复 + 处理指令"接回消息序列(tell_exec.go:375),流从断点续上。


4. 深入实现(一步 execTellPlan 的完整装配)

给要读源码的人:一次 execTellPlan(tell_exec.go:86)从进入到发流,按序做这些事。

  1. 取活动 plan、装 recover。GetActivePlan,defer 一个 panic 兜底把错误送进 StreamDoneCh(:103-122)。
  2. WillExecPlan hook + 状态置 Replying(:124-154)。
  3. loadTellPlan 载入会话、子任务、摘要、上下文地图等到 state(tell_load.go:20;其中 state.hasContextMap/contextMapEmpty:371-377 定)。
  4. resolveCurrentStage 定阶段(§3.1),同时把上一步的 ActivatedPaths 继承过来供 context phase 用(tell_stage.go:74-83)。
  5. 选 tentative 模型 + max tokens,按阶段分支(tell_exec.go:181-204)。
  6. dryRunCalculateTokensWithoutContext——先克隆一份 state,不含上下文地拼一遍 prompt 算 token,确认没超限(tell_exec.go:584)。这里也是"没有当前子任务可做"这类错误的兜出口(AllTasksCompletedMsg,:621)。
  7. formatModelContext 按阶段拼上下文消息:实现阶段只拼 basic/smart context;规划阶段先拼可缓存的 planStageSharedMsgs(含地图),Tasks phase 再拼 auto-context(:215-276)。上下文如何被选,见 03
  8. getTellSysPrompt 按阶段挑 prompt 文本并把子任务清单插进去(§3.2 / §4.1)。
  9. 算 token、选最终模型(按 requestTokensGetRoleForInputTokens 细化,:397-409),按能力裁剪消息(不支持 cache-control / 图片就删对应部分,:431-454)。
  10. WillSendModelRequest hook → doTellRequest(:464-481):组 ExtendedChatCompletionRequest(Stream:true、stop=<PlandexFinish/>),处理 fallback,CreateChatCompletionStream,再 go state.listenStream(tell_exec.go:494-582)。
  11. 可选:queuePendingBuilds,并在 ActivePlan 上登记本次 replyId(:483-490)。

4.1 getTellSysPrompt 的分支(tell_sys_prompt.go:24)

同一个函数按 currentStage 走两条完全不同的路:

  • Planning。 先放 planStageSharedMsgs(地图等,可缓存);Context phase 追加 GetAutoContextTellPrompt;Tasks phase 追加 GetPlanningPrompt + formatSubtasks()(:56-102)。
  • Implementation。 若没有 currentSubtask 直接返回 AllTasksCompletedMsg 错误(:127-129);否则放 GetImplementationPrompt(currentSubtask.Title) + 子任务清单 + 上下文消息(:131-174)。

注意它对"阶段和消息类型不匹配"做了强校验:规划阶段收到 implementation 消息、或实现阶段收到 planningShared 消息,都直接报错(:122-124:176-179)——阶段隔离是硬约束。

4.2 落库:storeOnFinished(tell_stream_store.go:30)

流结束后把这一步的成果原子地写进 git-backed 存储(ExecRepoOperation 加写锁)。核心是结算子任务 + 计算 flags:

  • 若有新增/删除子任务或当前子任务完成,重新计算 currentSubtask(下一个未完成)与 allSubtasksFinished(:64-94);否则给当前子任务 NumTries++(:95-98)。
  • 组装 ConvoMessageFlags:DidWriteCode(有操作)、DidMakePlan(有新/删任务)、DidCompleteTaskDidCompletePlan(全完成)等(:104-133)。这些 flag 就是下一步 resolveCurrentStage 读的状态——状态机的"记忆"落在这里。
  • storeAssistantReply 把回复连同 flags、当前子任务、激活路径写成一条 ConvoMessage(:248-305),再存 description、存子任务、GitAddAndCommit(:172-210)。

5. 巧妙之处(可借鉴的技术)

  • 状态机不靠内存变量,靠"上一条会话消息的 flag"。 resolveCurrentStage 每步现算,阶段跃迁全凭落库的 DidMakePlan/CurrentStage(tell_stage.go:60-70)。好处:进程重启、并发、断点续传都不会丢状态——状态就是持久化的对话本身。
  • 子任务用纯文本 ### Tasks 而非 function-calling。 ParseSubtasks 手写解析(parse/subtasks.go:10)。牺牲一点严格性,换来对所有模型通用(不依赖 tool-use 能力)、且对流式友好(边流边能看清单)。
  • "半个标签先缓存"的流解析。 bufferOrStreamawaiting* 状态位把可能被切断的 <PlandexBlock> 标签攒齐再翻译成 markdown(tell_stream_processor.go:242-347),既能实时 stream 给用户,又不会把内部协议标签泄露出去。
  • 信任完成标记,而不是再叫一个 LLM 复核。 exec_status.go:60-99 的注释是一份难得的"反面经验":多智能体互相验证会陷入扯皮与成本爆炸,于是果断退回"信任 marker + 硬上限防死循环"。
  • 两道数值护栏防失控。 MaxAutoContinueIterations=200(整个计划)与 MaxPreviousMessages=4(单个子任务)一大一小,分别防"计划无限循环"和"卡在一个任务上"。
  • 缺失文件是"停流—问人—原地续"而非报错。 handleMissingFile 取消流、阻塞等用户、再以同一 iteration 递归续写(tell_stream_processor.go:679-723),把人审边界干净地嵌进流里。相关人审设计见 05-diff-sandbox-cli

6. 边界与局限

  • 不做 diff 构建。 本循环只把回复解析成"操作"并排队;把 <PlandexBlock> 内容真正合进文件是独立的构建流水线,见 02
  • 不做上下文选取。 context phase 只是"触发"自动选上下文,具体怎么按项目地图/PageRank 选,见 03
  • 完成判定可能误判。 既然"信任完成标记",模型谎报 has been completed 就会让子任务被过早标记完成(设计上"宁可判做完")。
  • 子任务解析对格式敏感。 ParseSubtasks 依赖 ### Tasks 段和 ^\d+\.\s 格式(parse/subtasks.go:33);模型若用字母列表或缩进错乱,可能解析不到——所以 prompt 里反复强调"必须用整数编号"。
  • 单步 token 超限直接失败。 装配阶段多处在"加对话前就超限"时直接报错返回(tell_exec.go:339-351),不会自动截断重试。

7. 横向对比

同 shelf 的其它编码 agent 各有取舍,可对照阅读:

  • 阶段划分。 Plandex 把"规划 / 实现"做成显式的、持久化的两阶段状态机(靠会话 flag 跃迁);很多轻量 agent 只有单阶段"边想边写"。
  • 子任务表示。 这里用人类可读的 markdown 清单当"计划的真相源",而非隐藏的 JSON/tool 结构——便于人审、便于跨模型。
  • 人审边界。 缺失文件时"停流问人"是 Plandex 的显式护栏;完整的差异沙箱与 CLI 人审工作流见 05-diff-sandbox-cli

(跨库第一性原理对比见各 shelf 总库文档的"规划-执行循环"一节。)


8. 代码地图(导航索引)

主题文件路径关键符号
Tell 入口 / handler 调用app/server/handlers/plans_exec.goTell(在 :98 被调)
一步的总入口与装配app/server/model/plan/tell_exec.goTellTellParamsexecTellPlanexecTellPlanParamsdoTellRequestdryRunCalculateTokensWithoutContext
两阶段状态机app/server/model/plan/tell_stage.goresolveCurrentStagelastSuccessfulConvoMessage
阶段 flag 定义app/server/model/plan/tell_stream_store.goConvoMessageFlags(DidMakePlan 等,:104-133)
system prompt 组装app/server/model/plan/tell_sys_prompt.gogetTellSysPromptgetTellSysPromptParamsAllTasksCompletedMsg
规划 / 实现 prompt 文本app/server/model/prompts/planning.goprompts/implement.goGetPlanningPromptGetAutoContextTellPromptGetImplementationPrompt
子任务写回 / 合并app/server/model/plan/tell_subtasks.goformatSubtaskscheckNewSubtaskscheckRemoveSubtasks
子任务文本解析app/server/model/parse/subtasks.goParseSubtasksParseRemoveSubtasks
流主循环app/server/model/plan/tell_stream_main.golistenStreamfirstTokenTimeout
chunk 解析 / 缓存app/server/model/plan/tell_stream_processor.goprocessChunkbufferOrStreamhandleNewOperationsreplaceCodeBlockOpeningTagopeningTagRegex
流状态与处理器结构app/server/model/plan/tell_state.goactiveTellStreamStatechunkProcessor(awaiting* 状态位)
子任务完成判定app/server/model/plan/exec_status.goexecStatusShouldContinueMaxPreviousMessages
自动推进判定app/server/model/plan/tell_stream_status.gowillContinuePlanwillContinuePlanParamshandleDescAndExecStatus
流结束收尾 / 递归续跑app/server/model/plan/tell_stream_finish.gohandleStreamFinishedMaxAutoContinueIterations
落库与 flagsapp/server/model/plan/tell_stream_store.gostoreOnFinishedstoreAssistantReplystoreOnFinishedParams
缺失文件交互app/server/model/plan/tell_stream_processor.gotell_missing_file.gohandleMissingFilehandleMissingFileResponse