实战 01:简易 OpenCode——产品边界与架构

2026-09-01
26569 分钟
...

1、这一系列最终要做出什么

目标是一个真正可使用的本地编码 Agent,而不只是“模型生成 Patch”的示例。完成后应支持:

  • 启动本地 Server,TUI/CLI 作为独立客户端连接。
  • 一个项目有多个 Session;Session 中保存用户消息、模型文本、推理/工具 Part、费用和状态。
  • Plan 模式默认只读,Build 模式按权限执行读取、搜索、编辑、测试。
  • 运行过程通过 SSE 发送增量事件;客户端断开后 Session 仍保留并可重新加载。
  • 危险工具产生 Permission Request,用户可以本次允许、记住规则或拒绝。
  • 支持多模型 Provider、上下文压缩、代码规则、LSP/MCP 扩展与可撤销改动。

第一版仍不自动推送 Git、不访问生产凭证、不运行宿主任意命令。它是完整的本地产品骨架,不是 OpenCode 的一比一复制。

2、为什么必须是 Client/Server 架构

官方 OpenCode 的 TUI 本质上也是 Server 客户端;Server 暴露 OpenAPI 和事件流,从而允许 TUI、Web、IDE 和 SDK 共用同一运行时。简易版也采用这个边界:

TUI / CLI / Web / IDE
HTTP + SSE

Local Agent Server
  ├─ Project / Session API
  ├─ Model Runtime
  ├─ Context Builder / Compactor
  ├─ Tool Registry / Permission Broker
  ├─ Workspace / VCS / LSP / MCP
  └─ SQLite + Event Log + Artifacts

好处是任务不会依赖终端进程寿命;多个客户端看到同一 Session;权限请求有统一入口;Server 可以生成 SDK;TUI 不拥有额外的文件或 shell 权限。

默认只监听 loopback。需要远程访问时再增加认证、TLS/反向代理与明确 CORS,不能把无认证 Server 绑定到公网地址。

3、领域模型:Session、Message、Part

仅保存一串聊天文本无法表达工具、权限和流式状态。建议使用三层模型:

type Session = {
  id: string; projectId: string; parentId?: string;
  title: string; mode: "plan" | "build";
  status: "idle" | "running" | "waiting_permission" | "failed";
  createdAt: string; updatedAt: string;
};

type Message = {
  id: string; sessionId: string; role: "user" | "assistant";
  model?: string; parentMessageId?: string;
  createdAt: string; completedAt?: string;
};

type Part =
  | { id: string; type: "text"; text: string }
  | { id: string; type: "reasoning"; text: string; visibility: "hidden" | "summary" }
  | { id: string; type: "tool"; callId: string; tool: string; status: ToolStatus; input: unknown; output?: unknown }
  | { id: string; type: "file"; path: string; mime: string; artifactId?: string }
  | { id: string; type: "step"; name: string; status: "started" | "finished"; usage?: Usage };

Part 是 append/update 的最小流式单位:文本 Part 不断追加 delta;工具 Part 从 pending → running → completed/error;权限等待挂在对应 call ID 上。这样重载 Session 时能准确恢复 UI,而不是重新解析 Markdown。

4、Project 与 Workspace

Project 保存规范化根目录、VCS 信息、配置和规则文件版本。每个运行使用 Workspace:

Project root用户真实仓库默认只读观察
Session workspace独立 Git worktree 或隔离副本
Artifacts大日志测试报告图片生成文件

编辑和测试发生在 Session workspace。所有路径先规范化,必须仍位于允许根目录;外部目录单独权限;.git、密钥、缓存与依赖目录有默认策略。Session 结束时可以保留 worktree、导出 Patch、应用回用户仓库或清理。

5、Model Runtime 不能直接等于某家 SDK

Provider Adapter 统一处理模型标识、认证、消息转换、工具 schema、流式 chunk、usage、停止原因与 provider 错误:

interface ModelProvider {
  listModels(): Promise<ModelInfo[]>;
  stream(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelChunk>;
}

type ModelChunk =
  | { type: "text.delta"; text: string }
  | { type: "tool.start"; callId: string; name: string }
  | { type: "tool.args.delta"; callId: string; json: string }
  | { type: "finish"; reason: "stop" | "tool_calls" | "length"; usage: Usage };

模型差异不能全泄漏到 Session 层。Provider 切换要经过能力检查:是否支持工具、图片、结构化输出、上下文长度;模型变更后运行同一评测。

6、Agent Loop 的一次完整执行

用户消息入库
Context Builder 选择规则最近消息文件引用和工具
Provider 流式响应 Assistant Message/Parts/Event Log
若最终 stop完成 turn
 tool call解析完整 JSONTool Registry 查找工具
Permission Engine: allow / deny / ask
ask创建 Permission RequestSession 暂停
allowTool Runner 执行结果写 Tool Part
回到模型直到完成取消或预算耗尽

每个 turn 设置 deadline、最大模型调用、最大工具调用、token/费用上限和 doom-loop 检测。客户端取消通过 AbortSignal 传至模型和可取消工具,但已经完成的文件写入不能假装撤销。

7、工具注册表与风险分类

interface Tool<I, O> {
  name: string;
  description: string;
  schema: ZodType<I>;
  risk(input: I): "read" | "edit" | "execute" | "network" | "external";
  execute(input: I, ctx: ToolContext): Promise<O>;
}

首批工具:list/glob/grep/readwrite/edit/apply_patchrun_suite;可选 lsp。不要先提供任意 bash。需要 shell 时在 Sandbox 中通过细粒度 command policy 控制。

工具输出不能无限回填模型:保留摘要、退出码和 artifact ID;大结果写 artifact。所有工具使用 Session/Project context,不让模型传 root、user 或凭证。

8、Permission Engine 是独立子系统

权限动作有 allow/ask/deny,规则按 agent、mode、tool、路径或命令匹配。优先级要确定并可解释:更具体规则优先;deny 不应被宽泛 allow 覆盖;Plan mode 强制禁止 edit/execute。

type PermissionRequest = {
  id: string; sessionId: string; callId: string;
  tool: string; risk: string; inputPreview: unknown;
  matchedRule?: string; status: "pending" | "allowed" | "denied";
  expiresAt: string;
};

“记住允许”生成新的精确规则,例如只允许 npm test,不能把一次批准扩大成所有 bash。审批后重新验证 Session、call ID、输入 hash 和 workspace 版本。

9、事件流与客户端状态

Server 写事件日志并通过 SSE 发布:

type ServerEvent =
  | { seq: number; type: "session.updated"; session: Session }
  | { seq: number; type: "message.created"; message: Message }
  | { seq: number; type: "part.updated"; messageId: string; part: Part }
  | { seq: number; type: "permission.requested"; request: PermissionRequest }
  | { seq: number; type: "session.error"; code: string; retryable: boolean };

客户端 reducer 先拉 snapshot,再从 after=lastSeq 订阅事件。断线只重连,不重发 prompt;窗口过期则重新拉 snapshot。事件必须能去重且保持单 Session 顺序。

10、VCS、Undo 与 Redo

每个用户 turn 前记录 workspace tree/hash 或 Git snapshot;工具写入产生 Patch artifact;undo 恢复该 turn 的文件变更并回退 UI 指针,不能用破坏用户真实仓库的命令;redo 重新应用保存的 Patch,并重新检查基线 hash。

外部用户修改导致 hash 不一致时返回冲突,要求重新分析,不能强行覆盖。

11、存储建议

SQLite 表至少包括 projects、sessions、messages、parts、events、permission_requests、tool_executions、snapshots、artifacts、model_usage。Event Log 用于重连和审计,不代替规范化当前状态;大内容用 artifact 文件/对象存储,只在数据库保存元数据和 hash。

12、开发里程碑

  1. Server + SQLite + Session/Message/Part + SSE,模型用 FakeProvider。
  2. 只读工具与 Plan mode,完成问答/代码解释。
  3. Permission Broker + Patch workspace + Build mode。
  4. 固定测试套件、取消、预算、错误恢复、Undo/Redo。
  5. 真实 Provider、上下文压缩、规则文件。
  6. LSP、MCP、Skills/插件与 Sandbox。

每个里程碑都有 API 集成测试和客户端重连测试。下一篇用 Python 实现 Server、存储、事件流、Agent Loop 与权限暂停;再下一篇完成 TypeScript Server/TUI。

官方参考:OpenCode ServerToolsPermissions

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
AI2026-09-01
Deep Agents 01:何为 Agent Harness,以及如何开始
1、本篇任务:完成一份多步骤、带证据的技术调研 普通客服 Agent 的问题短、工具少、输出即时。技术调研或编码任务会持续很久,产生计划、搜索结果、文件和中间结论。Deep Agents 在 LangChain/LangGraph 之上预装规划、虚拟文件系统、上下文压缩和子 Agent,适合这类开放任务。 本课让 Agent 比较两种向量数据库,并交付一份可验证报告。 2、什么时候需要 Deep Agent 满足以下两项以上再考虑:任务...
学习
AI2026-09-01
Deep Agents 02:子 Agent、虚拟文件系统与长期记忆
1、本篇任务:让主管只看结论,让子 Agent 处理细节 技术调研会产生几十次搜索和大量文件。如果全部进入主管上下文,真正的目标会被噪音淹没。本课用两个子 Agent: 收集证据, 检查结论;主管负责计划与最终合成。 2、什么时候委派,什么时候直接调用工具 适合委派:子任务有多步;需要专门提示或工具;会产生大量中间结果;只需返回有限结论。不适合:一步查询;主管需要全部中间上下文;协调成本超过任务本身。 3、配置专门子 Agent Pyt...
学习
AI2026-09-01
Deep Agents 03:生产化、Sandbox、权限与上线验收
1、本篇任务:让 Deep Agent 在隔离环境中分析代码 只读研究 Agent 风险有限;编码 Agent 需要读写文件、安装依赖和执行测试。本课不讲如何让模型写更漂亮的代码,只讲执行环境、权限、恢复和上线验收。 2、先做威胁模型 资产包括源代码、用户文件、云凭证、生产网络和发布权限;攻击入口包括用户消息、仓库内容、网页、依赖包、MCP 返回和命令输出。 Prompt injection 不是靠一句 system prompt 解决...
学习
AI2026-09-01
LangChain 01:全景、原理与学习路线
1、本篇学完要得到什么 这一篇只解决三个问题:LangChain 到底负责什么;它与 LangGraph、Deep Agents、LangSmith 是什么关系;后面应按什么顺序学习。 贯穿整套课程的项目是“退款政策与订单助手”。它最终能够:回答知识库中的退款规则;查询当前用户的订单;生成结构化答复;对真正的退款操作进行人工审批;断线后恢复;通过评测后发布。 先记住一句话: 模型负责理解与生成,应用负责数据、权限、状态和副作用。 如果把...
学习
AI2026-09-01
LangChain 02:模型、消息与结构化输出
1、本篇任务:让模型输出成为程序可以依赖的合同 上一课只证明 Agent 能运行。本课暂时不接业务工具,只完成一个“客服分诊器”:输入用户问题,输出意图、紧急程度、是否需要人工和给用户的答复。 本课的核心不是学更多模型参数,而是理解三层合同:消息决定模型看到了什么;schema 决定程序期待什么;业务校验决定结果是否真的可用。 2、消息不是一段字符串,而是一条执行记录 一次工具型对话通常包含四种消息: | 类型 | 由谁产生 | 作用...
学习
AI2026-09-01
LangChain 03:工具与 Agent——从函数到可控行动
1、本篇任务:让 Agent 安全地读取订单 上一课得到结构化分诊结果,但模型不知道真实订单。本课增加一个只读工具 ,走通完整 Agent 循环,并把模型、工具包装和领域服务的责任分开。 完成后,用户问“我的 A100 发货了吗”,Agent 会选择工具;工具只按当前登录用户查询;模型基于工具结果回答。它仍然不能退款,因为我们没有提供写工具。 2、工具的本质是受 schema 约束的应用函数 一个好工具需要:稳定名称、清楚描述、窄输入...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录