运行生命周期与容器编排流程
这一章从用户视角出发,解释当前代码里一次会话、一次运行和一次容器编排到底怎样流动。
1. 从用户视角看,一次会话会经历什么
用户在产品里看到的主流程可以理解为下面几步:
- 打开一个
session(会话) - 上传文件,或选择已有文件
- 选择一个
agent package - 发送一轮 prompt
- backend 为这轮 prompt 创建一个新的
run - K3s 为这个
run拉起一个独立 Pod agentcore在 Pod 里处理本轮任务并写结果- backend 把结果状态写回
run,把主回答写回session history - 用户离开页面,稍后回来继续使用同一个
session
这个流程里,“连续的是 session”,“独立的是 run”。
2. 后端进程生命周期
main(进程入口)启动后会依次完成:
- 初始化 tracing
- 兼容旧环境变量别名
- 解析
AppConfig(进程级配置) - 调
build_application(应用装配入口) - 绑定 HTTP 地址
- 用 axum 对外提供 API
- 监听退出信号并优雅关闭
如果 backend 运行在 Apply 模式,K3sOrchestrator::new(K3s 编排器初始化)还会确保:
- namespace 存在
- service account 存在
- service account 关闭自动挂载 token
2.1 启动伪代码
async fn main() {
init_tracing(); // 初始化日志
apply_legacy_env_aliases(); // 兼容旧环境变量
let config = AppConfig::parse(); // 解析进程配置
let app = build_application(config.clone()).await?; // 装配 store/catalog/orchestrator/router
let listener = TcpListener::bind(config.bind_addr).await?; // 绑定 HTTP 地址
axum::serve(listener, app.router)
.with_graceful_shutdown(shutdown_signal())
.await?;
}
3. session 的生命周期
SessionRecord(逻辑会话记录)当前只有两个状态:
ACTIVECLOSED
生命周期如下:
POST /api/v1/sessions创建,初始为ACTIVE- 可反复用于多轮
create_run POST /api/v1/sessions/{id}/close后变成CLOSEDPOST /api/v1/sessions/{id}/reopen后回到ACTIVECLOSED状态下仍可查询历史和结果
这里要特别注意:close 表示“归档并拒绝新写入”,会话数据会继续保留。
4. run 的生命周期
RunRecord(一次运行记录)常见状态包括:
PREVIEWEDSUBMITTEDPENDINGCOMPLETEDFAILEDCANCELLED
其生命周期是:
- 用户请求创建 run
- backend 准备工作区和上下文
- backend 提交或渲染 K3s 资源
- Pod 执行
- backend 刷新状态
- run 进入完成、失败或取消态
5. create_run 的完整用户视角流程
5.1 用户看到的过程
从用户角度,一次“发送消息”背后发生的是:
- backend 读取当前会话
- backend 读取本轮所选文件和挂载授权
- backend 检查集群是否还能接收新任务
- backend 把旧回答补回会话历史
- backend 把本轮用户输入追加到会话历史
- backend 创建本轮独立工作区
- backend 把上下文写成
run-context.json - backend 构造
ConfigMap、Secret、Job - K3s 启动本轮 Pod
- Pod 把本轮结果写回工作区
5.2 当前代码的伪代码
#![allow(unused)]
fn main() {
async fn create_run(command: CreateRunCommand) -> RunRecord {
let run_id = new_uuid_v7(); // 生成本轮唯一 id
let session = store.get_session(command.user_id, command.session_id).await?; // 读取会话
ensure_session_accepts_writes(session)?; // 关闭态会话拒绝新 run
let package = packages.get(command.package_name)?; // 读取 agent package
let runtime_profile = resolve_runtime_profile(command, package)?; // 解析运行时模板
let provider_env = merge_provider_env(runtime_profile, command.provider_env); // 合并 provider 配置
let artifacts = store.get_artifacts(command.user_id, command.artifact_ids).await?; // 读取输入文件
let mount_grant = resolve_mount_grant(command.mount_grant_id).await?; // 读取可选挂载授权
let workspace_mode = resolve_workspace_mode(command, package); // 决定 copy 或 mount
ensure_cluster_accepts_runs().await?; // 检查是否有可调度节点
sync_session_conversation_history(command.user_id, command.session_id).await; // 把旧 run 的回答补回 history
append_user_message(command.session_id, run_id, command.user_prompt).await?; // 写入本轮用户消息
let workspace_root = create_workspace(run_id, artifacts).await?; // 创建 /in /out /tmp
write_run_context(workspace_root, session, package, artifacts, command.user_prompt).await?; // 写上下文文件
let plan = LaunchPlan { ... }; // 汇总 package / runtime / workspace / provider / k8s 参数
let outcome = orchestrator.submit_run(plan).await?; // 渲染并提交 ConfigMap / Secret / Job
let run = build_run_record(run_id, workspace_root, outcome); // 落盘 RunRecord
store.insert_run(run).await
}
}
6. K3s 资源怎样从业务对象变成 Pod
6.1 ApplicationService(应用服务层)负责业务装配
ApplicationService::create_run 会把会话、文件、挂载、provider 环境变量和 RuntimeProfile 组装成 LaunchPlan。
6.2 job_builder 负责渲染资源定义
job_builder::build_bundle(构建运行资源包)会在内存里构造:
- 承载
agent package文件的ConfigMap - 承载 provider 环境变量的
Secret - 真正执行 run 的
Job
6.3 K3sOrchestrator 负责和集群交互
K3sOrchestrator::submit_run(提交运行)在 apply 模式下会调用:
Api<ConfigMap>::createApi<Secret>::createApi<Job>::create
这就是当前 Rust 侧真正把业务运行提交到 K8s 的代码路径。
6.4 一次 create 从 backend 到 Pod running 的时序
如果把 create 再展开一层,当前时序更准确地说是:
frontend/helper
-> POST /api/v1/runs
-> backend create_run
-> build_bundle(ConfigMap/Secret/Job)
-> Kubernetes API Server 持久化这些资源
-> Job Controller 观察到新的 Job
-> Job Controller 创建 Pod
-> Scheduler 为 Pod 选节点
-> kubelet 在目标节点准备 volume / secret / configmap / image
-> container runtime 启动容器
-> agentcore 执行并写出结果
这里要特别注意两个事实:
- backend 只负责把资源写入 API Server,不直接驱动 Pod 生命周期。
- Pod 能否真的跑起来,还取决于调度、镜像、节点磁盘、挂载卷、provider 凭据等后续条件。
6.5 create 成功不等于业务已经开始执行
当前系统里至少有三种“create 成功”的层次:
- backend 成功返回
RunRecord - Kubernetes API 成功创建
Job - Pod 真的被调度并开始运行
这三层不是一回事。
所以今天 backend 的状态刷新才需要同时看:
/workspace/out/*输出标记Job/Pod的集群状态
因为“资源对象已创建”并不等于“容器已经完成执行”。
7. Pod 内执行生命周期
7.1 容器启动后先做什么
job_builder::build_run_script(构建容器启动脚本)会生成启动脚本,脚本大致会:
- 创建
/workspace/in、/workspace/out、/workspace/tmp - 打印 run、session、user、workspace 信息
- 如有挂载授权,列出挂载目录文件
- 根据
installStrategy决定是否做运行时安装 - 执行 verify 命令
- 输出 package 和输入文件信息
- 进入具体执行模式
7.2 当前执行模式怎样理解
当前代码里的 ExecutionMode(执行模式)主要有两种:
CliVerifyOpenAiCompatibleApi
它们表达的是容器内执行入口的行为方式。从架构角度,这仍然属于 agentcore 的执行适配层。
7.3 容器内执行伪代码
容器启动
-> 读取 /workspace/in/run-context.json
-> 读取 /opt/agent/package/*
-> 读取 provider 环境变量
-> 运行 agentcore
-> 把主回答写到 /workspace/out/final-answer.md
-> 把调试信息写到 /workspace/out/*
-> 进程退出
8. backend 如何知道 run 已经结束
ApplicationService::refresh_run_status(刷新运行状态)会做两类检查。
8.1 第一类:看输出目录
它会检查:
runtime-error.txtfinal-answer.mdprovider-response.jsonllm-run-summary.jsonexecution-mode.txt
如果这些文件已经出现,就可以直接推断 run 已完成或失败。
8.2 第二类:看 K8s Job / Pod 状态
如果输出目录还看不出来,backend 会调用 K3sOrchestrator::inspect_run_status(检查运行状态),内部通过:
Api<Job>::get_optApi<Pod>::list
读取 Job 和 Pod,再映射成 RunStatus。
8.3 状态刷新伪代码
#![allow(unused)]
fn main() {
async fn refresh_run_status(run: RunRecord) -> RunRecord {
if let Some(status) = detect_output_status(run.workspace_root.join("out")).await {
update_run_status(run, status); // 输出已经落盘,直接完成状态刷新
} else if let Some(observation) = orchestrator.inspect_run_status(&run).await? {
update_run_status(run, observation); // 读取 Kubernetes Job / Pod 状态
}
if run.status == COMPLETED {
sync_run_assistant_message(&run).await?; // 把 final-answer.md 写回 session history
}
persist_run(run).await // 保存新的 run 状态
}
}
9. 同一 session 内如何保持连续性
这是当前生命周期里最关键的一部分。
9.1 文本对话连续性
文本对话连续性由下面两个动作保证:
sync_session_conversation_history(同步会话历史)
在新 run 开始前刷新同一 session 下旧 run 的状态。sync_run_assistant_message(同步助手回答)
在 run 完成后读取final-answer.md,把它回填进SessionRecord.history。
因此当前同一 session 的下一轮一定可以拿到上一轮的主回答。
9.2 文件结果连续性
run 完成后,当前代码会保留:
RunRecordworkspace_rootworkspaces/<run-id>/out/*
因此用户下次回来时,仍能查看历史结果文件。
9.3 上传文件连续性
只要 ArtifactRecord 和 uploads/ 下的文件还在,后续 run 就可以继续引用这些文件。
9.4 agentcore checkpoint 连续性
从运行时模型看,agentcore checkpoint 连续性应采用“同一 session 挂回同一状态目录”的方式。当前代码已经支持:
- 按 session 保存历史
- 按 session reopen
- 按 run 保存结果
sessions/<session-id>/级别的持久化状态目录- 每次 run 都把该目录挂到
/workspace/session - 扫描最近 checkpoint 摘要并写回
SessionRecord.runtime_state
当前代码还没有显式支持:
- restore / resume API
- 用户选择“恢复到哪个 checkpoint”的控制面接口
- checkpoint retention / GC 策略
- 输出文件自动进入下一轮输入
因此今天已经落地的是“会话历史连续 + session 级状态目录连续”,还没有落地的是“平台级可恢复交互式运行”。
9.5 当前为什么一个 run 不能在容器里直接接收下一条用户消息
当前 run 本质上是 batch 语义:
- backend 的
create_run每次只接收一轮user_prompt - K3s 侧使用的是
Job - 容器脚本读取一次
run-context.json后就执行并退出 RunStatus里没有WAITING_USER_INPUT、WAITING_APPROVAL、WAITING_ARTIFACTS
因此今天的模型是:
- 同一
session可以连续创建多个run - 但同一个正在运行的容器不会持续监听并接收新的用户消息
9.6 K3s 能否支持“等待用户 / 审批 / 补文件 / 再继续”
可以,但要分清楚K3s 能提供的容器编排能力和backend 必须补的控制面语义。
K3s 可以提供:
- 长驻 Pod、
Deployment、StatefulSet Service暴露稳定访问入口readinessProbe/livenessProbeLease或自定义心跳,用于 worker 占有与回收- PVC 或其它持久化卷,用于 session state 和 checkpoint
- 显式删除 Pod / Deployment,实现“空闲超时回收”
但 K3s 本身不会替你决定:
- 什么时候应该向用户提问
- 哪个工具调用需要审批
- 用户迟迟不上传文件时应进入什么业务状态
- 收到新消息后是复用旧容器还是重启新 run
这些都必须由 backend 控制面建模。
9.7 两种可行的交互式运行模式
模式 A:冷恢复模式
这是最稳的方式,也是最贴近当前架构的方式:
- run 执行到需要用户输入、审批或文件时,把 checkpoint 写入
/workspace/session - run 上报一个阻塞事件,例如
WAITING_USER_INPUT - 容器退出,释放 CPU / 内存
- backend 等待用户回复、审批结果或新 artifact
- backend 创建一个新的 run,从同一 session state 恢复继续执行
这个模式下,K3s 仍然主要使用 Job。
模式 B:Warm Sandbox 模式
如果想优化“用户刚回复就秒回”的体验,可以让容器短时间保活:
- 第一次请求启动一个长驻 Pod,而不是一次性
Job - 容器进入
idle,等待同一 session 的下一条事件 - backend 给它设置
idle_ttl - 用户在 TTL 内回复、审批或补文件,就把事件投递给这个 Pod
- 超过 TTL 仍无新事件,就让容器 checkpoint 后退出
这个模式下,更适合使用:
Deployment+ 每 session 一个 worker- 或
StatefulSet/ 命名 Pod + PVC
它能改善连续体验,但复杂度明显高于当前 batch run。
模式 B 下,长驻 Pod 如何等待并接收下一条用户事件
如果后续采用 warm sandbox,那么“等待”不应实现成容器里的 busy loop,而应实现成一个显式事件循环:
- backend 为 session worker 分配稳定标识,例如
session_id+worker_id - worker 启动后向 backend 注册可用状态,进入
idle - backend 通过显式事件通道投递下一条事件,例如:
USER_MESSAGEAPPROVAL_RESULTARTIFACT_READYCANCELSHUTDOWN
- worker 收到事件后进入
busy,处理完后再回到idle - 如果超过
idle_ttl仍无新事件,则 worker checkpoint 后退出
事件通道本身可以后选:
- HTTP / gRPC
- WebSocket
- 长轮询
- 队列
但“worker 通过什么机制收到下一条事件”必须在控制面里明确,否则长驻 Pod 只是在空转。
模式 B 下,长驻 Pod 如何向用户提问
如果 worker 需要 ask_user、申请权限或索取新文件,建议统一走下面的链路:
- worker 向 backend 上报一个
InteractionRequest - backend 持久化为
PendingActionRecord - backend 把问题、审批请求或缺失文件要求展示给前端
- worker 进入
waiting_user或回到idle - 用户回复后,backend 再把新事件路由回该 worker;如果 worker 已被回收,则改走冷恢复模式
这样可以同时支持两种恢复路径:
- worker 仍在:直接复用原长驻 Pod
- worker 已回收:从 checkpoint 启动新的 resumed run
模式 B 下,长驻 Pod 如何访问容器外部数据库
K3s 可以承载这种能力,但实现方式必须是“服务接入”,而不是“宿主机目录共享”。
建议统一约束:
- 数据库运行在集群内时,通过
Service访问 - 数据库运行在集群外时,通过稳定地址访问
- 连接字符串、账号密码、token、TLS 材料通过
Secret或投影卷注入 - worker 启动时做最小连接探活,并把失败原因回传给 backend
- 如有需要,再补
NetworkPolicy或 egress 白名单
不建议:
- 让长驻 Pod 直接 mount 宿主机数据库数据目录
- 把数据库凭据硬编码进镜像
- 把访问数据库等同于“进入宿主机环境”
9.8 job_ttl 和“空闲等待 TTL”不是一回事
当前已有的 K3s Job TTL 指的是:
- run 已完成后
- K8s 再保留多久 Job / Pod
它不能实现:
- 容器空闲等待用户下一句话
- 容器等待审批结果
- 容器等待用户上传新文件
如果后续要支持这些能力,需要新增一套独立的“空闲等待 TTL”语义,由 backend 负责:
- 判断 session worker 何时进入 idle
- 判断 idle 何时超时
- 触发 checkpoint、摘除路由、删除 Pod
9.9 Pod 能不能把“当前整个状态”打包成镜像
标准 K3s / Kubernetes 语义下,不能把“当前 Pod 的完整可恢复状态”简单理解为一个可打包镜像。
原因有四类:
- 镜像主要表达的是容器文件系统层,不表达:
- 进程内存
- 打开的文件描述符
- TCP 连接
- PID / namespace 运行态
- 当前真正重要的业务状态主要放在挂载卷里,例如:
/workspace/workspace/session/workspace/session/checkpoints这些内容本来就不应该依赖镜像层保存。
ConfigMap、Secret、hostPath、PVC 都是运行时挂载语义,不会因为“镜像化”自动被完整封装。- Kubernetes 没有一个标准 API 能把“正在运行的 Pod 全状态”直接 commit 成可移植镜像,再在别处完整恢复。
9.10 那底层 runtime 能不能做 checkpoint / restore
底层容器 runtime 在某些场景里可能存在:
commit风格的文件系统快照- CRIU 风格的 checkpoint / restore
但这不等于当前平台可以把它当成标准方案,原因是:
- 强依赖具体 runtime 与节点内核能力
- 对 K3s / containerd 版本和宿主机环境耦合很强
- 很难作为跨节点、跨环境、可审计的恢复语义
- 和当前基于卷保存 checkpoint 的架构并不一致
因此对当前 Agent Store 架构,更合理的恢复方式仍然是:
- 镜像保持不可变运行时底座
- checkpoint / 状态数据库写入持久化卷或外部状态存储
- 需要恢复时启动新 Pod 并重新挂载这些状态
9.11 这部分的工程判断
如果只是想固定“文件系统产物”,可以考虑:
- 持久化卷
- 对象存储
- 导出 artifact
如果想固定“可恢复执行状态”,应该优先依赖:
- checkpoint 文件
- checkpoint 数据库
- session state 元数据
而不是把 Pod 当成一台可以随时“拍扁成镜像再复活”的虚拟机。
建议保留为 TODO:
- TODO:只有在出现“必须恢复进程内存态”的硬需求时,再专项评估 runtime-level checkpoint / restore
- TODO:在此之前,统一沿用“新 Pod + 持久化状态挂载 + resumed run”模型
10. [TODO] 面向交互式运行的 K3s 演进方案
下面这些项可以用 K3s 承载,但当前 backend 还没有实现。
- TODO:新增阻塞态,例如
WAITING_USER_INPUT、WAITING_APPROVAL、WAITING_ARTIFACTS、EXPIRED - TODO:新增
PendingActionRecord或InteractionRequestRecord,显式保存“问题内容、审批请求、所需文件、超时时间、恢复目标 run” - TODO:把
builtin.ask_user从提示词元数据升级成真实工具调用链路 - TODO:把工具权限判断落到 backend 控制面,形成
allow / ask / deny - TODO:支持“冷恢复模式”:run 遇到阻塞先 checkpoint,再退出,等待新事件后启动 resumed run
- TODO:支持“warm sandbox 模式”:为 session 维持一个带
idle_ttl的长驻 Pod - TODO:为 warm sandbox 增加
Service、健康检查、心跳或Lease - TODO:为 warm sandbox 明确事件通道:backend 如何向 worker 投递
USER_MESSAGE/APPROVAL_RESULT/ARTIFACT_READY - TODO:为 worker 到 backend 的回调定义契约:
ASK_USER、REQUEST_APPROVAL、REQUEST_ARTIFACT、CHECKPOINT_READY、HEARTBEAT - TODO:为 warm sandbox 补齐
idle、busy、waiting_user、draining的状态边界 - TODO:把 session state 从
hostPath升级到 PVC 或外部状态存储,以支持更可靠的 Pod 重建 - TODO:区分
job_ttl_seconds与未来的session_idle_ttl_seconds - TODO:增加“用户上传新文件后继续当前 session”的恢复 API,而不是要求用户手工重新拼装上下文
- TODO:为访问外部数据库或状态服务补齐
Secret、TLS、连接探活和网络白名单约束
11. 用户离开后再回来,会发生什么
10.1 只是关闭前端页面
如果用户只是关闭前端页面,没有关闭 session,那么后续直接拿原来的 session_id 继续创建 run 即可。
10.2 显式关闭 session
如果用户调用了 close,会话仍保留历史和结果,但会拒绝新的写入。后续调用 reopen 后,同一个 session_id 又可以继续创建新的 run。
10.3 一个月后继续同一个 session
今天这套代码已经能保证下面这些内容在一个月后仍可恢复:
- 会话历史
- 上传文件索引
- 历史 run 记录
- 历史结果文件
如果还要恢复 agentcore 内部 checkpoint,就需要把 checkpoint 写到持久化挂载目录,并在 backend 里保存该目录索引。这是当前生命周期模型下一步最自然的扩展点。
12. run 结束后,K8s 如何处理资源
当前语义如下:
- Job / Pod:由 Job TTL 或取消逻辑处理
ConfigMap/Secret:取消时会显式删除,正常完成后的统一 GC 还需补齐workspaces/:backend 主机磁盘继续保留state.json:backend 继续保留
K8s 只负责容器对象生命周期,不负责清理对话历史和业务文件索引。