代码结构

这份指南讲各类行为该放在哪里,以及怎样在仓库里顺藤摸瓜找到它们。环境搭建和检查看 development.md,服务边界和安全边界看 AGENTS.md。

先看部署边界

apps/
  backend/
    cloud_agent/          Control plane and its HTTP APIs
    cloud_agent_host/     Host execution and retained sandbox management
    cloud_agent_shared/   Contracts and algorithms shipped in both images
  frontend/              Next.js browser UI and backend-for-frontend routes
gateway_contracts/       Gateway HTTP contracts and control-plane OpenAPI
migrations/              Agent-owned database schema, managed by Alembic
deploy/                  Image builds and deployment entry points
scripts/                 Development, qualification, and release tooling
tests/                   Backend behavior, integration, and boundary checks
docs/                    Maintained guides and dated design evidence

控制平面和主机端是分开部署的两套实现,谁也不导入谁。共用代码只放双方都认可的事实和契约,本身也不导入任何一方的实现。和网关之间通过 HTTP 契约对接。Agent 数据库和网关数据库始终是分开的。

跟着一个控制平面请求走

flowchart TD
    App[app.py: listener and route assembly] --> API[api/: HTTP adapters]
    API --> Flows[application/ and services: orchestration]
    API --> Ports[ports/: capability contracts]
    Flows --> Ports
    Flows --> Domain[domain/: rules and vocabulary]
    Bootstrap[bootstrap/: resource and lifecycle assembly] --> Workers[workers/: background loops]
    Workers --> Flows
    Bootstrap --> Storage[storage/: Postgres implementations]
    Storage --> Ports
    Storage --> Domain

app.py 挂载所有者/管理员 API 和单独的 MCP 专用监听器,并装上统一的 HTTP 响应和异常处理策略。两个监听器上还都装了一个依赖,它排在每个路由自己的依赖之前(api/_parameters.py):路径或查询参数里带 NUL,或者路由的整数参数超出了 bigint 的范围,都会在任何查询读到它之前直接返回 422。由所有者、管理员或值班中继调用的路由,JSON 请求体里带 NUL 或者只有半个代理对,也一样拒绝;主机和沙箱的上报有自己的模型,会把这些字符替换掉,而不是拒绝。bootstrap/lifespan.py 负责创建资源,以及启动、停止后台任务;服务的启动和关闭不归路由模块管。

目录或模块

负责的内容

什么时候改这里

api/

HTTP 解析、依赖注入、授权入口、响应映射

请求或响应的行为要变

schemas/

Pydantic 请求和响应模型,以及主机/worker 之间的传输模型

传输字段或校验规则要变

application/

准入、后续消息、Fork、投递、调和等业务流程

一个用例要协调好几项能力

domain/

执行状态、请求身份和业务术语

规则本身和持久化、HTTP 都无关

ports/

范围很窄、带类型的能力接口

调用方需要适配器提供新的操作

storage/assembly.py

组装 Postgres repository 提供的各项能力

要把一项存储能力接入服务

storage/repositories/

SQL,以及事务和栅栏逻辑

持久化的查询或写入要变

workers/

定时任务和恢复循环

这项工作不依附于某个请求,要自己持续运行;队列和目标的恢复拖不住 attempt 租约的恢复

bootstrap/

资源和生命周期

某个组件需要创建、启动或关闭

mcp/

MCP 托管、传输、策略和进程内运行时

协议行为或上游会话行为要变

goals/

目标(goal)的编排和重试策略

目标的调度或进度策略要变

providers/

外部服务商的具体实现

某个服务商的对接方式要变

存储组装对外提供 store.host_pool、store.pending_messages 这样的能力。调用方的类型只针对自己用到的能力来写,既不导入 Postgres repository,也不绕过组装直接去拿它的连接池。应用流程可以沿用现有的 FastAPI 异常约定;文件挪了位置,并不代表这些接口已经和框架脱钩。

MCP 会话归 mcp/runtime.py 管:按需创建 host、带栅栏的会话回收、日常维护和关闭。中继的处理函数和 bootstrap 都直接调用这个运行时。立即发送(send-now)交接任务的保留和取消在 application/send_now.py;崩溃恢复仍然靠持久化的投递记录和恢复 worker。

API schema 按领域划分

公开的模型从 cloud_agent.schemas 导入,由它的 __init__.py 显式导出。实现代码在各领域模块之间直接互相导入,不要再绕回门面(facade)导入自己。

模块

包含的模型

jobs, conversations, messages, goals

任务、会话、队列、提问和目标

events, resources, workspace

事件流、测量数据、文件、终端和 Git 视图

workers, hosts, admin_hosts

worker 请求、主机注册、心跳和管理员的主机控制

archive_settings, audit, config

归档设置、管理员审计和对外公布的配置

runtime_upgrades

从管理员设置页面发起的 Agent CLI 升级

integrations, mcp, session

代码托管平台、MCP 选择、身份和用户偏好

teams

团队、成员、邀请和加入申请

_host_reports, _sandbox_manager_wire, _prompts

私有辅助模块:主机和 worker 上报内容的上下限、Sandbox Manager 字段名兼容,以及提示词文本规则

做结构性重构时,模型名称和序列化结果都不能变。跨领域的类型注解在导入后必须能立即解析。有意修改传输格式时,要按开发者指南的说明同步更新契约。

跟着一次主机执行走

runner.py 负责认领任务、attempt 租约、执行轮次、心跳和排空。claimed_job.py 定义认领的数据结构和租约错误;control_plane_client.py 负责以 attempt 为范围的 worker HTTP 调用、请求超时和事件重放。runner 会显式地把这两个模块原有的公开名称再导出一遍,客户端的创建仍留在 runner 的编排层。

chat_sandbox_models.py 里放的是保留容器的规格、检查和状态值、归属标签,以及身份字段的上下限。沙箱组的网桥、中继成员关系、Kata guest 路由同步,以及网络的证明和释放,都放在单独拆出来的 chat_sandbox_network.py 组件里。这个组件只依赖一个很窄的容器协议;精确的容器身份、有限制的 exec,以及出错就停的操作仍然由 manager 负责。manager 原有的网络入口都转交给这个组件处理。网络快照仍只在一次操作内有效,运维日志的名称保持不变。chat_sandbox.py 管理保留的沙箱组和组内成员;容器和进程之间的边界归 sandbox.py。原生会话和工作区操作由 Sandbox Manager 和会话 supervisor 共同协调。supervisor 的元数据锁只保护内存里的归属关系。停止、孤儿恢复和归档删除在动 Docker 或读写标记文件之前,会先占住这一个沙箱组的生命周期;拿到结果后,先重新核对组、成员、容器和生命周期的身份,核对通过才采纳。其他沙箱组不受影响。核对失败会释放临时占用,但持久的归档屏障和原生刷新屏障依然有效。原来通过 chat_sandbox 导入公开沙箱模型的写法照样可用。

运行时适配器是 cloud_agent_host/runtimes/ 下的一个包:

模块

职责

base.py

适配器契约、能力声明、MCP 配置和运行时错误

events.py

统一的事件类型,以及控制数据大小的辅助函数

preview.py

把部分回复攒成批次做预览

claude.py, codex.py, pi.py, opencode.py, kilo.py

某一种 CLI 的命令格式和事件解析

generic.py

通用的按行解析的适配器

registry.py

查找和创建适配器

已有的调用方都通过 cloud_agent_host.runtimes 使用适配器,这个包会显式导出公开 API。运行时的发现信息放在不可变的 cloud_agent_shared/runtime_catalog.py 里,所以控制平面不用加载主机端的适配器,也能对外公布运行时 ID、是否支持 MCP 和可选的推理强度。运行时的版本固定仍在 cloud_agent_shared/shared_protocol.py 和镜像文件里。

共用的操作性能剖析

产生剖析数据的代码照旧导入 cloud_agent_shared.operation_profiling。具体实现放在 cloud_agent_shared/profiling/ 下,按 schema、模型、运行解析、策略、测量采集、阶段观察器、进程上下文、导出和时钟访问分成几个模块。详见这个包的职责与契约说明。

门面保留了原有的 API 和运维诊断功能。时钟访问会用它那个可替换的 time 对象,所以测试结果仍然确定,默认关闭的路径也不受影响。新的剖析逻辑放进对应的模块里,不要往门面里堆。

跟着一次浏览器操作走

Next.js 的路由文件在 apps/frontend/src/app/ 下。各功能共用的界面在 components/features/ 下:agents 是任务和对话,settings 是设置页面,另外还有 teams、profile 和 auth。可复用的展示组件在 components/ 下,浏览器端的工具函数和 API 客户端在 lib/ 下。只能在服务端运行的集成放在现有的服务端模块里;浏览器组件的导入链里绝不能带进服务端凭据。

lib/api/agents.ts 是显式的公开 API 门面,具体工作交给 lib/api/agents/ 下各管一摊的模块:任务、对话、会话、目标、配置、集成、资源和用量查询、工作区文件、终端和事件流。创建和 Fork 共用的幂等响应处理只在这个包内部使用。端点路径、重试、凭据、游标处理和通知,都跟着它所属的那个操作放。

任务页面由 components/features/agents/JobDetail.tsx 组装而成,拆出去的实现都在 job-detail/ 下:

模块

职责

JobDetailHeader

页面顶部和对话相关操作

JobTranscript

对话记录的呈现和导航

JobComposer, useJobComposerSettings

消息输入框和输入设置

RunOutcome, GitPanel

运行结果、发布和 Git 交互

JobDetailSidePanels, LazyPanels

工作区和检查器面板的渲染与延迟加载

useJobDetailPanels

面板的选择、焦点、尺寸调整,以及保留的面板状态

useJobConversation

轮次导航、对话记录滚动和阅读位置

useJobRunActions

停止、Fork 和恢复操作,带防重复请求的保护

状态放在负责它的那一块里。保留的面板在隐藏时也会留住终端、文件预览和草稿;切换作用范围时,必须把相关状态重置掉。拆组件时,hook 的依赖要写明,延迟加载也要保留。

加功能时别把结构弄乱

  1. 对照上面的几张表,确定这项行为归哪个服务、哪一块负责。

  2. 领域规则、传输模型和 port,确实需要时才新增或修改。持久化操作在 storage 里实现。

  3. 编排逻辑放在应用流程或负责它的服务里,再由 HTTP 适配器或 worker 去调用。新的资源通过 bootstrap 接入。

  4. 门面的公开导出要想清楚再加。包内部直接导入实现模块,这样依赖关系一目了然。

  5. 测试要覆盖外部可观察的行为和受影响的边界。做结构性重构时,比对前后的契约,并复用已有的行为测试。

  6. 跑完相关的完整检查;如果职责归属、命令或行为变了,就更新对应的维护中指南。

后端的架构检查会确保:服务镜像彼此独立、API 的导入方向正确、domain 和 port 不依赖其他层、repository 被隔离。后端导入必须用安装后的包名(cloud_agent、cloud_agent_host 和 cloud_agent_shared),不能用仓库里的命名空间 apps.backend。主机镜像会把这些包展平,带仓库前缀的导入在本地也许能用,部署后就会失败,所以三个包里都会拦下这种写法。导入扫描会覆盖相对导入、函数内部的导入和 TYPE_CHECKING 块。后端的行为测试放在 tests/ 下;前端测试和它测试的功能或工具函数放在一起。