运行时总体架构
这一章解释 backend/ 里“谁负责什么”,以及一次运行到底由哪些对象拼出来。
1. 启动时会装配哪些核心组件
build_application(应用装配入口)在启动时会把下面这些组件接起来:
| 代码对象 | 当前职责 |
|---|---|
AppConfig(进程级配置) | 读取 HTTP 监听地址、存储根目录、包目录、运行时配置、集群模式、默认命名空间、默认服务账号、挂载白名单、Job TTL |
FileBackedStore(文件型状态存储) | 维护 state.json、uploads/、workspaces/、sessions/,保存 session、artifact、mount grant、run |
PackageCatalog(Agent 包目录索引) | 把 backend/packages/* 加载成内存里的 LoadedPackage(已加载 Agent 包) |
RuntimeProfileCatalog(运行时模板目录索引) | 把 backend/config/runtime_profiles.yaml 加载成内存里的 RuntimeProfile |
K3sOrchestrator(K3s 编排器) | 渲染和可选提交 ConfigMap、Secret、Job,并观察 K8s 状态 |
ApplicationService(应用服务层) | 串起 API、状态存储、工作区准备、K8s 提交、状态刷新、多轮会话同步 |
Router(HTTP 路由) | 对外暴露 /api/v1/* 接口 |
这意味着当前后端是一个“单进程控制面 + K3s 执行面”的结构:
- API 请求先进入
Router(HTTP 路由) Router调ApplicationService(应用服务层)ApplicationService同时依赖本地持久化状态和K3sOrchestrator(K3s 编排器)- 真正执行
agentcore的单元是被后端提交出去的 K3sJob
2. 当前系统里最重要的几个对象
2.1 SessionRecord(逻辑会话记录)
SessionRecord 表示“同一用户的一段逻辑会话”。核心字段包括:
user_iddefault_packagestatushistorylatest_run_idclosed_at
它的职责是保存多轮对话时间线,容器实例由后续的 run 单独创建。
2.2 ArtifactRecord(上传文件记录)
ArtifactRecord 表示用户上传到 backend 的文件元数据。文件本体会先落到 uploads/,后续在创建 run 时复制进当前 run 的工作区。
2.3 MountGrantRecord(宿主机挂载授权记录)
MountGrantRecord 表示“允许某个用户把某个 backend 所在主机目录挂进容器”。它是显式授权对象,包含:
host_pathmount_pathaccess_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 本身的业务层意图,主要包括:
runtimeentrypolicyassets
当前包目录示例位于:
backend/packages/codex-reviewer/backend/packages/claude-code-builder/
这些目录名是样例包名,架构层仍统一按 agent package 理解。
3. agentcore 和 agent package 当前怎样组合
3.1 agentcore 在当前代码里对应什么
如果把 agentcore 理解为“真正提供执行底座的运行时”,那在当前代码里它主要对应三部分:
RuntimeProfile(运行时配置模板)- 运行时镜像
image - 容器入口
executable
它们以共享模板的方式存在。
3.2 agent package 在当前代码里对应什么
如果把 agent package 理解为“开发者提供的提示词、工具声明、环境事实和策略”,那它对应:
agent.yamlprompt/system.mdbuild_in_tools.yamlenv/env_facts.yaml- 其它包内文本文件
这些文件在运行时由后端加载成 LoadedPackage(已加载 Agent 包),再通过 ConfigMap 注入 Pod 内的 /opt/agent/package。
3.3 两者的绑定时机
LaunchPlan(一次运行的不可变启动计划)是 agentcore 和 agent package 真正结合的地方。它同时持有:
packageruntime_profileworkspace_modeprovider_envmount_grantworkspace_host_pathnamespaceservice_account
随后 K3sOrchestrator 会把这个 LaunchPlan 渲染成三类资源:
ConfigMapSecretJob
3.4 当前组合模式的结论
结论很明确:
- 共享的是
RuntimeProfile和基础镜像 - 独立的是每次运行的 Pod、工作区、
ConfigMap、Secret - 一次 run 会把“共享运行时模板 + 当前
agent package+ 当前工作区 + 当前 provider 凭据”拼成一个独立运行单元
4. 当前隔离模型与连续性模型
4.1 逻辑隔离按 session
SessionRecord 保存:
- 历史消息
- 默认包
- 会话状态
- 最近一次 run
它定义的是“同一段对话时间线”。
4.2 物理隔离按 run
每次 create_run(创建运行)都会创建:
- 一个独立工作区目录
- 一个独立
ConfigMap - 一个可选独立
Secret - 一个独立 K3s
Job - 一个独立 Pod
这意味着当前模型是:
- 逻辑连续性按
session - 物理执行隔离按
run
4.3 会话连续性今天靠什么保持
当前代码里的连续性主要来自四块持久化数据:
SessionRecord.history(会话消息历史)ArtifactRecord(上传文件记录)和uploads/下的原始文件RunRecord(运行记录)和workspaces/<run-id>/out/下的结果文件SessionRecord.runtime_state和sessions/<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.jsonjob_builder还会注入AGENT_SESSION_STATE_ROOT和AGENT_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->K3sOrchestratorConfigLoadedPackage->AgentPackageSpecRuntimeProfile->RuntimeProfileSpecMountGrantRecord->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构造ConfigMap、Secret - 调
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>::deleteApi<ConfigMap>::deleteApi<Secret>::delete
workload_job.rs(Job 工作负载渲染模块)会把下面这些设置落到资源定义里:
/workspace工作区挂载/opt/agent/package包目录挂载- 可选的用户授权挂载
allowPrivilegeEscalation: falsecapabilities.drop = ["ALL"]runAsNonRoot = truefsGroup = 1000seccompProfile = RuntimeDefaultttl_seconds_after_finished
5.3 backend/crates/app/src/service.rs 负责把业务对象和 K8s 编排串起来
ApplicationService::create_run(创建运行)是控制流入口,它会:
- 校验 session
- 解析 package
- 解析
RuntimeProfile - 合并 provider 配置
- 校验输入文件和挂载
- 做集群预检
- 同步历史回答
- 追加本轮用户消息
- 创建工作区
- 写
run-context.json - 组装
LaunchPlan - 调
K3sOrchestrator::submit_run - 持久化
RunRecord
5.4 K3s create 链路的技术细节
如果把“一次 run 真正怎样进入 K3s”拆成更细的阶段,当前链路是:
ApplicationService::create_run先把业务输入收敛成LaunchPlanK3sOrchestrator::submit_run调job_builder::build_bundlebuild_bundle生成三类资源名:agent-run-<run-id>:Jobagent-pkg-<run-id>:承载 package 文件的ConfigMapagent-env-<run-id>:承载 provider 凭据的Secret
- backend 在
apply模式下按顺序调用 Kubernetes API:Api<ConfigMap>::createApi<Secret>::createApi<Job>::create
- 注意:backend 不会直接创建 Pod
Job创建成功后,真正创建 Pod 的是 KubernetesJob Controller- Pod 被调度到某个节点后,由该节点上的 kubelet / container runtime 完成:
- 拉取镜像
- 准备
hostPath - 准备
ConfigMapvolume - 注入
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 自己的网络命名空间,不是宿主机回环地址。
因此如果数据库仍然部署在宿主机,至少要满足:
- 数据库监听在宿主机可路由的地址上,例如节点
InternalIP或受控网卡地址 - 业务 Pod 通过 TCP 访问这个地址和端口
- 凭据通过
Secret注入 - 如有需要,再补防火墙、TLS 或
NetworkPolicy
在 K3s 场景下,更推荐的接入顺序是:
- 最佳:把数据库容器化并放进集群内,通过
Service暴露 - 次优:数据库继续跑在宿主机,但在集群内创建一个无 selector 的
Service,再配套Endpoints/EndpointSlice指向宿主机 IP 和端口 - 仅开发期可接受:直接把宿主机 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 的完整可恢复状态标准化地打包成镜像
原因很直接:
- Kubernetes 没有标准 API 把“运行中的 Pod 全状态”导出成可移植镜像
- 镜像层只表达容器根文件系统,不表达:
- 进程内存
- 打开的连接
- 当前执行位置
- Secret / ConfigMap / PVC /
hostPath这类运行时挂载关系
- 当前真正关键的状态本来就在外部:
/workspace/workspace/session- checkpoint 文件
- checkpoint 数据库
所以如果你们说的“固定目前状态”只包含:
- 已安装软件
- 运行时依赖
- CLI
- 通用缓存
那可以理解为:
- 把容器根文件系统预热后做成新镜像
但这仍然不是“恢复 Pod 全状态”,而只是“固化 image layer”。
5.10 如果只想固定文件系统产物,K3s 下更合理的做法是什么
如果目标是减少下一次启动时的安装成本,更合理的做法是:
- 保持基础镜像不可变
- 单独做“预热镜像”流水线,把软件和依赖 bake 进新镜像
- workspace、session state、checkpoint 继续放在卷或外部状态存储里
- 下次启动时使用新镜像,再重新挂载这些状态
换句话说:
- 可以固定 image layer
- 不能指望镜像替代 state layer
因此当前平台更合理的标准模型仍然是:
- 新 Pod
- 新镜像或预热镜像
- 重新挂载状态卷
- 从 checkpoint 或 session state 恢复
6. 当前样例 runtime profile 如何理解
runtime_profiles.yaml 里当前有多组样例 profile。它们在代码里沿用了历史命名,例如:
codex-standardclaude-code-standardcodex-cli-standardclaude-code-cli-standard
这些名字只代表当前仓库里已有的样例配置。架构层需要关注的是下面这些字段:
imageexecutableinstallStrategyexecutionModerequestslimits
因此如果后续接入新的 Rust agentcore、Python agentcore 或其它容器入口,后端运行时模型本身不需要变化,变化点主要是:
- 基础镜像
- 容器入口
- checkpoint 目录约定
- 执行模式适配器
7. backend 与 agentcore 的责任边界
当前更合理的责任边界是:
agentcore负责自己的执行逻辑、checkpoint、工作目录状态、工具调用和恢复逻辑agent package负责提示词、工具声明、环境事实和策略- backend 负责保存
session、run、上传文件、工作区目录、K8s 资源引用、结果文件索引以及 reopen/resume 入口
这条边界与 agentcore 的实现语言无关。