Agent Store Backend 运行时文档
本文档基于当前 backend/ 源码实现整理,目标是把“今天这套代码到底怎么运行、怎么编排、怎么隔离、怎样保持会话连续性、哪些地方已经实现、哪些地方还需要补齐”说清楚。
文档目标
这本书重点说明以下问题:
agent-store-sandbox-backend(云端运行时编排后端)如何接收session(会话)、artifact(上传文件)、mount grant(挂载授权)和run(一次运行)agentcore(Agent 核心运行时)与agent package(开发者提供的 Agent 包)如何组合成一次真实运行- 当前隔离粒度到底是“一个用户一个环境”“一个会话一个环境”还是“一个运行一个环境”
- 容器与宿主机、用户上传文件、本地 Helper 之间如何交换文件
- 一次运行从 API 请求到 K3s
Job(Kubernetes 批处理任务)结束的完整生命周期是什么 - 同一
session(会话)内的历史、结果文件和可恢复状态如何保持连续 - 当前代码里哪些能力已经落地,哪些能力还属于下一阶段演进
文档中的统一术语
本文档统一使用下面这组术语:
agentcore(Agent 核心运行时):负责执行推理、文件操作、状态恢复、checkpoint、工具调用的运行时执行器。它可以是 Rust 二进制,也可以是镜像内的其它可执行入口。agent package(Agent 包):开发者提供的提示词、工具声明、环境事实、策略和附带资源。RuntimeProfile(运行时配置模板):描述基础镜像、容器入口、资源限制、安装策略和执行模式的模板。SessionRecord(逻辑会话记录):保存同一会话的历史消息、默认包、状态和最近一次运行引用。RunRecord(一次运行记录):保存一次真实运行的工作区、状态、K8s 资源引用和结果索引。
当前代码里的 RuntimeFamily::Codex、RuntimeFamily::ClaudeCode、codex-standard、claude-code-standard 这些名字仍会出现在源码和配置里。本文档把它们统一视为样例 runtime family / profile 标识符。从运行时架构角度,真正决定行为的是:
- 基础镜像
image - 容器入口
executable - 安装策略
installStrategy - 执行模式
executionMode - 工作区挂载与资源限制
也就是说,架构并不依赖 agentcore 的实现语言,差异主要体现在基础镜像和容器入口。
一句话结论
当前实现的物理隔离单元是一次 RunRecord(一次运行记录),它会被渲染成一个独立的 K3s Job 和一个独立 Pod;SessionRecord(逻辑会话记录)负责保存多轮历史和会话状态。
当前代码已经保证:
- 同一
session内的对话历史连续 - 已上传文件和历史结果文件长期保留
- 用户关闭前端或关闭 session 后,后续仍可重新打开同一
session
当前代码已经把同一 session 的持久化运行时状态目录挂到每轮 run 的 /workspace/session,并把 checkpoint 摘要同步回 SessionRecord.runtime_state;但还没有独立的 restore / resume API、跨节点存储抽象和统一 GC 策略。
必须先澄清的三个事实
1. 当前按 run 创建独立容器
同一个 SessionRecord(逻辑会话记录)会跨越多次 create_run(创建运行)。每一轮都会新建一个独立工作区、独立 Job、独立 Pod。前一轮结束后继续复用的是:
SessionRecord.history(会话消息历史)ArtifactRecord(上传文件记录)- 已落盘的历史结果文件
- 选定的
agent package - 选定的
RuntimeProfile
2. 当前采用“共享运行时模板 + 按 run 注入 package”的模式
RuntimeProfile(运行时配置模板)提供共享的运行时底座;AgentPackageManifest(Agent 包清单)提供本次运行要注入的业务层资源;LaunchPlan(启动计划)把两者与当前工作区、当前 provider 凭据、当前挂载授权拼成一次独立运行。
3. 当前会话连续性主要来自持久化状态
当前代码里,会话连续性主要来自:
SessionRecord.historySessionRecord.latest_run_idSessionRecord.runtime_stateArtifactRecordRunRecordbackend/data/workspaces/<run-id>/out/下的结果文件backend/data/sessions/<session-id>/下的 session 级运行时状态目录
如果 agentcore 自带 checkpoint,当前推荐始终写到 /workspace/session/checkpoints;backend 已会扫描最近 checkpoint 摘要,但还没有把“恢复到哪个 checkpoint”做成独立 API。
代码入口
如果要从源码反向阅读整个流程,建议按下面顺序看:
backend/crates/app/src/main.rsbackend/crates/app/src/lib.rsbackend/crates/app/src/api/mod.rsbackend/crates/app/src/service.rsbackend/crates/app/src/k8s/mod.rsbackend/crates/k3s-runtime/src/lib.rsbackend/crates/app/src/storage/mod.rsbackend/crates/app/src/provider.rsbackend/crates/app/src/package/mod.rs
其中有两个 K8s 相关入口需要先记住:
backend/crates/app/src/k8s/mod.rs:当前是 backend 适配层,负责把 backend 领域对象转换成独立 K3s crate 的输入输出。backend/crates/k3s-runtime/src/lib.rs:当前是独立 K3s 编排门面,负责通过kube(Rust Kubernetes 客户端)执行集群访问、资源渲染、状态检查和清理。
接下来的章节会按这个顺序拆开讲。
安装与本地预览
这一章只说明如何把当前项目跑起来,以及如何预览本书本身。
1. Rust Workspace
仓库根目录是一个 Cargo workspace,包含两个成员:
backend/macos-helper/
在仓库根执行:
cargo test --workspace
如果需要更严格的静态检查:
cargo clippy --workspace --tests -- -D warnings
2. 启动云端后端
2.1 本地 dry-run 预览模式
这种模式不会真的调用 Kubernetes API,只会把即将提交的资源渲染出来。
export AGENT_STORE_BACKEND_CLUSTER_MODE=dry-run
cargo run -p agent-store-sandbox-backend
2.2 真实 K3s 模式
这种模式会调用 K3s,把每次运行提交成 Job。
export AGENT_STORE_BACKEND_BIND_ADDR=0.0.0.0:8090
export AGENT_STORE_BACKEND_CLUSTER_MODE=apply
export AGENT_STORE_BACKEND_DEFAULT_NAMESPACE=agent-store-runtime-demo
export AGENT_STORE_BACKEND_DEFAULT_SERVICE_ACCOUNT=agent-runner
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
export DEEPSEEK_API_KEY=replace-me
export DEEPSEEK_MODEL=deepseek-chat
export DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
cargo run -p agent-store-sandbox-backend
2.3 远程部署
当前仓库已经提供了部署模板:
backend/deploy/backend.env.example:环境变量模板backend/deploy/agent-store-backend.service:systemd 单元模板backend/deploy/install_backend_remote.sh:一键远程安装脚本
当前部署脚本会优先读取 docs/k8s-poc/ 下的 K3s 安装与镜像源配置,复用 /opt/agent-store/backend/target-cache 做增量构建,并在替换二进制后显式重启 agent-store-backend 再检查 /health 和 /ready。
3. 启动本地 Helper
agent-store-macos-helper(macOS 本地桥接服务)负责把本地工作目录同步成云端 artifact,并把远端结果回写到本地。
export AGENT_STORE_BACKEND_BASE_URL=http://127.0.0.1:8090
export AGENT_STORE_USER_ID=demo-user
export MACOS_HELPER_BIND_ADDR=127.0.0.1:7480
export MACOS_HELPER_POLL_TIMEOUT_MS=120000
cargo run -p agent-store-macos-helper
浏览器访问:
http://127.0.0.1:7480
4. 打包 Helper
bash macos-helper/scripts/package_macos_helper.sh
输出文件:
macos-helper/dist/Agent Store Helper.appmacos-helper/dist/Agent Store Helper.zip
5. 构建本书
本书采用 mdBook 目录结构。首次安装工具:
cargo install mdbook --locked
构建 HTML:
mdbook build backend/docs
本地预览(本机装了 mdbook 时会启用热重载;否则回退到直接托管已构建好的 backend/docs/book/):
bash backend/scripts/serve_docs.sh
访问:
http://127.0.0.1:3000
如果要把文档跑在远端机器或沙箱并允许任意位置访问,且远端已经有 backend/docs/book/index.html:
DOCS_SERVE_MODE=static MDBOOK_HOST=0.0.0.0 MDBOOK_PORT=3452 \
bash backend/scripts/serve_docs.sh
浏览器访问:
http://<remote-host>:3452
6. 快速回归命令
后端附带一个 smoke 脚本:
BACKEND_BASE_URL=http://127.0.0.1:8090 \
AGENT_STORE_USER_ID=smoke-user \
bash backend/scripts/smoke_backend_run.sh
如果只想看资源渲染,把环境变量 AGENT_STORE_APPLY_TO_CLUSTER=false 加进去即可。
运行时总体架构
这一章解释 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 的实现语言无关。
隔离模型、权限模型与安全边界
这一章专门回答“隔离做到哪一层”“权限到底有多强”“哪些能力是声明性元数据,哪些能力已经强制生效”。
1. 隔离粒度:当前分成三层
当前代码里实际上存在三种不同层次的隔离。
1.1 user_id(用户所有权边界)是对象级隔离
后端要求所有用户态 API 都带 x-user-id。FileBackedStore(文件型状态存储)在读取 SessionRecord、ArtifactRecord、MountGrantRecord、RunRecord 时都会调用 ensure_owner(所有者校验)。
这意味着:
- 一个用户看不到另一个用户的 session / run / artifact / mount grant
- 即使知道 UUID,跨用户读取也会返回 “not found”
这是一层应用层所有权隔离。当前代码没有扩展成完整身份认证体系。
1.2 SessionRecord(逻辑会话记录)是上下文隔离层
一个 SessionRecord 拥有自己的:
history- 默认 package
- 关闭状态
它能保证多轮对话上下文按 session 归组。一个 session 可以连续创建多个 run,这些 run 共享逻辑历史,容器实例按 run 单独创建。
1.3 RunRecord(一次运行记录)是物理执行隔离
每次 create_run 都会创建:
- 一个独立工作区目录
- 一个独立
ConfigMap - 一个可选独立
Secret - 一个独立 K3s
Job - 一个独立 Pod
所以当前最强的实际隔离边界是:
一次 run = 一个独立容器执行环境
2. 当前隔离强度怎么理解
如果用“隔离强度”来拆,当前可以分成四类。
2.1 进程与文件系统隔离:中等偏强
每个 run 都在独立 Pod 中运行,工作区也是独立目录,默认路径是:
- 宿主机:
backend/data/workspaces/<run-id>/ - 容器内:
/workspace
因此不同 run 的输出文件天然分目录隔离。
当前工作区挂载方式是 hostPath(宿主机目录直挂)。当前隔离强度属于较轻量的容器级隔离;如果后续要提升隔离,可以继续引入 PVC、CSI 或更强沙箱方案。
2.2 Linux 权限隔离:中等
Job 里的容器安全上下文当前明确设置了:
allowPrivilegeEscalation: falsecapabilities.drop = ["ALL"]runAsNonRoot: truerunAsUser: 1000runAsGroup: 1000fsGroup: 1000seccompProfile.type = RuntimeDefaultautomountServiceAccountToken: falserestartPolicy: Never
这说明当前 Pod 默认不会拿到:
- root 身份
- Linux capabilities
- 自动挂载的 K8s API token
这部分是代码里真实生效的安全边界。
2.3 网络隔离:弱
AgentPackageManifest(Agent 包清单)里虽然有 networkProfile(网络档位),例如 restricted-egress,但当前 backend 不会依据这个字段创建 NetworkPolicy。
networkProfile现在只是包元数据和提示信息- 真正能否访问外网,取决于集群网络、节点出网能力、镜像内工具和 provider API 可达性
所以当前网络隔离强度偏弱,不能把 networkProfile 当成已落地的强制策略。
2.4 能力权限隔离:部分生效,部分只是声明
PackagePolicy(Agent 包策略)里的两个字段要分开看:
workspaceMode:真实生效,会影响输入文件是 copy 还是 mountpermissionProfile:当前只是一段包策略元数据,没有被后端强制映射成 Linux/K8s/工具权限
所以目前真正“强制落地”的权限主要来自:
- Pod 安全上下文
- 工作区 copy / mount 选择
- 挂载白名单
readOnly/readWrite挂载模式
permissionProfile 当前承担的是描述和标识作用。
3. 一个 session、一个用户、一个 run,分别隔离什么
| 隔离单元 | 当前是否独立容器 | 当前是否独立工作区 | 当前是否独立历史 | 当前用途 |
|---|---|---|---|---|
| 用户 | 否 | 否 | 否 | 作为所有权边界 |
| Session | 否 | 否 | 是 | 作为多轮上下文边界 |
| Run | 是 | 是 | 只记录本轮 | 作为实际执行边界 |
因此如果要回答“目前是一个会话一个隔离环境,还是一个用户一个隔离环境”,答案是:
- 逻辑隔离按 session
- 物理隔离按 run
4. copy 和 mount 两种工作区模式的安全差异
4.1 WorkspaceMode::Copy(复制模式)
复制模式下,后端会把 ArtifactRecord 对应的文件从 uploads/ 复制到当前 run 的 /workspace/in。
优点:
- 每次 run 的输入快照固定
- 容器只看到本轮复制进去的文件
- 不会直接暴露宿主机原始目录树
代价:
- 大目录会产生重复拷贝
- 用户本地后续改动不会自动映射到已提交的 run
4.2 WorkspaceMode::Mount(挂载模式)
挂载模式下,必须先创建 MountGrantRecord。后端会把 grant 的 host_path 作为额外 hostPath volume 挂到容器内的 mount_path。
优点:
- 不需要复制大目录
- 容器看到的是宿主机目录的实时内容
风险:
- 隔离性明显弱于 copy
- 一旦授权
readWrite,容器就可以直接修改宿主机目录
因此 mount 模式本质上是“受控直通”能力。
5. 挂载授权当前怎么控
validate_mount_path(挂载路径校验)会做三件事:
- 把用户给的路径 canonicalize
- 确认它是一个已存在目录
- 确认它落在
AGENT_STORE_BACKEND_ALLOWED_MOUNT_ROOTS白名单内
默认白名单是:
backend/data/workspaces
这说明当前 mount grant 的默认设计目标是:
- 给 backend 自己管理的目录做受控回挂
- 默认不开放整个宿主机文件系统
6. 容器内到底能访问什么文件
当前容器内常见文件可分成四类。
6.1 始终存在的工作区
/workspace/in/workspace/out/workspace/tmp
这三者都来自 run 对应的宿主机工作区目录。
6.2 始终存在的 package 目录
/opt/agent/package
这里挂的是 ConfigMap,里面是 package 的文本资产。
6.3 可选的 provider 凭据
provider 凭据不会作为文件卷挂进去,而是通过 envFrom.secretRef 注入环境变量。
6.4 可选的用户授权挂载
如果本次 run 绑定了 MountGrantRecord,还会额外挂一个 user-mount volume 到指定路径。
7. 容器如何访问外部工具、宿主机工具和本地工具
7.1 容器内可以访问哪些“外部工具”
当前真正可执行的工具只有三类:
- 基础镜像里自带的工具,例如
bash、node inlineNpm(运行时内联 npm 安装)装出来的 CLI- 容器内通过 HTTP 访问的模型 API
7.2 build_in_tools.yaml 当前是提示词工具清单
build_in_tools.yaml(内置工具清单)虽然声明了 builtin.read、builtin.write、builtin.ask_user 等工具,但当前实现只是把它们写进 run-context.json 供模型参考。
后端目前没有:
- 独立的 tool call RPC 通道
- tool execution dispatcher
- tool result callback 回路
当前实现里,这一层承担的是提示词上下文拼装职责。
7.3 容器不能直接访问 backend 主机任意工具
当前没有实现:
- 宿主机命令代理
- SSH proxy 执行
- helper 本地工具转发
- 本地 IDE / Git / shell 代理
除非某个宿主机目录被显式 mount 进来,否则容器看到的只是:
- 镜像内容
- 工作区内容
- package 内容
- 环境变量
7.4 容器也不能直接访问 macOS 本地工具
macos-helper 当前承担的是“文件同步桥”职责。它会把本地文件上传成 artifact,当前没有把 macOS 上的 shell、git、编辑器或 CLI 映射成远端容器可调用工具。
7.5 支持本地工具调用的可行方案
如果后续要让远端 agentcore 访问本地工具,当前最可行的方案有三类:
macos-helper(macOS 本地桥接服务)增加本地工具 RPC 层,由 helper 执行 Git、shell、编辑器等受控工具- helper 与 backend 之间建立长连接工具隧道,由 backend 转发工具调用请求
- 继续保持当前文件同步模式,把本地工具执行放在 helper 侧完成,再把结果同步成文件或补丁
无论采用哪种方案,都需要补齐:
- 工具白名单
- 参数校验
- 会话级认证
- 审批模型
- 超时与取消
- 审计日志
7.6 当前未实现点
当前代码还没有:
- 远端容器到本地 helper 的工具调用协议
- helper 侧的受控命令执行器
- 本地工具的权限和审批模型
- 本地工具执行结果回写到
agentcore的标准事件流
8. provider 凭据隔离如何做
ProviderDefaults(provider 默认值合并器)会合并三层来源:
- backend 进程环境变量
RuntimeProfile里的默认baseUrl/model- 单次请求的
provider_env
合并后得到的环境变量会被写成每 run 独立的 K8s Secret。渲染返回给前端时,providerSecretYaml 里的值会被替换成 <redacted>。
这说明当前 provider 凭据隔离粒度是:
- 按 run 注入
- 按 Secret 隔离
- 按用户 API 读取权限隔离
9. 当前安全边界的真实结论
如果只基于代码,不做理想化表述,可以下结论:
- 当前已经有“每 run 独立 Pod + 非 root + drop capabilities + seccomp + 禁止自动 SA token”这一层容器安全基线。
- 当前默认更安全的输入方式是
copy,mount属于更强权限能力。 - 当前 mount 具备白名单和只读/读写控制,但仍然属于较强权限能力,应谨慎开放。
- 当前
permissionProfile、networkProfile主要还是描述性元数据,不应误认为强制安全策略。 - 当前没有把本地 macOS 工具或 backend 宿主机工具安全地暴露给容器的机制。
运行生命周期与容器编排流程
这一章从用户视角出发,解释当前代码里一次会话、一次运行和一次容器编排到底怎样流动。
1. 从用户视角看,一次会话会经历什么
用户在产品里看到的主流程可以理解为下面几步:
- 打开一个
session(会话) - 上传文件,或选择已有文件
- 选择一个
agent package - 发送一轮 prompt
- backend 为这轮 prompt 创建一个新的
run - K3s 为这个
run拉起一个独立 Pod agentcore在 Pod 里处理本轮任务并写结果- backend 把结果状态写回
run,把主回答写回session history - 用户离开页面,稍后回来继续使用同一个
session
这个流程里,“连续的是 session”,“独立的是 run”。
2. 后端进程生命周期
main(进程入口)启动后会依次完成:
- 初始化 tracing
- 兼容旧环境变量别名
- 解析
AppConfig(进程级配置) - 调
build_application(应用装配入口) - 绑定 HTTP 地址
- 用 axum 对外提供 API
- 监听退出信号并优雅关闭
如果 backend 运行在 Apply 模式,K3sOrchestrator::new(K3s 编排器初始化)还会确保:
- namespace 存在
- service account 存在
- service account 关闭自动挂载 token
2.1 启动伪代码
async fn main() {
init_tracing(); // 初始化日志
apply_legacy_env_aliases(); // 兼容旧环境变量
let config = AppConfig::parse(); // 解析进程配置
let app = build_application(config.clone()).await?; // 装配 store/catalog/orchestrator/router
let listener = TcpListener::bind(config.bind_addr).await?; // 绑定 HTTP 地址
axum::serve(listener, app.router)
.with_graceful_shutdown(shutdown_signal())
.await?;
}
3. session 的生命周期
SessionRecord(逻辑会话记录)当前只有两个状态:
ACTIVECLOSED
生命周期如下:
POST /api/v1/sessions创建,初始为ACTIVE- 可反复用于多轮
create_run POST /api/v1/sessions/{id}/close后变成CLOSEDPOST /api/v1/sessions/{id}/reopen后回到ACTIVECLOSED状态下仍可查询历史和结果
这里要特别注意:close 表示“归档并拒绝新写入”,会话数据会继续保留。
4. run 的生命周期
RunRecord(一次运行记录)常见状态包括:
PREVIEWEDSUBMITTEDPENDINGCOMPLETEDFAILEDCANCELLED
其生命周期是:
- 用户请求创建 run
- backend 准备工作区和上下文
- backend 提交或渲染 K3s 资源
- Pod 执行
- backend 刷新状态
- run 进入完成、失败或取消态
5. create_run 的完整用户视角流程
5.1 用户看到的过程
从用户角度,一次“发送消息”背后发生的是:
- backend 读取当前会话
- backend 读取本轮所选文件和挂载授权
- backend 检查集群是否还能接收新任务
- backend 把旧回答补回会话历史
- backend 把本轮用户输入追加到会话历史
- backend 创建本轮独立工作区
- backend 把上下文写成
run-context.json - backend 构造
ConfigMap、Secret、Job - K3s 启动本轮 Pod
- Pod 把本轮结果写回工作区
5.2 当前代码的伪代码
#![allow(unused)]
fn main() {
async fn create_run(command: CreateRunCommand) -> RunRecord {
let run_id = new_uuid_v7(); // 生成本轮唯一 id
let session = store.get_session(command.user_id, command.session_id).await?; // 读取会话
ensure_session_accepts_writes(session)?; // 关闭态会话拒绝新 run
let package = packages.get(command.package_name)?; // 读取 agent package
let runtime_profile = resolve_runtime_profile(command, package)?; // 解析运行时模板
let provider_env = merge_provider_env(runtime_profile, command.provider_env); // 合并 provider 配置
let artifacts = store.get_artifacts(command.user_id, command.artifact_ids).await?; // 读取输入文件
let mount_grant = resolve_mount_grant(command.mount_grant_id).await?; // 读取可选挂载授权
let workspace_mode = resolve_workspace_mode(command, package); // 决定 copy 或 mount
ensure_cluster_accepts_runs().await?; // 检查是否有可调度节点
sync_session_conversation_history(command.user_id, command.session_id).await; // 把旧 run 的回答补回 history
append_user_message(command.session_id, run_id, command.user_prompt).await?; // 写入本轮用户消息
let workspace_root = create_workspace(run_id, artifacts).await?; // 创建 /in /out /tmp
write_run_context(workspace_root, session, package, artifacts, command.user_prompt).await?; // 写上下文文件
let plan = LaunchPlan { ... }; // 汇总 package / runtime / workspace / provider / k8s 参数
let outcome = orchestrator.submit_run(plan).await?; // 渲染并提交 ConfigMap / Secret / Job
let run = build_run_record(run_id, workspace_root, outcome); // 落盘 RunRecord
store.insert_run(run).await
}
}
6. K3s 资源怎样从业务对象变成 Pod
6.1 ApplicationService(应用服务层)负责业务装配
ApplicationService::create_run 会把会话、文件、挂载、provider 环境变量和 RuntimeProfile 组装成 LaunchPlan。
6.2 job_builder 负责渲染资源定义
job_builder::build_bundle(构建运行资源包)会在内存里构造:
- 承载
agent package文件的ConfigMap - 承载 provider 环境变量的
Secret - 真正执行 run 的
Job
6.3 K3sOrchestrator 负责和集群交互
K3sOrchestrator::submit_run(提交运行)在 apply 模式下会调用:
Api<ConfigMap>::createApi<Secret>::createApi<Job>::create
这就是当前 Rust 侧真正把业务运行提交到 K8s 的代码路径。
6.4 一次 create 从 backend 到 Pod running 的时序
如果把 create 再展开一层,当前时序更准确地说是:
frontend/helper
-> POST /api/v1/runs
-> backend create_run
-> build_bundle(ConfigMap/Secret/Job)
-> Kubernetes API Server 持久化这些资源
-> Job Controller 观察到新的 Job
-> Job Controller 创建 Pod
-> Scheduler 为 Pod 选节点
-> kubelet 在目标节点准备 volume / secret / configmap / image
-> container runtime 启动容器
-> agentcore 执行并写出结果
这里要特别注意两个事实:
- backend 只负责把资源写入 API Server,不直接驱动 Pod 生命周期。
- Pod 能否真的跑起来,还取决于调度、镜像、节点磁盘、挂载卷、provider 凭据等后续条件。
6.5 create 成功不等于业务已经开始执行
当前系统里至少有三种“create 成功”的层次:
- backend 成功返回
RunRecord - Kubernetes API 成功创建
Job - Pod 真的被调度并开始运行
这三层不是一回事。
所以今天 backend 的状态刷新才需要同时看:
/workspace/out/*输出标记Job/Pod的集群状态
因为“资源对象已创建”并不等于“容器已经完成执行”。
7. Pod 内执行生命周期
7.1 容器启动后先做什么
job_builder::build_run_script(构建容器启动脚本)会生成启动脚本,脚本大致会:
- 创建
/workspace/in、/workspace/out、/workspace/tmp - 打印 run、session、user、workspace 信息
- 如有挂载授权,列出挂载目录文件
- 根据
installStrategy决定是否做运行时安装 - 执行 verify 命令
- 输出 package 和输入文件信息
- 进入具体执行模式
7.2 当前执行模式怎样理解
当前代码里的 ExecutionMode(执行模式)主要有两种:
CliVerifyOpenAiCompatibleApi
它们表达的是容器内执行入口的行为方式。从架构角度,这仍然属于 agentcore 的执行适配层。
7.3 容器内执行伪代码
容器启动
-> 读取 /workspace/in/run-context.json
-> 读取 /opt/agent/package/*
-> 读取 provider 环境变量
-> 运行 agentcore
-> 把主回答写到 /workspace/out/final-answer.md
-> 把调试信息写到 /workspace/out/*
-> 进程退出
8. backend 如何知道 run 已经结束
ApplicationService::refresh_run_status(刷新运行状态)会做两类检查。
8.1 第一类:看输出目录
它会检查:
runtime-error.txtfinal-answer.mdprovider-response.jsonllm-run-summary.jsonexecution-mode.txt
如果这些文件已经出现,就可以直接推断 run 已完成或失败。
8.2 第二类:看 K8s Job / Pod 状态
如果输出目录还看不出来,backend 会调用 K3sOrchestrator::inspect_run_status(检查运行状态),内部通过:
Api<Job>::get_optApi<Pod>::list
读取 Job 和 Pod,再映射成 RunStatus。
8.3 状态刷新伪代码
#![allow(unused)]
fn main() {
async fn refresh_run_status(run: RunRecord) -> RunRecord {
if let Some(status) = detect_output_status(run.workspace_root.join("out")).await {
update_run_status(run, status); // 输出已经落盘,直接完成状态刷新
} else if let Some(observation) = orchestrator.inspect_run_status(&run).await? {
update_run_status(run, observation); // 读取 Kubernetes Job / Pod 状态
}
if run.status == COMPLETED {
sync_run_assistant_message(&run).await?; // 把 final-answer.md 写回 session history
}
persist_run(run).await // 保存新的 run 状态
}
}
9. 同一 session 内如何保持连续性
这是当前生命周期里最关键的一部分。
9.1 文本对话连续性
文本对话连续性由下面两个动作保证:
sync_session_conversation_history(同步会话历史)
在新 run 开始前刷新同一 session 下旧 run 的状态。sync_run_assistant_message(同步助手回答)
在 run 完成后读取final-answer.md,把它回填进SessionRecord.history。
因此当前同一 session 的下一轮一定可以拿到上一轮的主回答。
9.2 文件结果连续性
run 完成后,当前代码会保留:
RunRecordworkspace_rootworkspaces/<run-id>/out/*
因此用户下次回来时,仍能查看历史结果文件。
9.3 上传文件连续性
只要 ArtifactRecord 和 uploads/ 下的文件还在,后续 run 就可以继续引用这些文件。
9.4 agentcore checkpoint 连续性
从运行时模型看,agentcore checkpoint 连续性应采用“同一 session 挂回同一状态目录”的方式。当前代码已经支持:
- 按 session 保存历史
- 按 session reopen
- 按 run 保存结果
sessions/<session-id>/级别的持久化状态目录- 每次 run 都把该目录挂到
/workspace/session - 扫描最近 checkpoint 摘要并写回
SessionRecord.runtime_state
当前代码还没有显式支持:
- restore / resume API
- 用户选择“恢复到哪个 checkpoint”的控制面接口
- checkpoint retention / GC 策略
- 输出文件自动进入下一轮输入
因此今天已经落地的是“会话历史连续 + session 级状态目录连续”,还没有落地的是“平台级可恢复交互式运行”。
9.5 当前为什么一个 run 不能在容器里直接接收下一条用户消息
当前 run 本质上是 batch 语义:
- backend 的
create_run每次只接收一轮user_prompt - K3s 侧使用的是
Job - 容器脚本读取一次
run-context.json后就执行并退出 RunStatus里没有WAITING_USER_INPUT、WAITING_APPROVAL、WAITING_ARTIFACTS
因此今天的模型是:
- 同一
session可以连续创建多个run - 但同一个正在运行的容器不会持续监听并接收新的用户消息
9.6 K3s 能否支持“等待用户 / 审批 / 补文件 / 再继续”
可以,但要分清楚K3s 能提供的容器编排能力和backend 必须补的控制面语义。
K3s 可以提供:
- 长驻 Pod、
Deployment、StatefulSet Service暴露稳定访问入口readinessProbe/livenessProbeLease或自定义心跳,用于 worker 占有与回收- PVC 或其它持久化卷,用于 session state 和 checkpoint
- 显式删除 Pod / Deployment,实现“空闲超时回收”
但 K3s 本身不会替你决定:
- 什么时候应该向用户提问
- 哪个工具调用需要审批
- 用户迟迟不上传文件时应进入什么业务状态
- 收到新消息后是复用旧容器还是重启新 run
这些都必须由 backend 控制面建模。
9.7 两种可行的交互式运行模式
模式 A:冷恢复模式
这是最稳的方式,也是最贴近当前架构的方式:
- run 执行到需要用户输入、审批或文件时,把 checkpoint 写入
/workspace/session - run 上报一个阻塞事件,例如
WAITING_USER_INPUT - 容器退出,释放 CPU / 内存
- backend 等待用户回复、审批结果或新 artifact
- backend 创建一个新的 run,从同一 session state 恢复继续执行
这个模式下,K3s 仍然主要使用 Job。
模式 B:Warm Sandbox 模式
如果想优化“用户刚回复就秒回”的体验,可以让容器短时间保活:
- 第一次请求启动一个长驻 Pod,而不是一次性
Job - 容器进入
idle,等待同一 session 的下一条事件 - backend 给它设置
idle_ttl - 用户在 TTL 内回复、审批或补文件,就把事件投递给这个 Pod
- 超过 TTL 仍无新事件,就让容器 checkpoint 后退出
这个模式下,更适合使用:
Deployment+ 每 session 一个 worker- 或
StatefulSet/ 命名 Pod + PVC
它能改善连续体验,但复杂度明显高于当前 batch run。
模式 B 下,长驻 Pod 如何等待并接收下一条用户事件
如果后续采用 warm sandbox,那么“等待”不应实现成容器里的 busy loop,而应实现成一个显式事件循环:
- backend 为 session worker 分配稳定标识,例如
session_id+worker_id - worker 启动后向 backend 注册可用状态,进入
idle - backend 通过显式事件通道投递下一条事件,例如:
USER_MESSAGEAPPROVAL_RESULTARTIFACT_READYCANCELSHUTDOWN
- worker 收到事件后进入
busy,处理完后再回到idle - 如果超过
idle_ttl仍无新事件,则 worker checkpoint 后退出
事件通道本身可以后选:
- HTTP / gRPC
- WebSocket
- 长轮询
- 队列
但“worker 通过什么机制收到下一条事件”必须在控制面里明确,否则长驻 Pod 只是在空转。
模式 B 下,长驻 Pod 如何向用户提问
如果 worker 需要 ask_user、申请权限或索取新文件,建议统一走下面的链路:
- worker 向 backend 上报一个
InteractionRequest - backend 持久化为
PendingActionRecord - backend 把问题、审批请求或缺失文件要求展示给前端
- worker 进入
waiting_user或回到idle - 用户回复后,backend 再把新事件路由回该 worker;如果 worker 已被回收,则改走冷恢复模式
这样可以同时支持两种恢复路径:
- worker 仍在:直接复用原长驻 Pod
- worker 已回收:从 checkpoint 启动新的 resumed run
模式 B 下,长驻 Pod 如何访问容器外部数据库
K3s 可以承载这种能力,但实现方式必须是“服务接入”,而不是“宿主机目录共享”。
建议统一约束:
- 数据库运行在集群内时,通过
Service访问 - 数据库运行在集群外时,通过稳定地址访问
- 连接字符串、账号密码、token、TLS 材料通过
Secret或投影卷注入 - worker 启动时做最小连接探活,并把失败原因回传给 backend
- 如有需要,再补
NetworkPolicy或 egress 白名单
不建议:
- 让长驻 Pod 直接 mount 宿主机数据库数据目录
- 把数据库凭据硬编码进镜像
- 把访问数据库等同于“进入宿主机环境”
9.8 job_ttl 和“空闲等待 TTL”不是一回事
当前已有的 K3s Job TTL 指的是:
- run 已完成后
- K8s 再保留多久 Job / Pod
它不能实现:
- 容器空闲等待用户下一句话
- 容器等待审批结果
- 容器等待用户上传新文件
如果后续要支持这些能力,需要新增一套独立的“空闲等待 TTL”语义,由 backend 负责:
- 判断 session worker 何时进入 idle
- 判断 idle 何时超时
- 触发 checkpoint、摘除路由、删除 Pod
9.9 Pod 能不能把“当前整个状态”打包成镜像
标准 K3s / Kubernetes 语义下,不能把“当前 Pod 的完整可恢复状态”简单理解为一个可打包镜像。
原因有四类:
- 镜像主要表达的是容器文件系统层,不表达:
- 进程内存
- 打开的文件描述符
- TCP 连接
- PID / namespace 运行态
- 当前真正重要的业务状态主要放在挂载卷里,例如:
/workspace/workspace/session/workspace/session/checkpoints这些内容本来就不应该依赖镜像层保存。
ConfigMap、Secret、hostPath、PVC 都是运行时挂载语义,不会因为“镜像化”自动被完整封装。- Kubernetes 没有一个标准 API 能把“正在运行的 Pod 全状态”直接 commit 成可移植镜像,再在别处完整恢复。
9.10 那底层 runtime 能不能做 checkpoint / restore
底层容器 runtime 在某些场景里可能存在:
commit风格的文件系统快照- CRIU 风格的 checkpoint / restore
但这不等于当前平台可以把它当成标准方案,原因是:
- 强依赖具体 runtime 与节点内核能力
- 对 K3s / containerd 版本和宿主机环境耦合很强
- 很难作为跨节点、跨环境、可审计的恢复语义
- 和当前基于卷保存 checkpoint 的架构并不一致
因此对当前 Agent Store 架构,更合理的恢复方式仍然是:
- 镜像保持不可变运行时底座
- checkpoint / 状态数据库写入持久化卷或外部状态存储
- 需要恢复时启动新 Pod 并重新挂载这些状态
9.11 这部分的工程判断
如果只是想固定“文件系统产物”,可以考虑:
- 持久化卷
- 对象存储
- 导出 artifact
如果想固定“可恢复执行状态”,应该优先依赖:
- checkpoint 文件
- checkpoint 数据库
- session state 元数据
而不是把 Pod 当成一台可以随时“拍扁成镜像再复活”的虚拟机。
建议保留为 TODO:
- TODO:只有在出现“必须恢复进程内存态”的硬需求时,再专项评估 runtime-level checkpoint / restore
- TODO:在此之前,统一沿用“新 Pod + 持久化状态挂载 + resumed run”模型
10. [TODO] 面向交互式运行的 K3s 演进方案
下面这些项可以用 K3s 承载,但当前 backend 还没有实现。
- TODO:新增阻塞态,例如
WAITING_USER_INPUT、WAITING_APPROVAL、WAITING_ARTIFACTS、EXPIRED - TODO:新增
PendingActionRecord或InteractionRequestRecord,显式保存“问题内容、审批请求、所需文件、超时时间、恢复目标 run” - TODO:把
builtin.ask_user从提示词元数据升级成真实工具调用链路 - TODO:把工具权限判断落到 backend 控制面,形成
allow / ask / deny - TODO:支持“冷恢复模式”:run 遇到阻塞先 checkpoint,再退出,等待新事件后启动 resumed run
- TODO:支持“warm sandbox 模式”:为 session 维持一个带
idle_ttl的长驻 Pod - TODO:为 warm sandbox 增加
Service、健康检查、心跳或Lease - TODO:为 warm sandbox 明确事件通道:backend 如何向 worker 投递
USER_MESSAGE/APPROVAL_RESULT/ARTIFACT_READY - TODO:为 worker 到 backend 的回调定义契约:
ASK_USER、REQUEST_APPROVAL、REQUEST_ARTIFACT、CHECKPOINT_READY、HEARTBEAT - TODO:为 warm sandbox 补齐
idle、busy、waiting_user、draining的状态边界 - TODO:把 session state 从
hostPath升级到 PVC 或外部状态存储,以支持更可靠的 Pod 重建 - TODO:区分
job_ttl_seconds与未来的session_idle_ttl_seconds - TODO:增加“用户上传新文件后继续当前 session”的恢复 API,而不是要求用户手工重新拼装上下文
- TODO:为访问外部数据库或状态服务补齐
Secret、TLS、连接探活和网络白名单约束
11. 用户离开后再回来,会发生什么
10.1 只是关闭前端页面
如果用户只是关闭前端页面,没有关闭 session,那么后续直接拿原来的 session_id 继续创建 run 即可。
10.2 显式关闭 session
如果用户调用了 close,会话仍保留历史和结果,但会拒绝新的写入。后续调用 reopen 后,同一个 session_id 又可以继续创建新的 run。
10.3 一个月后继续同一个 session
今天这套代码已经能保证下面这些内容在一个月后仍可恢复:
- 会话历史
- 上传文件索引
- 历史 run 记录
- 历史结果文件
如果还要恢复 agentcore 内部 checkpoint,就需要把 checkpoint 写到持久化挂载目录,并在 backend 里保存该目录索引。这是当前生命周期模型下一步最自然的扩展点。
12. run 结束后,K8s 如何处理资源
当前语义如下:
- Job / Pod:由 Job TTL 或取消逻辑处理
ConfigMap/Secret:取消时会显式删除,正常完成后的统一 GC 还需补齐workspaces/:backend 主机磁盘继续保留state.json:backend 继续保留
K8s 只负责容器对象生命周期,不负责清理对话历史和业务文件索引。
当前实现边界与演进建议
这一章用于把今天代码里已经实现到哪里、还没有实现到哪里,明确说清楚。
1. 当前已经实现的能力
1.1 控制面与状态管理
已经具备:
session、artifact、mount grant、run的完整 APISessionStatus的close / reopen语义x-user-id基础所有权隔离- 本地
state.json持久化
1.2 运行时编排
已经具备:
RuntimeProfile装载agent package装载- 每 run 独立 K3s
Job - package
ConfigMap - provider
Secret - Job / Pod 状态观察
- 集群节点健康与预检 API
1.3 文件管理
已经具备:
- 上传文件持久化
- 相对路径保留
- 每 run 独立工作区
- session 级 runtime state 目录与 checkpoint 摘要同步
- 输出文件列举与安全读取
- mount grant 白名单校验
1.4 多轮对话
已经具备:
SessionRecord.history累积- 已完成 run 的主回答回填
- 同一
session的 reopen 后继续运行
2. 当前最重要的实现边界
2.1 当前 session 连续性来自持久化状态
当前代码已经连续保存:
- 对话历史
- 已上传文件
- 历史结果文件
- run 元数据
当前代码还没有复用:
- 常驻容器进程
- 进程内缓存
- shell 状态
- 工具服务进程
因此当前连续性属于“持久化状态连续”。
2.2 当前已经有 session 级 runtime state,但恢复能力还不完整
这是当前最需要被准确描述的边界。
今天的代码里:
- 每个
run都有独立workspace_root - 历史结果文件会长期保留
- 会话历史会长期保留
- 每个
session都有自己的sessions/<session-id>/持久化状态目录 - 每次 run 都会把该目录挂到
/workspace/session - backend 会扫描
checkpoints/并把最近 checkpoint 摘要写回SessionRecord.runtime_state
今天的代码里还没有:
- restore / resume API
- 明确的 checkpoint retention / GC 策略
- 输出文件自动进入下一轮输入的受控规则
- 适用于多节点集群的 PVC / 对象存储抽象
因此,今天的 backend 已经能承载“同一 session 的状态目录连续”,但距离“平台级可恢复运行时”还差恢复接口、治理策略和跨节点存储抽象。
2.3 当前 runtime family 名称仍是样例标识符
当前源码里的 RuntimeFamily(运行时家族)枚举仍使用样例标识符,例如:
CodexClaudeCode
这不影响运行时架构本身,因为真正决定容器行为的是:
imageexecutableinstallStrategyexecutionMode
但它说明当前代码在“可扩展 family 标识”上仍有代码级约束。如果后续要把 runtime family 做成完全开放的扩展点,可以把它进一步放宽成字符串或可扩展枚举。
2.4 当前 K8s 资源回收和工作区回收还没有完全打通
当前 run 结束后:
- Job / Pod 会被 TTL 或取消逻辑处理
- workspace、artifact、run record、session history 会继续保留
- 正常完成态 run 的
ConfigMap和Secret还没有统一 GC
因此如果后续 run 数量很多,需要补充后台 GC 或 retention policy,把:
- 旧
workspaces/ - 旧
uploads/ - 旧
ConfigMap - 旧
Secret
纳入统一清理策略。
2.5 当前工作区基于 hostPath
hostPath 的优点是简单直观、便于单节点 K3s 验证。当前代价也很明确:
- 强依赖 backend 所在节点的本地磁盘
- 磁盘压力会直接影响调度
- 不适合做跨节点迁移
- 隔离强度弱于更高级的沙箱方案
另外,当前代码还没有 nodeSelector、affinity 或 PVC 绑定逻辑,因此多节点场景下默认仍应视为“单节点优先”设计。
2.6 当前 backend 采用本地持久化控制面
FileBackedStore 使用本地:
state.jsonuploads/workspaces/sessions/
所以现在 backend 更像:
- 单实例控制面
- 方便联调和演示的最小实现
多副本水平扩展控制面和外部数据库支撑的生产级 control plane 还需要后续演进。
2.7 当前没有流式输出和事件推送
现在前端 / helper 获取 run 进度主要靠轮询:
GET /api/v1/runs/{id}GET /api/v1/runs/{id}/results
当前没有:
- WebSocket
- SSE
- stdout / stderr 实时流
- token 级别的模型输出流
2.8 当前 run 仍然是 batch Job,不是交互式 session worker
当前 backend 的真实语义是:
- 一次
POST /runs创建一次新的执行 - K3s 拉起一个新的
Job/ Pod - 容器读取一次上下文后执行并退出
因此当前还没有:
- 一个长驻容器持续等待同一 session 的下一条消息
- 一个运行中的容器直接接收新的用户 prompt
- 一个运行中的容器等待审批或文件补充后继续
K3s 可以承载这类模式,但需要从“每 run 一个 Job”演进到“每 session 一个可回收 worker Pod”或“checkpoint 后冷恢复”。
2.9 当前没有“Pod 状态镜像化恢复”能力
当前没有:
- 把正在运行的 Pod 全状态 commit 成镜像的标准链路
- 把该镜像作为平台级恢复语义的控制面设计
- 依赖 runtime-level checkpoint / restore 的跨节点恢复方案
当前更现实的恢复语义仍然是:
- 新 Pod
- 重新挂载 session state
- 从 checkpoint 文件或 checkpoint 数据库恢复
因此如果后续真的需要“冻结当前运行态”,推荐优先冻结持久化状态,而不是冻结 Pod 本身。
2.10 当前没有“远端容器直接调用本地用户工具”的桥
这点必须明确写清楚。
当前没有:
- 本地 Git CLI 代理
- 本地 shell 代理
- 本地 IDE / editor RPC
- 本地文件系统 FUSE 挂载代理
macos-helper 当前只负责文件同步、结果回写和本地 UI 代理。
3. 支持本地工具调用的可行方案
虽然当前没有本地工具桥,但有几种可行方案可以接到现有架构上。
3.1 方案 A:Helper 本地工具 RPC 桥
最自然的方案是让 macos-helper(macOS 本地桥接服务)增加一个本地工具执行层:
- helper 在本地暴露受控 RPC 接口
- backend 在创建 run 时下发一组 session 级或 run 级工具描述
agentcore通过受控 API 调用 helper- helper 在本机执行 Git、shell、编辑器脚本等工具
- 执行结果再回传给
agentcore
这个方案需要补齐:
- 工具白名单
- 参数校验
- 审批模型
- 超时和取消
- 审计日志
- 会话级认证
3.2 方案 B:Helper 反向长连接工具隧道
可以让 helper 主动和 backend 保持长连接,由 backend 把工具请求转发给 helper,再由 helper 本地执行。
这个方案的好处是:
- 后端不需要直接访问用户本机
- 连接模型更适合 NAT 后的用户机器
需要补齐的能力包括:
- 双向流式消息协议
- 工具请求队列
- 断线重连
- session / run 级权限绑定
3.3 方案 C:本地只负责文件,同步后由远端容器执行
这是当前代码最接近的模式:
- helper 只负责把本地目录同步成
artifact - 远端容器只处理文件,不直接调用本地工具
它实现简单,安全边界清楚,但能力上限较低。
3.4 方案 D:受控宿主机工具代理
如果未来不仅要访问本地工具,还要访问 backend 所在 Linux 主机上的受控工具,也可以增加宿主机工具代理层。
这个方案需要非常严格的:
- 白名单
- 身份隔离
- 审计
- 资源限制
因为它的权限风险明显高于文件同步。
4. 当前架构为什么仍然是合理的
尽管上面有不少边界,但以当前代码目标来看,这套方案仍然成立,因为它先解决了最核心的几件事:
- 把 Agent 运行从 backend 进程里剥离到了独立容器
- 把
agent package、运行时模板、provider、workspace 拆成了清晰的组合关系 - 把 session 多轮上下文和 run 物理隔离分开建模
- 让前端、helper、backend、K3s 之间有了可验证的 API 契约
5. 如果继续演进,优先级建议是什么
5.1 第一优先级:补齐 session 级状态目录
优先补齐:
- resume / restore API
- checkpoint retention / GC 策略
- 输出文件到下一轮输入的受控提升机制
- 面向多节点的持久化卷抽象
这是把“会话历史连续”升级成“完整运行时状态连续”的关键一步。
5.2 第二优先级:把“声明型策略”变成“强执行策略”
优先补齐:
permissionProfile到真实能力矩阵networkProfile到真实NetworkPolicybuiltInTools到真实工具调度层
5.3 第三优先级:补齐交互式等待与恢复模型
建议优先补齐:
WAITING_USER_INPUT、WAITING_APPROVAL、WAITING_ARTIFACTSPendingAction数据模型- 冷恢复模式下的 resume API
job_ttl_seconds和未来session_idle_ttl_seconds的区分- 需要时再增加 warm sandbox 模式
这一步是把“连续对话”升级成“可中断、可等待、可恢复执行”的关键。
5.4 第四优先级:明确 checkpoint store 形态
建议优先明确:
- checkpoint 是文件 / 嵌入式数据库
- 还是外部数据库服务
因为这会直接影响:
- Pod 如何访问状态
- 是否必须引入
Service/ Secret / 网络治理 - 是否还能继续沿用当前
/workspace/session模型
5.5 第五优先级:补齐本地工具桥
建议优先落地 Helper 侧 RPC 桥,因为它和当前“本地 Helper + 云端 backend”结构最匹配。
5.6 第六优先级:把工作区从 hostPath 升级出去
重点是:
- 降低宿主机磁盘压力对调度的直接影响
- 支持更强的多节点与弹性调度
- 把 workspace 生命周期和 node 生命周期更好地解耦
5.7 第七优先级:把 backend 升级成可水平扩展控制面
也就是逐步把:
state.json- 本地 uploads
- 本地 workspaces 元数据
迁移到更适合多副本的外部存储体系。
6. 当前技术方案的最终判断
如果基于当前源码评价,这套 backend 已经具备“Agent 商店运行时最小可用控制面”的完整骨架:
- 会话有了
- 文件有了
- 挂载有了
- run 有了
- K3s 编排有了
- provider 注入有了
- 多轮历史有了
- 节点预检和状态观察也有了
当前离“生产级多租户高隔离 Agent Runtime 平台”的主要差距集中在:
- session 级 checkpoint 连续性
- 交互式等待与恢复模型
- checkpoint store 形态收敛
- 本地工具桥
- 强权限
- 强网络策略
- 工作区存储外部化
- 控制面外部化持久存储
7. [TODO] K3s 交互式运行改造清单
下面这些能力都可以建立在 K3s 之上,但今天还没落地:
- TODO:为 run / session 增加阻塞态和超时态
- TODO:增加
PendingAction、ApprovalRequest、ArtifactRequest之类的控制面对象 - TODO:支持 checkpoint 后退出、等用户事件后 resumed run 的冷恢复链路
- TODO:支持可选 warm sandbox:
Deployment或StatefulSet承载每 session worker - TODO:为 warm sandbox 增加
Service、探针、心跳和空闲回收 - TODO:把
builtin.ask_user升级为真实工具而不是仅供模型参考的元数据 - TODO:把权限审批统一收敛到 backend,而不是由容器内提示词自行决定
- TODO:把 session state 和工作区从
hostPath逐步迁移到更适合多节点的持久化存储 - TODO:明确 checkpoint store 是文件型、嵌入式数据库型,还是外部数据库服务型
- TODO:如果采用外部数据库服务,为其补齐
Service/ Secret / 网络策略 / 恢复契约
K3s 运行时模块文档
本文档面向需要了解 K3s 运行时模块工作原理的所有人,包括产品经理、运维人员和对技术细节感兴趣的非开发人员。文档会在每个技术术语第一次出现时用“术语(中文解释)“的格式进行说明。
提示:文档中的流程图使用 Mermaid 语法。在 GitHub 上可直接渲染;本地使用 mdBook 阅读时,可安装
mdbook-mermaid插件。
这个模块是什么
K3s 运行时模块是整个智能体商店(Agent Store)的执行引擎层。它负责:
- 把用户的一次“运行请求“变成一个真实运行在本地机器上的容器(Container,一种轻量级隔离运行环境)
- 管理容器的创建、监控和清理
- 确保每次运行之间互不干扰
- 保证同一个会话的多轮对话能够保持上下文连续
简单来说:后端控制面负责“决定做什么“,K3s 运行时模块负责“真正去做“。
统一术语表
下面这些术语会在后续文档中反复出现。建议先浏览一遍,后续遇到时可随时回来查阅。
| 术语 | 中文解释 | 简要说明 |
|---|---|---|
| K3s | 轻量级 Kubernetes 发行版 | 一种管理容器的编排系统,可以在单台机器上运行,也可以扩展到多台机器 |
| Pod | 容器组 | Kubernetes 中最小的调度单位,包含一个或多个容器 |
| Deployment | 部署 | Kubernetes 中用于管理长驻 Pod 的资源类型,支持滚动更新和自动恢复 |
| Service | 服务 | Kubernetes 中为 Pod 提供稳定网络访问入口的资源类型 |
| ConfigMap | 配置映射 | Kubernetes 中用于存储非敏感配置数据的资源,可以被容器读取 |
| Secret | 密钥资源 | Kubernetes 中用于存储敏感信息(如 API 密钥)的资源 |
| Namespace | 命名空间 | Kubernetes 中用于隔离和组织资源的逻辑分区 |
| Service Account | 服务账号 | 容器在集群中运行时使用的身份标识 |
| hostPath | 宿主机目录挂载 | 把运行容器的物理机器上的某个目录映射到容器内部 |
| PVC | 持久卷声明 | 向 Kubernetes 申请持久化存储空间的标准方式 |
| Node | 节点 | Kubernetes 集群中的一台工作机器 |
| agentcore | 智能体核心运行时 | 容器内真正执行推理、调用工具、读写文件的可执行程序 |
| agent package | 智能体包 | 开发者提供的提示词、工具声明、配置和附带资源的集合 |
| Session(会话) | 逻辑会话 | 代表用户的一段连续对话,可以包含多轮运行 |
| Run(运行) | 单次运行 | 一次具体的执行过程,由 SessionWorker 处理 |
| RuntimeProfile | 运行时配置模板 | 描述容器镜像、资源限制、执行模式等的模板 |
| LaunchPlan | 启动计划 | 一次运行的完整不可变描述,包含所有启动参数 |
| checkpoint | 检查点 | agentcore 保存的运行中间状态,用于后续恢复 |
| workspace | 工作区 | 每次运行独有的文件目录,存放输入和输出 |
| backend | 后端控制面 | 负责 API 接收、状态管理、会话持久化的服务进程 |
| Helper | 本地桥接服务 | 运行在用户 macOS 上的客户端,负责文件同步和本地 UI |
| TTL | 存活时间 | Time To Live 的缩写,指资源的自动过期时间 |
核心设计理念
在阅读后续文档前,请先记住以下三个关键设计原则:
- 逻辑连续按会话,物理隔离按运行:同一个 Session(会话)可以包含多次 Run(运行),每次运行都是一个全新的容器,但它们共享同一个会话状态目录。
- 镜像层负责“能跑“,状态层负责“能续“:容器镜像只包含运行时环境和工具;用户数据、对话历史、检查点文件等状态信息存储在外部挂载的目录中。
- 声明式编排,分层职责:后端只负责“声明需要什么资源“,真正创建和管理容器的是 K3s 系统本身。
文档阅读指南
本系列文档按照从整体到细节的顺序组织,建议按以下顺序阅读:
| 章节 | 内容 | 适合谁看 |
|---|---|---|
| 系统架构总览 | 各组件关系与整体数据流向 | 所有人 |
| 同步与异步语义 | 每类操作到底是同步、异步还是混合 | 产品、架构、开发 |
| 运行生命周期 | 从用户发消息到获得结果的完整流程 | 所有人 |
| 输入输出流程 | 数据如何进入容器、结果如何产出 | 产品经理、运维 |
| 文件挂载与访问 | 容器如何访问工作区、会话状态和开发者包 | 运维、开发者 |
| 外部文件访问 | 用户文件如何从本地进入运行流程 | 产品经理、运维 |
| 外部数据库访问 | Backend 如何统一访问数据库并通过协议提供给容器 | 运维、架构师 |
| 镜像固化 | 如何加速启动、哪些状态可以固化到镜像 | 架构师、运维 |
| AgentCore 与前端交互通信 | 用户消息如何到达运行中的智能体 | 所有人 |
| 技术可行性分析 | 各项关键技术的可行性评估与建议 | 架构师、决策者 |
如需深入了解代码层面的模块拆分,还可以参考以下技术附录:
系统架构总览
本章用一张图说清楚 K3s 运行时模块在整个系统中的位置,以及各个组件之间的关系。
1. 整体系统组成
整个智能体商店系统由四个主要部分组成:
graph TB
subgraph 用户侧["🖥️ 用户侧(macOS 本地)"]
User["用户"]
Helper["Helper(本地桥接服务)"]
UI["Web UI(浏览器界面)"]
end
subgraph 后端控制面["☁️ 后端控制面"]
API["HTTP API 路由层"]
Service["应用服务层<br/>ApplicationService"]
Store["状态存储<br/>FileBackedStore"]
DataGateway["数据访问网关<br/>统一查库/写库"]
PackageCatalog["Agent 包目录索引"]
ProfileCatalog["运行时配置索引"]
end
subgraph K3s运行时["⚙️ K3s 运行时模块"]
Orchestrator["K3s 编排器<br/>K3sOrchestrator"]
K3sAPI["Kubernetes API"]
end
subgraph 执行层["📦 容器执行层(K3s 集群)"]
DEP1["Deployment(长驻部署)"]
SVC1["Service(网络服务)"]
Pod1["Pod(容器组)"]
AgentCore["agentcore<br/>(智能体核心运行时)"]
end
User -->|"发送消息/上传文件"| UI
UI -->|"HTTP 请求"| Helper
Helper -->|"转发 API 请求"| API
API -->|"调用"| Service
Service -->|"读写状态"| Store
Service -->|"统一数据访问"| DataGateway
Service -->|"查询包"| PackageCatalog
Service -->|"查询配置"| ProfileCatalog
Service -->|"提交运行"| Orchestrator
Orchestrator -->|"创建资源"| K3sAPI
K3sAPI -->|"调度执行"| DEP1
DEP1 -->|"创建"| Pod1
SVC1 -->|"路由流量 :8081"| Pod1
Pod1 -->|"运行"| AgentCore
各组件职责一句话总结
| 组件 | 职责 |
|---|---|
| Helper(本地桥接服务) | 运行在用户 macOS 上,提供 Web UI,将用户操作转化为 API 请求发送到后端 |
| 后端控制面(Backend) | 接收请求、管理会话/文件/运行记录、组装启动计划、持久化所有状态,并统一负责数据库查询与写入 |
| K3s 编排器(K3sOrchestrator) | 将启动计划渲染成 Kubernetes 资源(Deployment + Service + ConfigMap + Secret),提交到集群,监控运行状态 |
| K3s 集群 | 真正调度和运行容器的底层系统 |
| agentcore(智能体核心运行时) | 容器内的执行程序,负责推理、工具调用、文件读写 |
2. 控制面统一数据访问
当前方案有一个必须先记住的原则:
所有数据库访问都由 Backend 负责,容器内的 Worker 不直接连接数据库。
这包括三类数据:
- 会话数据库:对话历史、RunRecord、SessionRuntimeState 等
- 检查点数据库:checkpoint、恢复快照、执行中间状态
- 业务数据库:PostgreSQL / MySQL / Redis / 向量库等外部数据源
对应关系如下:
graph LR
H["Helper / 用户请求"] --> B["Backend"]
W["Worker / agentcore"] -->|"协议请求"| B
B -->|"查询 / 写入"| DB1["会话数据库"]
B -->|"查询 / 写入"| DB2["检查点数据库"]
B -->|"查询 / 写入"| DB3["业务数据库"]
这样设计有四个直接好处:
- 安全更简单:数据库凭据只保留在 Backend,不进容器
- 协议更稳定:Worker 只需要理解“协议”,不需要关心数据库类型和地址
- 审计更集中:所有查库、写库都可以统一记录和限流
- 同步/异步更清晰:哪些操作必须等结果,哪些可以异步流式返回,都由 Backend 统一定义
3. 两层架构:控制面 + 执行面
当前系统采用**“单进程控制面 + K3s 执行面”**的架构:
graph LR
subgraph 控制面["控制面(Backend 进程)"]
direction TB
A1["接收 API 请求"]
A2["管理会话、文件和数据库访问"]
A3["组装 LaunchPlan(启动计划)"]
A4["持久化运行记录与检查点"]
end
subgraph 执行面["执行面(K3s 集群)"]
direction TB
B1["创建 ConfigMap / Secret / Deployment / Service"]
B2["调度 Pod 到节点"]
B3["拉取镜像、挂载目录"]
B4["启动 agentcore,通过协议与 Backend 通信"]
end
控制面 -->|"LaunchPlan"| 执行面
执行面 -->|"运行状态 / WebSocket 事件"| 控制面
关键理解:后端(Backend)不会直接创建容器。它只负责创建四种“声明式资源“——ConfigMap(配置映射)、Secret(密钥资源)、Deployment(部署)、Service(服务)。真正把这些声明变成运行中容器的,是 K3s 系统内部的控制器和节点代理程序。
同时,Backend 也不会把数据库直接暴露给容器。容器只拿到:
- 运行上下文文件(
run-context.json) - 必要的文件挂载
- 与 Backend 通信所需的协议地址
4. 同步 / 异步的总分工
从系统层面看,操作被分成三类:
| 操作 | 语义 | 例子 |
|---|---|---|
| 同步 | 发起方必须等待结果 | 创建会话、读取会话详情、Worker 请求 Backend 查库 |
| 异步 | 发出后继续执行,结果稍后返回 | 流式输出、状态变化、心跳、工具进度 |
| 混合 | 提交同步,执行异步 | 创建 Run、取消 Run、ask_user 等待用户回复 |
详细矩阵见:同步与异步语义
5. 代码模块分层
从代码角度看,K3s 相关的逻辑分成了两个独立的层:
graph TB
subgraph Backend应用层["Backend 应用层 (app crate)"]
SVC["service.rs<br/>应用服务层"]
K8S["k8s/mod.rs<br/>适配层"]
DOM["domain.rs<br/>领域类型"]
end
subgraph K3s运行时层["K3s 运行时 crate"]
LIB["lib.rs — 编排器门面"]
TYPES["types.rs — 输入输出类型"]
APPLY["apply.rs — 资源创建与清理"]
INSPECT["inspect.rs — 状态观察"]
WL_WK["workload_worker.rs — SessionWorker 渲染"]
VOL["volumes.rs — 卷布局"]
PKG["package_bundle.rs — ConfigMap/Secret 渲染"]
NAM["naming.rs — 资源命名"]
SB["service_bindings.rs — 外部服务绑定"]
end
SVC -->|"组装 LaunchPlan"| K8S
K8S -->|"转换成 crate 输入"| LIB
LIB --> APPLY
LIB --> INSPECT
LIB --> WL_WK
LIB --> VOL
LIB --> PKG
LIB --> NAM
WL_WK --> VOL
WL_WK --> PKG
WL_WK --> NAM
为什么要分成两层
这种分层设计带来三个明确的好处:
- 职责清晰:Backend 可以独立演进会话管理、权限校验等业务逻辑,不需要了解 Kubernetes API 的细节。
- 可替换执行层:如果未来需要把 K3s 替换成其他容器编排系统,只需要替换 K3s 运行时 crate,Backend 的代码基本不用改。
- 便于并行开发:不同开发者可以同时修改不同模块而不互相冲突。
6. 适配层的桥梁作用
Backend 和 K3s 运行时之间通过一个适配层(k8s/mod.rs)连接。适配层只做四件事:
| 序号 | 工作内容 | 方向 |
|---|---|---|
| 1 | 把 Backend 的进程配置(AppConfig)转换成编排器配置 | Backend → K3s |
| 2 | 把 Agent 包、运行时模板、挂载授权转换成 K3s crate 能理解的 DTO(数据传输对象) | Backend → K3s |
| 3 | 把 K3s crate 返回的渲染结果、集群状态、运行状态转换回 Backend 领域对象 | K3s → Backend |
| 4 | 把 K3s crate 的错误映射成 Backend 的 HTTP/API 错误 | K3s → Backend |
7. 一次运行涉及的 Kubernetes 资源
每次用户发起一个运行请求,K3s 编排器会创建以下四种资源:
graph TB
subgraph 一次Run的K8s资源["一次 Run(运行)对应的 K8s 资源"]
CM["ConfigMap<br/>📋 agent-pkg-<run-id><br/>存放 Agent 包的文本文件"]
SEC["Secret<br/>🔒 agent-env-<run-id><br/>存放 API 密钥等敏感信息"]
DEP["Deployment<br/>⚙️ agent-worker-<run-id><br/>定义长驻 Worker 容器"]
SVC["Service<br/>🌐 agent-worker-svc-<run-id><br/>暴露 8081 控制端口"]
POD["Pod<br/>📦 由 Deployment 自动创建<br/>运行 agentcore 的容器"]
end
CM -->|"挂载为文件目录"| POD
SEC -->|"注入为环境变量"| POD
DEP -->|"自动创建"| POD
SVC -->|"路由 :8081 流量"| POD
重要:Backend 只负责创建前四个(ConfigMap、Secret、Deployment、Service)。Pod(容器组)是由 Kubernetes 的 Deployment Controller(部署控制器)自动创建的,Backend 不直接操控 Pod。Deployment 通过 OwnerReference(所有者引用)级联管理所有关联资源——删除 Deployment 会自动清理 Service、ConfigMap 和 Secret。
8. 当前部署模型
当前系统采用本地部署 + 单节点 K3s 模式:
- 用户的本地机器就是宿主机——Backend 进程、K3s 集群、Helper 前端都运行在同一台机器上
- 用户通过本机浏览器访问 Helper 提供的 Web UI
- 所有工作区文件和会话状态存储在本机磁盘,容器通过 hostPath 直接读写
graph LR
subgraph LOCAL["用户的本地机器"]
H["Helper + Web UI<br/>(本地前端)"]
B["Backend 进程<br/>(本地后端)"]
K["K3s 集群<br/>(本地容器引擎)"]
D["本地磁盘<br/>uploads / workspaces / sessions"]
end
H -->|"localhost"| B
B -->|"Kubernetes API"| K
B -->|"读写文件"| D
K -->|"hostPath 挂载"| D
本地部署的优势:
- 零网络延迟:Helper → Backend → 容器全部通过 localhost 通信
- 文件无需传输:用户的本地文件可以直接通过 hostPath 挂载进容器
- 部署简单:用户只需安装一次,无需维护远程服务器
- 数据私密:所有数据留在用户本地机器,不经过网络
未来扩展:如果要支持远程部署或多节点集群,需要把文件存储从本地磁盘升级为共享存储(如 PVC 或网络文件系统),后续章节会详细分析这一点。
下一步
理解了整体架构后,请继续阅读 运行生命周期 了解一次运行从开始到结束的完整流程。
同步与异步语义
本章专门说明 K3s Runtime 里每类操作到底是同步还是异步。这是理解整套交互协议的关键。
当前统一原则:Backend(后端控制面)负责所有数据库查询与持久化,Worker(容器内 agentcore)不直接连接数据库。 Worker 只通过协议向 Backend 发起请求或上报事件。
1. 三种语义先分清
| 类型 | 含义 | 典型特征 |
|---|---|---|
| 同步 | 发起方必须等待结果,才能继续后续逻辑 | 有明确请求和响应,超时会中断当前步骤 |
| 异步 | 发起方发出后即可继续,不阻塞当前主流程 | 结果稍后以事件、回调或状态更新返回 |
| 混合 | 提交动作本身同步,但实际执行过程异步 | API 立即返回 accepted,结果稍后到达 |
注意:这里说的“同步/异步”是业务语义,不是底层网络库是不是
async/await。即使底层使用 WebSocket 异步 I/O,也可以承载“语义上同步”的请求-响应。
2. 总体规则
graph TB
A["用户 / Helper 发起操作"] --> B{"操作是否必须等结果?"}
B -->|"是"| C["同步语义<br/>请求 → 等待响应"]
B -->|"否"| D["异步语义<br/>提交 → 继续执行"]
D --> E["后续通过事件返回进度 / 结果"]
C --> F["超时或失败立即影响当前步骤"]
3. Helper → Backend 操作矩阵
| 操作 | 语义 | 原因 |
|---|---|---|
健康检查 /health | 同步 | 需要立即知道服务是否可用 |
创建会话 /sessions | 同步 | 需要立即拿到 session_id |
获取会话 /sessions/{id} | 同步 | 读取型查询,返回即完成 |
上传附件 /artifacts/upload | 同步 | 需要先确认文件已被 Backend 接收 |
创建挂载授权 /mount-grants | 同步 | 需要先确认路径校验和授权结果 |
创建运行 /runs | 混合 | 请求提交同步返回 run_id,真正执行异步发生 |
查询运行 /runs/{id} | 同步 | 读取当前状态快照 |
取消运行 /runs/{id}/cancel | 混合 | 接收取消请求同步返回,Worker 实际停止异步完成 |
关闭会话 /sessions/{id}/close | 混合 | 归档动作同步完成,Worker 退出异步完成 |
重开会话 /sessions/{id}/reopen | 同步 | 状态切换立即生效 |
4. Backend → Worker 协议矩阵
| 协议消息 | 语义 | 说明 |
|---|---|---|
push_message | 异步 | Backend 把用户消息推给 Worker,执行结果稍后返回 |
cancel_task | 异步 | Backend 发出取消信号,不等待瞬时完成 |
ping | 异步 | 心跳探测,不改变业务流程 |
backend_query_result | 同步响应 | 用于回应 Worker 发起的同步查询 |
backend_command_result | 同步响应 | 用于回应 Worker 发起的同步写入/变更 |
checkpoint_get_result | 同步响应 | 返回恢复执行所需的检查点数据 |
checkpoint_put_ack | 同步响应 | 确认 Backend 已持久化检查点 |
5. Worker → Backend 协议矩阵
| 协议消息 | 语义 | 说明 |
|---|---|---|
state_change | 异步 | Worker 上报状态变化,Backend 记录即可 |
stream_chunk | 异步 | 高频流式片段,不阻塞 Worker 主流程 |
message_response | 异步 | 最终回复事件,由 Backend 接收后写入会话历史 |
ask_user | 混合 | 发送本身异步,但 Worker 会暂停,等待后续 user_reply |
tool_call / tool_progress | 异步 | 通知型事件,便于前端展示进度 |
error | 异步 | 失败事件,Backend 收到后再统一处理 |
backend_query | 同步 | Worker 请求 Backend 查询数据库,必须等结果再继续 |
backend_command | 同步 | Worker 请求 Backend 执行写操作,必须等确认 |
checkpoint_get | 同步 | Worker 恢复状态时请求历史检查点 |
checkpoint_put | 同步 | Worker 发送检查点并等待持久化 ACK,确保可恢复性 |
pong | 异步 | 心跳响应 |
6. 为什么“查库”必须是同步语义
当前方案下,业务数据库、会话数据库、检查点数据库都由 Backend 统一访问,因此 Worker 的“取数据”动作本质上是:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: backend_query
B->>DB: 查询数据库
DB-->>B: 返回结果
B-->>W: backend_query_result
这类操作必须定义成同步语义,原因有三个:
- Worker 后续推理往往依赖查询结果,不能“先继续再补结果”。
- Backend 可以在同一次响应里附带权限校验、数据裁剪和审计结果。
- 超时、权限不足、数据不存在等错误必须立刻返回给 Worker,便于当前步骤决定重试、降级或询问用户。
7. 为什么“流式输出”必须是异步语义
stream_chunk、state_change、tool_progress 这类消息的目标是边执行边展示。如果把它们设计成同步语义,会导致:
- Worker 每输出一个 token 都要等待确认
- Backend 和前端很容易成为吞吐瓶颈
- 整个回复速度明显下降
因此这些消息统一采用异步事件语义:尽快发送、尽快渲染,必要时依靠 WebSocket 自身的 TCP 背压控制限速。
8. 需要特别注意的“混合语义”操作
| 操作 | 为什么是混合 |
|---|---|
| 创建运行 | API 层要立刻返回 run_id,但容器启动和执行要异步完成 |
| ask_user | 事件推送本身异步,但 Worker 的业务流程会暂停等待用户答复 |
| 取消运行 | 提交取消请求同步成功,不代表 Worker 已经立刻退出 |
| 关闭会话 | 会话状态可以立刻改成 closed,但 Deployment 回收还需要异步完成 |
9. 当前推荐约束
为了让协议清晰且稳定,推荐遵循以下约束:
- 所有数据库读写统一走 Backend,Worker 不直接持有数据库 DSN、账号和驱动配置。
- 所有需要“等结果才能继续”的协议消息都必须有明确响应消息,不能只靠超时猜测结果。
- 所有高频输出都采用异步事件,不要为每个 token 设计同步确认。
- 所有长任务都拆成“启动 + 进度 + 完成”三段式,不要让单次同步请求占住连接太久。
下一步
理解了同步/异步语义后,建议继续阅读:
运行生命周期
本章从用户视角出发,完整描述“用户发一条消息后,系统到底做了什么“。系统采用 SessionWorker(会话工作进程)模式——容器以 Deployment(部署)方式长驻运行,通过 WebSocket(网络套接字) 双向通道与 Backend 实时通信。
1. SessionWorker 核心特性
| 特性 | 说明 |
|---|---|
| K8s 资源类型 | Deployment(部署)+ Service(服务)→ 长驻 Pod |
| 容器生命周期 | 跨越多轮对话持续运行,按空闲策略自动回收 |
| 通信方式 | WebSocket 双向:Worker 启动后主动连接 Backend WS 端点,消息和事件通过同一持久连接传输 |
| 实时交互 | ✅ 支持 agentcore 向用户提问、请求权限确认 |
| 流式输出 | ✅ token 片段实时推送,用户即时看到推理过程 |
| 状态连续性 | 内存中直接保持 + Worker 通过协议把 checkpoint 持久化到 Backend |
| 安全机制 | 非 root 运行、丢弃所有 capabilities、seccomp 限制 |
2. 首次启动:从用户消息到容器就绪
sequenceDiagram
participant U as 用户
participant H as Helper(本地前端)
participant B as Backend(本地后端)
participant K as K3s 编排器
participant W as SessionWorker(长驻容器)
rect rgb(230, 245, 255)
Note over U,W: ① 创建会话
U->>H: 选择智能体,开始对话
H->>B: POST /api/v1/sessions
B->>B: 创建 SessionRecord,初始化会话状态目录
B-->>H: 返回 session_id
end
rect rgb(255, 245, 230)
Note over U,W: ② 发送首条消息
U->>H: 输入消息
H->>B: POST /api/v1/runs
Note over B: 校验会话 → 解析 Agent 包<br/>→ 合并 API 密钥 → 查库生成 run-context.json<br/>→ 组装 LaunchPlan
end
rect rgb(230, 255, 230)
Note over B,W: ③ K3s 编排器创建资源
B->>K: 提交 LaunchPlan
K->>K: 渲染 Deployment + Service
K->>W: 创建 ConfigMap(包文件)
K->>W: 创建 Secret(API 密钥)
K->>W: 创建 Deployment(Worker Pod)
K->>W: 创建 Service(探针 + 备用端口)
end
rect rgb(255, 230, 255)
Note over W: ④ Worker 启动流程
W->>W: 创建工作目录
W->>W: 写入运行时元数据文件
W->>W: touch /tmp/agent-store-worker.ready
W->>W: 注册 TERM 信号处理器
W->>W: 启动心跳循环(每 30 秒)
W->>W: 启动 agentcore 进程
W->>B: 建立 WebSocket 连接 ws://backend/ws/workers/RUN_ID
W->>B: WS state_change IDLE
W->>W: 读取 /workspace/in/run-context.json
Note over W: readinessProbe 通过 → Pod 就绪
end
rect rgb(230, 245, 255)
Note over B,W: ⑤ 推送首条消息并返回结果
B->>B: 检测到 Worker IDLE 且 WS 连接就绪
B->>W: WS push_message(用户消息)
W->>W: agentcore 调用大模型推理
W-->>B: WS stream_chunk × N
B-->>H: 转发流式内容
H-->>U: 实时显示 AI 回复
W->>B: WS message_response isComplete
B->>B: 写入数据库
end
3. 后续对话:Worker 已就绪
当 Worker 已处于 IDLE 状态时,后续消息无需重新创建 Deployment,延迟极低:
sequenceDiagram
participant U as 用户
participant H as Helper
participant B as Backend
participant W as Worker Pod
U->>H: 继续对话
H->>B: POST /api/v1/runs(同一 session_id)
B->>B: 检测到 Worker 已就绪(IDLE)
B->>W: WS push_message(新消息 + 更新的上下文)
W->>W: agentcore 处理
W-->>B: WS stream_chunk + message_response
B-->>H: 转发
H-->>U: 实时显示
Note over U,W: 全程在 localhost,延迟在毫秒级
4. 交互式通信:agentcore 向用户提问
这是 SessionWorker 最重要的能力之一——agentcore 可以在执行过程中主动向用户发起请求:
sequenceDiagram
participant U as 用户
participant H as Helper
participant B as Backend
participant W as Worker Pod
Note over W: agentcore 执行工具前需要用户确认
W->>B: WS ask_user 事件
B->>B: 标记 Worker 状态 → WAITING_USER
B->>H: WS 推送询问
H->>U: 弹出确认对话框
alt 用户同意
U->>H: 点击"确认"
H->>B: POST 用户回复
B->>W: WS user_reply(用户确认)
B->>B: 标记 Worker 状态 → BUSY
W->>W: 继续执行
else 用户拒绝
U->>H: 点击"取消"
H->>B: POST 用户拒绝
B->>W: WS user_reply(用户拒绝)
W->>W: 跳过该操作,继续推理
end
典型场景:
- 删除文件前请求确认
- 需要用户提供额外信息(如数据库连接串)
- 执行高风险操作前的授权确认
- 多选题或分支决策
5. 分阶段详解
阶段一:校验与准备(Backend 负责)
当 Backend 收到创建运行的请求后,依次执行:
- 校验 Session(会话):确认会话存在且未关闭
- 解析 agent package(智能体包):从包目录索引中读取开发者包
- 解析 RuntimeProfile(运行时配置模板):确定基础镜像和资源配额
- 合并 provider 配置:将 API 密钥从环境变量、运行时模板、本次请求三层合并
- 校验输入文件和挂载:确认文件存在,挂载路径在白名单内
阶段二:创建工作区(Backend 负责)
Backend 为本次运行创建独立的工作区目录:
workspaces/[run-id]/
in/ ← 输入文件:附件 + run-context.json
out/ ← 输出文件(SessionWorker 模式下主要通过回调返回)
tmp/ ← 临时文件
同时从数据库中提取数据,生成 in/run-context.json(详见 输入输出流程)。
阶段三:组装 LaunchPlan(启动计划)
LaunchPlan 是整个运行的完整不可变描述:
| 类别 | 内容 |
|---|---|
| 身份与路由 | run_id、session_id、user_id、package_name |
| 运行时与包 | 镜像地址、容器入口、资源限制、Agent 包文件 |
| 文件与状态 | 工作区路径、可选的本地缓存路径、checkpoint 恢复策略 |
| 编排策略 | 卷后端类型(HostPath / PVC)、镜像模式、协议通道类型 |
| 交互配置 | worker_event_channel(事件通道)、worker_idle_policy(空闲策略) |
| 集群目标 | namespace、service account、是否真实提交 |
阶段四:K3s 编排器渲染和提交
编排器执行以下步骤:
workload_worker.rs组装 Deployment + Service 定义- 容器暴露 8081 端口(control 控制端口)
- 配置 readiness/liveness 探针(检查
/tmp/agent-store-worker.ready标记文件) - 注入所有环境变量(事件通道、空闲策略、包信息、会话信息等)
apply.rs按顺序创建:ConfigMap → Secret → Service → Deployment- 设置 OwnerReference(Deployment 拥有 Service、ConfigMap、Secret)
阶段五:容器内执行
graph TB
START[Pod 启动] --> DIRS[创建工作目录]
DIRS --> META[写入运行时元数据]
META --> READY["创建 ready 标记<br/>/tmp/agent-store-worker.ready"]
READY --> TRAP[注册 TERM 信号处理]
TRAP --> BEAT[启动心跳循环]
BEAT --> SERVE[建立 WebSocket 连接到 Backend]
SERVE --> CTX[读取 run-context.json]
CTX --> WAIT[等待 Backend 推送消息]
WAIT --> RECV{收到消息}
RECV -->|用户消息| INFER[调用大模型推理]
RECV -->|用户回复| RESUME[恢复被暂停的执行]
RECV -->|TERM 信号| DRAIN[优雅关闭]
INFER --> NEED{需要用户输入?}
NEED -->|是| ASK[WS 发送 ask_user 事件]
ASK --> WAIT
NEED -->|否| STREAM[流式推送 token]
STREAM --> TOOLS[执行工具调用]
TOOLS --> DBREQ{需要查库/写库?}
DBREQ -->|是| SYNCREQ[发送 backend_query / backend_command]
SYNCREQ --> DBRESP[等待 Backend 响应]
DBRESP --> DONE[WS 发送 message_response]
DBREQ -->|否| DONE
DONE --> SAVE[发送 checkpoint_put 并等待 ACK]
SAVE --> WAIT
RESUME --> INFER
DRAIN --> SAVE_EXIT[发送最终 checkpoint_put 后退出]
阶段六:状态回收
| 行为 | 说明 |
|---|---|
| 结果写入 | Backend 实时接收协议事件,写入数据库 |
| Worker 状态更新 | Backend 通过 K8s 状态观察 + 协议事件共同更新 Worker 生命周期状态 |
| 资源清理 | Worker DRAINING 后自动退出,K8s 清理 Deployment + Service + ConfigMap + Secret |
| 会话连续性 | 内存状态持续保持;checkpoint 通过协议写入 Backend 管理的数据库 |
各阶段的同步 / 异步定义
| 阶段 | 语义 | 说明 |
|---|---|---|
| 创建 Session | 同步 | 需要立即返回 session_id |
| 提交 Run | 混合 | API 返回 run_id 是同步,Worker 启动和执行是异步 |
| K3s 创建资源 | 异步 | Deployment 提交成功不代表 Pod 已立刻就绪 |
| Worker 建立 WS 连接 | 异步 | 连接建立后由 state_change(IDLE) 告知 Backend |
| Backend 推送消息 | 异步 | 推送后结果稍后通过流式事件返回 |
| Worker 查库 / 取 checkpoint | 同步 | 必须等 Backend 响应才能继续 |
| Worker 流式输出 | 异步 | token 片段持续推送,不阻塞推理主循环 |
| Worker 保存 checkpoint | 同步 | 发送 checkpoint_put 后等待 ACK,确保状态可恢复 |
| ask_user 交互 | 混合 | 发起提问异步,但业务流程暂停等待回复 |
| 空闲回收 | 异步 | Backend / K8s 触发回收,Worker 稍后完成退出 |
6. Worker 生命周期状态机
stateDiagram-v2
[*] --> PENDING : Backend 创建 Deployment
PENDING --> IDLE : Pod 就绪, ready 标记文件存在
IDLE --> BUSY : Backend 通过 WS 推送消息
BUSY --> WAITING_USER : agentcore 回调 ask_user
WAITING_USER --> BUSY : 用户回复经 WS 推送
BUSY --> IDLE : 任务完成
IDLE --> DRAINING : 空闲超时, idle_ttl_seconds
DRAINING --> [*] : 优雅关闭完成
BUSY --> FAILED : 执行出错
PENDING --> FAILED : Pod 启动失败
IDLE --> FAILED : livenessProbe 失败
| 状态 | 含义 | Backend 行为 |
|---|---|---|
| PENDING | Deployment 已创建,Pod 尚未就绪 | 等待,轮询 Pod 状态 |
| IDLE | 容器就绪,等待消息 | 可以推送新消息 |
| BUSY | 正在执行用户请求 | 接收流式事件,不发新消息 |
| WAITING_USER | agentcore 在等待用户回复 | 转发询问给用户,等待回复 |
| DRAINING | 空闲超时,正在优雅关闭 | 不再推送消息,等待退出 |
| FAILED | 容器启动或执行失败 | 上报错误,可能需要重新创建 |
7. 空闲回收与优雅关闭
7.1 空闲策略(WorkerIdlePolicy)
为避免长驻容器浪费资源,通过空闲策略控制回收:
| 参数 | 类型 | 说明 |
|---|---|---|
idle_ttl_seconds | 必填 | 空闲多少秒后自动关闭(如 300 = 5 分钟) |
max_lifetime_seconds | 可选 | 容器最大存活时间(如 3600 = 1 小时) |
7.2 优雅关闭流程
当容器收到 TERM 信号时(空闲超时、手动取消或 K8s 重调度):
1. SIGTERM 信号到达
2. 写入时间戳到 /workspace/session/worker-draining.log
3. 删除 /tmp/agent-store-worker.ready 标记文件
4. readinessProbe 立即失败 → K8s 停止路由新请求到此 Pod
5. 当前任务完成(如果有),通过 `checkpoint_put` 把状态发给 Backend
6. 进程退出(exit 0)
7. K8s 等待 terminationGracePeriodSeconds(30 秒)后 SIGKILL
7.3 Worker 重启恢复
当用户在 Worker 关闭后继续对话,Backend 会自动创建新的 Deployment:
- 新 Worker 启动,Backend 从数据库读取会话历史和最近 checkpoint 摘要,生成
run-context.json - Worker 建立 WebSocket 连接后,如需完整恢复数据,发送
checkpoint_get - Backend 查询数据库并返回最新可用 checkpoint
- agentcore 根据返回结果恢复执行状态
- 从用户角度看,对话无缝继续
8. 多轮对话的连续性
8.1 内存级连续性
容器不退出期间,agentcore 在内存中保持:
- 完整的对话上下文
- 大模型的会话状态
- 工具调用结果缓存
8.2 Backend 统一持久化
当前方案下,checkpoint 与会话状态的权威存储都在 Backend 管理的数据库中。
数据流如下:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: checkpoint_put
B->>DB: 写入 checkpoint / session state
DB-->>B: 写入成功
B-->>W: checkpoint_put_ack
恢复时则反过来:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: checkpoint_get
B->>DB: 查询最新 checkpoint
DB-->>B: 返回 snapshot
B-->>W: checkpoint_get_result
这意味着:
- 容器内不需要直接打开数据库文件
- 容器内不需要持有数据库连接信息
- Backend 统一负责权限、审计和数据裁剪
本地文件目录仍然可以保留给缓存、日志、临时产物使用,但它不再是权威状态来源。
9. 会话的关闭与重新打开
| 操作 | 效果 | 数据影响 |
|---|---|---|
| close(关闭) | 标记为已归档;Worker 收到 TERM 信号优雅退出 | 历史、文件、状态目录全部保留 |
| reopen(重开) | 恢复为活跃状态 | 可继续发送消息,Worker 按需重新启动 |
10. 运行状态流转
stateDiagram-v2
[*] --> PENDING : 创建 RunRecord
PENDING --> SUBMITTED : K3s Deployment 创建成功
PENDING --> PREVIEWED : dry-run 模式, 仅渲染 YAML
SUBMITTED --> COMPLETED : Worker 回调 message_response
SUBMITTED --> FAILED : 执行失败
SUBMITTED --> CANCELLED : 用户取消
| 状态 | 含义 |
|---|---|
| PENDING | 已记录但未提交到 K3s |
| SUBMITTED | Deployment + Service 已创建或 Worker 已就绪 |
| PREVIEWED | 只渲染了 YAML,未真正提交(dry-run 调试) |
| COMPLETED | 正常完成 |
| FAILED | 执行出错 |
| CANCELLED | 用户取消,已清理资源 |
下一步
了解了完整的生命周期后,请继续阅读 输入输出流程。
输入输出流程
本章说明每次运行(Run)到底传了什么给容器、容器又产出了什么。如果你想了解“数据是怎么进去的、结果是怎么出来的“,这里是最完整的答案。
重要背景:系统的核心数据(对话历史、会话状态、运行记录、checkpoint 元数据等)统一由 Backend 管理并查询数据库。传递给容器的文件(如
run-context.json)只是启动时的上下文快照;容器后续如需更多数据或要持久化状态,均通过 Worker ↔ Backend 协议完成,而不是直接查库。
1. 数据流全景
graph LR
subgraph CONTROL["控制面"]
DB["Backend 数据库"]
BACKEND["Backend 组装"]
LP[LaunchPlan]
end
subgraph K3S_ORCH["K3s 编排"]
CM[ConfigMap<br/>Agent 包文件]
SEC[Secret<br/>API 密钥]
DEP["Deployment + Service"]
end
subgraph INSIDE["Worker 容器"]
IN["/workspace/in/<br/>run-context.json + 附件"]
EXEC["agentcore"]
CB["WebSocket → Backend"]
SESS["协议化状态访问<br/>checkpoint / query / command"]
end
DB --> BACKEND --> LP
LP --> CM
LP --> SEC
LP --> DEP
DEP --> IN
CM -->|挂载为目录| IN
SEC -->|注入为环境变量| EXEC
IN --> EXEC
EXEC --> CB
EXEC --> SESS
2. 输入:进入容器的内容
2.1 run-context.json(运行上下文投递文件)
这是 agentcore 的初始输入,由 Backend 在每次 Run 前从数据库中提取数据生成:
| 字段 | 数据来源 | 说明 |
|---|---|---|
sessionId | 数据库 SessionRecord | 所属会话的唯一标识 |
latestRunId | 数据库 SessionRecord | 会话中最近一次 Run 的 ID |
history | 数据库 SessionRecord.history | 截止到本轮为止的完整对话历史 |
sessionRuntimeState | 数据库 SessionRecord.runtime_state | 会话运行时状态(含 checkpoint 信息) |
package | 数据库 PackageCatalog | Agent 包声明:名称、描述、工具列表等 |
builtInTools | 数据库 LoadedPackage | Agent 包内置工具定义 |
envFacts | 数据库 LoadedPackage | 环境事实(如运行时版本信息) |
inputArtifacts | 本次请求 | 用户上传的附件文件名列表 |
systemPrompt | 数据库 PackageCatalog | Agent 包开发者定义的系统提示词 |
userPrompt | 本次请求 | 用户本轮发送的文本消息 |
数据流向:数据库 → Backend write_run_context() → JSON 文件 → 容器 /workspace/in/run-context.json
为什么用文件传递初始上下文? 容器启动前 Backend 无法直接与容器通信。
run-context.json用于将完整的会话历史和上下文“搬运“进容器,使 agentcore 启动后立刻拥有完整的上下文。后续的用户消息通过 HTTP 推送到容器的 8081 端口。
2.2 用户上传的附件
用户通过 Helper 上传的文件会被 Backend 复制到工作区的 in/ 目录:
/workspace/in/
run-context.json ← 从数据库生成的初始上下文
report.pdf ← 用户上传
data.csv ← 用户上传
2.3 Agent 包文件
开发者在 Agent 包中定义的所有文件(提示词模板、工具脚本、配置文件等),通过 ConfigMap 挂载到容器内的 /workspace/pkg/ 目录:
/workspace/pkg/
system-prompt.md ← 系统提示词
tools.json ← 工具声明
helper-script.py ← 辅助脚本
2.4 API 密钥与凭据
大模型 API 密钥等敏感信息通过 Secret 以环境变量形式注入,不会写入文件系统:
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
凭据来源优先级:本次请求参数 > RuntimeProfile 模板 > 进程环境变量。
2.5 后续用户消息(协议推送,异步)
Worker 就绪后,后续用户消息不再写入文件,而是由 Backend 通过协议直接推送给 Worker。
{
"type": "push_message",
"id": "msg-001",
"timestamp": "2026-04-17T02:30:00Z",
"payload": {
"runId": "019xxx",
"sessionId": "019xxx",
"content": "请帮我分析这个数据集",
"artifacts": ["data.csv"]
}
}
语义:这是异步操作。Backend 负责把消息送达 Worker,但真正的推理和结果返回稍后发生。
2.6 会话状态(多轮场景)
如果不是第一轮运行,Worker 会从两个地方获得连续性信息:
run-context.json初始快照:由 Backend 查库后生成- 协议化状态请求:Worker 运行时按需向 Backend 请求更完整的 checkpoint 或业务数据
这意味着容器不再把“数据库文件”当作主要状态来源。权威状态仍在 Backend 管理的数据库中。
2.7 运行时数据请求(同步)
当 Worker 在执行过程中需要更多数据时,会通过协议向 Backend 发起同步请求:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: backend_query / checkpoint_get
B->>DB: 查询
DB-->>B: 返回结果
B-->>W: backend_query_result / checkpoint_get_result
语义:这是同步操作。因为 Worker 必须等到结果,才能继续后续推理或恢复。
3. 输出:结果如何返回
agentcore 通过 WebSocket(网络套接字)双向通道 将结果实时推送给 Backend;需要持久化的 checkpoint 也通过协议发送给 Backend,由 Backend 统一写入数据库。
3.1 WebSocket 实时输出
agentcore 启动后主动连接 Backend 的 WebSocket 端点 ws://backend/ws/workers/{run_id},所有输出事件通过同一条持久连接推送:
sequenceDiagram
participant A as agentcore
participant B as Backend
participant DB as 数据库
participant H as Helper
rect rgb(230, 255, 230)
Note over A,H: 流式输出
loop 推理过程中
A->>B: WS stream_chunk
B->>H: WS 转发 token 片段
H->>H: 实时渲染
end
end
rect rgb(230, 245, 255)
Note over A,H: 最终结果
A->>B: WS message_response
Note over A,B: 携带完整回复内容 + 渲染格式
B->>DB: 追加到 session.history
B->>H: WS 推送完成通知
end
rect rgb(255, 245, 230)
Note over A,B: 持久化
A->>B: checkpoint_put
B->>DB: 写入 checkpoint 数据库
B-->>A: checkpoint_put_ack
end
3.2 事件类型
| 事件类型 | 说明 | 典型内容 |
|---|---|---|
message_response | agentcore 的最终回答 | 完整文本 + 渲染格式(Markdown/代码/表格等) |
stream_chunk | 流式推理片段 | token 级文本片段 |
ask_user | 向用户提问 | 问题文本 + 可选选项列表 |
tool_call | 工具调用通知 | 工具名、参数、执行结果 |
state_change | 状态变更 | Worker 生命周期状态(BUSY/IDLE/WAITING_USER) |
error | 运行出错 | 错误类型 + 错误信息 |
3.3 消息格式
{
"runId": "019xxx",
"sessionId": "019xxx",
"workerName": "agent-worker-abc123",
"eventType": "message_response",
"payload": {
"content": "根据数据分析,销售额环比增长了 15%...",
"format": "markdown",
"isComplete": true
},
"timestamp": "2025-01-15T10:30:00Z"
}
与文件输出的区别:agentcore 的响应直接通过 WebSocket 发送给 Backend,不再写入
final-answer.md等文件。这使得:
- 支持多种渲染格式(Markdown、代码块、表格、图表等),前端可以根据
format字段选择渲染方式- 支持流式推送,用户无需等待完整结果(WebSocket 帧开销仅 2-10 字节,比 HTTP 请求头 300+ 字节大幅降低)
- 支持中间交互,agentcore 可以随时发送
ask_user事件
3.4 Checkpoint 持久化(同步写入 Backend)
Worker 不直接把 checkpoint 写到数据库文件,而是通过协议把 checkpoint 快照发送给 Backend:
{
"type": "checkpoint_put",
"id": "ckp-001",
"timestamp": "2026-04-17T02:30:05Z",
"payload": {
"sessionId": "019xxx",
"runId": "019xxx",
"snapshot": {
"historyCursor": 12,
"toolState": {},
"memoryState": {}
}
}
}
随后 Backend:
- 校验该 checkpoint 是否属于当前 Session / Run
- 写入 Backend 管理的数据库
- 返回
checkpoint_put_ack - 更新
SessionRuntimeState
语义:这是同步操作。Worker 只有在收到 ACK 后,才能认为这份状态已经可恢复。
4. 完整输入输出汇总
graph TB
subgraph INPUT["输入(进入 Worker)"]
RC["run-context.json<br/>(从数据库生成)"]
ATTACH["用户附件<br/>(上传或挂载)"]
PKG["Agent 包文件<br/>(ConfigMap 挂载)"]
KEYS["API 密钥<br/>(Secret 环境变量)"]
MSG["后续消息<br/>(WebSocket 推送)"]
end
subgraph OUTPUT["输出(从 Worker 返回)"]
CB_OUT["WebSocket 事件<br/>(结果 + 流式 + 交互)"]
CKP["Checkpoint 协议写回<br/>(Backend 持久化)"]
LOGS["日志/本地缓存<br/>(可选本地文件)"]
end
RC --> EXEC[agentcore]
ATTACH --> EXEC
PKG --> EXEC
KEYS --> EXEC
MSG --> EXEC
EXEC --> CB_OUT
EXEC --> CKP
EXEC --> LOGS
5. LaunchPlan 的完整结构
LaunchPlan(启动计划)是 Backend 传递给 K3s 编排器的核心数据结构:
LaunchPlan
├── identity
│ ├── run_id: UUID
│ ├── session_id: UUID
│ ├── user_id: String
│ └── package_name: String
├── runtime
│ ├── image: String // 容器镜像地址
│ ├── install_strategy: enum // 运行时安装策略
│ ├── execution_mode: enum // 执行模式
│ ├── requests/limits // CPU、内存、磁盘资源
│ └── family: RuntimeFamily // 运行时系列(Codex/ClaudeCode)
├── package_spec
│ ├── files: Map<String,String> // Agent 包文件映射
│ └── verify_args: Vec<String> // 验证命令参数
├── paths
│ ├── workspace_host_path // 本机工作区根目录
│ ├── session_state_host_path // 本机会话状态目录
│ ├── checkpoint_mount_path // 容器内 checkpoint 路径
│ └── mount_grant: Option // 用户授权挂载
├── strategy
│ ├── volume_backend: HostPath // 卷后端(本地部署使用 HostPath)
│ ├── runtime_image_mode // 镜像获取模式
│ └── checkpoint_store_mode // Checkpoint 存储模式
├── interaction
│ ├── worker_event_channel // 事件通道(WebSocket / HTTP 回调 / 队列消费)
│ └── worker_idle_policy // 空闲回收策略
├── services
│ ├── provider_env: Map // 大模型 API 密钥
│ └── external_service_bindings // 外部服务绑定(数据库等)
└── cluster
├── namespace: String // K8s 命名空间
├── service_account: String // 服务账号
└── apply_to_cluster: bool // 是否真正提交
6. 渲染后的 K8s 资源示例
一次真实运行会产生以下 K8s 资源:
ConfigMap(存储 Agent 包文件)
apiVersion: v1
kind: ConfigMap
metadata:
name: agent-pkg-abc123
data:
system-prompt.md: |
你是一个数据分析助手...
tools.json: |
[{"name": "python_exec", ...}]
Secret(存储凭据)
apiVersion: v1
kind: Secret
metadata:
name: agent-env-abc123
stringData:
OPENAI_API_KEY: sk-xxx
Deployment + Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-worker-abc123
ownerReferences: [] # 级联删除:删 Deployment 自动清理所有资源
spec:
replicas: 1
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: worker
image: registry.example.com/agentcore:latest
ports:
- containerPort: 8081
name: control
protocol: TCP
readinessProbe:
exec:
command: ["bash", "-lc", "test -f /tmp/agent-store-worker.ready"]
initialDelaySeconds: 2
periodSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command: ["bash", "-lc", "test -f /tmp/agent-store-worker.ready"]
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
env:
- name: AGENT_WORKLOAD_KIND
value: session-worker
- name: AGENT_WORKER_EVENT_CHANNEL_KIND
value: websocket
- name: AGENT_WORKER_WS_URL
value: ws://host.k3s.internal:PORT/ws/workers/RUN_ID
envFrom:
- secretRef:
name: agent-env-abc123
volumeMounts:
- name: workspace
mountPath: /workspace
- name: pkg
mountPath: /workspace/pkg
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: agent-worker-svc-abc123
spec:
type: ClusterIP
ports:
- port: 8081
targetPort: control
protocol: TCP
selector:
app: agent-worker-abc123
下一步
了解了输入输出后,请继续阅读 文件挂载机制 了解容器如何访问本机文件。
文件挂载机制
本章说明容器内部的文件系统是怎么构造出来的。每个容器里看到的 /workspace 目录并不是凭空出现的,而是通过 K8s 的 Volume(卷)机制把宿主机(运行 K3s 的物理机器)上的目录映射进去的。
1. 容器内的文件系统视图
agentcore 启动后看到的文件结构:
/workspace/ ← 容器根工作目录
├── in/ ← 输入(只读区域)
│ ├── run-context.json ← Backend 从数据库生成的运行上下文
│ ├── report.pdf ← 用户上传的文件
│ └── data.csv ← 用户上传的文件
├── out/ ← 输出(agentcore 写入)
│ └── artifacts/ ← agentcore 生成的文件(可选)
├── tmp/ ← 临时目录
├── session/ ← 本地缓存与日志(可选)
│ ├── cache/ ← 本地缓存
│ └── logs/ ← Worker 日志与排障信息
├── pkg/ ← Agent 包文件(只读)
│ ├── system-prompt.md
│ └── tools.json
└── mounts/ ← 用户授权的外部目录(可选)
└── my-data/ ← 用户授权挂载的本地目录
2. 四类挂载卷
系统使用四种不同的 Volume(卷)来构建上述文件结构:
graph TB
subgraph INSIDE["容器内"]
WS["/workspace<br/>hostPath 卷"]
SESS["/workspace/session<br/>hostPath 卷"]
PKG["/workspace/pkg<br/>ConfigMap 卷"]
MNT["/workspace/mounts/xxx<br/>hostPath 卷"]
end
subgraph HOST_SIDE["宿主机"]
H_WS["workspaces/[run-id]/"]
H_SESS["sessions/[session-id]/"]
H_PKG["ConfigMap 资源<br/>(K3s 管理)"]
H_MNT["用户指定的本地目录"]
end
H_WS -.->|挂载| WS
H_SESS -.->|挂载| SESS
H_PKG -.->|挂载| PKG
H_MNT -.->|挂载| MNT
2.1 工作区卷(Workspace Volume)
| 属性 | 说明 |
|---|---|
| 类型 | hostPath(宿主机目录挂载) |
| 宿主机路径 | workspaces/[run-id]/ |
| 容器挂载点 | /workspace |
| 生命周期 | 随 Run 创建,Run 结束后保留供结果读取 |
| 读写权限 | 可读可写 |
这是每次运行的独占工作区,不同 Run 之间互不共享。
2.2 会话本地目录卷(Session Local Volume)
| 属性 | 说明 |
|---|---|
| 类型 | hostPath |
| 宿主机路径 | sessions/[session-id]/ |
| 容器挂载点 | /workspace/session |
| 生命周期 | 随 Session 创建,可跨 Run 保留 |
| 读写权限 | 可读可写 |
这个目录主要用于缓存、日志、临时产物等本地文件。
当前方案下,多轮连续性的权威状态由 Backend 查询数据库并通过协议提供给 Worker,而不是依赖容器直接读取这里的数据库文件。
2.3 包文件卷(Package Volume)
| 属性 | 说明 |
|---|---|
| 类型 | ConfigMap(K8s 配置映射资源) |
| 数据来源 | Agent 包中的所有文件 |
| 容器挂载点 | /workspace/pkg |
| 生命周期 | 随 Run 创建和销毁 |
| 读写权限 | 只读 |
Agent 包文件不是从宿主机目录挂载的,而是通过 K8s 的 ConfigMap 机制以“虚拟文件“形式出现在容器内。这种方式不依赖文件在宿主机的具体路径。
限制:ConfigMap 单个资源有 1 MB 的大小限制。如果 Agent 包文件总量超过此限制,需要考虑其他方案。
2.4 用户挂载卷(Mount Grant Volume)
| 属性 | 说明 |
|---|---|
| 类型 | hostPath |
| 宿主机路径 | 用户通过 mount_grant 指定的路径 |
| 容器挂载点 | /workspace/mounts/[名称] |
| 生命周期 | 仅本次 Run |
| 读写权限 | 根据配置可只读或读写 |
mount_grant(挂载授权)允许用户将本地目录直接挂进容器。安全限制:
- 路径必须在白名单内
- 不能挂载系统目录(如
/etc、/var) - 支持只读模式
3. 卷的构建过程
graph TB
LP[LaunchPlan] --> VOL["volumes.rs<br/>卷布局模块"]
VOL --> V1["workspace_volume()<br/>工作区卷"]
VOL --> V2["session_state_volume()<br/>会话状态卷"]
VOL --> V3["package_volume()<br/>包文件卷"]
VOL --> V4["mount_grant_volumes()<br/>用户挂载卷"]
V1 --> WK["workload_worker.rs<br/>组装到 Deployment 定义"]
V2 --> WK
V3 --> WK
V4 --> WK
volumes.rs(卷布局模块)读取 LaunchPlan 中的路径信息,为每种卷生成对应的 K8s Volume 和 VolumeMount 定义,最终由 workload_worker.rs(SessionWorker 渲染模块)统一写入 Deployment 的 Pod 模板。
4. 安全上下文
除了卷挂载,每个容器还应用了严格的安全策略:
| 安全措施 | 说明 |
|---|---|
runAsNonRoot: true | 禁止以 root 身份运行 |
allowPrivilegeEscalation: false | 禁止权限提升 |
capabilities: drop ALL | 移除所有 Linux 特权能力 |
| seccomp RuntimeDefault | 使用系统默认的系统调用过滤策略 |
automountServiceAccountToken: false | 不自动挂载 K8s API 凭证 |
这意味着容器内的 agentcore 只能访问明确挂载进来的目录,无法访问宿主机的其他文件,也无法调用 K8s API。数据库访问也不会直接从容器发起,而是统一回到 Backend。
5. hostPath 的限制与未来方向
当前所有文件相关的卷都使用 hostPath 方式,这意味着:
| 约束 | 影响 |
|---|---|
| Pod 调度限于本机 | 本地部署模式下无影响;远程多节点部署时需要 nodeAffinity 固定 |
| 无法跨机器共享 | 本地单节点不受影响;远程多节点需要引入 NFS 或 CSI |
| 不提供自动备份 | 需要自行实现备份策略 |
代码中已通过 VolumeBackend 枚举预留了扩展点,未来可以切换到 PersistentVolumeClaim(持久卷声明)等方案。
下一步
了解了挂载机制后,请继续阅读 外部文件访问 了解如何让容器访问用户本地的大文件和目录。
外部文件访问
本章说明如何让运行中的容器访问“不在 Agent 包里“的外部文件,例如用户本地的大型数据集或文档。
本地部署前提:当前系统设计为本地部署,用户的本地机器就是 Backend 和 K3s 所在的宿主机。因此,用户的本地文件可以直接通过 hostPath 挂载进容器,无需网络传输。
1. 两种场景
graph LR
subgraph SMALL["场景一:小文件上传"]
U1[用户上传] -->|附件| B1["Backend 复制到 in/"]
B1 --> C1["容器读取 /workspace/in/"]
end
subgraph LARGE["场景二:大文件或本地目录"]
U2[用户授权挂载] -->|mount_grant| B2[Backend 生成 hostPath 卷]
B2 --> C2["容器读取 /workspace/mounts/"]
end
场景一:小文件上传
用户通过 Helper 界面上传文件,文件经过以下链路到达容器:
- 用户在 Web UI 中选择文件
- Helper 将文件传递给 Backend(由于都在本机,这一步几乎是瞬时的)
- Backend 将文件复制到
workspaces/[run-id]/in/ - 容器启动后从
/workspace/in/读取
适用范围:文档、表格、配置文件等小于 100 MB 的文件。
场景二:大文件或本地目录挂载
对于大型数据集或需要整个目录的场景,使用 mount_grant(挂载授权)机制:
- 用户在创建 Run 时指定本地目录路径(如
/Users/xxx/datasets/) - Backend 校验路径是否在白名单内
- K3s 编排器将路径添加为 hostPath 卷
- 容器内通过
/workspace/mounts/[名称]直接读取
适用范围:GB 级数据集、代码仓库、日志目录等。
本地部署优势:由于用户的机器就是宿主机,hostPath 直接指向用户本地磁盘上的目录,无需复制或传输文件,容器可以即时访问。
2. 本地文件访问流程
在本地部署模式下,文件访问的完整流程如下:
sequenceDiagram
participant U as 用户
participant H as Helper(Web UI)
participant B as Backend(本机)
participant C as 容器
U->>H: 选择本地文件/目录
alt 小文件上传
H->>B: 传递文件(localhost,无网络延迟)
B->>B: 复制到 workspaces/[run-id]/in/
B->>C: 容器通过 hostPath 挂载工作区
C->>C: 读取 /workspace/in/ 中的文件
else 大文件/目录挂载
H->>B: 发送 mount_grant 请求(目录路径)
B->>B: 校验路径白名单
B->>C: 直接 hostPath 挂载用户本地目录
C->>C: 读取 /workspace/mounts/[名称]
end
关键优势:
- 所有通信走 localhost,零网络延迟
- 大文件/目录通过 hostPath 直接挂载,无需复制
- 用户本地文件系统的读写速度等于容器内的读写速度
2.1 这些操作是同步还是异步
| 操作 | 语义 | 说明 |
|---|---|---|
| 小文件上传 | 同步 | 需要先确认 Backend 已接收并落盘 |
| 创建挂载授权 | 同步 | 需要立即知道路径是否合法、是否授权成功 |
| mount_grant 生效 | 混合 | 授权创建同步完成,但真正挂载要等下一次 Run 启动时生效 |
| 容器读取挂载目录 | 同步 | 直接本地文件 I/O,读取时立即拿结果 |
| 容器上报文件输出元数据 | 异步 | 结果文件生成后再通过协议通知 Backend |
3. 访问网络文件
当前系统不内置网络文件下载功能,但 agentcore 可以:
- 在容器内下载:如果容器网络策略允许出站流量,agentcore 可以通过工具调用直接从 URL 下载文件
- 通过工具桥接:未来计划通过 Helper 工具桥(tool bridge)让容器间接访问 Helper 能触达的网络资源
4. 安全约束
| 约束 | 说明 |
|---|---|
| 路径白名单 | mount_grant 只接受白名单内的路径 |
| 禁止系统目录 | 不能挂载 /etc、/var、/usr 等系统目录 |
| 可选只读 | mount_grant 支持 read_only: true |
| 无符号链接穿透 | hostPath 不跟随符号链接 |
5. 限制与未来改进
| 当前限制 | 计划改进方向 |
|---|---|
| 仅支持本地文件 | 未来支持 S3/OSS 等对象存储直接挂载 |
| hostPath 受限于本机磁盘 | 远程部署模式下需引入 NFS/CSI 共享存储 |
| 白名单需手动配置 | 提供 UI 引导式目录选择和授权 |
下一步
了解了外部文件访问后,请继续阅读 外部数据库访问。
外部数据库访问
本章说明当前方案下“数据库访问”到底是怎么做的。
当前统一方案:Backend(后端控制面)负责所有数据库查询和写入,容器内的 Worker / agentcore 不直接连接数据库。 Worker 只需要实现与 Backend 的协议通信。
这意味着:
- Worker 不直接连接 PostgreSQL / MySQL / Redis / 向量数据库
- Worker 不直接打开 checkpoint 数据库文件
- Worker 不持有数据库地址、账号、密码
- 所有数据库访问都由 Backend 统一代理、审计和裁剪
1. 为什么要这样设计
过去最容易出现的误解是:既然容器在本地机器上运行,那是不是可以让容器直接连数据库?
当前方案明确不这么做,原因有四个:
| 原因 | 说明 |
|---|---|
| 安全 | 数据库凭据只留在 Backend,容器内不暴露 |
| 简化容器 | Worker 只实现协议,不需要感知数据库类型、网络地址、驱动差异 |
| 审计一致 | 所有查库、写库都在 Backend 一侧统一记录 |
| 演进容易 | 将来数据库从本地迁到云端,Worker 协议无需改变 |
2. 整体模型:Backend 统一查库
graph TB
subgraph USER_SIDE["用户侧"]
H["Helper / Web UI"]
end
subgraph CONTROL["Backend 控制面"]
API["API + WS 路由"]
SVC["ApplicationService"]
GATE["数据访问网关"]
end
subgraph EXEC["执行面"]
W["Worker / agentcore"]
end
subgraph DATA["数据层"]
DB1["会话数据库"]
DB2["检查点数据库"]
DB3["业务数据库"]
end
H --> API
W -->|"协议请求"| API
API --> SVC --> GATE
GATE --> DB1
GATE --> DB2
GATE --> DB3
一句话概括就是:
Worker 只会说“我要什么数据 / 我要写什么状态”,真正的查库和写库由 Backend 完成。
3. 本地部署时为什么更适合 Backend 查库
当前系统是本地部署:
- 用户的本地机器 = Backend 所在宿主机
- K3s 也在这台机器上运行
因此如果数据库也在本机,Backend 访问数据库最直接:
graph LR
subgraph HOST["用户本地机器 / 宿主机"]
B["Backend"]
DB["本地数据库"]
K["K3s"]
W["Worker Pod"]
end
B -->|"localhost / 本机 socket / 本机地址"| DB
W -->|"协议通信"| B
这样可以直接避免几个典型问题:
- Pod 内
localhost不是宿主机localhost - 容器不需要知道宿主机 IP、Service、Endpoints 这些网络细节
- 容器里不需要再装数据库驱动和连接配置
所以在你们当前的架构里,“宿主机上的数据库怎么让容器访问”这个问题本身就被收敛掉了:不是容器去访问,而是 Backend 去访问。
4. Worker 与 Backend 的数据库协议
既然 Worker 不直接查库,那么就必须通过协议把“我要读什么 / 我要写什么”表达给 Backend。
推荐把协议操作分成四类:
| 协议操作 | 用途 | 语义 |
|---|---|---|
backend_query | 读取业务数据 / 会话数据 / 配置数据 | 同步 |
backend_command | 写入业务状态或执行事务性操作 | 同步 |
checkpoint_get | 拉取恢复所需的检查点 | 同步 |
checkpoint_put | 上报新的检查点快照 | 同步 |
4.1 backend_query(同步)
用于 Worker 需要立刻拿到结果才能继续时,例如:
- 读取某个表的数据
- 获取某个对象详情
- 拉取最新会话状态
- 查询某个权限配置
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: backend_query
B->>DB: 执行查询
DB-->>B: 返回结果
B-->>W: backend_query_result
为什么定义成同步?
因为 Worker 的后续推理通常依赖这个结果,不能“先继续、稍后补回来”。
示例:
{
"type": "backend_query",
"id": "query-001",
"timestamp": "2026-04-17T02:30:00Z",
"payload": {
"resource": "customer_profile",
"operation": "get",
"arguments": {
"customerId": "cust-001"
}
}
}
响应:
{
"type": "backend_query_result",
"id": "query-001",
"timestamp": "2026-04-17T02:30:00Z",
"payload": {
"ok": true,
"data": {
"customerId": "cust-001",
"name": "Alice"
}
}
}
4.2 backend_command(同步)
用于 Worker 需要 Backend 执行有副作用的数据库操作,例如:
- 创建一条记录
- 更新某个状态
- 记录审批结果
- 触发一个事务性写入
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: backend_command
B->>DB: 执行写入 / 事务
DB-->>B: 返回提交结果
B-->>W: backend_command_result
这类操作也应是同步语义,因为 Worker 需要明确知道写入是否成功,才能决定下一步。
4.3 checkpoint_get(同步)
Worker 启动后,如果需要恢复上一次状态,不直接读本地数据库文件,而是向 Backend 请求:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 检查点数据库
W->>B: checkpoint_get(session_id, run_id)
B->>DB: 查询最新检查点
DB-->>B: 返回 checkpoint snapshot
B-->>W: checkpoint_get_result
这是同步语义,因为恢复流程没拿到 checkpoint 结果就不能继续。
4.4 checkpoint_put(同步)
Worker 在关键步骤结束后,把新的检查点快照通过协议发给 Backend:
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 检查点数据库
W->>B: checkpoint_put
B->>DB: 持久化 checkpoint
DB-->>B: 写入成功
B-->>W: checkpoint_put_ack
这同样建议定义成同步语义,因为只有拿到 ACK,Worker 才能认为“这份状态已经可恢复”。
5. 哪些操作是异步的
虽然数据库操作统一走同步语义,但并不是所有协议都同步。
以下仍然是异步事件:
| 协议消息 | 语义 | 原因 |
|---|---|---|
stream_chunk | 异步 | 高频流式输出,不能每个 token 都等确认 |
state_change | 异步 | 状态通知型消息 |
tool_progress | 异步 | 进度展示用,不阻塞主流程 |
error | 异步 | 失败上报,Backend 收到后再统一处理 |
ask_user | 混合 | 发送是异步,Worker 业务流程会暂停等待回复 |
所以整体规则可以简单记为:
查库 / 写库 / checkpoint 恢复与保存 = 同步语义
流式输出 / 状态变化 / 进度通知 = 异步语义
6. run-context.json 在这里扮演什么角色
run-context.json 仍然存在,但它的角色是:
- 给 Worker 提供启动时的初始快照
- 减少刚启动时第一批必须走协议请求的次数
- 让 Worker 在还没发出第一条协议消息前,就能拿到基础上下文
它通常包含:
- 当前会话摘要
- 最近对话历史
- 已授权的输入文件信息
- 最近检查点的元数据摘要
但它不是数据库本身,也不是容器的长期状态源。
长期、权威的数据源仍然是 Backend 管理的数据库。
7. 本地数据库、远程数据库、云数据库有什么区别
在当前方案里,这三种数据库对 Worker 来说没有本质区别。
区别只发生在 Backend 这一层:
| 数据库位置 | 实际连接方 | Worker 是否感知地址差异 |
|---|---|---|
| 宿主机本地数据库 | Backend | 否 |
| 局域网数据库 | Backend | 否 |
| 云数据库 / 托管数据库 | Backend | 否 |
这就是当前方案最大的架构收益之一:Worker 不需要因为数据库部署位置不同而改变协议。
8. 安全与治理收益
| 能力 | 收益 |
|---|---|
| 凭据集中管理 | 数据库用户名、密码、Token 都只保留在 Backend |
| 权限收敛 | Worker 没有直接数据库访问权限 |
| 审计统一 | 所有数据库操作都可以在 Backend 侧统一留痕 |
| 限流与缓存 | Backend 可统一做缓存、熔断、重试和速率控制 |
| 数据裁剪 | Backend 可以只返回 Worker 真正需要的字段 |
9. 技术可行性判断
| 方案 | 可行性 | 复杂度 | 推荐度 |
|---|---|---|---|
| Worker 直接连数据库 | 可行但不推荐 | 中 | 低 |
| Worker 直接读 checkpoint DB 文件 | 可行但不推荐 | 低 | 低 |
| Backend 统一查库 + Worker 协议请求 | 强烈推荐 | 中 | 高 |
推荐 Backend 统一代理的原因不是“唯一能做”,而是它最符合你们当前的本地部署模型和协议化架构。
下一步
理解了数据库访问模型后,建议继续阅读:
镜像固化
本章讨论一个经常被问到的问题:能不能把容器运行后的状态“固化“成一个新的镜像,下次直接启动就能恢复?
答案是不能直接做到,但有更好的替代方案。本章解释为什么,以及正确的做法。
1. 为什么不能直接固化运行状态为镜像
容器镜像的本质
容器镜像(Container Image)由多层只读的文件层叠加而成:
graph TB
subgraph IMAGE_LAYERS["镜像层 - 只读,构建时确定"]
L1["基础层:操作系统 + Python"]
L2["应用层:agentcore 代码"]
L3["配置层:默认配置文件"]
end
subgraph RUNTIME_LAYER["运行时层 - 可写,容器存活期间"]
RW["运行时写入:checkpoint、缓存、临时文件"]
end
L1 --- L2 --- L3 --- RW
关键区别:
- 镜像层:构建时通过 Dockerfile 确定,所有容器共享,不可修改
- 运行时层:容器启动后产生的文件修改,仅存于内存/临时存储,容器销毁后消失
为什么 docker commit 不可行
虽然 Docker 提供了 docker commit 命令可以把运行时层“拍照“成新镜像,但在 K8s/K3s 环境下这条路不可行:
| 原因 | 说明 |
|---|---|
| K3s 中没有 docker commit 等效操作 | K3s 使用 containerd 作为运行时,不直接暴露此功能 |
| 运行时状态不断变化 | SessionWorker 容器在运行中持续处理请求,快照时机难以确定 |
| 状态不可预测 | 每次运行产生的文件不同,固化后的镜像难以复用 |
| 镜像膨胀 | 每次运行都生成新镜像,存储成本不可控 |
| 安全风险 | 运行时镜像可能包含凭据等敏感信息 |
2. 正确的替代方案
方案一:Backend 管理的状态持久化(当前推荐)
通过 Backend 统一持久化状态,不依赖把运行状态固化进镜像:
graph LR
subgraph "第一轮 Run"
R1[agentcore] -->|checkpoint_put| B1["Backend"]
end
subgraph DATA_SIDE["数据层"]
HOST["Backend 数据库"]
end
subgraph "第二轮 Run"
B2["Backend"] -->|checkpoint_get_result| R2[agentcore]
end
B1 -.->|写入| HOST
HOST -.->|读取| B2
优势:状态权威来源集中在 Backend,容器内不需要直接持有数据库连接或数据库文件。
方案二:预热镜像(Prewarmed Image)
将不变的基础环境和依赖预先打包到镜像中,减少每次运行的初始化时间:
graph TB
subgraph BUILD_PHASE["构建时 - 开发者操作"]
BASE["基础镜像<br/>Python + 基础库"]
DEP["安装 Agent 依赖<br/>pip install xxx"]
MODEL["预下载模型文件"]
IMG["预热镜像<br/>registry/agent-prewarmed:v1"]
end
BASE --> DEP --> MODEL --> IMG
subgraph RUN_PHASE["运行时"]
POD["容器启动"]
FAST["跳过安装步骤<br/>直接开始执行"]
end
IMG -->|拉取| POD
POD --> FAST
适用场景:
- Agent 依赖大型 Python 库(如 PyTorch、Transformers)
- Agent 需要预下载模型文件
- 需要减少冷启动时间
操作方式:Agent 包开发者编写自定义 Dockerfile,在 RuntimeProfile 中指定预热镜像地址。
方案三:镜像层缓存
让 K3s 节点缓存常用的基础镜像层,避免每次运行都从 Registry(镜像仓库)拉取:
| 策略 | 说明 |
|---|---|
| imagePullPolicy: IfNotPresent | 本地已有的镜像层不重复拉取 |
| 镜像预拉取 | 在节点上提前拉取常用镜像 |
| 本地 Registry | 在集群内部署镜像缓存代理 |
3. 推荐实践
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| 多轮对话状态保持 | Backend 管理的 checkpoint 持久化 | 权威状态集中,恢复路径更清晰 |
| 减少冷启动时间 | 预热镜像 + imagePullPolicy | 平衡复用性和灵活性 |
| 大型依赖环境 | 预热镜像(包含所有依赖) | 避免每次运行重复安装 |
| 共享运行环境 | 统一基础镜像 + Agent 包分层 | 基础镜像共享,Agent 逻辑通过 ConfigMap 注入 |
4. 技术可行性总结
| 技术路线 | 可行性 | 复杂度 | 推荐度 |
|---|---|---|---|
| docker commit 式快照 | ❌ 不可行 | — | 不推荐 |
| Backend 管理的 checkpoint | ✅ 推荐 | 中 | ⭐⭐⭐ 强烈推荐 |
| 预热镜像 | ✅ 可行 | 中 | ⭐⭐ 推荐 |
| OCI 镜像层缓存 | ✅ 可行 | 低 | ⭐⭐ 推荐 |
| 容器快照(CRIU) | ⚠️ 实验性 | 高 | 不推荐 |
下一步
了解了镜像策略后,请继续阅读 AgentCore 通信机制。
AgentCore 通信机制
本章详细说明容器内的 agentcore(智能体核心进程)如何与 Backend 以及最终用户进行双向实时通信。这是理解“用户发消息 → agentcore 处理 → 实时看到回复 → agentcore 还能反过来向用户提问“这条完整链路的关键。
设计原则:系统采用 SessionWorker(会话工作进程)模式——容器以 Deployment(部署)方式长驻运行,通过 WebSocket(网络套接字) 双向通道与 Backend 实时通信。所有数据库访问都收敛到 Backend;容器内的 Worker 只负责协议通信,不直接连接数据库。
1. 通信架构总览
1.1 三层通信模型
graph TB
subgraph LOCAL["用户本地机器(同一台设备)"]
U["用户"]
H["Helper(前端)"]
B["Backend(后端服务)"]
subgraph K3S["K3s 集群"]
W["SessionWorker Pod<br/>agentcore 进程"]
end
end
U -->|"操作界面"| H
H -->|"WebSocket<br/>双向实时通道"| B
B <-->|"WebSocket<br/>双向实时通道"| W
B -->|"推送更新"| H
H -->|"渲染显示"| U
style LOCAL fill:#f5f5f5,stroke:#333
style K3S fill:#e8f4fd,stroke:#0969da
1.2 通信协议一览
| 链路 | 协议 | 方向 | 说明 |
|---|---|---|---|
| Backend ↔ Worker | WebSocket(推荐) | 双向实时 | Worker 启动后主动连接 Backend 的 WS 端点,所有消息和事件通过同一条持久连接传输 |
| Helper ↔ Backend | WebSocket | 双向实时 | Helper 连接 Backend 的 WS 端点,实时接收流式输出和状态更新 |
| Helper → Backend | HTTP REST | 请求-响应 | 创建会话、创建运行、上传文件等操作仍使用 REST API |
| Backend ↔ Worker(降级) | HTTP POST | 单向推送 | 当 WebSocket 不可用时降级为 HTTP 回调模式 |
1.3 协议职责边界
| 组件 | 负责什么 | 不负责什么 |
|---|---|---|
| Worker / agentcore | 推理、工具调用、文件读写、协议消息收发 | 不直接查数据库,不直接持有数据库凭据 |
| Backend | 会话管理、数据库查询、状态持久化、权限与审计 | 不替 Worker 执行推理 |
| Helper | 展示 UI、收集用户输入、展示流式结果 | 不直接查数据库,不直接和 Pod 建立协议通道 |
因此 Backend ↔ Worker 的协议除了“消息推送”和“流式输出”,还必须承担:
- Worker 请求 Backend 查询数据
- Worker 请求 Backend 写入状态
- Worker 请求读取 checkpoint
- Worker 上报新的 checkpoint
1.4 为什么选择 WebSocket 而非 HTTP
| 对比维度 | HTTP POST 回调(旧方案) | WebSocket(推荐方案) |
|---|---|---|
| 流式输出开销 | 每个 token 片段 = 1 次 HTTP 请求(约 300 字节头 + JSON 体),500 token = 500 次请求 | 每个 token 片段 = 1 个 WS 帧(2-10 字节帧头 + JSON 体),开销降低 95% |
| 连接管理 | 每次请求需要 TCP 连接(即使用 keep-alive 也有头部开销) | 单个持久 TCP 连接,零连接建立开销 |
| 消息顺序 | HTTP 请求可能乱序到达(多连接并发时) | TCP 保证帧顺序,无需额外序列号 |
| 背压控制 | 无内建机制,Worker 可能 POST 过快压垮 Backend | TCP 流控天然提供背压(backpressure),自动限速 |
| 双向通信 | 需要两套独立的 HTTP 通道(Backend→Worker + Worker→Backend) | 同一条连接双向传输,架构更简单 |
| 连接状态感知 | 需要轮询或心跳检测 Worker 是否存活 | 连接断开时 Backend 立即感知 |
| 延迟 | 本地部署约 1-2ms / 请求(TCP 握手 + HTTP 解析) | 本地部署约 0.1ms / 帧(仅帧解析) |
1.5 协议语义总规则
| 类别 | 语义 | 代表消息 |
|---|---|---|
| 同步请求-响应 | 必须等待结果再继续 | backend_query、backend_command、checkpoint_get、checkpoint_put |
| 异步事件流 | 发出后继续执行 | stream_chunk、state_change、tool_progress |
| 混合语义 | 提交异步,但业务流程暂停等待 | ask_user / user_reply |
2. SessionWorker 容器内部架构
2.1 容器结构
SessionWorker 以 Deployment(部署)方式运行,每个 Pod 内包含:
+---------------------------------------------+
| SessionWorker Pod |
| |
| +----------------------------------------+ |
| | 启动脚本(bash) | |
| | 1. 创建工作目录 | |
| | 2. 写入运行时元数据文件 | |
| | 3. 创建 ready 标记文件 | |
| | 4. 注册 TERM 信号处理(优雅关闭) | |
| | 5. 启动心跳循环(每 30 秒) | |
| +----------------------------------------+ |
| |
| +----------------------------------------+ |
| | agentcore 进程 | |
| | * 建立 WS 连接到 Backend | |
| | * 监听 :8081(HTTP 控制端口,探针用) | |
| | * 通过 WS 接收用户消息 | |
| | * 调用大模型 API 进行推理 | |
| | * 执行工具调用 | |
| | * 通过 WS 推送结果给 Backend | |
| +----------------------------------------+ |
| |
| 探针检查(保留 HTTP): |
| * readinessProbe: test -f /tmp/agent-store-worker.ready |
| * livenessProbe: test -f /tmp/agent-store-worker.ready |
+---------------------------------------------+
注意:K8s 健康探针(readinessProbe / livenessProbe)仍使用文件检查方式,这是 K8s 原生支持的最简单方案。WebSocket 仅用于数据通信,不替代探针。
2.2 容器环境变量
Backend 通过环境变量向容器注入所有配置信息:
| 环境变量 | 含义 | 示例值 |
|---|---|---|
RUN_ID | 本次运行 ID | 550e8400-e29b-41d4-a716-446655440000 |
SESSION_ID | 会话 ID | 660e8400-e29b-41d4-a716-446655440001 |
USER_ID | 用户 ID | user-001 |
AGENT_PACKAGE_NAME | 智能体包名 | code-assistant |
AGENT_PACKAGE_ROOT | 包文件挂载路径 | /opt/agent/package |
AGENT_SESSION_STATE_ROOT | 会话本地目录(缓存/日志) | /workspace/session |
AGENT_CHECKPOINT_ROOT | 本地缓存或临时状态目录(可选) | /workspace/session/checkpoints |
AGENT_WORKLOAD_KIND | 工作负载类型 | session-worker |
AGENT_RUNTIME_IMAGE_MODE | 镜像模式 | prewarmed-base |
AGENT_CHECKPOINT_STORE_MODE | 检查点持久化策略 | backend-managed |
AGENT_WORKER_SERVICE_NAME | Worker 的 K8s Service 名称 | agent-worker-svc-xxxx |
AGENT_WORKER_EVENT_CHANNEL_KIND | 事件通道类型 | websocket(推荐) / http-callback(降级) |
AGENT_WORKER_WS_URL | WebSocket 连接地址(WS 模式) | ws://host.k3s.internal:3000/ws/workers/RUN_ID |
AGENT_WORKER_HTTP_CALLBACK_URL | 回调 URL(HTTP 降级模式) | http://host.k3s.internal:3000/api/v1/worker-events |
AGENT_WORKER_IDLE_TTL_SECONDS | 空闲超时时间(秒) | 300 |
AGENT_WORKER_MAX_LIFETIME_SECONDS | 最大存活时间(秒,可选) | 3600 |
3. WebSocket 通信协议详解(推荐方案)
3.1 连接建立
Worker 启动后,agentcore 主动发起 WebSocket 连接到 Backend:
sequenceDiagram
participant W as Worker Pod
participant B as Backend
Note over W: agentcore 启动完成
W->>B: WebSocket 握手 ws://backend:3000/ws/workers/RUN_ID
B->>B: 验证 run_id, 注册 WS 连接
B-->>W: 101 Switching Protocols
Note over W,B: WebSocket 连接建立, 双向通道就绪
W->>B: WS state_change IDLE
Note over B: 标记 Worker 为 IDLE, 可以推送消息
WebSocket 端点设计:
| Backend 端点 | 用途 | 连接方 |
|---|---|---|
ws://backend:PORT/ws/workers/RUN_ID | Backend ↔ Worker 数据通道 | Worker 主动连接 |
ws://backend:PORT/ws/sessions/SESSION_ID | Backend → Helper 实时推送 | Helper 主动连接 |
连接方向说明: Worker 主动连接到 Backend(而非 Backend 连接到 Worker),这样设计的好处是:
- Worker 已通过环境变量知道 Backend 地址,无需额外服务发现
- Backend 无需等待 K8s Service 就绪再连接
- 连接生命周期与 Worker 生命周期天然绑定
3.2 消息帧格式
所有 WebSocket 消息均为 JSON 文本帧,统一格式:
{
"type": "消息类型",
"id": "消息唯一 ID(UUID)",
"timestamp": "2024-01-15T10:30:00Z",
"payload": {}
}
3.3 Backend → Worker 消息类型
| 消息类型 | 用途 | 说明 |
|---|---|---|
push_message | 推送用户消息给 agentcore | 包含用户输入、上下文、附件 |
user_reply | 转发用户对 ask_user 的回复 | 包含回复内容和原始 questionId |
cancel_task | 取消当前正在执行的任务 | agentcore 应尽快停止当前推理 |
backend_query_result | 返回 Worker 请求的数据查询结果 | 对应同步查询 |
backend_command_result | 返回 Worker 请求的写入结果 | 对应同步写入 |
checkpoint_get_result | 返回恢复所需的 checkpoint 数据 | 对应同步恢复 |
checkpoint_put_ack | 确认 checkpoint 已被 Backend 持久化 | 对应同步保存 |
ping | 心跳检查 | Worker 应回复 pong |
push_message 示例:
{
"type": "push_message",
"id": "msg-uuid-001",
"timestamp": "2024-01-15T10:30:00Z",
"payload": {
"sessionId": "session-uuid",
"runId": "run-uuid",
"content": "请帮我分析这段代码",
"artifacts": [
{ "name": "main.py", "relativePath": "src/main.py" }
],
"context": {
"history": [],
"sessionRuntimeState": {}
}
}
}
user_reply 示例:
{
"type": "user_reply",
"id": "reply-uuid-001",
"timestamp": "2024-01-15T10:30:05Z",
"payload": {
"questionId": "q-uuid-001",
"answer": "确认删除",
"approved": true
}
}
3.4 Worker → Backend 消息类型
| 消息类型 | 用途 | 频率 |
|---|---|---|
stream_chunk | 流式输出的 token 片段 | 高频:每 token 一条 |
message_response | 最终回答(完整或部分) | 每轮对话 1 条 |
ask_user | agentcore 向用户提问 | 按需 |
state_change | 生命周期状态变更 | 状态切换时 |
tool_call | 工具调用通知(需要用户授权时) | 按需 |
backend_query | 请求 Backend 查询数据库 | 按需,同步语义 |
backend_command | 请求 Backend 执行数据库写入 | 按需,同步语义 |
checkpoint_get | 请求恢复 checkpoint | 启动或恢复时,同步语义 |
checkpoint_put | 上报最新 checkpoint | 关键步骤后,同步语义 |
error | 执行错误报告 | 异常时 |
pong | 心跳回复 | 响应 ping |
stream_chunk 示例(最高频消息):
{
"type": "stream_chunk",
"id": "chunk-042",
"timestamp": "2024-01-15T10:30:01.123Z",
"payload": {
"chunkIndex": 42,
"content": "这段代码",
"renderFormat": "markdown",
"isComplete": false
}
}
对比 HTTP 回调:同样的
stream_chunk在 HTTP 模式下需要完整的 HTTP POST 请求(约 300 字节 HTTP 头 + 约 100 字节 JSON 体 = 约 400 字节),而 WebSocket 帧仅需约 2 字节帧头 + 约 100 字节 JSON 体 = 约 102 字节。单条消息开销降低约 75%。
backend_query 示例(同步请求):
{
"type": "backend_query",
"id": "query-001",
"timestamp": "2026-04-17T02:30:00Z",
"payload": {
"resource": "session_memory",
"operation": "get",
"arguments": {
"sessionId": "session-uuid"
}
}
}
backend_query_result 示例:
{
"type": "backend_query_result",
"id": "query-001",
"timestamp": "2026-04-17T02:30:00Z",
"payload": {
"ok": true,
"data": {
"summary": "最近 3 轮都在讨论数据库迁移"
}
}
}
checkpoint_put 示例:
{
"type": "checkpoint_put",
"id": "ckp-001",
"timestamp": "2026-04-17T02:30:02Z",
"payload": {
"sessionId": "session-uuid",
"runId": "run-uuid",
"snapshot": {
"historyCursor": 12,
"memoryState": {},
"toolState": {}
}
}
}
message_response 示例:
{
"type": "message_response",
"id": "resp-uuid-001",
"timestamp": "2024-01-15T10:30:02Z",
"payload": {
"messageId": "resp-uuid-001",
"content": "这段代码的主要问题是...",
"renderFormat": "markdown",
"isComplete": true,
"usage": {
"promptTokens": 1500,
"completionTokens": 800,
"totalTokens": 2300
}
}
}
ask_user 示例:
{
"type": "ask_user",
"id": "ask-uuid-001",
"timestamp": "2024-01-15T10:30:01.500Z",
"payload": {
"questionId": "q-uuid-001",
"question": "我需要删除这个文件,是否确认?",
"questionType": "confirmation",
"options": ["确认删除", "取消"],
"timeout_seconds": 300
}
}
state_change 示例:
{
"type": "state_change",
"id": "sc-uuid-001",
"timestamp": "2024-01-15T10:30:00Z",
"payload": {
"previousState": "BUSY",
"newState": "IDLE",
"reason": "task completed"
}
}
error 示例:
{
"type": "error",
"id": "err-uuid-001",
"timestamp": "2024-01-15T10:30:03Z",
"payload": {
"errorCode": "LLM_API_TIMEOUT",
"message": "调用大模型 API 超时",
"recoverable": true,
"details": {}
}
}
3.5 WebSocket 连接管理
心跳机制
| 方向 | 间隔 | 超时 | 动作 |
|---|---|---|---|
| Backend → Worker | 每 15 秒发送 ping | 30 秒无 pong | Backend 判定 Worker 异常,标记 FAILED |
| Worker → Backend | 收到 ping 立即回复 pong | — | — |
WebSocket 协议本身支持 Ping/Pong 控制帧(RFC 6455),可以直接使用协议层心跳,无需应用层额外实现。
断线重连
当 WebSocket 连接意外断开时:
graph TD
A["WS 连接断开"] --> B{"Worker 是否仍在运行?"}
B -->|"是"| C["Worker 尝试重连<br/>指数退避: 1s, 2s, 4s, 8s..."]
B -->|"否 进程退出"| D["Backend 标记 Worker FAILED"]
C --> E{"重连成功?"}
E -->|"是"| F["恢复通信<br/>Worker 发送 state_change"]
E -->|"否, 超过 30 秒"| G["Worker 自行退出"]
G --> D
重连策略:
- 初始等待 1 秒,指数退避最大 8 秒
- 最大重试时间 30 秒(与 terminationGracePeriodSeconds 一致)
- 重连成功后 Worker 发送
state_change通知 Backend 当前状态
3.6 数据访问为什么走协议而不是直连数据库
sequenceDiagram
participant W as Worker
participant B as Backend
participant DB as 数据库
W->>B: backend_query
B->>DB: 查库
DB-->>B: 返回结果
B-->>W: backend_query_result
这样做的直接收益是:
- 数据库账号、密码、DSN 只保留在 Backend
- Worker 只面对统一协议,不区分 PostgreSQL / MySQL / Redis / 向量库
- Backend 可以在查库前做权限控制、审计、缓存和限流
优雅关闭
Worker 收到 SIGTERM
-> 通过 WS 发送 state_change(DRAINING)
-> Backend 停止推送新消息
-> 等待当前任务完成
-> 发送 WebSocket Close 帧(状态码 1000: Normal Closure)
-> 关闭连接
-> 进程退出
3.7 事件通道机制(WorkerEventChannel)
代码中的 WorkerEventChannel(工作进程事件通道)枚举定义了通信模式。推荐升级后支持三种:
| 通道类型 | 环境变量 | 工作方式 | 状态 | 适用场景 |
|---|---|---|---|---|
| WebSocket | AGENT_WORKER_WS_URL | Worker 主动连接 Backend WS 端点,双向通信 | 推荐 | 所有场景,特别是流式输出 |
| HttpCallback | AGENT_WORKER_HTTP_CALLBACK_URL | Worker 主动 POST 到 Backend + Backend POST 到 Worker 8081 端口 | 降级备选 | WebSocket 不可用时 |
| QueueConsumer | AGENT_WORKER_QUEUE_NAME | Worker 从消息队列拉取/推送 | 预留 | 未来高并发分布式场景 |
WorkerEventChannel Rust 类型定义(推荐升级后):
#![allow(unused)]
fn main() {
pub enum WorkerEventChannel {
/// 推荐:Worker 主动连接 Backend 的 WebSocket 端点
WebSocket { backend_ws_url: String },
/// 降级:双向 HTTP POST 回调
HttpCallback { callback_url: String },
/// 预留:消息队列模式
QueueConsumer { queue_name: String },
}
}
4. HTTP 降级模式(当前实现)
当 WebSocket 不可用时(如容器运行时不支持长连接),系统降级为 HTTP POST 回调模式。
4.1 Backend → Worker:HTTP 控制端口
Backend 通过 K8s Service 将 HTTP 请求转发到 Worker Pod 的 8081 端口(控制端口)。
| 端点 | 方法 | 用途 |
|---|---|---|
/messages | POST | 推送用户消息给 agentcore |
/cancel | POST | 取消当前正在执行的任务 |
/status | GET | 查询当前生命周期状态 |
/health | GET | 健康检查 |
4.2 Worker → Backend:HTTP 回调
Worker 通过 AGENT_WORKER_HTTP_CALLBACK_URL 向 Backend 推送事件。每种事件类型(stream_chunk、message_response、ask_user 等)都是一个独立的 HTTP POST 请求。
缺点:每个
stream_chunktoken 片段都需要一个完整的 HTTP 请求,500 token 的回复将产生约 500 次 HTTP POST,这在性能和可靠性上都不理想。
5. Helper ↔ Backend API 详解
Helper 与 Backend 之间通过 WebSocket + HTTP REST 混合模式通信(均在本机 localhost)。
5.1 HTTP REST API(请求-响应操作)
这些 API 的语义也并不完全一样:
- 同步:
GET /sessions/{id}、POST /artifacts/upload、POST /mount-grants - 混合:
POST /runs、POST /runs/{id}/cancel - 异步结果承载:真正的执行结果仍通过 WebSocket 返回
| 操作 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 健康检查 | GET | /health | 返回 200 表示服务正常 |
| 就绪检查 | GET | /ready | 返回 200 表示服务可接受请求 |
| 集群状态 | GET | /api/v1/cluster/status | K3s 集群健康状态 |
| 运行时配置列表 | GET | /api/v1/runtime-profiles | 可用的运行时环境配置 |
| 智能体包列表 | GET | /api/v1/packages | 可用的智能体包 |
| 智能体包详情 | GET | /api/v1/packages/NAME | 指定包的详细信息 |
| 会话列表 | GET | /api/v1/sessions | 所有会话列表 |
| 创建会话 | POST | /api/v1/sessions | 开启新对话会话 |
| 获取会话 | GET | /api/v1/sessions/ID | 会话详情和对话历史 |
| 关闭会话 | POST | /api/v1/sessions/ID/close | 关闭会话 |
| 重开会话 | POST | /api/v1/sessions/ID/reopen | 重新打开已关闭的会话 |
| 运行列表 | GET | /api/v1/runs | 所有运行记录 |
| 创建运行 | POST | /api/v1/runs | 核心:发送消息并启动/复用 Worker |
| 获取运行 | GET | /api/v1/runs/ID | 运行状态和结果 |
| 取消运行 | POST | /api/v1/runs/ID/cancel | 取消正在执行的运行 |
| 运行结果列表 | GET | /api/v1/runs/ID/results | 输出文件列表 |
| 读取结果文件 | POST | /api/v1/runs/ID/results/read | 读取指定输出文件内容 |
| 文件上传 | POST | /api/v1/artifacts/upload | 上传用户文件 |
| 文件列表 | GET | /api/v1/artifacts | 已上传文件列表 |
| 挂载授权列表 | GET | /api/v1/mount-grants | 目录挂载授权列表 |
| 创建挂载授权 | POST | /api/v1/mount-grants | 授权容器访问本地目录 |
| 审计事件 | GET | /api/v1/audit-events | 操作审计日志 |
5.2 WebSocket 实时通道(流式推送)
Helper 连接 ws://localhost:PORT/ws/sessions/SESSION_ID,实时接收 Worker 的流式输出和状态更新:
| Backend 推送的消息类型 | 含义 | 来源 |
|---|---|---|
stream_chunk | AI 输出的 token 片段 | Worker 转发 |
message_response | 最终回答 | Worker 转发 |
ask_user | agentcore 向用户提问 | Worker 转发 |
state_change | Worker 状态变更 | Worker 转发或 Backend 生成 |
run_status | 运行状态更新(SUBMITTED, COMPLETED 等) | Backend 生成 |
5.3 创建运行(核心 API)请求格式
{
"sessionId": "会话 UUID",
"packageName": "智能体包名",
"userPrompt": "用户发送的消息内容",
"workloadKind": "sessionWorker",
"runtimeProfile": "codex-standard",
"runtimeImageMode": "prewarmedBase",
"checkpointStoreMode": "backendManaged",
"workerEventChannel": {
"webSocket": {
"backendWsUrl": "ws://host.k3s.internal:3000/ws/workers"
}
},
"workerIdlePolicy": {
"idleTtlSeconds": 300,
"maxLifetimeSeconds": 3600
},
"providerEnv": {
"ANTHROPIC_API_KEY": "sk-..."
},
"artifactIds": [],
"externalServiceBindings": []
}
6. 完整交互时序
6.1 首次会话(Worker 冷启动 + WebSocket 建连)
sequenceDiagram
participant U as 用户
participant H as Helper
participant B as Backend
participant K as K3s API
participant W as Worker Pod
rect rgb(230, 245, 255)
Note over U,W: 1 创建会话
U->>H: 选择智能体, 开始对话
H->>B: POST /api/v1/sessions
B->>B: 创建 SessionRecord, 初始化状态目录
B-->>H: 返回 session_id
H->>B: 建立 WS 连接 ws://backend/ws/sessions/SESSION_ID
end
rect rgb(255, 245, 230)
Note over U,W: 2 发送首条消息, 启动 Worker
U->>H: 输入消息
H->>B: POST /api/v1/runs
B->>B: 构建 LaunchPlan
B->>K: 创建 ConfigMap + Secret + Deployment
B-->>H: 返回 run_id
H-->>U: 显示启动中
end
rect rgb(230, 255, 230)
Note over W: 3 Worker 启动并建立 WS 连接
K->>W: 调度 Pod
W->>W: 创建工作目录, 写入元数据, touch ready
W->>B: WebSocket 连接 ws://backend/ws/workers/RUN_ID
B-->>W: 101 连接建立
W->>B: WS state_change IDLE
Note over B: Worker 就绪
end
rect rgb(255, 230, 255)
Note over U,W: 4 Backend 通过 WS 推送消息
B->>W: WS push_message(用户消息 + 初始上下文)
W->>W: agentcore 开始推理
W->>B: WS backend_query(如需更多状态)
B-->>W: WS backend_query_result
W-->>B: WS stream_chunk x N
B-->>H: WS 转发 stream_chunk
H-->>U: 实时显示 AI 回复
W->>B: WS message_response isComplete=true
W->>B: WS checkpoint_put
B->>B: 写入数据库并返回 ACK
B->>H: WS run_status COMPLETED
end
6.2 后续对话(Worker 已就绪,WS 连接已存在)
sequenceDiagram
participant U as 用户
participant H as Helper
participant B as Backend
participant W as Worker Pod
U->>H: 继续对话
H->>B: POST /api/v1/runs(同一 session_id)
B->>B: 检测到 Worker IDLE 且 WS 连接存在
B->>W: WS push_message(新消息)
W->>W: agentcore 处理
W->>B: WS backend_query(按需)
B-->>W: WS backend_query_result
W-->>B: WS stream_chunk x N
B-->>H: WS 转发流式片段
H-->>U: 实时显示
W->>B: WS message_response
Note over U,W: 无需重新创建资源, 延迟极低
6.3 agentcore 向用户提问(ask_user)
sequenceDiagram
participant U as 用户
participant H as Helper
participant B as Backend
participant W as Worker Pod
W->>W: 执行工具调用前需要用户确认
W->>B: WS ask_user
B->>B: 标记 Worker 状态 WAITING_USER
B->>H: WS ask_user(转发给 Helper)
H->>U: 弹出确认对话框
alt 用户同意
U->>H: 点击确认
H->>B: POST 用户回复
B->>W: WS user_reply(approved=true)
B->>B: 标记 Worker 状态 BUSY
W->>W: 继续执行被中断的任务
else 用户拒绝
U->>H: 点击取消
H->>B: POST 用户拒绝
B->>W: WS user_reply(approved=false)
W->>W: 跳过该工具调用, 继续推理
end
7. Worker 生命周期状态机
7.1 状态定义
stateDiagram-v2
[*] --> PENDING : Backend 创建 Deployment
PENDING --> IDLE : Pod 就绪, WS 连接建立
IDLE --> BUSY : Backend 通过 WS 推送消息
BUSY --> WAITING_USER : agentcore 发送 ask_user
WAITING_USER --> BUSY : 用户回复经 WS 推送
BUSY --> IDLE : 任务完成
IDLE --> DRAINING : 空闲超过 idle_ttl_seconds
DRAINING --> [*] : 优雅关闭, WS Close
PENDING --> FAILED : Pod 启动失败
BUSY --> FAILED : agentcore 异常退出
IDLE --> FAILED : WS 连接断开且重连失败
7.2 状态说明
| 状态 | 含义 | WebSocket 状态 | Backend 行为 |
|---|---|---|---|
| PENDING | Deployment 已创建,Pod 正在启动 | 未连接 | 等待 WS 连接 |
| IDLE | Worker 就绪,等待消息 | 已连接 | 可以推送消息 |
| BUSY | 正在处理用户请求 | 已连接,收发中 | 接收流式事件 |
| WAITING_USER | agentcore 在等待用户回复 | 已连接,等待中 | 转发 ask_user,等待用户操作 |
| DRAINING | 空闲超时,正在优雅关闭 | 发送 Close 帧 | 不再推送消息 |
| FAILED | 启动或执行失败 | 断开或未建立 | 上报错误 |
7.3 空闲策略(WorkerIdlePolicy)
| 参数 | 类型 | 说明 |
|---|---|---|
idle_ttl_seconds | 必填 | Worker 空闲多久后自动关闭(秒) |
max_lifetime_seconds | 可选 | Worker 最长存活时间,无论是否空闲 |
7.4 优雅关闭流程
当 K8s 发送 TERM 信号时,Worker 执行以下步骤:
1. 收到 SIGTERM 信号
2. 通过 WS 发送 state_change(DRAINING)
3. 写入时间戳到 /workspace/session/worker-draining.log
4. 删除 /tmp/agent-store-worker.ready 标记文件
5. readinessProbe 失败, K8s 停止路由新请求
6. 等待当前任务完成(terminationGracePeriodSeconds: 30 秒)
7. 发送 WebSocket Close 帧(1000 Normal Closure)
8. 进程退出
8. 安全机制
8.1 容器安全上下文
Worker Pod 的安全配置(已在代码中实现):
securityContext:
runAsNonRoot: true # 禁止 root 运行
runAsUser: 1000 # 以 UID 1000 运行
runAsGroup: 1000 # 以 GID 1000 运行
fsGroup: 1000 # 文件系统组
seccompProfile:
type: RuntimeDefault # 使用容器运行时默认 seccomp 配置
allowPrivilegeEscalation: false # 禁止提权
capabilities:
drop: ["ALL"] # 丢弃所有 Linux capabilities
readOnlyRootFilesystem: false # 允许写入(agentcore 需要)
8.2 WebSocket 连接安全
| 安全措施 | 说明 |
|---|---|
| 连接认证 | WS 握手时在 URL 中携带 run_id,Backend 验证该 run 存在且状态为 SUBMITTED |
| 连接唯一性 | 每个 run_id 只允许一条 WS 连接,重复连接将被拒绝 |
| TLS(可选) | 本地部署时为 ws://(明文),生产环境应升级为 wss://(加密) |
| 消息校验 | Backend 验证每条 WS 消息的 type 字段是否为已知类型 |
8.3 凭据传递
API 密钥和敏感凭据通过 K8s Secret 注入,不出现在 ConfigMap 或环境变量明文中,也不通过 WebSocket 传输:
envFrom:
- secretRef:
name: agent-provider-secret-RUN_ID
optional: false
9. 本地工具桥接(规划中)
未来 Helper 计划提供“工具桥“(Tool Bridge)功能,让容器内的 agentcore 能间接调用用户本地的工具。WebSocket 的双向特性使得工具桥实现更加自然:
graph LR
A["agentcore"] -->|"WS tool_call"| B["Backend"]
B -->|"WS tool_request"| H["Helper"]
H -->|"执行本地命令"| M["本地工具"]
M -->|"返回结果"| H
H -->|"WS tool_result"| B
B -->|"WS user_reply"| A
本地部署优势:由于 Helper、Backend、K3s 都在同一台机器上,整条 WebSocket 链路全部走 localhost,延迟小于 1ms。
10. 技术实现要点
10.1 Backend 端(Rust / axum)
axum 的 WebSocket 支持只需在 Cargo.toml 中启用 ws feature:
# 当前
axum = { version = "0.8.6", features = ["multipart", "macros"] }
# 升级后
axum = { version = "0.8.6", features = ["multipart", "macros", "ws"] }
Backend 需要新增的路由:
#![allow(unused)]
fn main() {
// WebSocket 路由
.route("/ws/workers/:run_id", get(ws_worker_handler))
.route("/ws/sessions/:session_id", get(ws_session_handler))
}
10.2 Worker 端(agentcore)
agentcore 启动后根据 AGENT_WORKER_EVENT_CHANNEL_KIND 环境变量选择通信模式:
if AGENT_WORKER_EVENT_CHANNEL_KIND == "websocket":
url = AGENT_WORKER_WS_URL
连接 WebSocket, 双向通信
elif AGENT_WORKER_EVENT_CHANNEL_KIND == "http-callback":
url = AGENT_WORKER_HTTP_CALLBACK_URL
启动 HTTP 服务 :8081 + HTTP POST 回调
10.3 K8s Service 变化
WebSocket 模式下,Worker 不再需要 K8s Service 进行数据通信(因为是 Worker 主动连接 Backend),但 K8s Service 仍保留用于:
- Backend 对 Worker 的 HTTP 健康检查(可选,因为 WS 连接状态本身就是健康指标)
- 未来可能的直接 HTTP 调用(如管理接口)
下一步
了解了通信机制后,请继续阅读:
- 输入与输出 — 了解 run-context 和结果回调的详细数据结构
- 生命周期管理 — 了解 Worker 从创建到销毁的完整流程
- 资源配置与容量管理 — CPU / 内存 / 磁盘限制的配置与原理
- 技术可行性分析 — 各项能力的综合评估
资源配置与容量管理
本章详细说明 K3s 集群中 SessionWorker 容器的资源限制机制——CPU、内存、磁盘如何被分配和隔离,以及 Backend 如何配置和监控这些资源。
核心概念:K8s(Kubernetes,容器编排系统)通过 Linux cgroups(控制组)技术实现资源隔离。每个 Pod(容器组)运行在独立的 cgroup 中,操作系统内核负责强制执行资源上限,确保一个智能体容器不会影响宿主机或其他容器的正常运行。
1. 资源限制的底层原理:cgroups
1.1 什么是 cgroups
cgroups(Control Groups,控制组)是 Linux 内核提供的资源隔离机制。当 K3s 创建一个 SessionWorker Pod 时,containerd(容器运行时)会为该容器创建一个 cgroup,内核通过该 cgroup 控制容器可使用的资源上限。
graph TB
subgraph HOST["用户本地机器"]
KERNEL["Linux 内核<br/>cgroups 子系统"]
subgraph CGROUP["cgroup: agent-worker-xxx"]
CPU_CG["CPU 控制<br/>cpu.cfs_quota_us / cpu.cfs_period_us"]
MEM_CG["内存控制<br/>memory.limit_in_bytes"]
IO_CG["磁盘控制<br/>ephemeral-storage 监控"]
end
subgraph POD["SessionWorker Pod"]
AC["agentcore 进程"]
end
end
KERNEL --> CGROUP
CGROUP --> POD
AC -.->|"受 cgroup 限制"| CPU_CG
AC -.->|"受 cgroup 限制"| MEM_CG
AC -.->|"受 cgroup 限制"| IO_CG
style HOST fill:#f5f5f5,stroke:#333
style CGROUP fill:#fff3e0,stroke:#e65100
style POD fill:#e8f4fd,stroke:#0969da
1.2 cgroups 如何控制 CPU
K8s 将 CPU requests/limits 转换为 cgroups v2 的以下参数:
| K8s 配置 | cgroups 参数 | 作用 |
|---|---|---|
requests.cpu: 500m | cpu.weight(按比例分配) | 保证容器在 CPU 竞争时至少获得 0.5 核的计算时间 |
limits.cpu: 500m | cpu.max = 50000 100000(50ms/100ms 周期) | 硬限制:每 100ms 周期内最多使用 50ms CPU 时间,即 0.5 核 |
500m表示 500 毫核(millicores),即 0.5 个 CPU 核心1000m=1= 1 个完整核心- 超过 limits 时:进程不会被杀死,而是被节流(throttled)——内核暂停进程直到下一个周期
1.3 cgroups 如何控制内存
| K8s 配置 | cgroups 参数 | 作用 |
|---|---|---|
requests.memory: 1Gi | 调度参考(保证节点有足够内存) | K8s 调度器只把 Pod 放到有 >= 1Gi 可分配内存的节点上 |
limits.memory: 1Gi | memory.max = 1073741824 | 硬限制:容器总内存不超过 1Gi |
- 超过 limits 时:内核触发 OOM Killer(Out-Of-Memory 杀手),直接终止容器进程
- 这比 CPU 严格得多——CPU 超限只是变慢,内存超限直接被杀
- 因此 agentcore 需要控制自身内存使用,特别是处理大型上下文时
1.4 cgroups 如何控制磁盘
ephemeral-storage(临时存储)的限制方式与 CPU/内存不同:
| K8s 配置 | 机制 | 作用 |
|---|---|---|
limits.ephemeral-storage: 4Gi | kubelet 周期性扫描容器写入量 | 超过限额时驱逐(evict)Pod |
- K8s 不使用 cgroups 限制磁盘,而是通过 kubelet(节点代理程序)定期检查
- 检查间隔默认约 10 秒
- 超限后 Pod 被驱逐(不是进程被杀,而是整个 Pod 被删除重建)
2. Requests 与 Limits 的区别
这两个概念是理解 K8s 资源管理的关键:
| 概念 | 含义 | 类比 |
|---|---|---|
| Requests(请求) | 容器运行的最低保障 | 酒店预订:保证你有房间 |
| Limits(限额) | 容器运行的最高上限 | 自助餐上限:最多吃这么多 |
graph LR
subgraph RANGE["资源使用范围"]
R["Requests<br/>最低保障<br/>500m CPU"]
L["Limits<br/>最高上限<br/>500m CPU"]
end
R -->|"实际使用在此范围内"| L
当前配置中 requests = limits(均为 500m CPU / 1Gi 内存),这意味着:
- 容器获得固定资源配额,不多不少
- 这种配置称为 Guaranteed QoS(保证型服务质量),是最稳定的模式
- 适合本地部署(只有一台机器,资源管理要简单可控)
3. 当前资源配置
3.1 RuntimeProfile 配置文件
Backend 通过 runtime_profiles.yaml(运行时配置文件)定义不同的资源档位。每个智能体包可以指定使用哪个 profile:
| Profile 名称 | CPU(requests/limits) | 内存(requests/limits) | 临时存储(requests/limits) |
|---|---|---|---|
codex-standard | 500m / 500m | 1Gi / 1Gi | 4Gi / 4Gi |
codex-large | 500m / 500m | 1Gi / 1Gi | 4Gi / 4Gi |
claude-code-standard | 500m / 500m | 1Gi / 1Gi | 4Gi / 4Gi |
claude-code-large | 500m / 500m | 1Gi / 1Gi | 4Gi / 4Gi |
当前状态:四个 profile 的资源配置完全相同。这是初始阶段的简化设计,未来可根据不同智能体的实际资源消耗进行差异化配置。
3.2 资源单位说明
| 单位 | 含义 | 换算 |
|---|---|---|
m(毫核) | CPU 时间片,1000m = 1 个核心 | 500m = 0.5 核 |
Mi / Gi | 内存,Mi = 1024*1024 字节 | 1Gi = 1024Mi = 约 1.07 GB |
Ki / Mi / Gi | 存储同上 | 4Gi = 约 4.29 GB |
3.3 资源在代码中的流转
graph TB
YAML["runtime_profiles.yaml<br/>资源档位配置"] --> RS["ResourceSettings<br/>cpu/memory/ephemeral_storage"]
RS --> RM["resource_map()<br/>转换为 K8s Quantity"]
RM --> SPEC["Container.resources<br/>requests + limits"]
SPEC --> K8S["K8s API Server"]
K8S --> KUBELET["kubelet 节点代理"]
KUBELET --> CG["创建 cgroup<br/>设置资源限制"]
style YAML fill:#e8f4fd,stroke:#0969da
style CG fill:#fff3e0,stroke:#e65100
ResourceSettings(资源设置)的 Rust 类型定义:
#![allow(unused)]
fn main() {
pub struct ResourceSettings {
pub cpu: String, // 如 "500m"
pub memory: String, // 如 "1Gi"
pub ephemeral_storage: String, // 如 "4Gi"
}
}
resource_map() 方法将其转换为 K8s API 所需的 BTreeMap<String, Quantity>,requests 和 limits 设置为相同值。
4. 容器安全上下文
除了资源限制,K8s 还通过 SecurityContext(安全上下文)限制容器的系统权限:
4.1 Pod 级安全设置
securityContext:
runAsNonRoot: true # 禁止以 root 身份运行
runAsUser: 1000 # 以 UID 1000 运行所有进程
runAsGroup: 1000 # 以 GID 1000 运行
fsGroup: 1000 # 挂载卷的文件组设为 1000
seccompProfile:
type: RuntimeDefault # 启用 seccomp 系统调用过滤
| 设置 | 作用 | 安全意义 |
|---|---|---|
runAsNonRoot | 强制非 root 运行 | 即使镜像 Dockerfile 指定 root,K8s 也会拒绝启动 |
runAsUser: 1000 | 固定 UID | agentcore 以普通用户身份运行,无法访问系统文件 |
fsGroup: 1000 | 挂载卷文件组 | 确保 agentcore 可以读写挂载的 workspace 目录 |
seccompProfile | 系统调用白名单 | 阻止容器执行危险的系统调用(如 mount、reboot) |
4.2 容器级安全设置
securityContext:
allowPrivilegeEscalation: false # 禁止提权
capabilities:
drop: ["ALL"] # 丢弃所有 Linux capabilities
readOnlyRootFilesystem: false # 允许写入(agentcore 需要临时文件)
| 设置 | 作用 |
|---|---|
allowPrivilegeEscalation: false | 阻止通过 setuid/setgid 提升权限 |
capabilities.drop: ALL | 移除所有特殊权限(网络配置、进程管理等) |
readOnlyRootFilesystem: false | agentcore 需要写入临时文件和日志 |
4.3 其他安全设置
automountServiceAccountToken: false # 不挂载 K8s 服务账号令牌
terminationGracePeriodSeconds: 30 # 优雅关闭等待 30 秒
automountServiceAccountToken: false:防止容器内进程访问 K8s API,消除容器逃逸风险terminationGracePeriodSeconds: 30:给 agentcore 30 秒时间保存状态后退出
5. 空闲回收策略(WorkerIdlePolicy)
SessionWorker 采用 Deployment 长驻模式,需要主动回收空闲资源:
5.1 策略参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
idle_ttl_seconds | u64(必填) | 300(5 分钟) | Worker 空闲多久后自动关闭 |
max_lifetime_seconds | Option(可选) | 3600(1 小时) | Worker 最长存活时间,无论是否空闲 |
5.2 回收流程
graph TD
IDLE["Worker 进入 IDLE 状态"] --> TIMER["启动空闲计时器"]
TIMER --> CHECK{"idle_ttl_seconds 到期?"}
CHECK -->|"否, 收到新消息"| RESET["重置计时器"]
RESET --> IDLE
CHECK -->|"是"| DRAIN["进入 DRAINING 状态"]
DRAIN --> SAVE["通过 WS 通知 Backend"]
SAVE --> CLOSE["发送 WS Close 帧"]
CLOSE --> EXIT["进程退出"]
EXIT --> K8S["K8s 检测到 Pod 退出"]
K8S --> DELETE["Deployment 缩容 / 删除"]
IDLE --> MAX{"max_lifetime_seconds 到期?"}
MAX -->|"是"| DRAIN
MAX -->|"否"| IDLE
6. 节点健康监控
Backend 通过 K8s API 实时监控本地 K3s 节点的健康状态。
6.1 监控的指标
| 指标名 | 含义 | 健康值 |
|---|---|---|
| Ready | 节点是否正常运行 | True |
| MemoryPressure | 节点内存是否紧张 | False |
| DiskPressure | 节点磁盘是否紧张 | False |
| PIDPressure | 节点进程数是否过多 | False |
| NetworkUnavailable | 节点网络是否不可用 | False |
6.2 ClusterNodeStatus
Backend 将 K8s 节点状态转换为 ClusterNodeStatus(集群节点状态)供 Helper 展示:
#![allow(unused)]
fn main() {
pub struct ClusterNodeStatus {
pub name: String, // 节点名称
pub conditions: Vec<NodeCondition>, // 上述 5 项指标
pub allocatable: ResourceSummary, // 可分配资源总量
pub capacity: ResourceSummary, // 资源总容量
}
}
Helper 通过 GET /api/v1/cluster/status 获取该信息,显示在右侧状态面板。
7. 容量规划指南
7.1 单个 Worker 的资源消耗
| 资源 | 配额 | 用途说明 |
|---|---|---|
| CPU | 500m(0.5 核) | 主要消耗在 agentcore 推理框架、工具执行、文本处理 |
| 内存 | 1Gi | 上下文缓存、推理中间状态、checkpoint 数据库缓存 |
| 磁盘 | 4Gi | workspace 文件、checkpoint 数据库、临时文件、日志 |
7.2 宿主机资源建议
| 场景 | 最小配置 | 推荐配置 |
|---|---|---|
| 1 个智能体同时运行 | 2 核 / 4Gi 内存 | 4 核 / 8Gi 内存 |
| 2-3 个智能体同时运行 | 4 核 / 8Gi 内存 | 8 核 / 16Gi 内存 |
| 4-6 个智能体同时运行 | 8 核 / 16Gi 内存 | 16 核 / 32Gi 内存 |
注意:上述配置需要为宿主机系统、K3s 组件、Backend 服务预留约 1-2 核 CPU 和 2-4Gi 内存。
7.3 磁盘空间规划
| 组件 | 典型大小 | 说明 |
|---|---|---|
| K3s 系统 | 约 500Mi | K3s 二进制 + 系统容器 |
| 容器镜像缓存 | 约 1-5Gi | 取决于使用的智能体镜像数量 |
| 每个 Worker workspace | 最大 4Gi | ephemeral-storage limits |
| checkpoint 数据库 | 通常 < 100Mi / 会话 | 取决于对话轮次和工具调用复杂度 |
8. 资源超限行为汇总
当容器超过资源限制时,K8s 的处理方式因资源类型而异:
| 资源 | 超限行为 | 严重程度 | 恢复方式 |
|---|---|---|---|
| CPU | 进程被节流(throttled),变慢但不被杀 | 低 | 等待下一个 CPU 周期自动恢复 |
| 内存 | OOM Killer 终止进程,Pod 可能重启 | 高 | K8s 自动重启 Pod(受 restartPolicy 控制) |
| 磁盘 | Pod 被驱逐(evicted),需要重新调度 | 高 | Backend 检测到 Pod 丢失,可能需要重建 Worker |
9. 资源配置调优建议
9.1 CPU 调优
- 当前
500m适合大多数场景(agentcore 主要等待大模型 API 响应,CPU 实际使用率较低) - 如果智能体需要执行 CPU 密集型工具(如代码编译、数据处理),可提高到
1000m或2000m - 通过
kubectl top pod观察实际 CPU 使用率来决定
9.2 内存调优
1Gi适合一般对话场景- 处理长上下文(> 100k tokens)或大文件分析时,可能需要
2Gi或更多 - 内存超限后果严重(OOM Kill),建议 limits 设为实际峰值使用量的 1.5 倍
9.3 磁盘调优
4Gi足够大多数场景- 如果智能体需要下载大型数据集或生成大量文件,可提高到
8Gi或16Gi - 注意:ephemeral-storage 是容器可写层 + emptyDir 的总和
10. 诊断命令
当需要排查资源问题时,可在本地终端执行以下命令:
| 命令 | 用途 |
|---|---|
kubectl top nodes | 查看节点 CPU / 内存使用率 |
kubectl top pods -n agent-sandbox | 查看各 Pod 的 CPU / 内存实时使用 |
kubectl describe pod <pod-name> -n agent-sandbox | 查看 Pod 的资源请求、限制、事件(含 OOMKilled 记录) |
kubectl get events -n agent-sandbox --sort-by=.metadata.creationTimestamp | 查看最近事件(驱逐、重启等) |
kubectl describe node | 查看节点可分配资源和已分配量 |
下一步
了解了资源管理后,请继续阅读:
- AgentCore 通信机制 — 了解 WebSocket 双向通信协议
- 生命周期管理 — 了解 Worker 从创建到销毁的完整流程
- 外部数据库访问 — 了解如何连接宿主机数据库
技术可行性分析
本章对 K3s Runtime 模块涉及的各项关键技术进行可行性评估,帮助决策者理解每项能力的实现难度、技术成熟度和风险。
1. 评估维度说明
每项能力从以下四个维度评估:
| 维度 | 说明 |
|---|---|
| 可行性 | 技术上能否实现(✅ 已实现 / ⚠️ 可行需开发 / ❌ 不可行) |
| 复杂度 | 实现和维护的难度(低 / 中 / 高) |
| 成熟度 | 所依赖技术的成熟程度(成熟 / 较新 / 实验性) |
| 风险 | 可能遇到的主要风险和限制 |
2. 核心能力评估
2.1 容器化隔离执行
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现 |
| 复杂度 | 中 |
| 成熟度 | 成熟(K3s/K8s 已广泛使用) |
| 风险 | K3s 本身稳定;主要风险在于 hostPath 卷的单节点限制 |
技术依据:K3s 是 CNCF 认证的轻量 K8s 发行版,支持 Deployment、Service、Pod、ConfigMap、Secret 等核心资源。当前实现利用 K8s 的 Deployment + Service 资源作为执行单元(SessionWorker 模式),Pod 长驻运行通过 8081 端口接收消息,namespace(命名空间)和安全上下文实现隔离。
关键代码:workload_worker.rs(Deployment + Service 渲染)、apply.rs(资源创建与回滚)
2.2 多轮对话连续性
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现 |
| 复杂度 | 中 |
| 成熟度 | 成熟 |
| 风险 | 依赖 Worker 正确通过协议上报 checkpoint;如果 ACK 机制设计不清晰,可能出现“以为已保存、实际未落库” |
技术依据:通过两层机制实现:(1)Backend 持久化对话历史和 RunRecord;(2)Worker 在关键步骤通过协议把 checkpoint 发送给 Backend,由 Backend 统一写入数据库,下一轮再通过 run-context.json 与 checkpoint_get 恢复。
关键代码:volumes.rs(session_state_volume)、service.rs(sync_session_state)
2.3 文件挂载与宿主目录访问
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现 |
| 复杂度 | 低 |
| 成熟度 | 成熟 |
| 风险 | hostPath 依赖单节点;mount_grant 路径白名单需要严格维护 |
技术依据:K8s 原生支持 hostPath 卷类型,K3s 完全兼容。当前实现定义了四种卷(workspace、session-state、package、mount-grant),通过 volumes.rs 统一管理布局。
2.4 外部数据库访问
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现(架构上明确) |
| 复杂度 | 中 |
| 成熟度 | 成熟 |
| 风险 | Backend 会成为统一数据网关,需要做好限流、缓存、审计与超时控制 |
技术依据:当前方案不是让 Pod 直接连接数据库,而是由 Backend 统一访问数据库,Worker 通过协议发送 backend_query / backend_command / checkpoint_get / checkpoint_put。这样数据库凭据不进入容器,协议边界更稳定。
关键代码:service_bindings.rs
2.5 Agent 包分发
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现 |
| 复杂度 | 低 |
| 成熟度 | 成熟 |
| 风险 | ConfigMap 有 1MB 大小限制;二进制文件不适合通过 ConfigMap 分发 |
技术依据:Agent 包文件通过 ConfigMap 以 data 字段挂载为容器内文件。K8s ConfigMap 是经过生产验证的配置分发机制。
关键代码:package_bundle.rs
3. 演进能力评估
3.1 交互式会话(SessionWorker)
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 已实现 |
| 复杂度 | 高 |
| 成熟度 | 成熟(基于 K8s Deployment + WebSocket 双向通信) |
| 风险 | 长驻 Pod 资源管理需要可靠的空闲回收策略;心跳和超时机制已内置 |
技术分析:
已将执行模型从一次性 Job 升级为 Deployment + Service(长驻服务),容器内 agentcore 启动后主动建立 WebSocket 连接到 Backend,通过单条持久连接双向传输消息和事件。
graph TB
subgraph IMPL["已实现的 SessionWorker 组件"]
DEP["Deployment + Service<br/>workload_worker.rs"]
EVT["WebSocket 双向事件通道<br/>WorkerEventChannel"]
HB["心跳检测 + 就绪探针<br/>readinessProbe / livenessProbe"]
GS["优雅关闭 + TERM 信号处理<br/>worker-draining.log"]
IDLE["空闲回收策略<br/>WorkerIdlePolicy"]
end
| 子任务 | 状态 | 所用技术 |
|---|---|---|
| 长驻 Pod 渲染 | ✅ 已实现 | K8s Deployment + ClusterIP Service |
| WebSocket 双向通道 | ✅ 推荐 | Worker → Backend WS 连接,axum ws feature |
| HTTP 降级通道 | ✅ 备选 | agentcore 监听 8081 + HTTP POST 回调 |
| 心跳与超时回收 | ✅ 已实现 | readinessProbe + livenessProbe + idle_ttl |
| 流式 token 传递 | ✅ 已实现 | WS stream_chunk 帧(2-10 字节开销) |
| 用户交互(ask_user) | ✅ 已实现 | WS ask_user + user_reply |
| Backend 数据协议 | ✅ 推荐 | backend_query / backend_command / checkpoint_get / checkpoint_put |
3.2 预热镜像与镜像缓存
| 维度 | 评估 |
|---|---|
| 可行性 | ✅ 可行 |
| 复杂度 | 低-中 |
| 成熟度 | 成熟 |
| 风险 | 镜像构建流程需要 CI/CD 支持;镜像版本管理需规范 |
技术分析:开发者编写 Dockerfile 预装依赖,CI/CD 构建后推送到 Registry。RuntimeProfile 指定镜像地址,K3s 节点通过 imagePullPolicy: IfNotPresent 缓存已拉取的镜像层。
3.3 本地工具桥接
| 维度 | 评估 |
|---|---|
| 可行性 | ⚠️ 可行,需要开发 |
| 复杂度 | 高 |
| 成熟度 | 较新 |
| 风险 | 安全性是最大挑战:需要严格的工具白名单和权限控制 |
技术分析:
需要建立 Container → Backend → Helper 的反向调用链路:
- agentcore 通过 HTTP 调用 Backend 的“工具代理“端点
- Backend 通过已建立的 WebSocket 连接转发给 Helper
- Helper 在本地执行工具并返回结果
安全约束:白名单工具列表、参数校验、执行超时、结果大小限制。
3.4 多节点扩展(远程部署场景,非当前重点)
注意:当前系统设计为本地部署(用户本机 = 宿主机),以下多节点分析为未来扩展预留。
| 维度 | 评估 |
|---|---|
| 可行性 | ⚠️ 可行,需要改造 |
| 复杂度 | 高 |
| 成熟度 | 成熟(NFS/CSI 是成熟技术) |
| 风险 | 存储性能下降;网络文件系统的一致性问题 |
技术分析:
当前 hostPath 卷依赖本地单节点,天然适合本地部署模式。若未来需要远程多节点扩展:
| 改造项 | 方案 |
|---|---|
| 文件存储 | hostPath → NFS/CSI PersistentVolume(持久卷) |
| Pod 调度 | 移除 nodeAffinity,依赖共享存储 |
| Session 状态 | 从本地目录迁移到共享存储 |
| ConfigMap | 不受影响(K8s 原生分布式) |
代码中 VolumeBackend 枚举已为此预留扩展点。
4. 综合评估矩阵
graph TB
subgraph Q1["✅ 优先实现(高价值 + 低复杂度)"]
A1["容器隔离执行"]
A2["多轮连续性"]
A3["文件挂载"]
end
subgraph Q2["📋 值得投入(高价值 + 高复杂度)"]
B1["交互式会话 SessionWorker"]
B2["本地工具桥"]
end
subgraph Q3["⏳ 按需实现(低价值 + 低复杂度)"]
C1["Backend 统一数据访问"]
C2["预热镜像"]
end
subgraph Q4["⚠️ 谨慎评估(低价值 + 高复杂度)"]
D1["多节点扩展"]
end
style Q1 fill:#d4edda,stroke:#28a745
style Q2 fill:#fff3cd,stroke:#ffc107
style Q3 fill:#e2e3e5,stroke:#6c757d
style Q4 fill:#f8d7da,stroke:#dc3545
5. 实施路线建议
短期(已实现或即将完成)
- ✅ 容器化隔离执行
- ✅ 多轮对话连续性
- ✅ 文件挂载与外部目录访问
- ✅ Backend 统一数据库访问
- ✅ Agent 包 ConfigMap 分发
中期
- 预热镜像支持
- 镜像层缓存优化
- QueueConsumer 事件通道(为高并发场景准备)
长期
- 本地工具桥接
- 远程部署模式与多节点存储迁移
附录:技术选型参考
| 技术领域 | 当前选择 | 替代方案 |
|---|---|---|
| 容器编排 | K3s | K8s、Docker Compose、Podman |
| 工作负载 | Deployment + Service(SessionWorker) | StatefulSet |
| 文件存储 | hostPath | NFS、Ceph CSI、Longhorn |
| 配置分发 | ConfigMap | OCI Artifact、Init Container |
| 密钥管理 | K8s Secret | Vault、Sealed Secrets |
| 通信协议 | WebSocket(推荐)+ HTTP REST | gRPC、NATS |
| 运行时 | containerd | CRI-O |