GPT-Pilot / Pythagora

一句话定位

GPT-Pilot 是一个把「从零构建一个全栈应用」拆解成 epics → tasks → steps → iterations 层级、由约 20 个专职 agent 接力完成的编码 harness。它的核心不是单条 ReAct 循环,而是一个在版本化项目状态之上运行的确定性有限状态机(FSM):下一个跑哪个 agent 完全由持久化的 ProjectState 决定,全程不使用任何原生 function calling,所有「工具」都是 prompt 内的 JSON / 标签 schema,由 pydantic 解析。

背景警示:本仓库 README 顶部标注「不再维护」(开发已转向闭源产品 pythagora.ai),且历史上出现过一次供应链投毒事件(见「安全与权限」章)。本 dossier 分析的是清理后的 HEAD。

核心架构总览(目录结构关键路径 + 引用的 commit)

  • 分析 commit:9b763fdaf0020c7d8abacc7b58b2b09e574946232026-06-12 09:28:32 +0200,合并 PR #1183「remove-malicious-telemetry-loader」)。这是 pilot v2 重写版,代码全部位于 core/(v1 曾在 pilot/,仅剩 core/db/v0importer.py + core/config/env_importer.py 兼容导入旧项目/.env)。
  • 语言栈:Python 3 async + SQLAlchemy/Alembic + Jinja2 + pydantic。Node/React 是目标产物的技术栈,不是 harness 本身。
  • 关键目录:
    • core/agents/ — 约 20 个 BaseAgent 子类(专职 agent)+ 编排器 orchestrator.py
    • core/state/state_manager.py — 版本化状态持久化(copy-on-write 的 ProjectState 链)
    • core/llm/ — provider 分发、无 function-calling 的消息适配、JSON schema 解析与重试
    • core/prompts/<agent_type>/ + core/prompts/partials/ — 每 agent 一套 Jinja 模板
    • core/proc/process_manager.py — 宿主机子进程执行(无沙箱)
    • core/telemetry/ — 可选 opt-in 遥测(清理后仅剩合法 __init__.py
  • 一段话架构:Orchestrator.create_agent() 读持久化状态返回下一个专职 agent;该 agent 运行、在一份 copy-on-write 的 next_state 上做修改、返回 AgentResponse;编排器把状态 commit 进 SQLite 再循环。

Agent Loop(主循环 / 何时继续何时停)

  • 主循环在 core/agents/orchestrator.py Orchestrator.run() L48-167:单个 while True:。每轮:redo 时重载 → update_stats()agent = self.create_agent(response)agent.run() → 按 response.type 分派。
  • 停止条件:外层循环只在 ResponseType.EXIT 时 break(L149)。ResponseType.DONEhandle_done(),commit 状态后 continue(L153-164)。外层没有 max-iteration 上限——推进由状态迁移保证,而非计数器。
  • 真正的「路由大脑」是 create_agent() L463-552:一条很长的 if/elif 阶梯,对 current_state(epics/spec/architecture/tasks/steps/iterations)判定该跑哪个 agent。跑哪个 agent 是持久化状态的纯函数
  • step 级分派:create_agent_for_step() L554-573,把某个 step 的 type 映射到 agent(save_file→CodeMonkey,command→Executor,human_intervention→HumanInput,create_readme→TechnicalWriter 等)。
  • 有限并行:L109-139,当 create_agent() 返回列表(目前仅 save_file/CodeMonkey)时用 asyncio.gather 并行;handle_parallel_responses() L345-368 只会合并 CodeMonkey 结果,否则抛异常。
  • agent 内部还有各自的重试小循环:Developer breakdown 标签补全循环 developer.py L268-282(max_retries=2);CodeMonkey review MAX_CODING_ATTEMPTS=3code_monkey.py L31);bug-hunt 对话轮次上限 CONVO_ITERATIONS_LIMITbug_hunter.py 引入)。

记忆与上下文管理(压缩、长期记忆、会话持久化)

  • 持久化 = DB 里的版本化状态链,而不是 chat log。 core/state/state_manager.pyStateManager 持有只读 current_state + 可写 next_statecommit() L433-489 保存 next_state,每步关闭并重开 SQLAlchemy session,再 create_next_state() fork 出一份新的可变状态。完整会话可恢复:load_project() L272 按 branch_id/step_index 加载,restore_files() L740 从 DB 重写整个工作区。
  • DB:默认 SQLite(sqlite+aiosqlite:///data/database/pythagora.dbconfig/__init__.py DBConfig ~L385),可选 PostgreSQL;Alembic 迁移在 core/db/migrations
  • 无向量库 / 无语义长期记忆。 「记忆」是结构化的:ProjectState 携带 epics/tasks/steps/iterations/files(含每文件 content.meta.description + references)和一个 knowledge_basepages/apis/user_options/utility_functions,orchestrator L75-82)。相关性靠显式按任务挑选而非 embedding 检索:Developer.get_relevant_files_parallel()(RelevantFilesMixin)在 breakdown 前让 LLM 判定哪些文件相关。
  • 上下文压缩粗糙且写死。 core/llm/base.py __call__ L167-182:若估算 prompt token > 150000,就找最后一条「Here are the backend/frontend logs」消息,对它之前的内容跑 trim_logs()core/utils/text.py)。这是唯一的自动压缩。此外 AgentConvo.trim/slice/remove_last_x_messagesagents/convo.py L91-135)是 agent 手动调用的编辑助手。
  • 每个 agent 每次调用都新建一份对话 AgentConvo(agent)agents/convo.py),system prompt 每次重渲染(L27-31)。跨 agent 没有持续增长的 chat buffer——历史从状态重建。

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

  • 无 function-calling 协议。 core/llm/base.py _adapt_messages L98-122 直接 raise ValueError("Anthropic Claude doesn't support function calling"),把角色塌缩为 user/assistant。整个代码库靠 prompt 指令下的结构化输出驱动工具。
  • 「工具集」= 一个 pydantic discriminated union of step typescore/agents/developer.py L32-82:
    • StepType 枚举:command / save_file / human_intervention / utility_function
    • schema:CommandStep{command, timeout, success_message}SaveFileStep{path}HumanInterventionStep{description}UtilityFunction{...};union = Step,外层 TaskSteps{steps:[...]}
  • 调用协议: LLM 产出符合 schema 的 JSON。AgentConvo.require_schema()agents/convo.py L112-128)把解引用后的 JSON schema 注入 prompt 并要求「your response MUST conform」,回复由 JSONParser(spec=...)core/llm/parser.py)解析。以自然语言定义工具语义的 prompt 是 core/prompts/developer/parse_task.prompt(含 command/save_file/human_intervention 语义与示例)。
  • 文件写入「工具」用的是第二套、基于标签的协议(非 JSON): Developer breakdown 把文件正文放进 <pythagoracode file="path">...</pythagoracode> 标签,由正则 extract_code_blocks()code_monkey.py L61-65)抽取,has_correct_num_of_tags()developer.py L84-88)做标签配平检查。
  • 注册 = 编排器里写死的分派,而非插件注册表。 create_agent_for_step()(orchestrator L554-573)是唯一把 step 类型绑到执行器的地方。
  • 执行权限门: core/agents/executor.py Executor.run() L86-102——每个 command step 运行前弹用户确认(ask_question yes/no,命令文本可编辑),选「no」则跳过。详见「安全与权限」。

Prompt 设计(系统提示结构、动态组装)

  • 每 agent 一套 Jinja2 文件模板,位于 core/prompts/<agent_type>/,共享片段在 core/prompts/partials/。加载器 JinjaFileTemplatecore/llm/prompt.py L33-45),StrictUndefined,autoescape 关闭。
  • 组装:AgentConvo.__init__agents/convo.py L23-31)构造时自动渲染 <agent_type>/system.prompt 作为 system message;.template(name, **kw) L74-83 渲染 <agent_type>/<name>.prompt 追加为一个 user turn,同时记录结构化 prompt_log(模板名 + 序列化上下文)供追溯。
  • 默认注入变量(_get_default_template_vars L41-52):state(整个 current_state)和 os(Windows/macOS/Linux),因此 prompt 是状态感知 + OS 感知的;如 parse_task 会说「must run on a {{ os }} machine」。
  • system prompt 简短、带角色色彩(developer/system.prompt:「You are a world class full stack software developer…」)。厚重的行为规则放在 partials 里用 {% include %} 拉入,如 partials/coding_rules.prompt(7+ 条编号规则:整文件重写格式、日志、错误处理、INPUT_REQUIRED {desc} 密钥占位约定、前端错误规则受 state.has_frontend() 门控)、partials/human_intervention_explanation.promptpartials/execution_order.prompt
  • require_schema() 对任意 pydantic 类型化输出,额外挂一条 JSON-schema 约束消息到对话上。

Router / 编排(任务分解、多 agent、子 agent)

  • 这是本 harness 的定义性特征:core/agents/ 下约 20 个专职 agent,都是 BaseAgent 子类(有 agent_type / display_name / run()),由 dim1 的 FSM 编排。角色表(文件 → 职责):
    • wizard.py Wizard(初始项目设置)、spec_writer.py SpecWriter(细化 spec)、architect.py Architect(架构 + 依赖)、tech_lead.py TechLead(epic→task 分解、套模板)、developer.py Developer(task→steps 分解)、code_monkey.py CodeMonkey(写/审单个文件)、executor.py Executor(跑 shell 命令)、troubleshooter.py Troubleshooter(运行/复核、bug 报告)、bug_hunter.py BugHunter(日志驱动调试循环)、problem_solver.py ProblemSolver(「卡循环」升级)、error_handler.py ErrorHandler、human_input.py HumanInput、importer.py Importer、frontend.py Frontend、external_docs.py ExternalDocumentation、tech_writer.py TechnicalWriter、task_completer.py TaskCompleter、legacy_handler.py LegacyHandler。
  • 分解层级: epics → tasks → steps → iterations。TechLead 把 epic 拆成 task;Developer.breakdown_current_task()developer.py L204-311)把一个 task 转成类型化 steps(两阶段:先流式自由文本 breakdown,再单独一次 parse_task LLM 调用转成 TaskSteps schema)。
  • 子 agent / 并行: 仅 CodeMonkey 对并行 save_file steps 做同类型 fan-out(orchestrator L109-139、L554-561);相关文件查找也跑并行 LLM 调用(mixins)。没有通用的嵌套子 agent 派生。
  • 共享行为靠 mixins(core/agents/mixins.py):ChatWithBreakdownMixin(接受前让用户与拟定 breakdown 对话)、RelevantFilesMixinFileDiffMixin

Skill / 插件体系

  • 未实现为可扩展体系。 无 skill/plugin 加载器、无 entry-points、无动态工具发现。最接近的三个类比:
    • 项目模板core/templates/tree/...,如 vite_react)由 TechLead 套用——是脚手架,不是运行时 skill。
    • utility_function step 类型developer.py L64-71,update_knowledge_base() L478-484)——agent 把可复用 helper 函数登记进项目 knowledge_base,是项目局部的能力备忘,不是 harness 插件。
    • 外部文档抓取external_docs.py,config 里 EXTERNAL_DOCUMENTATION_API)——为已知库拉取文档片段,是固定集成,不可由用户插拔。
  • 扩展 harness = 改 Python + prompt 文件。此维度记为未实现 / N/A

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

  • 无权重/prompt 自我改进,无跨项目持久学习。 但存在强的会话内 eval 驱动纠错循环
    • BugHuntercore/agents/bug_hunter.py)驱动日志埋点式调试循环。check_logs() L103+ 通过魔法词让 LLM 判定问题是否已定位或需更多日志(HuntConclusionTypeADD_LOGS / PROBLEM_IDENTIFIED,L31-39)。若需更多日志,Developer 插入 logging、用户复现、循环重复(IterationStatus 状态机在 orchestrator L518-549)。
    • 命令结果由 LLM 判定:Executor.check_command_output()executor.py L147-167)→ CommandResult{analysis, success} 决定任务继续还是触发调试。(注意 L132 if True or llm_response.success:失败→ErrorHandler 分支目前被死代码短路成常真,是已知 FIXME。)
    • CodeMonkey 做自我 code-reviewReviewChanges/Hunk{decision: apply|ignore|rework}code_monkey.py L35-49)逐 hunk 审自己的 diff,最多 MAX_CODING_ATTEMPTS=3 次 rework。
    • ProblemSolver 在用户报告卡住时升级;telemetry.trace_loop() 记录循环情形。
  • 上述任何一环都不回流到模型或持久 skill 库。

可观测性(日志 / trace 格式)

  • 结构化 Python logging: core/log/ get_loggerLogConfigconfig/__init__.py ~L208)默认落 data/pythagora.log,级别 DEBUG,带 max_lines 上限。编排器/agent 记录每次迁移(如 orchestrator L142「Running agent … (step N)」)。
  • 逐请求 LLM 日志: 每次调用产生一条 LLMRequestLogcore/llm/request_log.py),含 provider/model/temperature/messages/response/tokens/duration/status/error,在 base.py __call__ 创建、由 StateManager.log_llm_request()state_manager.py L491-523)持久化——但仅当 db.save_llm_requests 为真(默认 False);遥测计数器则始终记录。
  • 命令执行日志: ExecLogcore/proc/exec_log.py),含 cmd/cwd/timeout/status/stdout/stderr/analysis/success,经 log_command_run()state_manager.py L539)保存;用户输入经 log_user_input() L524 保存。
  • Phone-home 遥测: core/telemetry/__init__.py——单例、opt-in(settings.telemetry.enabled,可用 DISABLE_TELEMETRY env 关)。聚合计数(num_llm_requests/errors/tokensnum_steps/commands/inputsend_result、大/慢请求统计)POST 到配置的 endpoint;另有 trace_code_event() L363 / trace_loop() 记命名事件(如 trace-task-start,developer.py L303)。崩溃诊断(record_crash L214)截取最内层 3 个 core/ 栈帧。
  • UI 流式 trace: 每条 agent 消息/提问/流式 chunk 都打上 AgentSource(display_name, agent_type)project_state_idagents/base.py send_message/stream_handler L56-141),使 VSCode 扩展能把输出归因到具体 agent 与状态版本。

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

  • 命令审批门: Executor.run()executor.py L86-102)——任何 shell 命令都必须经用户 yes/no 确认(命令文本可编辑)才运行。这是主要安全边界RUN_COMMAND 常量来自 core/config/actions.py
  • Git 审批门: GitMixinagents/git.py)在 git init、每次 commit 前询问,并展示 LLM 生成的 commit message 供接受/编辑/拒绝。没有静默提交。
  • 密钥的人工输入门: INPUT_REQUIRED {config_description} 注释约定(定义在 partials/coding_rules.prompt 规则 4)——模型在 config/.env 文件里标记需用户填写的行(API key 等),而不是自己编造。StateManager.get_input_required()state_manager.py L1014-1028)扫描已保存文件里的该标记,命中即路由到 HumanInput agent(orchestrator L471-473,AgentResponse.input_required)。凭据被交还给人,而非虚构。
  • 密钥管理(harness 自身的 key): LLM API key 走 JSON config 里的 ProviderConfig.api_key,未设则用 provider env(config/__init__.py L102-105);旧 .envconfig/env_importer.py 导入。可选运行时 access_token(Pythagora/Bricks 代理)逐请求注入为 Authorization: Bearerllm/base.py L217-249),经 state_manager.update_access_tokenagents/base.py L120-122)存储。无 prompt 的密钥扫描/脱敏。
  • 离线改动安全: 启动时 offline_changes_check()(orchestrator L370-416)检测带外编辑,询问导入还是覆盖。
  • 供应链事件(本维度头号发现): README L1-16 的 [!CAUTION] 块记录:恶意 commit 065ee8eb(伪装成「Revert ‘Implemented weekend discount’」)于 2025-08-24 推入、2026-06-08 被公开报告、2026-06-11 删除、2026-06-12 清理合并至 HEAD。payload 是隐藏加载器 core/telemetry/_hooks.py(由 core/telemetry/__init__.py 自动启动),下载 Bun 运行时执行混淆的 core/telemetry/_runtime.bin——一个 Shai-Hulud 类窃密蠕虫(收割云/AWS key、GitHub/npm token、SSH key)。HEAD 已验证清洁core/telemetry/ 现仅剩 __init__.py,grep _hooks|_runtime|loader.lock|bun.sh 无加载器命中;但 CHANGELOG.md 仍留着攻击者伪装的 revert 条目(日期 2025-08-25)。项目「不再维护 + 一条恶意 commit 潜伏约 10 个月」这一安全姿态本身,是本维度真正的结论。

沙箱与执行隔离

  • 无。命令直接在宿主机运行。 core/proc/process_manager.py LocalProcess.start() L38-62:asyncio.create_subprocess_shell(cmd, cwd=..., env=...)——在项目根目录的裸 shell,继承完整环境(ProcessManager.__init__ L149-150 env = deepcopy(environ))。无容器、无 VM、无 seccomp、无降权、无网络/文件系统限制。
  • 唯一护栏:用户审批门(见「安全与权限」)+ 超时——MAX_COMMAND_TIMEOUT = 180s 硬上限(process_manager.py L21,L240 强制),超时则整棵进程树 SIGKILL(_terminate_process_tree L106-126,用 psutil)。
  • 后台/前台进程管理 + 非阻塞输出流由 watcher() 协程(L171-200)实现。仓库有 Dockerfile 但那是容器化整个 app 用于部署,agent 跑的命令相对于 harness 并未被沙箱化。

与模型的协同设计

  • provider 无关、逐 agent 的模型路由。 AgentLLMConfig 把每个 agent 名映射到 provider+model+temperature;Config.llm_for_agent()config/__init__.py L423-434)在没有专属配置时回退到 "default" agent 配置。细粒度 agent key(config/__init__.py L38-53,如 CodeMonkey.code_reviewCodeMonkey.implement_changesDeveloper.parse_taskBugHunter.check_logs)让不同子任务指向不同模型/温度。
  • Provider(LLMProvider L70-81):OpenAI、Anthropic、Groq、Azure、LM-Studio,以及 Relace(专用 fast-apply 合并模型)。client 类由 BaseLLMClient.for_provider()llm/base.py L415-440)选择。
  • Relace fast-apply 协同: CodeMonkey 先用 Relace 把编辑片段合并进文件(code_monkey.py L118-133,IMPLEMENT_CHANGES_AGENT_NAMErelace_client.pyOptionalCodeBlockParser),Relace 不可用才回退到 OpenAI 整文件重写——一个规划模型与廉价合并模型的明确分工。
  • 代码里默认模型偏旧(AgentLLMConfig.model 默认 gpt-4o-2024-05-13,L131;遥测默认亦然),反映项目 2024 年的冻结。
  • 因为没有 function-calling,设计刻意通过 JSON-schema-in-prompt + 稳健解析/重试面向任意 chat 模型(llm/base.py L389-402:解析 ValueError 时把错误回喂并重试至 max_retries)。

轨迹利用(session/trajectory 是否反哺训练/评测)

  • 无训练/RL/eval 循环。 全树无数据集导出、无 reward、无 fine-tuning 钩子。
  • 但轨迹完整持久化且可回放,用于工程/调试:版本化 ProjectState 链(branch_id + step_index)是完整可恢复的构建 trace(见「记忆」章);可选 save_llm_requests 在 DB 存下每条 prompt/response(见「可观测性」章);get_task_conversation_project_states() 为 UI「回看日志」面板重建某任务的对话(developer.py L377-390)。
  • 唯一外流是聚合 opt-in 遥测(见「可观测性」章)到 Pythagora endpoint——计数 + trace-* 事件 + 崩溃栈帧,而非供模型训练的原始轨迹。

与同类 harness 的关键差异(1-3 条)

  1. 确定性 FSM 而非 ReAct 循环:跑哪个 agent 是持久化状态的纯函数(orchestrator.create_agent()),外层循环无 max-iteration 计数,靠状态迁移保证推进——与大多数「LLM 自己决定下一步调什么工具」的 agent 截然不同。
  2. 全程零 function-callingllm/base.py 显式拒绝 function calling,所有工具/文件写入都是 prompt 内的 JSON schema 或 <pythagoracode> 标签 + pydantic 解析 + 出错回喂重试,因此可对接任意 chat 模型(含本地 LM-Studio)。
  3. 约 20 个高度专职 agent + epics→tasks→steps→iterations 层级分解:把「造一个 app」拆成软件团队角色(SpecWriter/Architect/TechLead/Developer/CodeMonkey/BugHunter…),并用 Relace fast-apply 做规划/合并模型分工——比通用单 agent coding harness 更像流水线。

原始源码定位

  • repo: https://github.com/Pythagora-io/gpt-pilot
  • commit/version analyzed: 9b763fdaf0020c7d8abacc7b58b2b09e57494623(2026-06-12,pilot v2 重写版,代码全在 core/;README 标注仓库不再维护)
  • 关键文件列表(相对仓库根):
    • core/agents/orchestrator.py — FSM 路由 + 主循环(run() L48-167、create_agent() L463-552、create_agent_for_step() L554-573)
    • core/agents/base.py — BaseAgent、get_llm、ask_question、stream/error handlers(L56-141)
    • core/agents/developer.py — task→steps、StepType/TaskSteps 工具 schema(L32-82)、breakdown(L204-311)
    • core/agents/code_monkey.py<pythagoracode> 写文件协议(L61-65)、自我 code-review(L35-49)、Relace 合并(L118-133)
    • core/agents/executor.py — 命令审批门(L86-102)、LLM 判定命令结果(L147-167)
    • core/agents/bug_hunter.py — 日志驱动调试循环、魔法词(L31-39)
    • core/agents/convo.py — AgentConvo、require_schema(L112-128)、trim/slice(L91-135)
    • core/agents/git.py — git 审批门(GitMixin)
    • core/llm/base.py — 无 function-calling(L98-122)、token-trim 压缩(L167-182)、provider 分发(L415-440)、重试(L389-402)
    • core/llm/prompt.pycore/llm/convo.py — Jinja 模板
    • core/proc/process_manager.py — 宿主机 subprocess 执行、180s 超时、无沙箱(L21/L38-62/L106-126)
    • core/config/__init__.py — LLMProvider、逐 agent 模型路由(L423-434)、DB/log 配置
    • core/telemetry/__init__.py — opt-in phone-home 遥测(已验证清洁)
    • core/state/state_manager.py — 版本化状态持久化(commit L433-489、load L272、restore L740、INPUT_REQUIRED 扫描 L1014-1028)
    • prompts: core/prompts/developer/system.prompt.../parse_task.promptcore/prompts/partials/coding_rules.prompt.../human_intervention_explanation.prompt

一手源存档(sources/)

存档于 /Users/zhao/projects/self-wiki/ai-research/sources/harness/gpt-pilot/(约 260K):

  • NOTES.md — Phase-1 逐维度源码级笔记(含 provenance、供应链事件、12 维证据与文件行号)
  • src/ — 从 clone 复制的关键源码子集:
    • src/agents/base.pybug_hunter.pycode_monkey.pyconvo.pydeveloper.pyexecutor.pygit.pyorchestrator.py
    • src/llm/base.pyconvo.pyprompt.py
    • src/state/state_manager.py
    • src/proc/process_manager.py
    • src/config/config_init.py
    • src/telemetry/telemetry_init.py
    • src/prompts/system.promptparse_task.promptcoding_rules.prompt
    • src/README-security-notice.md(README 安全通告留档)