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

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

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

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 只负责容器对象生命周期,不负责清理对话历史和业务文件索引。