系列:Cursor客户端解剖系列学习笔记
上一篇:(七)一次请求怎么走完

先记住的 7 条精髓

来自对 Cursor 3.13.25 应用包的学习结论:

  1. 平台继承,能力增量 —— 底座 VS Code;差异堆在 Agent 运行时 + 检索 + MCP + Glass
  2. AI 是一等公民,但不当唯一进程 —— UI 在 Workbench;执行/索引/MCP/私有推理拆进程
  3. 按故障域拆扩展 —— Host ≠ Exec ≠ Worker ≠ Retrieval ≠ Local Runtime
  4. 协议优于硬编码工具表 —— MCP、remote authority、proposed API、强类型 ToolCall
  5. 隔离写代码 —— Shadow / Worktree,降低直接污染主仓库的风险
  6. 远程模型复用 Remote-SSH 心智 —— background-composer 是自定义 remote authority
  7. UI 可换壳,服务尽量共用 —— Classic 与 Glass 双轨并存

护城河不太可能只是「哪个模型」

模型可切换、可路由。更难追的是组合:

包内证据
编辑器级入口 Fork + Composer/Glass 一等公民
上下文产品化 cursor-retrieval + ignore 契约
编排/执行分离 agent-host / agent-exec
工具协议 MCP + protobuf ToolCall
远程同构 background-composer resolver/socket
可观测 tracing / ndjson-ingest / fault injection
兼容迁移 proposed API 白名单 + 扩展替换表

一句话:

把 Agent 当成 IDE 操作系统里的一等运行时:
有编排、有隔离执行、有检索边界、有工具协议、有远程管道、有可观测性,
并且尽量站在 VS Code 已经赢过的架构上生长。

可迁移的设计原则清单

做自己的 AI 工具 / IDE / Agent 时,可以逐条打勾:

  • 故障域分离:编排 / 执行 / 索引 / 工具 / UI 不同命运
  • 协议面优先:工具与传输可替换
  • 激活策略显式:启动关键路径 vs 懒加载路径分开
  • 权限与边界文件化:ignore、permissions、environment、rules
  • 远程同构:后台任务按 remote authority 建模
  • 可观测性内建:trace、ingest、fault injection
  • UI 双轨可演进:壳可变,内核契约稳
  • 兼容层:替换表/迁移路径照顾存量用户

勾不满的地方,就是架构债。

反编译学习的现实边界

目标 可行性
理解分层与设计理念
阅读小扩展逻辑(已美化)
还原 Composer/Agent 完整 TS 源码 低(无 source map 的巨型 mangle bundle)
绕过授权/破解 不做

本系列站在第一条:把设计肌肉练出来。

若继续「手搓」

迷你骨架验收标准可以很硬:

  1. UI 进程只编排展示
  2. 执行进程可杀死重启
  3. 检索模块可 mock
  4. 一种工具协议(可简化 MCP)
  5. 一种隔离写文件策略
  6. 一份 permissions 式契约

关掉执行进程,UI 仍在;换一个工具实现,编排代码几乎不用改——这就抄到了精髓。

结束语

写完这个系列,我更确信:

体感强,往往不是因为某一刻的模型更神,
而是客户端把「该看的代码、能做的动作、可崩的边界」安排对了。

模型是引擎;Cursor 客户端展示的是变速箱、底盘和仪表盘怎么配。
引擎可以换,底盘哲学更值得学。

系列:Cursor客户端解剖系列学习笔记
上一篇:(六)工具循环

一句话

模型在 Cursor 服务端(或 Private Inference)里“想”;文件 / 终端 / 检索 / MCP 在你这台机器(或 Cloud VM)上“做”。中间用双向流协议把 ToolCall 传来传去。

任务处理时序

端到端:你提交一个任务之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用户输入(Chat / Agent / Composer)


① Workbench UI 收集:模式、模型、选中上下文、附件


② 本地组装上下文(不跑大模型)
· Rules / AGENTS.md / Skills / MCP 清单 / 检索元数据
· user_info、打开文件、选区、git 状态…


③ 发出 AgentRunRequest(流式)


④ 服务端 aiserver:拼最终 prompt + 调模型
→ 流式返回 thinking / text / ToolCall


⑤ 本地 Agent Exec 执行工具(可审批 / sandbox)


⑥ 工具结果回传 → 模型继续 → 循环直到结束


⑦ UI 展示:回复、diff、todo、提问、计划…

协议骨架(包内真实类型名)

方向 消息 含义
本地 → 服务 AgentClientMessage.run_request 开始/继续一轮 Agent
服务 → 本地 AgentServerMessage.interaction_update 文本/思考/工具调用增量
服务 ↔ 本地 exec_server_message / exec_client_message 工具执行通道
钩子 PreToolUse / PostToolUse / BeforeSubmitPrompt 拦截点

本地 vs 服务端

本地(Mac / SSH 机器 / Cloud Agent VM)

  • UI、会话展示
  • Rules / Skills 读取与热更新
  • cursor-retrieval 索引与搜索执行面
  • cursor-agent-exec真正跑 Shell / 读写文件 / Diff…
  • MCP(含 browser automation)
  • 权限确认、sandbox、shadow/worktree
  • Private Inference 时:cursor-local-agent-runtime

服务端(aiserver.v1.*

  • 最终 prompt 组装与模型推理(默认路径)
  • 流式 assistant / thinking / tool_call
  • WebSearch 等联网能力编排
  • Background Composer / Cloud Agent 控制面
  • 账号、模型目录、部分分析/埋点

后台/云端通道的特别之处

background-composer 不是 renderer 里随便开个 WebSocket,而是:

当作一种 Remote Window:权威解析 → 鉴权 → 传输 → 生命周期

参与者:cursor-resolver / cursor-resolver-helper / cursor-socket / cursor-always-local
cursor-socket 注册 SocketConnectionProvider,走 TCP/TLS——和 Remote-SSH 同构的心智。

提示词拼装:客户端给积木

完整最终 prompt 主要在服务端;客户端提供:

  1. 角色/模式 system 片段(Ask / Agent / Debug / Cloud / Subagent…)
  2. 用户 Rules / Skills / custom_system_prompt
  3. UserMessage(text、selected_context、mode…)
  4. ExtraContextEntry(打开文件、路径、blob)
  5. 内置工具 schema + 当前 mcp_tools
  6. 环境提醒(worktree、本地 vs cloud、是否禁止乱 commit…)

这也解释了高质量任务写法为何有效:目标 + 约束 + 验收 + @文件,正好对齐 selected_context / Rules / 停机条件。

组件对照表

组件 在任务中的角色
Workbench / Glass 收任务、展示流式结果
agent-host 编排会话
agent-exec 执行 ToolCall
retrieval 索引/搜索上下文
mcp + mcpProcess 外部工具
aiserver 模型与多数 prompt 终态
resolver/socket 后台/云端远程管道

边界说明

  • 最终发给模型的完整 prompt 文本大多在服务端,应用包看不到每一次全文
  • 本篇依据:客户端协议字段 + 内置 prompt 模板 + 工具 protobuf —— 还原的是设计逻辑
  • Private Inference / 自带 Key 时,推理位置会变,但 Tool 执行面仍在 Agent Exec

小结

一次请求的本质不变:

检索缩小世界,规则约束世界,模型提出行动,Exec 执行行动,结果再改变世界。

下一篇收束:这些证据指向的护城河与可迁移原则。

系列:Cursor客户端解剖系列学习笔记
上一篇:(五)Chat、Composer 与 Agent

工具不是提示词里的口头约定

学习材料依据包内 agent.v1.*ToolCall protobuf:工具是强类型消息,不是「请你自己注意要用工具」这种软约束。

循环骨架:

1
2
3
4
模型(服务端或 Private Inference)输出 ToolCall
→ 本地 Agent Exec 执行(可审批 / sandbox)
→ 工具结果回传
→ 模型继续,直到结束

协议上还能看到钩子:PreToolUse / PostToolUse / BeforeSubmitPrompt——执行前后、提交前可拦截。

本地工具清单(节选)

仓库只读

工具 作用
ReadToolCall 读文件(path/offset/limit)
GrepToolCall 精确搜索
GlobToolCall / LsToolCall 找文件 / 列目录
SemSearchToolCall 语义检索
ReadLintsToolCall 读诊断

改代码 / 改环境(常需审批)

工具 作用
EditToolCall / Write 族 / DeleteToolCall 改删文件
ApplyAgentDiffToolCall 应用 Agent diff
ShellToolCall 跑命令(含 sandbox_policy、timeout、后台等)
AwaitToolCall / WriteShellStdinToolCall 后台任务协作

与用户交互 / 规划

AskQuestionToolCall · UpdateTodosToolCall · CreatePlanToolCall · SwitchModeToolCall · TaskToolCall · ReflectToolCall

外部世界

工具 更可能在哪
WebSearchToolCall 服务端编排
McpToolCall / GetMcpToolsToolCall 本地 MCP 进程
ComputerUseToolCall 本地/云桌面
PR / SCM 族 混合

MCP:工具变成可插拔服务

包内落点:

  • 扩展 cursor-mcpworkspaceonStartupFinished + onUri
  • Utility:mcpProcess
  • cursor-browser-automation:通过 MCP Provider 暴露浏览器自动化
  • 依赖锁定 @modelcontextprotocol/sdk(版本钉死)

精髓:

IDE 内工具 ≠ 写死在 Agent 代码里的 if/else。
MCP 把工具变成可插拔服务;浏览器自动化也套同一形态,心智统一。

Rules / Skills:进 prompt 的积木

本地收集、塞进 RunRequest 的约束类积木包括:

  1. Rules 类型:Global / FileGlobs / ManuallyAttached / AgentFetched
    文件:.cursor/rules/**/*.mdcAGENTS.mdCLAUDE.md.cursorrules
  2. SkillsAgentSkill
  3. custom_system_prompt 字段

包内优先级文案大意:

系统提示 > AGENTS.md / rules / skills > 普通用户偏好(冲突时听 system)

Rules 偏常驻宪法;Skills 偏情境手册。和「工具强类型」一样,它们是产品化上下文,不是聊天里临时说一句。

权限与隔离:副作用管线

两层值得分开看:

  1. 执行时审批 / sandbox
    Shell 等工具带策略字段;Exec 描述强调 permissions & approvals。

  2. 落盘隔离

    • cursor-shadow-workspaceregisterShadow*Provider
    • cursor-worktree-textmate:给 .cursor/worktrees TextMate 高亮,不激活语言服务器

精髓:Agent 写代码的危险不在「会不会写」,在「写到哪」。影子世界 + 弱语言服务 = 可实验、少干扰、可控回滚。

另有文件化契约:

  • .cursor/environment.json
  • .cursor/permissions.json(由 cursor-always-local 等贡献 schema)

可进仓库、可审查、可同步——和 ignore 规则同一哲学。

可观测性不是事后补丁

  • 大量 cursorTracing
  • cursor-ndjson-ingest:本地 HTTP 吃 NDJSON → .cursor/debug.log
  • cursor-socket 提供失败注入类开发者命令

Agent 系统难测;从第一天就留 日志摄入 + 故障注入 口子。

小结

工具循环在包内的三句话:

  1. ToolCall 强类型 + 双向流,模型想、本地做
  2. MCP 把扩展工具标准化
  3. Rules/Skills/permissions/ignore 把边界文件化

下一篇把这些模块串成一次完整任务时序。

系列:Cursor客户端解剖系列学习笔记
上一篇:(四)Agent三件套

意图入口在 Workbench,不在第三方插件

能力流第一站是:

Composer / Glass —— 聊天与任务是工作台能力

包内能看到:

  • contrib/composerservices/aiservices/agentData 一类 workbench 增量
  • 双入口 bundle:workbench.desktop.main.jsworkbench.glass.main.js
  • Glass 目录下还有特效 worker、品牌媒体、启动视觉;Workbench 亦带 react-runtime/,部分新 UI 用 React 嵌入经典壳

设计含义:

UI 可换壳,服务尽量共用。
大产品做 UX 跃迁时,双轨并存比一夜重写更工程。

Chat / Composer / Agent:产品名会变,模式门闩更重要

对外名称常变,包内 prompt / 协议里更稳定的是模式

模式信号 系统侧在强调什么
Ask 不能跑非只读工具
Agent / IDE coding agent 在用户机器上作为 coding agent 运行,用工具查,不要猜
Debug 要运行时证据,专项调试人格
Cloud / Background 更自主;执行环境可能在云 VM
Orchestrator 编排者倾向委派,用 Task/子代理,而不是自己改所有文件

还有 SwitchModeToolCall:模式切换本身可以是工具调用——说明模式是运行时状态机,不是单纯 UI Tab。

子 Agent:人格化工具子集

协议里能看到专门子代理类型,例如:

Explore · Shell · BrowserUse · ComputerUse · Debug · CursorGuide · …

父 Agent 通过 TaskToolCall 派生子任务。各自带不同 system 片段与工具子集——这是「编排 / 执行」在产品层的再一次拆分:不是一个全能循环死磕到底,而是可委派。

本地先收积木,服务端再拼终态 prompt

你在 Chat/Agent 输入框提交后,Workbench 先收集(不跑大模型):

  • 模式、模型选择、选中上下文、附件
  • Rules / Skills / MCP 工具清单 / 检索元数据
  • user_info、打开文件、选区、git 状态等

然后发出流式 AgentRunRequest(字段包括 conversation_statemodel_detailsmcp_toolscustom_system_promptskill_options…)。

最终 system/user prompt 主要在服务端(aiserver)拼好并调用模型。
客户端负责积木与执行面;这解释了为什么「完整 prompt 抓包全文」在应用包里看不到,但协议字段足够还原设计逻辑。

和执行链的衔接

1
2
3
4
5
6
7
Glass / Classic UI
→ 收集意图与上下文积木
→ AgentRunRequest(流式)
→ aiserver:拼 prompt + 调模型
→ interaction_update(text / thinking / ToolCall)
→ agent-exec 执行
→ 结果回传 → 循环

UI 的职责是:收得全、展得清、批得动(diff、todo、提问、计划)。
真正危险的副作用不在 UI 进程里随便发生。

小结

Chat / Composer / Agent 在包内更准确的读法是:

  1. 意图入口属于 Workbench/Glass 一等公民
  2. 模式门闩改变工具可用面与 system 片段
  3. 子 Agent 是委派机制,不是换肤
  4. 双 UI 轨是演进策略,不是重复造轮子

下一篇进入工具循环:强类型 ToolCall、MCP、Rules/Skills、权限契约。

系列:Cursor客户端解剖系列学习笔记
上一篇:(三)上下文引擎

为什么不是「一个 Agent 扩展」

如果把编排、跑命令、改文件、常驻工人塞进同一个扩展进程:

  • 一处卡住,整条 Agent 链一起死
  • 权限模型难做细:编排逻辑和危险执行同命运
  • 远程/精简宿主场景更难裁剪依赖

Cursor 的答案是拆成三件套(外加私有推理运行时):

扩展 激活 一句话
cursor-agent-host * 编排宿主(控制面)
cursor-agent-exec * 真正跑工具/命令/文件(执行面)
cursor-agent-worker onStartupFinished 启动时安装并拉起 worker(工人面)
cursor-local-agent-runtime *extensionKind: ui Private Inference,放在普通 workspace extension host 之外

Host / Exec 的描述原文大意:

  • Host:在 AgentExec extension host 里做 orchestration
  • Exec:让 Agent run commands、interact with files、use tools,并强调 permissions and approvals

Proposed API:合法要宿主开洞

Host / Exec 都启用了例如:

  • cursorAgentHost
  • cursorPseudoterminal
  • cursorTracing
  • cursor

这不是「偷偷调 private API」,而是走 VS Code proposed API 白名单:宿主明确允许这些扩展使用实验能力。cursorPseudoterminal 和终端类工具执行直接相关;cursorTracing 则说明可观测性从第一天就焊在链路上。

Worker 还出现 cursorNoDeps:某些扩展要能在少依赖环境跑(远程/精简宿主)。这是进程放置与裁剪依赖的信号。

控制面 ≠ 执行面

结合任务流(第七篇会展开):

1
2
3
4
5
6
7
8
9
10
服务端模型输出 ToolCall


agent-host 编排会话 / 协调


agent-exec 真正执行(可审批、可 sandbox)


结果回传 → 模型继续

Exec 侧能看到 Shell 参数里的 sandbox_policyskip_approvaltimeout_behavior 一类字段——本地执行默认走权限与沙箱策略,不是模型一说就裸跑。

这就是故障域分离的实感:

编排可以重试、可以换模型;执行必须可杀、可审、可限权。

Local Runtime:敏感推理再隔离一层

cursor-local-agent-runtime 的描述非常明确:

Hosts Cursor Private Inference outside workspace extension hosts

extensionKind: ["ui"],却又不塞进普通 workspace host。含义是:

  • 私有推理和项目扩展隔离
  • 跟 UI 机绑定(模型跑在你这边时)
  • 即使走本地推理,Tool 执行面仍在 Agent Exec

「想」的位置可以云或本地;「做」的位置仍是受控执行环境。

和 Tab 的关系(顺手一提)

内置 cursor-* 目录里没有单独的 Tab 扩展。Tab 这类低延迟补全,更可能沉在 workbench core bundle(services/ai 一带),和 Agent 扩展集群不是同一条激活/隔离故事。

产品上 Tab 与 Agent 目标函数不同;包结构上也是不同落点。本系列后续仍以 Agent 执行链为主——那是 cursor-* 扩展集群最密集的证据区。

小结

Agent 三件套教你的不是类名,而是决策表:

决策问题 Cursor 的常见答案
必须一启动就在? Host/Exec:*
可以稍晚? Worker:onStartupFinished
编排与执行是否同进程同命运? 否,拆开
私有推理能否和项目扩展混住? 否,独立 runtime

下一篇回到意图入口:Chat / Composer / Glass / 模式门闩,看「任务从哪进系统」。

系列:Cursor客户端解剖系列学习笔记
上一篇:(二)整体架构鸟瞰

包内落点:cursor-retrieval

清单里写得很直白:

Handles indexing and retrieval for Cursor

关键证据(package.json):

  • extensionKind: ["workspace"] —— 索引跟着工作区走
  • activationEvents: ["onStartupFinished"]
  • Proposed API:textSearchProvider2cursorTracingcursorNoDeps
  • 贡献语言关联:把 .cursorignore.cursorindexingignore 当成 ignore 语法文件
  • 开发者命令:cursor.grepClient.debugcursor.codebaseTelemetry.triggerSnapshot

另有一个几乎空壳的 cursor-file-servicemain: null),描述也是 indexing/retrieval——有时能力在 core,扩展只占位/声明。

精髓:把「模型能看什么」做成产品边界

很多人以为上下文只是 prompt 里多贴几段代码。Cursor 的做法是:

可索引边界文件化,而不是口头约定。

.cursorignore / .cursorindexingignore 进入语言贡献点,意味着:

  1. 编辑器认识这些文件(高亮/编辑体验)
  2. 检索管线可以把它们当一等配置读取
  3. 团队可以把「别索引密钥目录 / 别扫构建产物」推进仓库

这和后面的 .cursor/permissions.json.cursor/environment.json 是同一设计肌肉:Agent 相关边界尽量变成可审查的文件契约

一次任务里,检索处在哪一段

能力流里,上下文组装发生在「意图入口之后、编排之前」:

1
2
3
4
Intent UI(Composer / Glass)
→ Context Service(cursor-retrieval + ignore 规则)
→ Orchestrator(agent-host)
→ Executor(agent-exec)

本地还会收集更多非向量上下文(详见第七篇):

  • .cursor/rulesAGENTS.mdCLAUDE.md.cursorrules
  • Skills、MCP 工具清单
  • 打开文件、选区、git 状态等

检索负责的是「仓库里哪段代码相关」;Rules/Skills 负责的是「行为约束与手册」。两者都进 RunRequest,但职责不同。

工具协议里的检索面孔

agent.v1.*ToolCall 一侧,和上下文相关的本地工具包括:

工具 作用
ReadToolCall 读文件
GrepToolCall 精确搜索
GlobToolCall / LsToolCall 按名/目录探索
SemSearchToolCall 语义检索
ReadLintsToolCall 读诊断

注意:语义搜索的执行面在本地索引;embedding/排序是否上云,视配置与隐私模式,学习材料把它标成混合能力。

口诀仍然成立:

决策与生成在云(或私有推理);副作用(读盘/搜盘)在执行环境。

为什么检索必须独立成扩展

  1. 故障域:索引挂了,编辑器 UI 还应活着
  2. 生命周期:跟 workspace 走,远程窗口/本地窗口放置清晰
  3. 演进:检索算法、ignore 规则、debug 命令可以单独迭代
  4. 权限心智:用户更容易理解「检索扩展」而不是「神秘 core 黑盒扫盘」

大扩展 cursor-retrieval 的 dist 也是巨型 bundle,美化收益低;学设计时读清单贡献点,比硬抠压缩 JS 更划算。

小结

上下文引擎在包内的产品化结论:

  1. 检索是 workspace 扩展,不是 Chat 临时逻辑
  2. ignore 规则是一等配置
  3. 精确搜 + 语义搜 + 读文件,都是强类型工具,不是提示词里的口头约定

下一篇拆 Agent 三件套:Host / Exec / Worker —— 编排与执行为什么必须分开。

系列:Cursor客户端解剖系列学习笔记
上一篇:(一)为什么是-VS-Code-Fork

先看分层总览

Cursor 分层架构

从上到下可以记成五层:Glass/Workbench UI → Workbench Services → Extension Host 集群 → Monaco/Platform → Electron 进程模型。细节见学习材料中的分层说明。

为什么这样分层

VS Code 的经典理念没有被推翻:

  • UI 与扩展隔离:扩展跑在 Extension Host,崩溃不拖死编辑器
  • 服务可替换platform / workbench/services 用 DI 组装
  • 贡献点:命令、视图、配置用 package.json 声明

Cursor 在这上面加了三刀:

  1. 把 AI 相关能力拆成多个内置扩展(Agent Exec / Host / Worker / Retrieval / MCP…)
  2. 在 workbench 里加 composer / ai / agentData 等核心服务与 UI
  3. Glass 做 Agent-first 的另一套入口壳

学习启示很直接:

UI 可以花,执行链必须硬。
先画「哪些必须和 UI 同命运、哪些必须可崩溃可重启」,再决定模块边界。

内置扩展集群:按能力域看

extension-catalog.json / 各扩展 package.json 归纳:

能力域 扩展 设计意图
Agent 编排与执行 cursor-agent-host / cursor-agent-exec / cursor-agent-worker 控制面 / 执行面 / 工人面拆开
本地私有推理 cursor-local-agent-runtime Private Inference 放在普通 workspace extension host 之外
代码检索 cursor-retrieval(及清单型 cursor-file-service 索引检索与编辑器进程解耦
工具协议 cursor-mcp / cursor-browser-automation MCP 一等公民;浏览器自动化也走 MCP 形态
隔离写代码 cursor-shadow-workspace / cursor-worktree-textmate Shadow/Worktree;TextMate-only 避免误启 LSP
后台/云端通道 cursor-resolver* / cursor-socket / cursor-always-local 自定义 remote authority:background-composer
产品化杂项 cursor-deeplink / cursor-commits / cursor-checkout / cursor-ndjson-ingest 深链、指标、分支迁移、调试日志摄入

激活策略:基础设施不能懒加载碰运气

很多 Cursor 扩展是 activationEvents: ["*"]onStartupFinished

含义很硬:Agent 基础设施必须尽早、确定地起来。
Host / Exec / Local Runtime 常见 *;MCP / Retrieval / Shadow / Worker 常见启动完成后激活。

另外还有 extensionKind

  • workspace:跟着工作区走(retrieval、mcp、shadow)
  • ui:必须在本地 UI 机(socket、resolver、local-agent-runtime、browser)

这是在回答:「这段代码该死在哪台机器、哪个进程」。

Utility 进程:重活出局

out/vs/code/electron-utility/ 下能看到诸如 mcpProcessconversationSearchalwaysLocalSingleton 等目录名。

长连接、重 IO 不进 UI 进程——和 VS Code 自己的 shared/utility 思路一脉相承。

一条能力流公式

学习材料给的精髓公式:

Intent UI → Context Service → Orchestrator → Sandboxed Executor → Protocol Tools → Isolated Apply

对应到扩展:

Agent 能力流

缺任何一段都会疼:只有 UI 没有隔离执行 → 不安全;只有执行没有检索 → 胡编;只有硬编码工具没有协议 → 扩展不动。

小结

鸟瞰之后记住三句话:

  1. Cursor = 编辑器平台 + Agent 运行时,不是侧边栏 Chat
  2. 能力按故障域拆成内置扩展,而不是一个超级扩展包打天下
  3. 后台任务按 Remote Authority 建模,而不是 renderer 里随便 fetch

下一篇进入上下文引擎:cursor-retrieval 如何把「模型能看什么」产品化。

系列:Cursor客户端解剖系列学习笔记

先看应用包里有什么

学习材料对应的是 macOS .appContents 目录,标准 Electron 布局:

路径 角色
MacOS/Cursor 原生启动器(Mach-O),几乎不含业务逻辑
Frameworks/ Electron / Helper 进程
Resources/app/out/ 主程序打包后的 JS(core)
Resources/app/extensions/cursor-* Cursor 内置扩展(Agent 能力的主要落点)
Resources/app/product.json 品牌、更新通道、扩展替换表、API proposal 白名单

不需要传统「二进制反编译」。 业务逻辑主要在 JS。难点是核心被打成超大 bundle(例如 workbench.desktop.main.js 约 45MB),变量名被 mangle,没有 source map。

所以「学 Cursor」更现实的路径是:对照开源 VS Code 的分层,再读 cursor-* 扩展的清单与小扩展逻辑。

一句话设计理念

包内导读把它概括成:

把「AI 编程代理」做成 IDE 的一等公民,同时尽量复用 VS Code 已验证的编辑器 / 扩展 / 进程隔离模型。

具体是三层叠加:

  1. 继承:Monaco + Workbench + Extension Host(VS Code 基因)
  2. 嵌入:Composer / Agent / Retrieval / MCP 作为内置能力
  3. 演化 UIglass 工作台尝试用新壳承载 Agent-first 交互

为什么不是「做一个插件」就够了

VS Code 扩展模型很强,但扩展始终是宿主菜单上的客人:

  • UI 与渲染主路径受 Extension API 边界约束
  • 很难把多文件 Diff、Agent 轨迹、Shadow Workspace 做成一等公民
  • 高频能力(检索、工具执行、长连接)不宜全塞进普通扩展生命周期里碰运气

Cursor 的选择是:Fork 内核拿控制权,再把增量能力拆成多个内置扩展
这是「fork 内核 + 能力插件化 + UI 渐进替换」,不是推倒重来。

Fork 换来的,不只是改皮肤

从包内能直接看到的增量:

  1. 双 Workbench 入口

    • workbench.desktop.main.js — 经典桌面工作台
    • workbench.glass.main.js — Glass 新壳(同量级大 bundle)
      UI 可以换壳,底层服务 / 扩展协议尽量共用。
  2. product.json 的 proposed API 白名单
    大量 extensionEnabledApiProposals(如 cursorcursorAgentHostcursorTracing)。
    含义:用 VS Code 的 proposed API 机制合法扩展宿主能力,而不是到处 hack private API。

  3. 扩展替换表
    extensionReplacementMapForImports 把社区/微软扩展映射到 Anysphere 发行版(例如语言服务、Remote SSH)。
    兼容用户心智,同时控制关键路径的实现版本。

和「手搓一个侧边栏 Chat」的差别

路线 你得到什么 你失去什么
插件 上线快、跟随宿主升级 交互与隔离上限受 API 约束
Fork 编辑器级体验、进程与协议可深度改造 持续 rebase 上游、自己扛发行通道

Cursor 押的是:当编程主路径被 Agent 接管时,编辑器必须跟得上。插件路线很难把「编排 / 执行 / 检索 / 远程权威」做成 IDE 操作系统级能力。

小结

「为什么是 VS Code Fork」在包里的答案很务实:

  • 底座已经证明能规模化(编辑器 + 扩展宿主 + 远程权威)
  • AI 差异集中堆在 Agent 运行时、检索、MCP、Glass
  • 用内置扩展集群表达能力,用 proposed API 扩展宿主,而不是把一切写死在 45MB bundle 的无法维护角落

下一篇把分层摊开:Glass / Workbench Services / Extension Host / Utility / Electron。

前言

最近一直在用 Cursor,越用越想搞清楚:它到底在客户端里做了什么。

网上讲用法的文章很多,但我更关心工程问题——Agent 能力是怎么嵌进 IDE 的。手头有一份对 Cursor macOS 应用包(Contents)的学习笔记:版本线索是 Cursor 3.13.25,底座 VS Code 1.128.0。主包 JS 被打成超大 bundle,谈不上完整源码还原;真正可读的路径是:

VS Code 开源分层 + Cursor 内置扩展的 package.json / 进程切分 + 协议字段(agent.v1.* / aiserver.v1.*

本系列据此写成,目标不是破解,而是把设计理念学到手。文中会尽量写清「包内证据」和「推断」,避免把猜测写成事实。

系列目录

学习材料里最重要的一张图

Cursor 分层架构

读完你会带走什么

  1. 为什么是「Fork 内核 + 能力插件化」,而不是侧边栏插件
  2. Host / Exec / Retrieval / MCP 为什么要拆成不同故障域
  3. 一次任务里:哪些在本地做、哪些在服务端想
  4. 做自己的 AI IDE / Agent 时,最值得抄的是哪几条架构原则

背景

在帮朋友开发的一个项目中,用了 Mybatis-plus作为 ORM框架,在一个简单的查询在执行的时候一直在报错,困惑了一整个小时,最后才发现是因为一个字段名的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Wrapped by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: SELECT  id,model_code,prompt_id,version_no,release_flag,default_flag,prompt_content,batch_recording_file_id,batch_recording_file_content,output,is_delete,tenant_id,create_dept,create_by,create_time,update_by,update_time  FROM ai_prompt_debug_log      WHERE  (prompt_id = ?)
at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:39) ~[mybatis-plus-core-3.5.8.jar!/:3.5.8]
at com.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:51) ~[mybatis-plus-extension-3.5.8.jar!/:3.5.8]
at com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.beforeQuery(TenantLineInnerInterceptor.java:67) ~[mybatis-plus-extension-3.5.8.jar!/:3.5.8]
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:78) ~[mybatis-plus-extension-3.5.8.jar!/:3.5.8]
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59) ~[mybatis-3.5.16.jar!/:3.5.16]
at jdk.proxy2/jdk.proxy2.$Proxy251.query(Unknown Source) ~[na:na]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154) ~[mybatis-3.5.16.jar!/:3.5.16]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147) ~[mybatis-3.5.16.jar!/:3.5.16]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142) ~[mybatis-3.5.16.jar!/:3.5.16]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:333) ~[mybatis-spring-3.0.4.jar!/:3.0.4]
... 112 common frames omitted

刚看到这个错误的时候感觉莫名其妙的,这么简单的一个查询,还能整出这个幺蛾子,就很奇怪了

问题分析

根据多年经验第一反应就是懵逼,再仔细看一下堆栈信息,感觉可能是这里面存在某些关键字导致的,但是看了每个字段,发现没有什么异常的地方,先顺着这个思路去验证一下

问题排查

  • 根据堆栈信息使用发现问题出现在了JsqlParserSupport.parserSingle这个方法
  • 因为JsqlParserSupport是一个抽象类,看日志信息实际使用的类是TenantLineInnerInterceptor,那就弄个测试类,去用相关的参数调试一下
  • 因为怀疑是关键字引起的,采取的方法是,一个字段一个字段删除,直到查询正常为止。
  • 当我删除掉output时候,结果正常了,说明output这个字段名是一个关键字,导致了查询失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.apache.commons.compress.utils.Lists;
import org.dromara.common.tenant.handle.PlusTenantLineHandler;
import org.dromara.common.tenant.properties.TenantProperties;

public class Test {
public static void main(String[] args) {
final TenantProperties tenantProperties = new TenantProperties();
tenantProperties.setEnable(true);
tenantProperties.setExcludes(Lists.newArrayList());
PlusTenantLineHandler tenantLineHandler = new PlusTenantLineHandler(tenantProperties);
TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor(tenantLineHandler);
tenantLineInnerInterceptor.parserSingle("SELECT id,model_code,prompt_id,version_no,release_flag,default_flag,prompt_content,is_delete,tenant_id,create_dept,create_by,create_time,update_by,update_time " +
"FROM ai_prompt_debug_log WHERE (prompt_id = ?)" ,
"123"
);
}
}

删除掉output字段后,日志显示正常,不再报错了,说明output这个字段名是一个关键字,导致了查询失败,既然找到了问题,就可以开始解决它。

解决办法

  • 因为output是一个关键字,所以在查询的时候需要用反引号括起来
  • 或者在表设计中将字段名改非关键字,比如 output_text

总结

问题根源:关键字冲突与SQL解析失败

  • output是SQL保留字:在许多数据库(如SQL Server)中,OUTPUT是一个关键命令,用于输出结果或操作影响的行。当 MyBatis-Plus 的 SQL 解析器(如 JSqlParser)遇到这个单词时,会试图按照其语法规则去理解,但它在您的上下文中是一个字段名,这导致了语法解析混乱。
  • 多租户插件是触发点:从您的错误堆栈看,问题发生在 TenantLineInnerInterceptor(多租户插件)处理 SQL 的阶段。这个插件需要解析原始SQL,以便自动注入租户ID条件。在解析到 output时,解析器失败了,从而抛出了异常。

最佳实践与根本解决策略

  • 在数据库设计阶段,尽量避免使用任何数据库的保留字作为表名或字段名。可以考虑将 output改为 output_info, output_result, output_content等非关键字,可以从根本上杜绝此类问题。
  • 对于关键字使用*``*括起来,避免一些意外的错误

写在结尾

这个关键字引发的问题,浪费了我不少时间,希望能提醒其他开发者,避免类似的问题。大家还知道有哪些关键字会导致类似的问题吗?

0%