Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

运行时总体架构

这一章解释 backend/ 里“谁负责什么”,以及一次运行到底由哪些对象拼出来。

1. 启动时会装配哪些核心组件

build_application(应用装配入口)在启动时会把下面这些组件接起来:

代码对象当前职责
AppConfig(进程级配置)读取 HTTP 监听地址、存储根目录、包目录、运行时配置、集群模式、默认命名空间、默认服务账号、挂载白名单、Job TTL
FileBackedStore(文件型状态存储)维护 state.jsonuploads/workspaces/sessions/,保存 sessionartifactmount grantrun
PackageCatalog(Agent 包目录索引)backend/packages/* 加载成内存里的 LoadedPackage(已加载 Agent 包)
RuntimeProfileCatalog(运行时模板目录索引)backend/config/runtime_profiles.yaml 加载成内存里的 RuntimeProfile
K3sOrchestrator(K3s 编排器)渲染和可选提交 ConfigMapSecretJob,并观察 K8s 状态
ApplicationService(应用服务层)串起 API、状态存储、工作区准备、K8s 提交、状态刷新、多轮会话同步
Router(HTTP 路由)对外暴露 /api/v1/* 接口

这意味着当前后端是一个“单进程控制面 + K3s 执行面”的结构:

  • API 请求先进入 Router(HTTP 路由)
  • RouterApplicationService(应用服务层)
  • ApplicationService 同时依赖本地持久化状态和 K3sOrchestrator(K3s 编排器)
  • 真正执行 agentcore 的单元是被后端提交出去的 K3s Job

2. 当前系统里最重要的几个对象

2.1 SessionRecord(逻辑会话记录)

SessionRecord 表示“同一用户的一段逻辑会话”。核心字段包括:

  • user_id
  • default_package
  • status
  • history
  • latest_run_id
  • closed_at

它的职责是保存多轮对话时间线,容器实例由后续的 run 单独创建。

2.2 ArtifactRecord(上传文件记录)

ArtifactRecord 表示用户上传到 backend 的文件元数据。文件本体会先落到 uploads/,后续在创建 run 时复制进当前 run 的工作区。

2.3 MountGrantRecord(宿主机挂载授权记录)

MountGrantRecord 表示“允许某个用户把某个 backend 所在主机目录挂进容器”。它是显式授权对象,包含:

  • host_path
  • mount_path
  • access_mode

2.4 RunRecord(一次运行记录)

RunRecord 描述一次真实运行,包含:

  • 所属会话 session_id
  • 使用的包 package_name
  • 使用的运行时模板 runtime_profile
  • 工作区模式 workspace_mode
  • 输入文件 artifact_ids
  • 可选挂载授权 mount_grant_id
  • provider 环境变量键名 provider_env_keys
  • 工作区根目录 workspace_root
  • 运行状态 status
  • K8s 资源引用 resources
  • 渲染结果 rendered

当前物理隔离单元就是 RunRecord

2.5 RuntimeProfile(运行时配置模板)

RuntimeProfile 决定一次 run 的基础运行时底座,主要包括:

  • 基础镜像 image
  • 容器入口 executable
  • 安装策略 installStrategy
  • 执行模式 executionMode
  • 默认模型和默认 Base URL
  • namespace、service account
  • CPU、内存、临时存储的 requests / limits

从运行时设计上看,RuntimeProfile 表达的是“怎样启动一个 agentcore 容器”,并不绑定实现语言。

2.6 AgentPackageManifest(Agent 包清单)

AgentPackageManifest 定义 agent package 本身的业务层意图,主要包括:

  • runtime
  • entry
  • policy
  • assets

当前包目录示例位于:

  • backend/packages/codex-reviewer/
  • backend/packages/claude-code-builder/

这些目录名是样例包名,架构层仍统一按 agent package 理解。

3. agentcoreagent package 当前怎样组合

3.1 agentcore 在当前代码里对应什么

如果把 agentcore 理解为“真正提供执行底座的运行时”,那在当前代码里它主要对应三部分:

  • RuntimeProfile(运行时配置模板)
  • 运行时镜像 image
  • 容器入口 executable

它们以共享模板的方式存在。

3.2 agent package 在当前代码里对应什么

如果把 agent package 理解为“开发者提供的提示词、工具声明、环境事实和策略”,那它对应:

  • agent.yaml
  • prompt/system.md
  • build_in_tools.yaml
  • env/env_facts.yaml
  • 其它包内文本文件

这些文件在运行时由后端加载成 LoadedPackage(已加载 Agent 包),再通过 ConfigMap 注入 Pod 内的 /opt/agent/package

3.3 两者的绑定时机

LaunchPlan(一次运行的不可变启动计划)是 agentcoreagent package 真正结合的地方。它同时持有:

  • package
  • runtime_profile
  • workspace_mode
  • provider_env
  • mount_grant
  • workspace_host_path
  • namespace
  • service_account

随后 K3sOrchestrator 会把这个 LaunchPlan 渲染成三类资源:

  1. ConfigMap
  2. Secret
  3. Job

3.4 当前组合模式的结论

结论很明确:

  • 共享的是 RuntimeProfile 和基础镜像
  • 独立的是每次运行的 Pod、工作区、ConfigMapSecret
  • 一次 run 会把“共享运行时模板 + 当前 agent package + 当前工作区 + 当前 provider 凭据”拼成一个独立运行单元

4. 当前隔离模型与连续性模型

4.1 逻辑隔离按 session

SessionRecord 保存:

  • 历史消息
  • 默认包
  • 会话状态
  • 最近一次 run

它定义的是“同一段对话时间线”。

4.2 物理隔离按 run

每次 create_run(创建运行)都会创建:

  • 一个独立工作区目录
  • 一个独立 ConfigMap
  • 一个可选独立 Secret
  • 一个独立 K3s Job
  • 一个独立 Pod

这意味着当前模型是:

  • 逻辑连续性按 session
  • 物理执行隔离按 run

4.3 会话连续性今天靠什么保持

当前代码里的连续性主要来自四块持久化数据:

  1. SessionRecord.history(会话消息历史)
  2. ArtifactRecord(上传文件记录)和 uploads/ 下的原始文件
  3. RunRecord(运行记录)和 workspaces/<run-id>/out/ 下的结果文件
  4. SessionRecord.runtime_statesessions/<session-id>/ 下的 session 级运行时状态目录

ApplicationService::sync_session_conversation_history(同步会话历史)会在新 run 开始前刷新该 session 下旧 run 的状态;ApplicationService::sync_run_assistant_message(同步助手回答)会把已完成 run 的 final-answer.md 回填进 SessionRecord.history。同时,create_session 会先创建 sessions/<session-id>/sync_session_runtime_state 会扫描其中的 checkpoints/ 并把最近 checkpoint 摘要写回 SessionRecord.runtime_state。这样同一 session 的下一轮既能拿到上一轮回答,也能拿到最近一次会话级状态摘要。

4.4 agentcore 的 checkpoint 连续性现在怎样接入

当前代码已经把 session 级状态目录接进了运行链路,具体模式是:

  • 为每个 session 分配 sessions/<session-id>/ 宿主机目录
  • 每次 run 都把这个目录挂到容器内固定路径 /workspace/session
  • 约定 checkpoint 默认写到 /workspace/session/checkpoints
  • write_run_context 会把 sessionRuntimeState 写入 /workspace/in/run-context.json
  • job_builder 还会注入 AGENT_SESSION_STATE_ROOTAGENT_CHECKPOINT_ROOT

当前代码已经具备:

  • run 级工作区持久化
  • session / run 元数据持久化
  • session 级持久化状态目录
  • 最近 checkpoint 摘要扫描
  • reopen 同一 session

当前代码还没有显式的:

  • restore / resume API
  • checkpoint retention / GC 策略
  • 跨节点可迁移的卷抽象
  • 输出文件自动提升为下一轮输入的规则

这部分会在“当前实现边界”章节里继续说明。

5. K8s Rust 调用在当前架构中的位置

这一节专门把代码和功能对应起来。

5.1 backend/crates/app/src/k8s/mod.rs 负责 backend 适配

当前 backend/crates/app/src/k8s/mod.rs 已经不再直接持有 kube 细节,而是做 backend 到独立 crate 的映射:

  • AppConfig -> K3sOrchestratorConfig
  • LoadedPackage -> AgentPackageSpec
  • RuntimeProfile -> RuntimeProfileSpec
  • MountGrantRecord -> MountGrantSpec
  • crate 返回的 RenderedResources / ClusterStatusSnapshot / ObservedRunStatus -> backend 领域对象

5.2 backend/crates/k3s-runtime/src/lib.rs 负责真正访问集群

K3sOrchestrator::new(K3s 编排器初始化)通过 Client::try_default() 创建 kube 客户端,并调用:

  • apply::ensure_cluster_baseline:确保 namespace 与 service account 的基础存在

K3sOrchestrator::submit_run(提交运行)负责:

  • naming.rs 生成稳定资源名
  • package_bundle.rs 构造 ConfigMapSecret
  • volumes.rs 生成卷布局
  • workload_job.rs 构造 Job
  • apply 模式下由 apply.rs 调用 Api<ConfigMap>::create
  • 调用 Api<Secret>::create
  • 调用 Api<Job>::create

K3sOrchestrator::inspect_run_status(检查运行状态)负责:

  • Api<Job>::get_opt 读取 Job 状态
  • Api<Pod>::list 读取同一 Job 对应 Pod 的状态
  • 把 Job / Pod 观察结果映射成 crate 内部 ObservedRunStatus

K3sOrchestrator::cluster_status(读取集群状态)负责:

  • Api<Node>::list 读取节点
  • 汇总可调度节点数、taint、磁盘压力和阻塞原因

K3sOrchestrator::cancel_run(取消运行)负责:

  • Api<Job>::delete
  • Api<ConfigMap>::delete
  • Api<Secret>::delete

workload_job.rs(Job 工作负载渲染模块)会把下面这些设置落到资源定义里:

  • /workspace 工作区挂载
  • /opt/agent/package 包目录挂载
  • 可选的用户授权挂载
  • allowPrivilegeEscalation: false
  • capabilities.drop = ["ALL"]
  • runAsNonRoot = true
  • fsGroup = 1000
  • seccompProfile = RuntimeDefault
  • ttl_seconds_after_finished

5.3 backend/crates/app/src/service.rs 负责把业务对象和 K8s 编排串起来

ApplicationService::create_run(创建运行)是控制流入口,它会:

  1. 校验 session
  2. 解析 package
  3. 解析 RuntimeProfile
  4. 合并 provider 配置
  5. 校验输入文件和挂载
  6. 做集群预检
  7. 同步历史回答
  8. 追加本轮用户消息
  9. 创建工作区
  10. run-context.json
  11. 组装 LaunchPlan
  12. K3sOrchestrator::submit_run
  13. 持久化 RunRecord

5.4 K3s create 链路的技术细节

如果把“一次 run 真正怎样进入 K3s”拆成更细的阶段,当前链路是:

  1. ApplicationService::create_run 先把业务输入收敛成 LaunchPlan
  2. K3sOrchestrator::submit_runjob_builder::build_bundle
  3. build_bundle 生成三类资源名:
    • agent-run-<run-id>Job
    • agent-pkg-<run-id>:承载 package 文件的 ConfigMap
    • agent-env-<run-id>:承载 provider 凭据的 Secret
  4. backend 在 apply 模式下按顺序调用 Kubernetes API:
    • Api<ConfigMap>::create
    • Api<Secret>::create
    • Api<Job>::create
  5. 注意:backend 不会直接创建 Pod
  6. Job 创建成功后,真正创建 Pod 的是 Kubernetes Job Controller
  7. Pod 被调度到某个节点后,由该节点上的 kubelet / container runtime 完成:
    • 拉取镜像
    • 准备 hostPath
    • 准备 ConfigMap volume
    • 注入 Secret 环境变量
    • 启动容器进程

这意味着当前 create 过程实际上分成两层:

  • backend 负责创建声明式资源对象
  • K3s 控制器和 kubelet 负责把这些对象收敛成真正运行中的 Pod

5.5 当前 create 路径里哪些地方最关键

当前实现里,create 成功与否最依赖下面几类资源:

  • ConfigMap:承载 agent package 文本资产
  • Secret:承载 provider API key 等敏感配置
  • Job:承载本轮执行的调度和生命周期
  • hostPath:承载 /workspace/workspace/session

其中有一个很关键的事实:

  • 当前的业务状态主要不在容器镜像里
  • 而在 /workspace/workspace/session 这类挂载卷里

所以对当前架构来说,卷准备成功与否比镜像本身是否可拉取同样关键

5.6 当前 create 路径的边界与 TODO

今天这条 create 路径已经能稳定工作,但仍有几个明显边界:

  • 当前使用的是 create,不是 server-side apply
  • ConfigMap / Secret / Job 之间还没有显式 ownerReferences
  • 如果 ConfigMap 创建成功而 Job 创建失败,可能留下部分残留资源
  • 还没有为 create 失败补一层统一回滚
  • 还没有把 warm sandbox 模式下的 Deployment / StatefulSet 纳入同一编排器

建议保留为后续 TODO:

  • TODO:为 ConfigMap / Secret / Job 增加更清晰的 owner / GC 关系
  • TODO:为 create 失败增加统一清理逻辑
  • TODO:评估 server-side apply 或 patch 语义,降低重复创建和并发创建问题
  • TODO:在需要交互式长驻 worker 时,把 Deployment / StatefulSet 纳入统一渲染层

5.7 K3s 下 Pod 如何访问宿主机上的数据库服务

如果这里说的“宿主机数据库”指的是:

  • 宿主机进程里运行的 PostgreSQL
  • 宿主机进程里运行的 MySQL
  • 宿主机进程里运行的 Redis

那么在 K3s 里,Pod 的正确访问方式是:

  • 通过网络连数据库服务
  • 而不是把数据库的数据目录 mount 给业务 Pod

这里有一个关键前提:

  • 如果数据库只监听宿主机的 127.0.0.1
  • 那 Pod 通常不能直接访问

因为 Pod 里的 127.0.0.1 是 Pod 自己的网络命名空间,不是宿主机回环地址。

因此如果数据库仍然部署在宿主机,至少要满足:

  1. 数据库监听在宿主机可路由的地址上,例如节点 InternalIP 或受控网卡地址
  2. 业务 Pod 通过 TCP 访问这个地址和端口
  3. 凭据通过 Secret 注入
  4. 如有需要,再补防火墙、TLS 或 NetworkPolicy

在 K3s 场景下,更推荐的接入顺序是:

  1. 最佳:把数据库容器化并放进集群内,通过 Service 暴露
  2. 次优:数据库继续跑在宿主机,但在集群内创建一个无 selector 的 Service,再配套 Endpoints / EndpointSlice 指向宿主机 IP 和端口
  3. 仅开发期可接受:直接把宿主机 IP:port 作为外部地址写入配置

这样做的意义是:

  • Pod 里仍然使用稳定的服务名
  • 后续数据库从“宿主机进程”迁移到“集群内服务”时,业务配置改动更小
  • 不会把业务恢复语义错误地绑定到宿主机文件系统

如果未来是多节点 K3s,还要额外注意:

  • “宿主机数据库”通常只在某一个节点上
  • 这会天然带来节点耦合
  • 如果 workload 会漂移到其它节点,就需要稳定路由,或者干脆把数据库升级为真正的集群内/集群外标准服务

5.7.1 推荐的实现技术

如果数据库还是宿主机进程,当前更推荐的技术组合是:

  • Service:给 Pod 一个稳定服务名
  • 手工 Endpoints / EndpointSlice:把服务名转发到宿主机 IP:port
  • Secret:注入账号密码、DSN、TLS 材料
  • 应用侧数据库客户端库:真正发起 PostgreSQL / MySQL / Redis 协议请求

不建议默认采用:

  • hostNetwork: true
  • 直接依赖宿主机 localhost
  • 直接共享数据库数据目录

原因是这三种方式都会让业务 Pod 和宿主机实现强耦合。

5.8 为什么不建议让 Pod 直接访问宿主机数据库数据目录

即使数据库物理上就在宿主机本地,也不建议:

  • 把 Postgres 数据目录 mount 给业务 Pod
  • 把 MySQL 数据目录 mount 给业务 Pod
  • 让业务容器直接读写 Redis 的持久化文件目录

原因是:

  • 数据库的数据一致性由数据库进程维护,而不是由业务 Pod 维护
  • 业务 Pod 直接碰底层数据文件,容易破坏 WAL、锁和恢复语义
  • 这类设计几乎无法自然迁移到多节点和生产环境

也就是说,K3s 里“访问宿主机数据库”的正确语义是:

  • 访问宿主机上的数据库服务

而不是:

  • 访问宿主机上的数据库数据目录

5.9 K3s 能不能把当前 Pod 的整个状态打包成镜像

如果问题是“能不能把当前正在运行的 Pod 固定下来,之后再直接加载回来”,那么标准 K3s / Kubernetes 语义下,答案是:

  • 不能把 Pod 的完整可恢复状态标准化地打包成镜像

原因很直接:

  1. Kubernetes 没有标准 API 把“运行中的 Pod 全状态”导出成可移植镜像
  2. 镜像层只表达容器根文件系统,不表达:
    • 进程内存
    • 打开的连接
    • 当前执行位置
    • Secret / ConfigMap / PVC / hostPath 这类运行时挂载关系
  3. 当前真正关键的状态本来就在外部:
    • /workspace
    • /workspace/session
    • checkpoint 文件
    • checkpoint 数据库

所以如果你们说的“固定目前状态”只包含:

  • 已安装软件
  • 运行时依赖
  • CLI
  • 通用缓存

那可以理解为:

  • 把容器根文件系统预热后做成新镜像

但这仍然不是“恢复 Pod 全状态”,而只是“固化 image layer”。

5.10 如果只想固定文件系统产物,K3s 下更合理的做法是什么

如果目标是减少下一次启动时的安装成本,更合理的做法是:

  1. 保持基础镜像不可变
  2. 单独做“预热镜像”流水线,把软件和依赖 bake 进新镜像
  3. workspace、session state、checkpoint 继续放在卷或外部状态存储里
  4. 下次启动时使用新镜像,再重新挂载这些状态

换句话说:

  • 可以固定 image layer
  • 不能指望镜像替代 state layer

因此当前平台更合理的标准模型仍然是:

  • 新 Pod
  • 新镜像或预热镜像
  • 重新挂载状态卷
  • 从 checkpoint 或 session state 恢复

6. 当前样例 runtime profile 如何理解

runtime_profiles.yaml 里当前有多组样例 profile。它们在代码里沿用了历史命名,例如:

  • codex-standard
  • claude-code-standard
  • codex-cli-standard
  • claude-code-cli-standard

这些名字只代表当前仓库里已有的样例配置。架构层需要关注的是下面这些字段:

  • image
  • executable
  • installStrategy
  • executionMode
  • requests
  • limits

因此如果后续接入新的 Rust agentcore、Python agentcore 或其它容器入口,后端运行时模型本身不需要变化,变化点主要是:

  • 基础镜像
  • 容器入口
  • checkpoint 目录约定
  • 执行模式适配器

7. backend 与 agentcore 的责任边界

当前更合理的责任边界是:

  • agentcore 负责自己的执行逻辑、checkpoint、工作目录状态、工具调用和恢复逻辑
  • agent package 负责提示词、工具声明、环境事实和策略
  • backend 负责保存 sessionrun、上传文件、工作区目录、K8s 资源引用、结果文件索引以及 reopen/resume 入口

这条边界与 agentcore 的实现语言无关。