OpenWork 是本地 Agent 工作台:Rust workspace + Tauri 2 / React 桌面应用,PostgreSQL 持久化;协作模式额外使用 Redis 传递 wake 与保存短期协调状态。
本文定义 crate 划分、依赖方向和所有权边界。单个功能的设计见各自文档(见 README 索引)。
desktop/src ← 只通过 Tauri Command/Event
desktop/src-tauri → openwork-core, openwork-models, openwork-collab
openwork-core → openwork-agent, openwork-chat-state, openwork-models,
openwork-tools, openwork-credentials
openwork-agent → openwork-models, openwork-tools
openwork-chat-state → openwork-models
openwork-tools → openwork-models
openwork-models → 无 OpenWork 依赖
openwork-credentials → 无 OpenWork 依赖
openwork-collab → 独立协作分支;内部单向依赖见下文
这个方向不可逆转。 具体禁止:
openwork-models/openwork-tools反向依赖openwork-core;- Desktop 直接依赖 SQLx、Provider Adapter 或 Tool Executor;
openwork-agent启动异步运行循环;openwork-chat-state执行工具或决定权限;openwork-core把数据库 Record 类型暴露给 Desktop。
最后一行是协作模式(见 collaboration.md),它是图里的独立分支:Server 拥有 PostgreSQL/Redis 与业务事实,本机 Computer daemon 拥有 Engine 运行时,二者只通过 crate 内 protocol 定义的契约通信。desktop/src-tauri 监督这两个进程并只使用 Desktop HTTP interface——不得让 Desktop 绕过 Collaboration Server 直接读写 collab_* 表。
工作台的唯一运行时入口。 OpenWorkCore 拥有 Provider Repository 与 Session Registry;凭证的存储与加解密已下沉到 openwork-credentials。
src/
├── core.rs OpenWorkCore facade 与 bootstrap
├── provider.rs Provider 预设与索引
├── session/ SessionActor、Agent Loop、压缩、Trace Guard
├── context/ System Context 物化
├── model_call/ 请求组装与预算估算
└── storage/ PostgreSQL 读写、Trace Recorder
它不拥有:模型协议编码、工具实现、文件系统细节。
回答"这个 Agent 是什么"——静态定义,不含运行状态:System Prompt、可用工具集、默认模型参数、最大 Model Call 次数、Permission Mode。
AgentBuilder::build 返回近似不可变的 Agent。它不得启动任何异步循环。
回答"模型下一次会看到什么":有序 Conversation、流式草稿、类型化 Conversation Item。
它是 Conversation 的唯一写者。 Core 通过 Command 写入、通过 Snapshot 读取,不直接改内部状态。
Message/ContentBlock、Model Request/Response/Event、ModelPort trait、各 Provider Adapter、SSE 解析、Transport Retry、错误归一化。
它不知道 Session、Turn、Trace 或 Desktop 的存在。 这是它能被独立测试的原因。
工具的四层运行时(契约 / 工具集 / 会话上下文 / 调用上下文)、路径安全策略、文件与进程后端、内置工具实现。详见 tools.md。
分工:Tools 提供风险信息并在执行时强制路径/进程边界;Core 拥有 Tool Call 生命周期和用户授权等待。
provider_credentials 的读写与 AES-GCM 加解密。无 OpenWork 依赖。 BYOA 协作模式不消费它:主推理与 triage 都使用 Computer 上 OpenCode 自己的登录态,Server 不持有对应 Provider 凭证。
协作模式继续使用一个 crate,并把独立生命周期放进内部模块:
protocol/ ← server/
protocol/ ← computer/
server/ ← bin/openwork-collab.rs
computer/ ← bin/openwork-collab.rs
protocol/ ← bin/openwork.rs
protocol/ 只保存 Server、Computer、shim 与 Desktop host 共同使用的线协议,不包含 SQL、Redis、进程管理、Engine 原生事件或业务判断。server/ 是协作世界的唯一持久事实写者,拥有 loopback HTTP/SSE、RuntimeSession、房间/消息/看板/Climate、Run、Agenda、PostgreSQL 与 Redis coordination;它不启动 Engine。computer/ 是 macOS 本机 BYOA 宿主,拥有 desired-state reconcile、AgentRunner、home、shim、EngineRegistry、EngineAdapter interface 与生产 OpenCodeAdapter;它不直接使用 SQLx、Redis、openwork-core 或 openwork-credentials,也不持有 Server 数据库凭证。
目标进程与通信边界:
React → Tauri Command → /desktop/* HTTP/SSE → Collaboration Server
Computer daemon → /computer/* HTTP/SSE → Collaboration Server
Agent shim → /agent/* HTTP → Collaboration Server
Collaboration Server → PostgreSQL + Redis
Computer daemon → per-Agent Engine child processes
Desktop 是唯一 supervisor:每次启动创建一个 RuntimeSession 和临时凭证,随后启动 Server 与 Computer;Desktop 退出时先停止 Computer 及其 Engine 进程组,再停止 Server。Runtime 不使用系统常驻任务、持久机器身份、固定端口或本地 socket transport。
单 crate 是有意选择:当前实现只随同一个 macOS 应用一起构建、安装和升级,protocol 没有独立发布者,Computer 也没有远程部署目标。若只为目录边界拆 crate,会增加 manifest、错误类型、测试 fixture 与版本协商,却不会增加实际隔离。
- 一个活动 Session 一个
SessionActor,它同时最多推进一个 Turn。 - OpenWork 自身的 Agent Loop 只有一处 ——
session/run_loop.rs。Trace、Storage、Desktop 都不能推进 Turn。协作模式的推理由 Computer 上的外部 Engine adapter 推进(V1 是OpenCodeAdapter),AgentRunner只交付 wake delta 与记录结果,见 collaboration.md。 - Conversation 只有一个写者 ——
openwork-chat-state。 - 模型总是用户显式选择(
providerId + model),没有自动选择或跨模型 fallback。 - Trace 是 best-effort —— Trace/队列/数据库失败不得让 Turn 失败。正文写入失败时 Span 本身仍须落库。
- 同一份内容只有一个权威副本 ——
messages已有的内容,Trace 只留指针不复制。 - 权限
Allow不能绕过ToolSessionContext的路径边界——对六个文件工具成立。bash没有执行期边界,审批即边界,见 permissions.md §1.2。
Desktop 只面对这些:
OpenWorkCore::create_session / list_sessions / load_session
OpenWorkCore::start_turn(session_id, client_request_id, input: Vec<UserInput>,
context_window_tokens)
OpenWorkCore::cancel_turn(session_id, turn_id)
OpenWorkCore::resolve_permission(session_id, turn_id, tool_call_id, decision)
OpenWorkCore::subscribe_updates()
OpenWorkCore::get_session_snapshot(session_id)
OpenWorkCore::compact_conversation / rewind_conversation
OpenWorkCore::list_traces / get_trace / get_trace_by_id
/ get_span_payload / list_compaction_spans
/ upsert_annotationReact 不直接调用这些。src-tauri 把它们映射成短生命周期 Command,并在进程启动时把 Core Update 转成 Tauri Event。详见 desktop.md。
Session
├── Turn 一次用户输入触发的完整 Agent Loop
│ ├── Model Call 循环内一次模型请求/响应
│ ├── Tool Call Provider Tool Call 从解析到结果
│ ├── Permission Request Tool Call 的临时等待状态,不是独立聚合
│ └── Turn Outcome
└── Sub-Agent 本身也是一个 Session,父子关系记在 sessions 表
其他术语:
- Prompt 只表示 System/User Prompt 等指令内容,不是运行聚合;
- Compaction 是把 Conversation 压成摘要投影的操作,见 compaction.md;
- SessionUpdate 是 Live UI 消息,不是持久化事实;
- Trace Span 是质量记录(模型看到什么、说了什么、烧了多少 token),不是恢复状态;
- Sub-Agent 是主 Agent 派生的只读从属 Session,见 multi-agent.md。它不是新的运行聚合——一切 Turn 语义与根会话完全相同;
- 协作模式 是与工作台运行时不相交的 BYOA 子系统:Collaboration Server 保存业务事实,本机 Computer daemon 运行 Engine adapter(当前只有
OpenCodeAdapter);每个协作 Agent 有显式engine_id、主模型、triage 模型、私有 home、当前 RuntimeSession JWT 与AgentRunner,见 collaboration.md。它的 Room、Computer daemon、协作 Agent 与本篇的 Session、Sub-Agent 没有继承关系,不要混用; - Agent Message 是子 Agent 回传给父的消息,以
message_kind = 'agent_message'存在父的 Conversation 里,永不触发 Turn。
不要重新引入的退役术语:StepId、ToolRunId、ApprovalId、TurnRecorderPort、JournalTurnRecorder、Event Journal。
工作台运行时不要添加:MCP、Memory、Artifact、Git/Diff、Worktree、跨进程未完成 Turn 恢复、Event Journal、有损压缩、后台任务恢复。
多智能体已从非目标移出,设计见 multi-agent.md。它不引入新 crate、不新增 Trace kind、不新增 SessionUpdate 类型:子 Agent 本身就是一个 Session,复用 SessionActor 与唯一的 Agent Loop;父子拓扑是 sessions 表的四个新列;五个控制工具按 update_plan 的先例由 Core 拥有;子 Agent 的 Trace 独立成树,靠 sessions.parent_session_id / spawn_span_id 关联。范围严格限定在只读、单层、异步——可写子 Agent、多层嵌套、角色文件加载、跨子 Agent 通信都仍是非目标。子 Agent 完成时只入队不唤醒父会话,因此"后台任务恢复"仍在上面那行里。若某次改动要求新增 crate、Trace kind 或让子 Agent 能写文件,先回到 multi-agent.md 确认是不是设计走偏了。
Skill 已从非目标移出,设计见 skills.md。它不引入新 crate、不新增 Trace kind:目录挂在 System Context 上,正文走 Conversation,启停偏好单独保存在 skill_status,资源与脚本复用 read / bash。用户在 Desktop 选择 $name 时,可见 token 与 { name, path } 绑定分离;Tauri 把文本和显式选择编码为同一个有序 Vec<UserInput>。Core 在接受 Turn 前把 UserInput::Skill 解析为持久化的 contextual User-role Text,模型内容层不定义 Skill 专用类型,ModelRequestBuilder 和 provider adapter 只处理已有 ContentBlock。文件读取不下沉到 Bridge、Chat State 或 provider adapter。若某次改动要求新增 Skill crate 或 Trace kind,先回到 skills.md 确认是不是设计走偏了。
协作模式由 collaboration.md 约束。它在 §1 的依赖图里是一条独立分支:openwork-collab 不依赖 openwork-core;其 server/、computer/ 与 protocol/ 按模块单向依赖。Engine 是每个 Agent 的显式领域属性,AgentRunner 通过 EngineRegistry 取得 adapter,当前生产 registry 只有 OpenCodeAdapter;以后接入 Codex 时增加真实 adapter,不预留空实现或 capability 矩阵。Provider 登录态只在对应本机 Engine 的 data root 中,协作分支不依赖 openwork-credentials。持久化落在 collab_* 表,短期协调落在 Redis,瞬时 Runner 状态落在 Computer 内存,三者都不碰 openwork-core migrations。
协作范围严格限定为本机 macOS、消息、群聊、Climate、看板和 Agenda,当前 Engine 只有 OpenCode。没有 Windows/Linux、远程 Mac、多 Computer assignment、系统后台常驻、MCP、审批、共享项目目录、Git/Diff、Worktree、Memory、Notes、Skills 或 reaction。未来 Codex adapter 属于已经确认的 Engine seam,但只有真实接入时才实现。其他范围若重新提出,应重新评审,而不是让 Computer 直连数据库、让 Server 启动 Engine、或让 Desktop 承担业务规则。
工作目录只是 Session 创建时确定、传给 ToolSessionContext 的路径值,不是独立领域对象——因此没有 openwork-workspace crate。只有当出现多个消费者共享的 Git、Sandbox 或 Checkpoint 能力时,才重新评估是否拆出。
每引入一个模块,回答六个问题:
- 谁创建它;
- 谁修改它;
- 谁决定下一状态;
- 谁持久化;
- 它失败是否会改变主流程;
- 是否产生反向依赖。
若一个类型需要同时回答两个以上的 Owner,先拆职责再落地。 最终应当能从目录结构直接读出一次 Turn 的完整控制路径,而不需要在多个 crate 之间反复跳转。