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 解析):

  1. Planning / phase Context(即 “architect”,仅当开启 AutoContext 且有 codebase map 时)——遍历项目 map 决定加载哪些文件,输出 ### Files<PlandexFinish/>
  2. Planning / phase Tasks —— 输出 ### Tasks 子任务清单,结尾必须 <PlandexFinish/>
  3. Implementation —— 逐个执行子任务(一轮一个)。

下一轮的阶段由上一条成功 convo 消息的 flags(DidMakePlanCurrentStage)推断(tell_stage.go:56-95)。

继续/停止判定 = willContinuePlan()tell_stream_status.go:138):

  • Context 阶段 → 总是继续进入 planning(除非纯 chat 且无文件):154-170
  • Planning-tasks → 当 AutoContinue 为真、存在未完成的新增/移除子任务、且有 currentSubtask 时继续 :172-200
  • Implementation → 只要还有未完成子任务就继续,硬上限 MaxAutoContinueIterations = 200tell_stream_finish.go:18,在 tell_stream_status.go:212 检查)。

发给模型的 stop 序列stop = ["<PlandexFinish/>"]tell_exec.go:502build_validate_and_fix.go:254 同);不支持 stop 参数的模型走手工 state.manualStoptell_exec.go:542)。模型被要求在 ### Tasks 段后必须吐 <PlandexFinish/>planning.go:103-105)。流式经由 github.com/sashabaranov/go-openaiCreateChatCompletionStreamtell_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.SmartContexttell_exec.go:216-220formatModelContext(smartContextEnabled: ...)),文档称之为”随步骤增长与收缩的滑动上下文窗口”。
  • codebase map 作为长期记忆:生成并常驻一份项目地图,architect 阶段遍历它决定拉哪些文件(architect_context.go)。没有向量库 / embedding RAG——检索是模型基于 map 的符号名/用途驱动的(### Files 段列出相关 symbol,见 architect_context.go:211)。

工具体系(定义/调用协议/注册/权限)

无 function-calling / 无 JSON tool schema / 无 MCP。mcpToolCallsfunction_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 式 CacheControl ephemeral 断点,使共享前缀(maps + manual context)被 prompt-cache(tell_sys_prompt.go:69-101;若模型不支持则在 tell_exec.go:431-439 剥掉 cache control)。
  • 各阶段 prompt 正文都在 model/prompts/
    • Context 阶段 → GetAutoContextTellPrompt / GetAutoContextChatPromptarchitect_context.go)。
    • Tasks 阶段 → GetPlanningPromptplanning.go:12)或纯 chat 的 GetChatSysPrompt
    • Implementation → GetImplementationPrompt(subtaskTitle)implement.go)。
  • Prompt 高度依赖 flags 条件分支CreatePromptParamsExecModeAutoContextIsGitRepoIsUserDebugContextTokenLimitplanning.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/hookshooks.ExecHook,如 WillExecPlanWillSendModelRequesttell_exec.go:126,464)——这是构建期 hook 接口,用于注入云/授权版的计费与鉴权,不是用户插件系统。

自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)

  • 无运行期自我改进 / 无跨 plan 学习记忆。 没有反思库、没有策略/权重更新、没有跨会话经验记忆。
  • 有一个针对产出 diff 的有界”生成→校验→修复”循环(针对的是产物,不是 agent 推理):build_validate_and_fix.gobuildValidateLoopMaxValidationFixAttempts = 3build_validate_and_fix.go:22),由 tree-sitter 语法校验server/syntax/validate.gostructured_edits_tree_sitter.go)驱动;若某次编辑产生非法语法,builder 模型被重新 prompt 去修。
  • 自动调试(docs execution-and-debugging.mdcli/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)。可观测性 = 详尽的 Go log.Printf 到 stdout,打点密集(如 [willContinuePlan][ExecStatus][TellExec] 之类前缀遍布 tell_*.go)。
  • 错误经 server/notifynotify.NotifyErr(severity, err))走运维告警。
  • 面向客户端的”trace” = 一套类型化 stream-message 协议shared.StreamMessage,类型如 DescribingLoadContextRepliesFinishedtell_stream_finish.go:56,173,269)驱动 CLI TUI;完整 plan 历史可从每 plan 的 git log 重建(plandex log,见”沙箱”)。

安全与权限(审批门、密钥管理)

  • autonomy levels 是核心权限模型(docs autonomy.mdcontext-management.md):none / basic / plus / semi / full,每档是一组布尔预设:auto-continueauto-buildauto-load-contextsmart-contextauto-update-contextauto-applycan-execauto-execauto-debugauto-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;被忽略文件不加载,除非 --forcesecurity.md)。

沙箱与执行隔离

  • “沙箱” = 每个 plan 一个的服务端影子 git 仓库,不是执行牢笼。db/git.goInitGitRepo(orgId, planId) → 在 getPlanDir()git init -b maingit.go:37-44);每个 plan 事件(context 变更、prompt、reply、build、apply、reject)都是一次 commit(GitAddAndCommitgit.go:67;docs version-control.md)。提议的编辑先作为 “pending changes” 在这里堆积,可 review(plandex diff)/ reject(plandex reject)/ rewind(GitRewindToShagit.go:95plandex rewind),在触及真实项目文件之前(docs reviewing-changes.md)。这就是”累积 diff review 沙箱”。
  • 命令执行不与主机隔离——_apply.sh 直接在用户机器上跑。隔离仅限于进程组 + Linux cgroup,用于干净地杀掉 agent 进程树:apply_proc.goSysProcAttr{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,已核对源文件):plannercoderarchitectsummarizerModelRolePlanSummary)、builderwhole-file-buildernamesModelRoleName)、commit-messagesauto-continueModelRoleExecStatus,即 exec-status 判定官)。每个角色有独立模型配置,在流水线对应步骤派发(planner/architect/coder 在 tell_exec.go:181-207 选择;exec-status 用 settings.GetModelPack().ExecStatusexec_status.go:36)。
  • 按角色、token 感知的模型选择GetRoleForInputTokens(requestTokens, settings)tell_exec.go:400-408)在请求很大时可选更大上下文的变体(有 ai_models_large_context.go)。fallback 链在出错/供应商失败时经 GetFallbackForModelErrortell_exec.go:500)做供应商回退 + 重试。
  • 能力自适应下调:消息按所选 base 模型下调——不支持则剥掉 cache-control 断点(!SupportsCacheControltell_exec.go:431)、不支持图像则剥掉 image part(!HasImageSupporttell_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 条)

  1. 纯文本操作协议而非 function-calling:绝大多数现代编码 agent(Claude Code / Aider tool 模式等)靠模型的 tool-calling 或结构化 tool schema;Plandex 反其道,全部动作用 ### 文本段 + <PlandexFinish/> 控制标签表达,手写流式解析器解码——并显式做”推理模型出松散块、便宜 builder 模型转精确 diff”的两模型分工。
  2. 服务端集中式 + 每 plan 影子 git 沙箱:agent 逻辑不在 CLU 端而在 server;编辑先落到服务端影子 git 仓库作为可 review/rewind 的 pending changes,与项目工作区解耦,这套版本化 review 沙箱比多数”直接改工作区 + 靠 host git 兜底”的 harness 更重也更安全。
  3. 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=200
    • app/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=3
    • app/server/model/prompts/planning.goarchitect_context.gofile_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.goapply_cgroup_linux.goapply_proc.go —— apply + 命令执行 + 进程组/cgroup 隔离
    • docs:docs/docs/core-concepts/{autonomy,context-management,reviewing-changes,execution-and-debugging,version-control}.mddocs/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.goapply.goapply_cgroup_linux.goapply_proc.goarchitect_context.gobuild_validate_and_fix.goexec_status.gofile_ops.gogit.goplanning.goreply.gotell_exec.gotell_stage.gotell_stream_finish.gotell_stream_status.gotell_subtasks.gotell_summary.gotell_sys_prompt.go

(原始 clone 在归档后已删除;上述 src/ 为核对与复现依据。)