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 的代码路径。

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 保存结果

当前代码还没有显式支持:

  • session 级状态卷
  • checkpoint_root 元数据
  • 自动把上一轮 checkpoint 挂回下一轮

因此今天已经落地的是“会话历史连续”,即将补齐的是“agentcore 内部状态连续”。

10. 用户离开后再回来,会发生什么

10.1 只是关闭前端页面

如果用户只是关闭前端页面,没有关闭 session,那么后续直接拿原来的 session_id 继续创建 run 即可。

10.2 显式关闭 session

如果用户调用了 close,会话仍保留历史和结果,但会拒绝新的写入。后续调用 reopen 后,同一个 session_id 又可以继续创建新的 run。

10.3 一个月后继续同一个 session

今天这套代码已经能保证下面这些内容在一个月后仍可恢复:

  • 会话历史
  • 上传文件索引
  • 历史 run 记录
  • 历史结果文件

如果还要恢复 agentcore 内部 checkpoint,就需要把 checkpoint 写到持久化挂载目录,并在 backend 里保存该目录索引。这是当前生命周期模型下一步最自然的扩展点。

11. run 结束后,K8s 如何处理资源

当前语义如下:

  • Job / Pod:由 Job TTL 或取消逻辑处理
  • ConfigMap / Secret:取消时会显式删除,正常完成后的统一 GC 还需补齐
  • workspaces/:backend 主机磁盘继续保留
  • state.json:backend 继续保留

K8s 只负责容器对象生命周期,不负责清理对话历史和业务文件索引。