Plandex
一句话定位
Plandex 是一个 client/server 架构的终端编码 agent:CLI 只是薄驱动,全部 agent 编排/prompt 组装/LLM 流式循环/子任务规划/“是否完成”判定都跑在服务端。它不是 function-calling / tool-calling agent,而是用一套嵌在流式回复里的纯文本操作协议(### 标签段 + 少量 XML 式控制标签如 <PlandexFinish/>)驱动模型,由手写流式解析器(app/server/types/reply.go)解析。分析所依据的 tip commit e2d7720(2025-10-03)附带 “cloud wind down” 说明——托管版 Plandex Cloud 正在关停,仓库保持开源自托管。
核心架构总览(目录结构关键路径 + 引用的 commit)
commit e2d772072efadbe41d2946d97d79be55532dbab5,Go 语言 monorepo,主干在 app/:
app/cli—— 终端客户端(薄驱动:发 prompt、收流、本地文件 I/O + 命令执行)app/server—— 所有 agent 逻辑(app/server/model/plan/*是核心)app/shared—— 共享类型 / 模型角色配置docs/docs—— 面向用户的核心概念与安全文档
一次 tell 的执行链条:CLI 发请求 → 服务端 Tell()(tell_exec.go:32)激活 plan 并在 goroutine 里跑 execTellPlan()(tell_exec.go:58)→ 组装分阶段系统 prompt → 走 SSE 流式拉模型回复 → 手写解析器边流边解析出各 ### 操作段 → 流结束进 handleStreamFinished()(tell_stream_finish.go:25)判定是否递归继续。产出的文件编辑先落到服务端每个 plan 一个的影子 git 仓库(pending changes),用户 review/apply 后才落到真实项目文件。
Agent Loop(主循环 / 何时继续何时停)
主循环不是 for 循环,而是尾递归的阶段状态机。 每次模型回复流结束后进入 handleStreamFinished(),若 willContinuePlan() 返回 true,就以 iteration+1 再次调用 execTellPlan()(tell_stream_finish.go:215)。
三个阶段(shared.TellStage,在 resolveCurrentStage() tell_stage.go:22 解析):
Planning/ phaseContext(即 “architect”,仅当开启 AutoContext 且有 codebase map 时)——遍历项目 map 决定加载哪些文件,输出### Files后<PlandexFinish/>。Planning/ phaseTasks—— 输出### Tasks子任务清单,结尾必须<PlandexFinish/>。Implementation—— 逐个执行子任务(一轮一个)。
下一轮的阶段由上一条成功 convo 消息的 flags(DidMakePlan、CurrentStage)推断(tell_stage.go:56-95)。
继续/停止判定 = willContinuePlan()(tell_stream_status.go:138):
- Context 阶段 → 总是继续进入 planning(除非纯 chat 且无文件)
:154-170。 - Planning-tasks → 当
AutoContinue为真、存在未完成的新增/移除子任务、且有currentSubtask时继续:172-200。 - Implementation → 只要还有未完成子任务就继续,硬上限
MaxAutoContinueIterations = 200(tell_stream_finish.go:18,在tell_stream_status.go:212检查)。
发给模型的 stop 序列:stop = ["<PlandexFinish/>"](tell_exec.go:502,build_validate_and_fix.go:254 同);不支持 stop 参数的模型走手工 state.manualStop(tell_exec.go:542)。模型被要求在 ### Tasks 段后必须吐 <PlandexFinish/>(planning.go:103-105)。流式经由 github.com/sashabaranov/go-openai 的 CreateChatCompletionStream(tell_exec.go:568)。
记忆与上下文管理(压缩、长期记忆、会话持久化)
- 会话持久化:全部存 Postgres(
server/db)加每个 plan 一个的 git 仓库。convo 消息(db.ConvoMessage)与摘要(db.ConvoSummary)跨tell调用持久,循环通过loadTellPlan()重新载入。 - 压缩 = 滚动摘要(
tell_summary.go):每次回复后后台 goroutine 跑summarizeConvo()(tell_stream_finish.go:117-147)。下次请求时addConversationMessages()(tell_summary.go:22)计算 convo token;一旦tokensBeforeConvo+conversationTokens超过GetPlannerEffectiveMaxTokens(),或 convo 本身超过GetPlannerMaxConvoTokens()(tell_summary.go:61),就用能塞进限额的最新摘要替换最老的一批消息(按LatestConvoMessageCreatedAt挑摘要,:72-),即”摘要 + 原始消息尾部”。 - 智能上下文 / 滑窗(docs
context-management.md):实现阶段只加载子任务在Uses:里列出的文件(req.SmartContext,tell_exec.go:216-220,formatModelContext(smartContextEnabled: ...)),文档称之为”随步骤增长与收缩的滑动上下文窗口”。 - codebase map 作为长期记忆:生成并常驻一份项目地图,architect 阶段遍历它决定拉哪些文件(
architect_context.go)。没有向量库 / embedding RAG——检索是模型基于 map 的符号名/用途驱动的(### Files段列出相关 symbol,见architect_context.go:211)。
工具体系(定义/调用协议/注册/权限)
无 function-calling / 无 JSON tool schema / 无 MCP。 对 mcp、ToolCalls、function_call 的 grep 在 server+shared 只命中命名、消息类型等辅助用途。agent 的”工具”就是它写进回复里的文本段,由 types/reply.go 解析:
### Tasks/### Remove Tasks—— plan 子任务(tell_subtasks.go+parse/subtasks.go)。- 带标签的代码块(fenced block 前一行是文件路径)→
OperationTypeFile文件写入(reply.go:161、:193-211)。 ### Move Files/### Remove Files/### Reset Changes→ 对 in-context/pending 文件的移动/删除/重置操作,以<EndPlandexFileOps/>终止(reply.go:233-310,prompt 在file_ops.go)。### Commands→ 写入特殊_apply.sh文件的 shell 命令(execution mode)。### Files→ architect 想在下一阶段加载的上下文文件集(architect_context.go)。
权限不是 per-tool 而是 per-autonomy-level(见”安全与权限”):控制 agent 是否可写命令(can-exec)、是否自动 apply、是否自动 exec 等。
Prompt 设计(系统提示结构、动态组装)
- 分阶段在
getTellSysPrompt()(tell_sys_prompt.go:24)组装成一个有序的ExtendedChatMessagePart列表(多 part 系统消息),并插入 Anthropic 式CacheControlephemeral 断点,使共享前缀(maps + manual context)被 prompt-cache(tell_sys_prompt.go:69-101;若模型不支持则在tell_exec.go:431-439剥掉 cache control)。 - 各阶段 prompt 正文都在
model/prompts/:- Context 阶段 →
GetAutoContextTellPrompt/GetAutoContextChatPrompt(architect_context.go)。 - Tasks 阶段 →
GetPlanningPrompt(planning.go:12)或纯 chat 的GetChatSysPrompt。 - Implementation →
GetImplementationPrompt(subtaskTitle)(implement.go)。
- Context 阶段 →
- Prompt 高度依赖 flags 条件分支(
CreatePromptParams:ExecMode、AutoContext、IsGitRepo、IsUserDebug、ContextTokenLimit,planning.go:3-10)。如 execution 模式分支会加入### Commands/_apply.sh指令(planning.go:43-97)。 - 当前子任务清单每轮重新注入,通过
formatSubtasks()(tell_subtasks.go:14)并带一段醒目的阶段守卫提醒(“你处在 PLANNING 阶段,绝对不许实现……”,tell_subtasks.go:63-74),防止阶段串味。 - 共享
Identity字符串作为 planner prompt 前缀(planning.go:13)。
Router / 编排(任务分解、多 agent、子 agent)
两段式 planner→implementer 分解,全程单线程、同属一个 plan 的顺序 turn。 architect(context 阶段)做高层规划并声明所需文件(architect_context.go 的 “Context Phase / Decide and Declare”);planner 输出显式编号的 ### Tasks 子任务清单;implementer 随后一轮实现一个子任务(GetImplementationPrompt(state.currentSubtask.Title),tell_sys_prompt.go:134)。
子任务生命周期在 tell_subtasks.go:解析新增(checkNewSubtasks :85)、移除(checkRemoveSubtasks :179)、把 currentSubtask 推进到首个未完成项(:156-163)。
不是并发意义上的多 agent——主路径无子 agent 派生 / 无并行 worker。所谓”角色”(见”与模型的协同设计”)是同一条流水线不同步骤调用的不同模型配置,不是自治 agent。唯一的并行是 diff 构建按文件并发(build_race.go),那是机械的按文件生成 diff,不是推理 agent。
Skill / 插件体系
未实现。 没有 skill/plugin 加载器,没有用户可扩展的 tool registry,没有 MCP。可扩展性仅限于换模型 / model-pack、开关内置能力(exec 模式、浏览器调试)。唯一的可插拔缝是 server/hooks(hooks.ExecHook,如 WillExecPlan、WillSendModelRequest,tell_exec.go:126,464)——这是构建期 hook 接口,用于注入云/授权版的计费与鉴权,不是用户插件系统。
自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)
- 无运行期自我改进 / 无跨 plan 学习记忆。 没有反思库、没有策略/权重更新、没有跨会话经验记忆。
- 有一个针对产出 diff 的有界”生成→校验→修复”循环(针对的是产物,不是 agent 推理):
build_validate_and_fix.go的buildValidateLoop,MaxValidationFixAttempts = 3(build_validate_and_fix.go:22),由 tree-sitter 语法校验(server/syntax/validate.go、structured_edits_tree_sitter.go)驱动;若某次编辑产生非法语法,builder 模型被重新 prompt 去修。 - 自动调试(docs
execution-and-debugging.md,cli/plan_exec/apply_exec.go:140):plandex debug 'cmd'跑命令、把失败输出喂回模型、应用修复、重试至多 N 次(默认 5)——一个 eval 驱动的纠错循环,但对本次任务是短暂的。 - 离线 eval 存在,是 promptfoo POC(
test/evals/promptfoo-poc/{build,fix,verify})——开发期 prompt 回归测试,不在 agent 内。
可观测性(日志 / trace 格式)
- 无 OpenTelemetry / 结构化 tracing / 分析(对 otel/opentelemetry/jaeger/posthog/segment/telemetry 在
app/**.go的 grep = 0)。可观测性 = 详尽的 Golog.Printf到 stdout,打点密集(如[willContinuePlan]、[ExecStatus]、[TellExec]之类前缀遍布tell_*.go)。 - 错误经
server/notify(notify.NotifyErr(severity, err))走运维告警。 - 面向客户端的”trace” = 一套类型化 stream-message 协议(
shared.StreamMessage,类型如Describing、LoadContext、RepliesFinished,tell_stream_finish.go:56,173,269)驱动 CLI TUI;完整 plan 历史可从每 plan 的 git log 重建(plandex log,见”沙箱”)。
安全与权限(审批门、密钥管理)
- autonomy levels 是核心权限模型(docs
autonomy.md、context-management.md):none / basic / plus / semi / full,每档是一组布尔预设:auto-continue、auto-build、auto-load-context、smart-context、auto-update-context、auto-apply、can-exec、auto-exec、auto-debug、auto-commit。只有full同时开auto-apply+auto-exec+auto-debug。 - CLI apply 流程里的审批门(
cli/lib/apply.go):除非AutoConfirm,否则会提示"Apply changes to N files?"(:181)、运行_apply.sh前"Execute now?"(:290)、"Did the commands succeed?"(:536)、"Commit Plandex updates now?"(:601)。_apply.sh仅在设置AutoExec时自动跑(:287)。 - API key 处理(docs
security.md):BYO-key 模式下 key 仅在内存中短暂存在——不写盘、不入日志、不入 DB,plan 流结束即擦除。key 以authVars随请求流转,不持久化。 - 尊重
.gitignore+.plandexignore;被忽略文件不加载,除非--force(security.md)。
沙箱与执行隔离
- “沙箱” = 每个 plan 一个的服务端影子 git 仓库,不是执行牢笼。
db/git.go:InitGitRepo(orgId, planId)→ 在getPlanDir()里git init -b main(git.go:37-44);每个 plan 事件(context 变更、prompt、reply、build、apply、reject)都是一次 commit(GitAddAndCommit,git.go:67;docsversion-control.md)。提议的编辑先作为 “pending changes” 在这里堆积,可 review(plandex diff)/ reject(plandex reject)/ rewind(GitRewindToSha,git.go:95;plandex rewind),在触及真实项目文件之前(docsreviewing-changes.md)。这就是”累积 diff review 沙箱”。 - 命令执行不与主机隔离——
_apply.sh直接在用户机器上跑。隔离仅限于进程组 + Linux cgroup,用于干净地杀掉 agent 进程树:apply_proc.go设SysProcAttr{Setpgid:true},KillProcessGroup对-pid发信号;apply_cgroup_linux.go把 PID 放进 transient systemd scope 并KillMode=control-group(:43),best-effort(不可用时记录并继续,:29)。无容器化。 文档明确建议对auto-exec/full使用干净 git 状态 / 隔离分支(execution-and-debugging.md“Safety”)。另有可选的基于 Chrome 的浏览器应用调试(plandex browser)。
与模型的协同设计
- 9 角色异构 “model pack”(
shared/ai_models_roles.go,已核对源文件):planner、coder、architect、summarizer(ModelRolePlanSummary)、builder、whole-file-builder、names(ModelRoleName)、commit-messages、auto-continue(ModelRoleExecStatus,即 exec-status 判定官)。每个角色有独立模型配置,在流水线对应步骤派发(planner/architect/coder 在tell_exec.go:181-207选择;exec-status 用settings.GetModelPack().ExecStatus,exec_status.go:36)。 - 按角色、token 感知的模型选择:
GetRoleForInputTokens(requestTokens, settings)(tell_exec.go:400-408)在请求很大时可选更大上下文的变体(有ai_models_large_context.go)。fallback 链在出错/供应商失败时经GetFallbackForModelError(tell_exec.go:500)做供应商回退 + 重试。 - 能力自适应下调:消息按所选 base 模型下调——不支持则剥掉 cache-control 断点(
!SupportsCacheControl,tell_exec.go:431)、不支持图像则剥掉 image part(!HasImageSupport,tell_exec.go:442-454);输出格式在 exec-status 判定官里按PreferredOutputFormat分支(XML vs JSON,exec_status.go:173-202)。 - 按设计分工:推理模型(planner/coder)吐松散的带标签代码块;另一个更便宜的 builder 模型把它们转成精确的结构化编辑/diff,再由 tree-sitter 校验(见”自进化”)。这把推理质量与编辑落地精度解耦。
轨迹利用(session/trajectory 是否反哺训练/评测)
未实现。 无遥测/分析管线(grep = 0),无用于训练的轨迹导出,无 RLHF/数据采集路径。轨迹只为用户自己持久化:Postgres convo 历史 + 每 plan git log,支撑 plandex log / convo / rewind / 分支续跑(version-control.md)。离线 promptfoo eval 资产(见”自进化”)是静态 fixture,不是采集来的用户会话。
与同类 harness 的关键差异(1-3 条)
- 纯文本操作协议而非 function-calling:绝大多数现代编码 agent(Claude Code / Aider tool 模式等)靠模型的 tool-calling 或结构化 tool schema;Plandex 反其道,全部动作用
###文本段 +<PlandexFinish/>控制标签表达,手写流式解析器解码——并显式做”推理模型出松散块、便宜 builder 模型转精确 diff”的两模型分工。 - 服务端集中式 + 每 plan 影子 git 沙箱:agent 逻辑不在 CLU 端而在 server;编辑先落到服务端影子 git 仓库作为可 review/rewind 的 pending changes,与项目工作区解耦,这套版本化 review 沙箱比多数”直接改工作区 + 靠 host git 兜底”的 harness 更重也更安全。
- 9 角色 model pack + token 感知路由 + 能力下调:把 planner/coder/architect/summarizer/builder/exec-status 等拆成独立可配模型并各司其职,配合 fallback 链和按模型能力剥 cache-control/image 的自适应,模型协同设计明显强于单模型 harness。反之 skill/插件、可观测性(无 otel)、轨迹反哺(无)都缺位。
原始源码定位
- repo: https://github.com/plandex-ai/plandex
- commit/version analyzed:
e2d772072efadbe41d2946d97d79be55532dbab5(2025-10-03 14:49:54 -0700,“link to cloud wind down post”) - 关键文件列表(相对仓库根):
app/server/model/plan/tell_exec.go——Tell()入口 + 每轮execTellPlan()、stop 序列、模型选择/能力下调app/server/model/plan/tell_stream_finish.go—— 流结束处理 + 自动继续递归、MaxAutoContinueIterations=200app/server/model/plan/tell_stream_status.go——willContinuePlan()继续/停止判定app/server/model/plan/tell_stage.go——resolveCurrentStage()阶段解析app/server/model/plan/tell_subtasks.go—— 子任务解析/格式化/增删/推进app/server/model/plan/exec_status.go—— 子任务完成判定(marker + LLM 兜底)app/server/model/plan/tell_sys_prompt.go—— 分阶段系统 prompt 组装app/server/model/plan/tell_summary.go—— 会话摘要 / 上下文压缩app/server/model/plan/build_validate_and_fix.go—— diff 构建 validate/fix 循环,MaxValidationFixAttempts=3app/server/model/prompts/planning.go、architect_context.go、file_ops.go—— prompt 文本app/server/types/reply.go—— 流式回复/操作解析器app/server/db/git.go—— 每 plan 影子 git 仓库(“沙箱”)app/shared/ai_models_roles.go—— 9 个模型角色app/cli/lib/apply.go、apply_cgroup_linux.go、apply_proc.go—— apply + 命令执行 + 进程组/cgroup 隔离- docs:
docs/docs/core-concepts/{autonomy,context-management,reviewing-changes,execution-and-debugging,version-control}.md、docs/docs/security.md
一手源存档(sources/)
/Users/zhao/projects/self-wiki/ai-research/sources/harness/plandex/:
NOTES.md—— 第一阶段源码级调研笔记(含 provenance、12 维度逐条 + 文件行号、scorecard)src/—— 18 个核心源文件留档(240K):ai_models_roles.go、apply.go、apply_cgroup_linux.go、apply_proc.go、architect_context.go、build_validate_and_fix.go、exec_status.go、file_ops.go、git.go、planning.go、reply.go、tell_exec.go、tell_stage.go、tell_stream_finish.go、tell_stream_status.go、tell_subtasks.go、tell_summary.go、tell_sys_prompt.go
(原始 clone 在归档后已删除;上述 src/ 为核对与复现依据。)