跳至正文
Agents — Cindy 源码分析:一次用户输入如何进入 Harness

Cindy 源码分析:一次用户输入如何进入 Harness

项目仓库:makecindy/cindy

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 导出

能力这份源码能确认的范围阅读时要分开的事实
多 HarnessDesktop 通过 maker-core 启动 Claude Code、Codex、Pi统一的是任务、队列和事件外形;上下文窗口与原生会话格式仍属上游
本地工作本地任务操作这台电脑上的文件和已授权工具SSH 与“另一台电脑上的 Agent”是另外两条执行位置
多端控制Mobile 经 device-link 把输入送到 Desktop手机不在本机启动这三种 Harness
云端能力客户端默认连接官方端点;Skip Sign-In 仍可跑本地 Agent服务器能力在未登录时不可用,源码可编译不等于可以自托管同等后端
Headlessapps/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/localDbSQLite 表。sessions 保存任务配置,消息在发送事务里写入
apps/mobileExpo / React Native 控制端。会话页发送走入队或 steer,不直接调用 maker:send
packages/maker-coreAgent 抽象、Session 编排、上游事件转换。不依赖 Electron
packages/device-linkWebSocket 连接与 IPC 通道白名单。默认不在表中的通道不能远程调用
packages/orca-workflowOrca 的 Lead 侧 MCP 与提示。Worker 仍是独立 Session
packages/maker-remote-ssh、maker-cc-manager、maker-pi-managerSSH 连接,以及远端 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 开发入口;本次没有执行安装和启动。

bash
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 数据目录,不等于与本机上游配置完全分离。开发启动,默认沙箱

不安装依赖时,可以在仓库根目录核对引擎约束和适配器导出:

bash
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

本文在固定提交的对应文件上运行后,输出为:

text
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 可以用来区分补发和一条新输入。

关联阅读

博客助手

正在打开博客助手…