Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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::CodexRuntimeFamily::ClaudeCodecodex-standardclaude-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.history
  • SessionRecord.latest_run_id
  • SessionRecord.runtime_state
  • ArtifactRecord
  • RunRecord
  • backend/data/workspaces/<run-id>/out/ 下的结果文件
  • backend/data/sessions/<session-id>/ 下的 session 级运行时状态目录

如果 agentcore 自带 checkpoint,当前推荐始终写到 /workspace/session/checkpoints;backend 已会扫描最近 checkpoint 摘要,但还没有把“恢复到哪个 checkpoint”做成独立 API。

代码入口

如果要从源码反向阅读整个流程,建议按下面顺序看:

  1. backend/crates/app/src/main.rs
  2. backend/crates/app/src/lib.rs
  3. backend/crates/app/src/api/mod.rs
  4. backend/crates/app/src/service.rs
  5. backend/crates/app/src/k8s/mod.rs
  6. backend/crates/k3s-runtime/src/lib.rs
  7. backend/crates/app/src/storage/mod.rs
  8. backend/crates/app/src/provider.rs
  9. backend/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.app
  • macos-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.jsonuploads/workspaces/sessions/,保存 sessionartifactmount grantrun
PackageCatalog(Agent 包目录索引)backend/packages/* 加载成内存里的 LoadedPackage(已加载 Agent 包)
RuntimeProfileCatalog(运行时模板目录索引)backend/config/runtime_profiles.yaml 加载成内存里的 RuntimeProfile
K3sOrchestrator(K3s 编排器)渲染和可选提交 ConfigMapSecretJob,并观察 K8s 状态
ApplicationService(应用服务层)串起 API、状态存储、工作区准备、K8s 提交、状态刷新、多轮会话同步
Router(HTTP 路由)对外暴露 /api/v1/* 接口

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

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

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

2.1 SessionRecord(逻辑会话记录)

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

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

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

2.2 ArtifactRecord(上传文件记录)

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

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

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

  • host_path
  • mount_path
  • access_mode

2.4 RunRecord(一次运行记录)

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

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

当前物理隔离单元就是 RunRecord

2.5 RuntimeProfile(运行时配置模板)

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

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

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

2.6 AgentPackageManifest(Agent 包清单)

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

  • runtime
  • entry
  • policy
  • assets

当前包目录示例位于:

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

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

3. agentcoreagent package 当前怎样组合

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

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

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

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

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

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

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

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

3.3 两者的绑定时机

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

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

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

  1. ConfigMap
  2. Secret
  3. Job

3.4 当前组合模式的结论

结论很明确:

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

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

4.1 逻辑隔离按 session

SessionRecord 保存:

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

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

4.2 物理隔离按 run

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

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

这意味着当前模型是:

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

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

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

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

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

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

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

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

当前代码已经具备:

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

当前代码还没有显式的:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.4 K3s create 链路的技术细节

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

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

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

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

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

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

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

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

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

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

5.6 当前 create 路径的边界与 TODO

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

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

建议保留为后续 TODO:

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

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

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

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

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

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

这里有一个关键前提:

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

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

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

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

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

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

这样做的意义是:

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

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

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

5.7.1 推荐的实现技术

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

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

不建议默认采用:

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

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

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

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

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

原因是:

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

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

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

而不是:

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

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

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

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

原因很直接:

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

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

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

那可以理解为:

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

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

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

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

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

换句话说:

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

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

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

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

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

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

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

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

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

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

7. backend 与 agentcore 的责任边界

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

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

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

隔离模型、权限模型与安全边界

这一章专门回答“隔离做到哪一层”“权限到底有多强”“哪些能力是声明性元数据,哪些能力已经强制生效”。

1. 隔离粒度:当前分成三层

当前代码里实际上存在三种不同层次的隔离。

1.1 user_id(用户所有权边界)是对象级隔离

后端要求所有用户态 API 都带 x-user-idFileBackedStore(文件型状态存储)在读取 SessionRecordArtifactRecordMountGrantRecordRunRecord 时都会调用 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: false
  • capabilities.drop = ["ALL"]
  • runAsNonRoot: true
  • runAsUser: 1000
  • runAsGroup: 1000
  • fsGroup: 1000
  • seccompProfile.type = RuntimeDefault
  • automountServiceAccountToken: false
  • restartPolicy: 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 还是 mount
  • permissionProfile:当前只是一段包策略元数据,没有被后端强制映射成 Linux/K8s/工具权限

所以目前真正“强制落地”的权限主要来自:

  • Pod 安全上下文
  • 工作区 copy / mount 选择
  • 挂载白名单
  • readOnly / readWrite 挂载模式

permissionProfile 当前承担的是描述和标识作用。

3. 一个 session、一个用户、一个 run,分别隔离什么

隔离单元当前是否独立容器当前是否独立工作区当前是否独立历史当前用途
用户作为所有权边界
Session作为多轮上下文边界
Run只记录本轮作为实际执行边界

因此如果要回答“目前是一个会话一个隔离环境,还是一个用户一个隔离环境”,答案是:

  • 逻辑隔离按 session
  • 物理隔离按 run

4. copymount 两种工作区模式的安全差异

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(挂载路径校验)会做三件事:

  1. 把用户给的路径 canonicalize
  2. 确认它是一个已存在目录
  3. 确认它落在 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 容器内可以访问哪些“外部工具”

当前真正可执行的工具只有三类:

  • 基础镜像里自带的工具,例如 bashnode
  • inlineNpm(运行时内联 npm 安装)装出来的 CLI
  • 容器内通过 HTTP 访问的模型 API

7.2 build_in_tools.yaml 当前是提示词工具清单

build_in_tools.yaml(内置工具清单)虽然声明了 builtin.readbuiltin.writebuiltin.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 默认值合并器)会合并三层来源:

  1. backend 进程环境变量
  2. RuntimeProfile 里的默认 baseUrl / model
  3. 单次请求的 provider_env

合并后得到的环境变量会被写成每 run 独立的 K8s Secret。渲染返回给前端时,providerSecretYaml 里的值会被替换成 <redacted>

这说明当前 provider 凭据隔离粒度是:

  • 按 run 注入
  • 按 Secret 隔离
  • 按用户 API 读取权限隔离

9. 当前安全边界的真实结论

如果只基于代码,不做理想化表述,可以下结论:

  1. 当前已经有“每 run 独立 Pod + 非 root + drop capabilities + seccomp + 禁止自动 SA token”这一层容器安全基线。
  2. 当前默认更安全的输入方式是 copymount 属于更强权限能力。
  3. 当前 mount 具备白名单和只读/读写控制,但仍然属于较强权限能力,应谨慎开放。
  4. 当前 permissionProfilenetworkProfile 主要还是描述性元数据,不应误认为强制安全策略。
  5. 当前没有把本地 macOS 工具或 backend 宿主机工具安全地暴露给容器的机制。

运行生命周期与容器编排流程

这一章从用户视角出发,解释当前代码里一次会话、一次运行和一次容器编排到底怎样流动。

1. 从用户视角看,一次会话会经历什么

用户在产品里看到的主流程可以理解为下面几步:

  1. 打开一个 session(会话)
  2. 上传文件,或选择已有文件
  3. 选择一个 agent package
  4. 发送一轮 prompt
  5. backend 为这轮 prompt 创建一个新的 run
  6. K3s 为这个 run 拉起一个独立 Pod
  7. agentcore 在 Pod 里处理本轮任务并写结果
  8. backend 把结果状态写回 run,把主回答写回 session history
  9. 用户离开页面,稍后回来继续使用同一个 session

这个流程里,“连续的是 session”,“独立的是 run”。

2. 后端进程生命周期

main(进程入口)启动后会依次完成:

  1. 初始化 tracing
  2. 兼容旧环境变量别名
  3. 解析 AppConfig(进程级配置)
  4. build_application(应用装配入口)
  5. 绑定 HTTP 地址
  6. 用 axum 对外提供 API
  7. 监听退出信号并优雅关闭

如果 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(逻辑会话记录)当前只有两个状态:

  • ACTIVE
  • CLOSED

生命周期如下:

  1. POST /api/v1/sessions 创建,初始为 ACTIVE
  2. 可反复用于多轮 create_run
  3. POST /api/v1/sessions/{id}/close 后变成 CLOSED
  4. POST /api/v1/sessions/{id}/reopen 后回到 ACTIVE
  5. CLOSED 状态下仍可查询历史和结果

这里要特别注意:close 表示“归档并拒绝新写入”,会话数据会继续保留。

4. run 的生命周期

RunRecord(一次运行记录)常见状态包括:

  • PREVIEWED
  • SUBMITTED
  • PENDING
  • COMPLETED
  • FAILED
  • CANCELLED

其生命周期是:

  1. 用户请求创建 run
  2. backend 准备工作区和上下文
  3. backend 提交或渲染 K3s 资源
  4. Pod 执行
  5. backend 刷新状态
  6. run 进入完成、失败或取消态

5. create_run 的完整用户视角流程

5.1 用户看到的过程

从用户角度,一次“发送消息”背后发生的是:

  1. backend 读取当前会话
  2. backend 读取本轮所选文件和挂载授权
  3. backend 检查集群是否还能接收新任务
  4. backend 把旧回答补回会话历史
  5. backend 把本轮用户输入追加到会话历史
  6. backend 创建本轮独立工作区
  7. backend 把上下文写成 run-context.json
  8. backend 构造 ConfigMapSecretJob
  9. K3s 启动本轮 Pod
  10. 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(构建运行资源包)会在内存里构造:

  1. 承载 agent package 文件的 ConfigMap
  2. 承载 provider 环境变量的 Secret
  3. 真正执行 run 的 Job

6.3 K3sOrchestrator 负责和集群交互

K3sOrchestrator::submit_run(提交运行)在 apply 模式下会调用:

  • Api<ConfigMap>::create
  • Api<Secret>::create
  • Api<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 执行并写出结果

这里要特别注意两个事实:

  1. backend 只负责把资源写入 API Server,不直接驱动 Pod 生命周期。
  2. Pod 能否真的跑起来,还取决于调度、镜像、节点磁盘、挂载卷、provider 凭据等后续条件。

6.5 create 成功不等于业务已经开始执行

当前系统里至少有三种“create 成功”的层次:

  1. backend 成功返回 RunRecord
  2. Kubernetes API 成功创建 Job
  3. Pod 真的被调度并开始运行

这三层不是一回事。

所以今天 backend 的状态刷新才需要同时看:

  • /workspace/out/* 输出标记
  • Job / Pod 的集群状态

因为“资源对象已创建”并不等于“容器已经完成执行”。

7. Pod 内执行生命周期

7.1 容器启动后先做什么

job_builder::build_run_script(构建容器启动脚本)会生成启动脚本,脚本大致会:

  1. 创建 /workspace/in/workspace/out/workspace/tmp
  2. 打印 run、session、user、workspace 信息
  3. 如有挂载授权,列出挂载目录文件
  4. 根据 installStrategy 决定是否做运行时安装
  5. 执行 verify 命令
  6. 输出 package 和输入文件信息
  7. 进入具体执行模式

7.2 当前执行模式怎样理解

当前代码里的 ExecutionMode(执行模式)主要有两种:

  • CliVerify
  • OpenAiCompatibleApi

它们表达的是容器内执行入口的行为方式。从架构角度,这仍然属于 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.txt
  • final-answer.md
  • provider-response.json
  • llm-run-summary.json
  • execution-mode.txt

如果这些文件已经出现,就可以直接推断 run 已完成或失败。

8.2 第二类:看 K8s Job / Pod 状态

如果输出目录还看不出来,backend 会调用 K3sOrchestrator::inspect_run_status(检查运行状态),内部通过:

  • Api<Job>::get_opt
  • Api<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 文本对话连续性

文本对话连续性由下面两个动作保证:

  1. sync_session_conversation_history(同步会话历史)
    在新 run 开始前刷新同一 session 下旧 run 的状态。
  2. sync_run_assistant_message(同步助手回答)
    在 run 完成后读取 final-answer.md,把它回填进 SessionRecord.history

因此当前同一 session 的下一轮一定可以拿到上一轮的主回答。

9.2 文件结果连续性

run 完成后,当前代码会保留:

  • RunRecord
  • workspace_root
  • workspaces/<run-id>/out/*

因此用户下次回来时,仍能查看历史结果文件。

9.3 上传文件连续性

只要 ArtifactRecorduploads/ 下的文件还在,后续 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_INPUTWAITING_APPROVALWAITING_ARTIFACTS

因此今天的模型是:

  • 同一 session 可以连续创建多个 run
  • 但同一个正在运行的容器不会持续监听并接收新的用户消息

9.6 K3s 能否支持“等待用户 / 审批 / 补文件 / 再继续”

可以,但要分清楚K3s 能提供的容器编排能力backend 必须补的控制面语义

K3s 可以提供:

  • 长驻 Pod、DeploymentStatefulSet
  • Service 暴露稳定访问入口
  • readinessProbe / livenessProbe
  • Lease 或自定义心跳,用于 worker 占有与回收
  • PVC 或其它持久化卷,用于 session state 和 checkpoint
  • 显式删除 Pod / Deployment,实现“空闲超时回收”

但 K3s 本身不会替你决定:

  • 什么时候应该向用户提问
  • 哪个工具调用需要审批
  • 用户迟迟不上传文件时应进入什么业务状态
  • 收到新消息后是复用旧容器还是重启新 run

这些都必须由 backend 控制面建模。

9.7 两种可行的交互式运行模式

模式 A:冷恢复模式

这是最稳的方式,也是最贴近当前架构的方式:

  1. run 执行到需要用户输入、审批或文件时,把 checkpoint 写入 /workspace/session
  2. run 上报一个阻塞事件,例如 WAITING_USER_INPUT
  3. 容器退出,释放 CPU / 内存
  4. backend 等待用户回复、审批结果或新 artifact
  5. backend 创建一个新的 run,从同一 session state 恢复继续执行

这个模式下,K3s 仍然主要使用 Job

模式 B:Warm Sandbox 模式

如果想优化“用户刚回复就秒回”的体验,可以让容器短时间保活:

  1. 第一次请求启动一个长驻 Pod,而不是一次性 Job
  2. 容器进入 idle,等待同一 session 的下一条事件
  3. backend 给它设置 idle_ttl
  4. 用户在 TTL 内回复、审批或补文件,就把事件投递给这个 Pod
  5. 超过 TTL 仍无新事件,就让容器 checkpoint 后退出

这个模式下,更适合使用:

  • Deployment + 每 session 一个 worker
  • StatefulSet / 命名 Pod + PVC

它能改善连续体验,但复杂度明显高于当前 batch run。

模式 B 下,长驻 Pod 如何等待并接收下一条用户事件

如果后续采用 warm sandbox,那么“等待”不应实现成容器里的 busy loop,而应实现成一个显式事件循环:

  1. backend 为 session worker 分配稳定标识,例如 session_id + worker_id
  2. worker 启动后向 backend 注册可用状态,进入 idle
  3. backend 通过显式事件通道投递下一条事件,例如:
    • USER_MESSAGE
    • APPROVAL_RESULT
    • ARTIFACT_READY
    • CANCEL
    • SHUTDOWN
  4. worker 收到事件后进入 busy,处理完后再回到 idle
  5. 如果超过 idle_ttl 仍无新事件,则 worker checkpoint 后退出

事件通道本身可以后选:

  • HTTP / gRPC
  • WebSocket
  • 长轮询
  • 队列

但“worker 通过什么机制收到下一条事件”必须在控制面里明确,否则长驻 Pod 只是在空转。

模式 B 下,长驻 Pod 如何向用户提问

如果 worker 需要 ask_user、申请权限或索取新文件,建议统一走下面的链路:

  1. worker 向 backend 上报一个 InteractionRequest
  2. backend 持久化为 PendingActionRecord
  3. backend 把问题、审批请求或缺失文件要求展示给前端
  4. worker 进入 waiting_user 或回到 idle
  5. 用户回复后,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 的完整可恢复状态”简单理解为一个可打包镜像

原因有四类:

  1. 镜像主要表达的是容器文件系统层,不表达:
    • 进程内存
    • 打开的文件描述符
    • TCP 连接
    • PID / namespace 运行态
  2. 当前真正重要的业务状态主要放在挂载卷里,例如:
    • /workspace
    • /workspace/session
    • /workspace/session/checkpoints 这些内容本来就不应该依赖镜像层保存。
  3. ConfigMapSecrethostPath、PVC 都是运行时挂载语义,不会因为“镜像化”自动被完整封装。
  4. 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_INPUTWAITING_APPROVALWAITING_ARTIFACTSEXPIRED
  • TODO:新增 PendingActionRecordInteractionRequestRecord,显式保存“问题内容、审批请求、所需文件、超时时间、恢复目标 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_USERREQUEST_APPROVALREQUEST_ARTIFACTCHECKPOINT_READYHEARTBEAT
  • TODO:为 warm sandbox 补齐 idlebusywaiting_userdraining 的状态边界
  • 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 控制面与状态管理

已经具备:

  • sessionartifactmount grantrun 的完整 API
  • SessionStatusclose / 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(运行时家族)枚举仍使用样例标识符,例如:

  • Codex
  • ClaudeCode

这不影响运行时架构本身,因为真正决定容器行为的是:

  • image
  • executable
  • installStrategy
  • executionMode

但它说明当前代码在“可扩展 family 标识”上仍有代码级约束。如果后续要把 runtime family 做成完全开放的扩展点,可以把它进一步放宽成字符串或可扩展枚举。

2.4 当前 K8s 资源回收和工作区回收还没有完全打通

当前 run 结束后:

  • Job / Pod 会被 TTL 或取消逻辑处理
  • workspace、artifact、run record、session history 会继续保留
  • 正常完成态 run 的 ConfigMapSecret 还没有统一 GC

因此如果后续 run 数量很多,需要补充后台 GC 或 retention policy,把:

  • workspaces/
  • uploads/
  • ConfigMap
  • Secret

纳入统一清理策略。

2.5 当前工作区基于 hostPath

hostPath 的优点是简单直观、便于单节点 K3s 验证。当前代价也很明确:

  • 强依赖 backend 所在节点的本地磁盘
  • 磁盘压力会直接影响调度
  • 不适合做跨节点迁移
  • 隔离强度弱于更高级的沙箱方案

另外,当前代码还没有 nodeSelectoraffinity 或 PVC 绑定逻辑,因此多节点场景下默认仍应视为“单节点优先”设计。

2.6 当前 backend 采用本地持久化控制面

FileBackedStore 使用本地:

  • state.json
  • uploads/
  • 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. 当前架构为什么仍然是合理的

尽管上面有不少边界,但以当前代码目标来看,这套方案仍然成立,因为它先解决了最核心的几件事:

  1. 把 Agent 运行从 backend 进程里剥离到了独立容器
  2. agent package、运行时模板、provider、workspace 拆成了清晰的组合关系
  3. 把 session 多轮上下文和 run 物理隔离分开建模
  4. 让前端、helper、backend、K3s 之间有了可验证的 API 契约

5. 如果继续演进,优先级建议是什么

5.1 第一优先级:补齐 session 级状态目录

优先补齐:

  • resume / restore API
  • checkpoint retention / GC 策略
  • 输出文件到下一轮输入的受控提升机制
  • 面向多节点的持久化卷抽象

这是把“会话历史连续”升级成“完整运行时状态连续”的关键一步。

5.2 第二优先级:把“声明型策略”变成“强执行策略”

优先补齐:

  • permissionProfile 到真实能力矩阵
  • networkProfile 到真实 NetworkPolicy
  • builtInTools 到真实工具调度层

5.3 第三优先级:补齐交互式等待与恢复模型

建议优先补齐:

  • WAITING_USER_INPUTWAITING_APPROVALWAITING_ARTIFACTS
  • PendingAction 数据模型
  • 冷恢复模式下的 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:增加 PendingActionApprovalRequestArtifactRequest 之类的控制面对象
  • TODO:支持 checkpoint 后退出、等用户事件后 resumed run 的冷恢复链路
  • TODO:支持可选 warm sandbox:DeploymentStatefulSet 承载每 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 的缩写,指资源的自动过期时间

核心设计理念

在阅读后续文档前,请先记住以下三个关键设计原则:

  1. 逻辑连续按会话,物理隔离按运行:同一个 Session(会话)可以包含多次 Run(运行),每次运行都是一个全新的容器,但它们共享同一个会话状态目录。
  2. 镜像层负责“能跑“,状态层负责“能续“:容器镜像只包含运行时环境和工具;用户数据、对话历史、检查点文件等状态信息存储在外部挂载的目录中。
  3. 声明式编排,分层职责:后端只负责“声明需要什么资源“,真正创建和管理容器的是 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 不直接连接数据库。

这包括三类数据:

  1. 会话数据库:对话历史、RunRecord、SessionRuntimeState 等
  2. 检查点数据库:checkpoint、恢复快照、执行中间状态
  3. 业务数据库: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

为什么要分成两层

这种分层设计带来三个明确的好处:

  1. 职责清晰:Backend 可以独立演进会话管理、权限校验等业务逻辑,不需要了解 Kubernetes API 的细节。
  2. 可替换执行层:如果未来需要把 K3s 替换成其他容器编排系统,只需要替换 K3s 运行时 crate,Backend 的代码基本不用改。
  3. 便于并行开发:不同开发者可以同时修改不同模块而不互相冲突。

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-&lt;run-id&gt;<br/>存放 Agent 包的文本文件"]
        SEC["Secret<br/>🔒 agent-env-&lt;run-id&gt;<br/>存放 API 密钥等敏感信息"]
        DEP["Deployment<br/>⚙️ agent-worker-&lt;run-id&gt;<br/>定义长驻 Worker 容器"]
        SVC["Service<br/>🌐 agent-worker-svc-&lt;run-id&gt;<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

这类操作必须定义成同步语义,原因有三个:

  1. Worker 后续推理往往依赖查询结果,不能“先继续再补结果”。
  2. Backend 可以在同一次响应里附带权限校验、数据裁剪和审计结果。
  3. 超时、权限不足、数据不存在等错误必须立刻返回给 Worker,便于当前步骤决定重试、降级或询问用户。

7. 为什么“流式输出”必须是异步语义

stream_chunkstate_changetool_progress 这类消息的目标是边执行边展示。如果把它们设计成同步语义,会导致:

  • Worker 每输出一个 token 都要等待确认
  • Backend 和前端很容易成为吞吐瓶颈
  • 整个回复速度明显下降

因此这些消息统一采用异步事件语义:尽快发送、尽快渲染,必要时依靠 WebSocket 自身的 TCP 背压控制限速。


8. 需要特别注意的“混合语义”操作

操作为什么是混合
创建运行API 层要立刻返回 run_id,但容器启动和执行要异步完成
ask_user事件推送本身异步,但 Worker 的业务流程会暂停等待用户答复
取消运行提交取消请求同步成功,不代表 Worker 已经立刻退出
关闭会话会话状态可以立刻改成 closed,但 Deployment 回收还需要异步完成

9. 当前推荐约束

为了让协议清晰且稳定,推荐遵循以下约束:

  1. 所有数据库读写统一走 Backend,Worker 不直接持有数据库 DSN、账号和驱动配置。
  2. 所有需要“等结果才能继续”的协议消息都必须有明确响应消息,不能只靠超时猜测结果。
  3. 所有高频输出都采用异步事件,不要为每个 token 设计同步确认。
  4. 所有长任务都拆成“启动 + 进度 + 完成”三段式,不要让单次同步请求占住连接太久。

下一步

理解了同步/异步语义后,建议继续阅读:

运行生命周期

本章从用户视角出发,完整描述“用户发一条消息后,系统到底做了什么“。系统采用 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 收到创建运行的请求后,依次执行:

  1. 校验 Session(会话):确认会话存在且未关闭
  2. 解析 agent package(智能体包):从包目录索引中读取开发者包
  3. 解析 RuntimeProfile(运行时配置模板):确定基础镜像和资源配额
  4. 合并 provider 配置:将 API 密钥从环境变量、运行时模板、本次请求三层合并
  5. 校验输入文件和挂载:确认文件存在,挂载路径在白名单内

阶段二:创建工作区(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 编排器渲染和提交

编排器执行以下步骤:

  1. workload_worker.rs 组装 Deployment + Service 定义
  2. 容器暴露 8081 端口(control 控制端口)
  3. 配置 readiness/liveness 探针(检查 /tmp/agent-store-worker.ready 标记文件)
  4. 注入所有环境变量(事件通道、空闲策略、包信息、会话信息等)
  5. apply.rs 按顺序创建:ConfigMap → Secret → Service → Deployment
  6. 设置 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 行为
PENDINGDeployment 已创建,Pod 尚未就绪等待,轮询 Pod 状态
IDLE容器就绪,等待消息可以推送新消息
BUSY正在执行用户请求接收流式事件,不发新消息
WAITING_USERagentcore 在等待用户回复转发询问给用户,等待回复
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:

  1. 新 Worker 启动,Backend 从数据库读取会话历史和最近 checkpoint 摘要,生成 run-context.json
  2. Worker 建立 WebSocket 连接后,如需完整恢复数据,发送 checkpoint_get
  3. Backend 查询数据库并返回最新可用 checkpoint
  4. agentcore 根据返回结果恢复执行状态
  5. 从用户角度看,对话无缝继续

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
SUBMITTEDDeployment + 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数据库 PackageCatalogAgent 包声明:名称、描述、工具列表等
builtInTools数据库 LoadedPackageAgent 包内置工具定义
envFacts数据库 LoadedPackage环境事实(如运行时版本信息)
inputArtifacts本次请求用户上传的附件文件名列表
systemPrompt数据库 PackageCatalogAgent 包开发者定义的系统提示词
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 会从两个地方获得连续性信息:

  1. run-context.json 初始快照:由 Backend 查库后生成
  2. 协议化状态请求: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_responseagentcore 的最终回答完整文本 + 渲染格式(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:

  1. 校验该 checkpoint 是否属于当前 Session / Run
  2. 写入 Backend 管理的数据库
  3. 返回 checkpoint_put_ack
  4. 更新 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 界面上传文件,文件经过以下链路到达容器:

  1. 用户在 Web UI 中选择文件
  2. Helper 将文件传递给 Backend(由于都在本机,这一步几乎是瞬时的)
  3. Backend 将文件复制到 workspaces/[run-id]/in/
  4. 容器启动后从 /workspace/in/ 读取

适用范围:文档、表格、配置文件等小于 100 MB 的文件。

场景二:大文件或本地目录挂载

对于大型数据集或需要整个目录的场景,使用 mount_grant(挂载授权)机制:

  1. 用户在创建 Run 时指定本地目录路径(如 /Users/xxx/datasets/
  2. Backend 校验路径是否在白名单内
  3. K3s 编排器将路径添加为 hostPath 卷
  4. 容器内通过 /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 可以:

  1. 在容器内下载:如果容器网络策略允许出站流量,agentcore 可以通过工具调用直接从 URL 下载文件
  2. 通过工具桥接:未来计划通过 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

这样可以直接避免几个典型问题:

  1. Pod 内 localhost 不是宿主机 localhost
  2. 容器不需要知道宿主机 IP、Service、Endpoints 这些网络细节
  3. 容器里不需要再装数据库驱动和连接配置

所以在你们当前的架构里,“宿主机上的数据库怎么让容器访问”这个问题本身就被收敛掉了:不是容器去访问,而是 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 ↔ WorkerWebSocket(推荐)双向实时Worker 启动后主动连接 Backend 的 WS 端点,所有消息和事件通过同一条持久连接传输
Helper ↔ BackendWebSocket双向实时Helper 连接 Backend 的 WS 端点,实时接收流式输出和状态更新
Helper → BackendHTTP 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 过快压垮 BackendTCP 流控天然提供背压(backpressure),自动限速
双向通信需要两套独立的 HTTP 通道(Backend→Worker + Worker→Backend)同一条连接双向传输,架构更简单
连接状态感知需要轮询或心跳检测 Worker 是否存活连接断开时 Backend 立即感知
延迟本地部署约 1-2ms / 请求(TCP 握手 + HTTP 解析)本地部署约 0.1ms / 帧(仅帧解析)

1.5 协议语义总规则

类别语义代表消息
同步请求-响应必须等待结果再继续backend_querybackend_commandcheckpoint_getcheckpoint_put
异步事件流发出后继续执行stream_chunkstate_changetool_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本次运行 ID550e8400-e29b-41d4-a716-446655440000
SESSION_ID会话 ID660e8400-e29b-41d4-a716-446655440001
USER_ID用户 IDuser-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_NAMEWorker 的 K8s Service 名称agent-worker-svc-xxxx
AGENT_WORKER_EVENT_CHANNEL_KIND事件通道类型websocket(推荐) / http-callback(降级)
AGENT_WORKER_WS_URLWebSocket 连接地址(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_IDBackend ↔ Worker 数据通道Worker 主动连接
ws://backend:PORT/ws/sessions/SESSION_IDBackend → 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_useragentcore 向用户提问按需
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 秒发送 ping30 秒无 pongBackend 判定 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

这样做的直接收益是:

  1. 数据库账号、密码、DSN 只保留在 Backend
  2. Worker 只面对统一协议,不区分 PostgreSQL / MySQL / Redis / 向量库
  3. Backend 可以在查库前做权限控制、审计、缓存和限流

优雅关闭

Worker 收到 SIGTERM
  -> 通过 WS 发送 state_change(DRAINING)
  -> Backend 停止推送新消息
  -> 等待当前任务完成
  -> 发送 WebSocket Close 帧(状态码 1000: Normal Closure)
  -> 关闭连接
  -> 进程退出

3.7 事件通道机制(WorkerEventChannel)

代码中的 WorkerEventChannel(工作进程事件通道)枚举定义了通信模式。推荐升级后支持三种:

通道类型环境变量工作方式状态适用场景
WebSocketAGENT_WORKER_WS_URLWorker 主动连接 Backend WS 端点,双向通信推荐所有场景,特别是流式输出
HttpCallbackAGENT_WORKER_HTTP_CALLBACK_URLWorker 主动 POST 到 Backend + Backend POST 到 Worker 8081 端口降级备选WebSocket 不可用时
QueueConsumerAGENT_WORKER_QUEUE_NAMEWorker 从消息队列拉取/推送预留未来高并发分布式场景

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 端口(控制端口)。

端点方法用途
/messagesPOST推送用户消息给 agentcore
/cancelPOST取消当前正在执行的任务
/statusGET查询当前生命周期状态
/healthGET健康检查

4.2 Worker → Backend:HTTP 回调

Worker 通过 AGENT_WORKER_HTTP_CALLBACK_URL 向 Backend 推送事件。每种事件类型(stream_chunk、message_response、ask_user 等)都是一个独立的 HTTP POST 请求。

缺点:每个 stream_chunk token 片段都需要一个完整的 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/uploadPOST /mount-grants
  • 混合POST /runsPOST /runs/{id}/cancel
  • 异步结果承载:真正的执行结果仍通过 WebSocket 返回
操作方法路径说明
健康检查GET/health返回 200 表示服务正常
就绪检查GET/ready返回 200 表示服务可接受请求
集群状态GET/api/v1/cluster/statusK3s 集群健康状态
运行时配置列表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_chunkAI 输出的 token 片段Worker 转发
message_response最终回答Worker 转发
ask_useragentcore 向用户提问Worker 转发
state_changeWorker 状态变更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 行为
PENDINGDeployment 已创建,Pod 正在启动未连接等待 WS 连接
IDLEWorker 就绪,等待消息已连接可以推送消息
BUSY正在处理用户请求已连接,收发中接收流式事件
WAITING_USERagentcore 在等待用户回复已连接,等待中转发 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 调用(如管理接口)

下一步

了解了通信机制后,请继续阅读:

资源配置与容量管理

本章详细说明 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: 500mcpu.weight(按比例分配)保证容器在 CPU 竞争时至少获得 0.5 核的计算时间
limits.cpu: 500mcpu.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: 1Gimemory.max = 1073741824硬限制:容器总内存不超过 1Gi
  • 超过 limits 时:内核触发 OOM Killer(Out-Of-Memory 杀手),直接终止容器进程
  • 这比 CPU 严格得多——CPU 超限只是变慢,内存超限直接被杀
  • 因此 agentcore 需要控制自身内存使用,特别是处理大型上下文时

1.4 cgroups 如何控制磁盘

ephemeral-storage(临时存储)的限制方式与 CPU/内存不同:

K8s 配置机制作用
limits.ephemeral-storage: 4Gikubelet 周期性扫描容器写入量超过限额时驱逐(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-standard500m / 500m1Gi / 1Gi4Gi / 4Gi
codex-large500m / 500m1Gi / 1Gi4Gi / 4Gi
claude-code-standard500m / 500m1Gi / 1Gi4Gi / 4Gi
claude-code-large500m / 500m1Gi / 1Gi4Gi / 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固定 UIDagentcore 以普通用户身份运行,无法访问系统文件
fsGroup: 1000挂载卷文件组确保 agentcore 可以读写挂载的 workspace 目录
seccompProfile系统调用白名单阻止容器执行危险的系统调用(如 mountreboot

4.2 容器级安全设置

securityContext:
  allowPrivilegeEscalation: false  # 禁止提权
  capabilities:
    drop: ["ALL"]                  # 丢弃所有 Linux capabilities
  readOnlyRootFilesystem: false    # 允许写入(agentcore 需要临时文件)
设置作用
allowPrivilegeEscalation: false阻止通过 setuid/setgid 提升权限
capabilities.drop: ALL移除所有特殊权限(网络配置、进程管理等)
readOnlyRootFilesystem: falseagentcore 需要写入临时文件和日志

4.3 其他安全设置

automountServiceAccountToken: false   # 不挂载 K8s 服务账号令牌
terminationGracePeriodSeconds: 30     # 优雅关闭等待 30 秒
  • automountServiceAccountToken: false:防止容器内进程访问 K8s API,消除容器逃逸风险
  • terminationGracePeriodSeconds: 30:给 agentcore 30 秒时间保存状态后退出

5. 空闲回收策略(WorkerIdlePolicy)

SessionWorker 采用 Deployment 长驻模式,需要主动回收空闲资源:

5.1 策略参数

参数类型默认值说明
idle_ttl_secondsu64(必填)300(5 分钟)Worker 空闲多久后自动关闭
max_lifetime_secondsOption(可选)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 的资源消耗

资源配额用途说明
CPU500m(0.5 核)主要消耗在 agentcore 推理框架、工具执行、文本处理
内存1Gi上下文缓存、推理中间状态、checkpoint 数据库缓存
磁盘4Giworkspace 文件、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 系统约 500MiK3s 二进制 + 系统容器
容器镜像缓存约 1-5Gi取决于使用的智能体镜像数量
每个 Worker workspace最大 4Giephemeral-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 密集型工具(如代码编译、数据处理),可提高到 1000m2000m
  • 通过 kubectl top pod 观察实际 CPU 使用率来决定

9.2 内存调优

  • 1Gi 适合一般对话场景
  • 处理长上下文(> 100k tokens)或大文件分析时,可能需要 2Gi 或更多
  • 内存超限后果严重(OOM Kill),建议 limits 设为实际峰值使用量的 1.5 倍

9.3 磁盘调优

  • 4Gi 足够大多数场景
  • 如果智能体需要下载大型数据集或生成大量文件,可提高到 8Gi16Gi
  • 注意: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查看节点可分配资源和已分配量

下一步

了解了资源管理后,请继续阅读:

技术可行性分析

本章对 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.jsoncheckpoint_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 的反向调用链路:

  1. agentcore 通过 HTTP 调用 Backend 的“工具代理“端点
  2. Backend 通过已建立的 WebSocket 连接转发给 Helper
  3. 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 事件通道(为高并发场景准备)

长期

  • 本地工具桥接
  • 远程部署模式与多节点存储迁移

附录:技术选型参考

技术领域当前选择替代方案
容器编排K3sK8s、Docker Compose、Podman
工作负载Deployment + Service(SessionWorker)StatefulSet
文件存储hostPathNFS、Ceph CSI、Longhorn
配置分发ConfigMapOCI Artifact、Init Container
密钥管理K8s SecretVault、Sealed Secrets
通信协议WebSocket(推荐)+ HTTP RESTgRPC、NATS
运行时containerdCRI-O