Cindy 源码分析:一次用户输入如何进入 Harness
AI 参与说明(Agent:Cursor):本文由 Cursor 阅读公开仓库文档与固定提交源码后撰写,并用项目
humanizer-zh编辑中文正文。源码基线为77255d7,该提交的提交者时间为 2026-10-27 12:47:38(Asia/Shanghai)。核对方式是静态阅读与一次不安装依赖的入口检查,没有执行pnpm install,也没有启动 Desktop、登录 Cindy 服务或运行真实 Agent 任务。运行记录:模型grok-4.7-high-fast,提供方 xAI,执行入口 Cursor Cloud。reasoning effort 未作为独立字段取得运行记录。
Cindy 是放在现有 Agent Harness 外面的开源客户端。一条用户消息先进入 Desktop Main 的输入队列,确认可以派发后,才由 maker-core 的 Session 交给 Claude Code、Codex 或 Pi。这个仓库不包含 Cindy 云端后端,也不重写上游的 Agent Loop。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Agent Harness | 智能体执行框架 | 负责调用模型、执行工具并维护自身上下文的程序,例如 Claude Code、Codex、Pi |
| Agent Loop | 智能体循环 | 请求模型、执行工具,再把结果交回模型决定下一步 |
| maker-core | 保留原名 | 不依赖 Electron 的会话编排包,把不同 Harness 收成统一的 Session 与事件 |
| Session | 业务会话 | Cindy 自己的任务会话;它保存业务 id,并另外记录上游的恢复键 |
| lazy-create | 延迟创建 | 内存里还没有 Session 时,等到第一次发送才启动对应 Harness |
| device-link | 设备互联 | 让手机或其他电脑把操作送到持有文件的那台 Desktop,并收回状态 |
| AgentEvent | 统一事件 | 适配器把上游通知翻译成宿主和界面都能处理的事件 |
| AppServerHost | 保留原名 | Codex 适配器里共享的 codex app-server 进程,多个 thread 复用这一个进程 |
项目定位与代码边界
Cindy is a client and orchestration layer around existing agent harnesses. The cloud backend is outside this repository. 产品原则把职责写成连接人、Agent、工具、文件、应用和设备,并写明不以重新实现 Claude Code、Codex 或其他 Agent Loop 为目标。产品原则,README
仓库是 pnpm monorepo,根许可证为 Apache-2.0,package.json 的作者字段为 XD Inc.。apps/desktop/package.json 的 version 是 0.0.0,这是构建占位,不能当成发行号。2026-10-27 读取 GitHub API 时,仓库约有 6748 stars、817 forks;当时最新正式发行是 v0.1.115,发布于 2026-10-23。这些数字只说明公开仓库当时的状态,不能用来证明任务成功率或某个模型组合已经兼容。根 package
README 把首先支持的 Harness 写成 Claude Code 与 Codex,并说更多 Harness 还在加入、原生 Harness 仍在开发。同一提交的 maker-core 已经导出 ClaudeCodeAgent、CodexAgent 和 PiAgent。因此 Pi 在这份源码里是已接入的适配器,不能按 README 的“首先支持”理解成尚未实现。三种适配器的传输不同,后文分开写。Agent 导出
| 能力 | 这份源码能确认的范围 | 阅读时要分开的事实 |
|---|---|---|
| 多 Harness | Desktop 通过 maker-core 启动 Claude Code、Codex、Pi | 统一的是任务、队列和事件外形;上下文窗口与原生会话格式仍属上游 |
| 本地工作 | 本地任务操作这台电脑上的文件和已授权工具 | SSH 与“另一台电脑上的 Agent”是另外两条执行位置 |
| 多端控制 | Mobile 经 device-link 把输入送到 Desktop | 手机不在本机启动这三种 Harness |
| 云端能力 | 客户端默认连接官方端点;Skip Sign-In 仍可跑本地 Agent | 服务器能力在未登录时不可用,源码可编译不等于可以自托管同等后端 |
| Headless | apps/cindy-headless 复用 maker-core,提供容器向 CLI | 其 0.4.3 声明的上游提交是 fa3fc88,不是本文基线 |
依据:README 的仓库范围与登录模式,Headless 说明。
目录与模块
根配置要求 Node.js >=22.12、pnpm >=10.7 <11,并把包管理器固定为 pnpm 10.33.2。安装还需要 Git LFS。pnpm install 的 postinstall 会尽力下载当前平台的 Claude、Codex、ripgrep 和 Pi 二进制;这些二进制不进 Git。环境准备
| 路径 | 阅读时看什么 |
|---|---|
apps/desktop/src/renderer | 输入界面和会话视图。它提交可序列化的输入,不持有队列事务 |
apps/desktop/src/preload | 把 maker:input:enqueue、maker:send 等调用送到 Main |
apps/desktop/src/main/maker-ipc | 输入队列、发送事务、事件回推和 Orca 等宿主服务 |
apps/desktop/src/main/localDb | SQLite 表。sessions 保存任务配置,消息在发送事务里写入 |
apps/mobile | Expo / React Native 控制端。会话页发送走入队或 steer,不直接调用 maker:send |
packages/maker-core | Agent 抽象、Session 编排、上游事件转换。不依赖 Electron |
packages/device-link | WebSocket 连接与 IPC 通道白名单。默认不在表中的通道不能远程调用 |
packages/orca-workflow | Orca 的 Lead 侧 MCP 与提示。Worker 仍是独立 Session |
packages/maker-remote-ssh、maker-cc-manager、maker-pi-manager | SSH 连接,以及远端 Claude / Pi 守护进程 |
apps/cindy-headless | 容器向运行时。它声明自己不重建 Desktop、Electron UI 或 Agent 二进制 |
仓库地图写明:mobile 只直接依赖 auth-client、device-link、maker-shared、model-providers、voice-input-core。模型目录、权限档的未知值处理放在共享包;真正启动 Harness 的代码留在 Desktop 所使用的 maker-core。仓库地图
flowchart TD
UI["Desktop Renderer"] --> Bridge["preload"]
Phone["Mobile"] --> Link["device-link"]
Bridge --> Main["Desktop Main"]
Link --> Main
Main --> Queue["AgentInputCoordinator"]
Queue --> Tx["makerSendTransaction"]
Tx --> Maker["Maker"]
Maker --> Session["Session"]
Session --> Store["SQLite sessions / messages"]
Session --> Claude["ClaudeCodeAgent"]
Session --> Codex["CodexAgent"]
Session --> Pi["PiAgent"]
Claude --> Loop["上游 Agent Loop"]
Codex --> Loop
Pi --> Loop
这张图只回答“一条输入经过哪些模块”。它不表示每个 Session 都会同时启动三种 Harness,也不展开 SSH。SSH 会把 Agent 进程和工作目录放到远端主机;agentDeviceId 则把 Agent 放到另一台已连接的电脑。Maker.createSession 在两者同时出现时直接拒绝,因为一个 Session 不能既在 SSH 主机上跑、又在另一台电脑上跑。互斥检查
一次用户输入如何进入 Harness
Desktop 的普通发送入口是 maker:input:enqueue。preload 把 Renderer 准备好的队列项交给 Main;Main 的 AgentInputCoordinator 持有队列、可见气泡、SQLite 消息行和 maker-core 派发边界。注释说明 Renderer 可以画队列,但不能做这四份状态的事务所有者。协调器说明,preload 入队
手机会话页的发送走 input:enqueue 或 input:steer,不调用 maker:send。控制端发出的来源字段不可信:被控端在 IPC 边界盖章,wire 上传来的 origin、插件来源和权限相关字段会被剥掉。device-link 对远程通道默认拒绝,未列入白名单的通道不能从另一台设备调用。手机发送约定,通道白名单,Electron 信任边界
enqueue 用 clientId 做幂等。同一个 id 已经在队列、活动轮次、steer 或最近受理窗口里时,这次投递被忽略并返回当前投影。这个分支挡住的是控制端补发,不是一条新消息。去重
队列排到队首后,宿主调用 createMakerSendTransaction 产生的发送事务。maker:send 仍是这条事务的 IPC 名字,旧的直接调用方也会进同一个处理函数;处理函数只校验参数,并把 wire 上不允许 Main 以外写入的字段去掉。发送事务契约,IPC 适配
lazy-create 与两种会话 id
内存里没有活着的 Session 时,发送事务按调用方带来的 createOpts 做 lazy-create:第一次发送才启动上游运行时。工作目录以数据库里的 working_dir 为准,避免队列快照把任务带回已经搬走之前的目录。lazy-create 说明
Maker.createSession 对同一个业务 id 去重。进程内已经有 Session,或同 id 正在创建,就复用,避免再拉起一份上游运行时。真正启动时,它把业务 Session id 和只在本次内存实例里有效的 sessionInstanceId 一起交给 agent.startSession。注释写明:业务 id 在关闭或重建后会复用,内存代号用来挡住迟到的旧 MCP 请求借用新 Session 的权限状态。创建与去重,交给适配器
上游返回的 handle id 写入 sessions.sdk_session_id。SQLite 的 agent_kind 默认值仍是历史字符串 cc;读取执行配置时,cc 会映射成 claude-code。新代码里的种类名是 claude-code、codex、pi,不能把默认列值理解成第四种 Harness。sessions 表,种类映射
accepted 之前和之后
Session.send 进入 dispatchSend。它先等待宿主轮次租约和权限变更结束;当前轮次仍在跑、或上一次发送尚未跨过受理边界时,拒绝这次发送。通过这些门之后,onAccepted 回调先执行,发送事务在这里把 user 消息写入 SQLite。随后才调用 handle.send。handle.send 正常返回后,结果才是 { accepted: true }。Session.send,onAccepted 与 handle.send,事务里的落库回调
sequenceDiagram
participant UI as "Renderer 或 Mobile"
participant Q as AgentInputCoordinator
participant TX as makerSendTransaction
participant S as Session
participant DB as SQLite
participant H as AgentSessionHandle
UI->>Q: maker:input:enqueue
Q->>TX: 队首派发
TX->>S: Session.send
S->>DB: onAccepted 写入 user 消息
S->>H: handle.send
H-->>S: 上游受理这次 turn
S-->>TX: accepted 为 true
accepted: true means the provider send resolved. It does not mean the model finished the task, the tools succeeded, or the user accepted a plan. 落库发生在 handle.send 之前,所以“消息行已经写入”和“Harness 已经受理”是两件先后发生的事。发送事务要求:lazy-create 失败、工作目录缺失,或上游在开始 turn 时失败,都要在 accepted: true 之前拒绝或返回未受理,让队列按未派发回滚。某个失败分支会不会删掉已经写入的消息,要看该错误自己的回滚代码;上面的时序只画受理成功的顺序。
调度、IM、插件和另一个 Session 的转发也可以进入同一条发送事务。它们带的 origin 由宿主写入,用来标记来源和后续展示;来源标记本身不是权限。运行中的插话走 input:steer,协调器在上游接受 steer 之后才落库。协调器状态表
三种传输
三种适配器都实现 BaseAgent.startSession,并返回不泄露上游协议类型的 AgentSessionHandle。差异在进程和协议。
| Harness | 适配器怎样启动 | 一份进程能服务几个 Session |
|---|---|---|
| Claude Code | @anthropic-ai/claude-agent-sdk 的 query,由适配器拉起 Claude Code 进程 | 每个 Session 走自己的 SDK query;MCP 与 system prompt 在 startSession 时装入 |
| Codex | 自有 AppServerHost:一个共享的 codex app-server 子进程,JSON-RPC NDJSON | 一个进程服务多个 thread;Session 关闭只退订自己的 thread |
| Pi | 启动 pi --mode rpc,用 JSONL/stdio 交换事件 | 本地 Session 使用这份 RPC;SSH 上的 Pi 改由宿主注入的远端 transport |
Claude 适配器把 system prompt 分成 SDK preset、maker-core 追加段和宿主运行时段落,并声明运行中切换模型、effort 和权限模式已经接到 SDK。Codex 适配器把审批请求转回与 Claude 相同的权限通道,Renderer 不需要认识 thread_id。Pi 适配器保留 Pi 自己的默认 prompt,再追加宿主段落;模型清单写进宿主注入的 models.json,API key 走环境变量,不落盘。Claude 适配器,Codex 适配器,AppServerHost,Pi 适配器
Codex 还有一层进程内租约。可恢复的 thread id 在 startSession 前后会被 claimCodexThread 认领,避免两个业务 Session 同时持有同一个原生 thread。认领冲突时,这次启动会关闭刚拿到的 handle,并按启动失败清理。认领
上游事件由各适配器译成 AgentEvent。Session 在派发前启动自己的事件循环;Desktop 的 deliverSessionEvent 再把准备好的事件经 maker:event 广播给窗口。device-link 也可以把同一类事件送到控制端。界面上的文字增量、工具调用和终态,来自这条回推,不来自 Renderer 自己调用模型。事件循环入口,广播
关键设计取舍
The client absorbs transport differences and keeps the upstream agent loop. 这让一个输入框可以切换 Harness,也让 MCP、权限和事件必须在适配器边界被翻译。翻译丢字段时,损失出现在 Cindy 层,而不是上游“突然不支持”。产品原则把这种损失写成要避免的结果,但源码里的能力表仍然按 Harness 分开声明;某个开关在一种适配器上不存在时,Session 抛出 NotSupportedError。产品原则中的非退化要求
maker-core 不能 import Desktop Renderer 或 Main。文件、网络、凭证和 MCP 列表由宿主在启动时注入。Main 进程侧还禁止运行时动态 import(),依赖用顶层静态 import。代价是宿主装配代码很长,register.ts 仍然集中挂接大量服务;仓库地图要求按函数名定位,不要从文件头通读。包边界
队列把“用户已经点了发送”和“上游已经开始这轮”拆开。这样插话、停止、重试和手机补发可以在派发前撤销。代价是状态更多:队列投影、气泡、数据库行和 accepted 必须一起看。只盯 Renderer 上的乐观气泡,不能判断 Harness 是否已经收到文本。
持久化也是分开的。sessions 行保存模型、权限档、sdk_session_id、orca_role 和执行位置;worktree_path 只是侧栏用的反范式快照,注释写明工作树的真源在 worktrees.json。删除工作树时不清理这个列。判断任务是否仍绑定工作树,要看工作树存储里还有没有这个 Session,不能只看数据库残留值。worktree 列
Orca 在这份提交里仍是 Lead 与 Worker 两类 Session,角色写在 orca_role,Lead 侧工具在 packages/orca-workflow。Worker 是完整 Session,不是把另一份上下文拼进 Lead。2026-10-10 的站内调研按更早提交写过回报进入 Lead 的时点;那些行号不属于 77255d7,本文不把它们复述成当前行号。
适用场景与边界
适合阅读这份源码的情况,是想确认 Cindy 如何复用已经安装的 Claude Code、Codex 或 Pi,并在同一个 Desktop 里处理队列、权限、多端控制和任务记录。本地任务的文件操作发生在持有工作目录的那台电脑上;手机改变的是谁在发指令。
下面几件事不在这个客户端仓库里一次性解决:
- 自托管与官方客户端等价的云端账号、计费和 Skill Hub。README 写明后端在另一个仓库,开发构建默认连接官方端点。
- 把
apps/cindy-headless当成当前 Desktop 的无界面替身。它复用maker-core,但自己的版本说明钉在更早的上游提交。 - 仅凭“本地运行”推断模型请求不出机器。Skip Sign-In 可以不登录 Cindy 账号并使用本地 Agent;选择云端模型时,为推理准备的上下文仍会发到所选模型服务。
- 把统计开关理解成所有网络出口。README 说明 TapDB 统计与诊断日志、登录心跳是分开的开关;Skip Sign-In 不向 TapDB 上报。本文没有审计全部请求。
- 认为切换 Harness 后,原生会话、压缩和工具行为保持一致。统一的是 Cindy 的 Session 和
AgentEvent。
官方发行包的安装入口在 cindy.app/download。Linux 还要按仓库里的 Linux 安装说明 区分发行版。客户端源码采用 Apache-2.0;模型、商标和单独声明的第三方组件仍可能使用其他条款。LICENSE,NOTICE
怎样在本地看到这条路径
普通使用不需要克隆。要对照本文,把仓库固定到同一提交再安装。下列命令来自仓库的环境准备和 Desktop 开发入口;本次没有执行安装和启动。
git clone https://github.com/makecindy/cindy.git
cd cindy
git checkout 77255d7e61fd84d3905f350557a1915073c3cf6f
git lfs pull
pnpm install
# Global account. Mainland China accounts use --region=cn.
pnpm restart:desktop:remote --region=global
restart 包装在没有显式 --isolated、--shared 或 --preserve-running 时,会补上 --isolated=dev。默认开发数据因此落在独立沙箱,而不是正式安装的 userData。要继续正式安装里的 Cindy 登录与会话,需要按开发文档显式选择共享模式。Claude 的配置目录仍是本机 ~/.claude;Codex 登录态在开发文档里还有只读复用规则。隔离 Cindy 数据目录,不等于与本机上游配置完全分离。开发启动,默认沙箱
不安装依赖时,可以在仓库根目录核对引擎约束和适配器导出:
node - <<'NODE'
const fs = require('node:fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const source = fs.readFileSync('packages/maker-core/src/agents/index.ts', 'utf8');
console.log(`Node: ${pkg.engines.node}`);
console.log(`pnpm: ${pkg.engines.pnpm}`);
for (const name of ['ClaudeCodeAgent', 'CodexAgent', 'PiAgent']) {
console.log(`${name}: ${source.includes(name) ? 'present' : 'not found'}`);
}
NODE
本文在固定提交的对应文件上运行后,输出为:
Node: >=22.12
pnpm: >=10.7 <11
ClaudeCodeAgent: present
CodexAgent: present
PiAgent: present
这只确认声明和导出还在。它不证明三种 Harness 在当前机器上都能完成一次工具调用。
对照源码时,可以沿一次发送向下看:Renderer 的 input.enqueue、AgentInputCoordinator.enqueue、createMakerSendTransaction、Maker.createSession、Session.dispatchSend,最后落到所选适配器的 handle.send。队列项的 clientId 可以用来区分补发和一条新输入。
关联阅读
- Cindy 项目调研:多 Harness 工作台、Orca 与本地执行边界:2026-10-10 提交上的产品范围、Orca Worker 与本地执行边界。行号属于那个提交。
- Pi Agent 框架架构:包边界、Agent Loop 与接入选择:Cindy 所适配的 Pi 自身怎样组织 Session 与 Agent Loop。
- Agent 栏目导航:同栏目的 Harness 与工作流文档。
- 仓库文档索引:区分产品规则、当前实现和开发约束。