Skip to content

Task Runtime

AceGuru-mjh edited this page Oct 1, 2026 · 4 revisions

任务运行时与持久化(T76)

🧠 Agent 内核 · 🏠 首页 › Task-Runtime

Home Version Kotlin Modules Tools License

Task-Runtime typing

📑 本页目录

直连引擎与编排器负责"跑一轮",TaskRuntime 负责"跑一个跨进程重启仍能续的长任务": 持久化 Task 模型、Checkpoint、暂停/恢复/取消/重试、崩溃恢复、工具幂等。 来源:docs/task-execution-architecture.md、docs/T76_ADR.md、docs/T76_ARCHITECTURE_AUDIT.md。

1. 为什么需要它

审计发现的三件真事:

事实 后果
SharedPrefsConversationMemory 是 O(n) 读-改-写、apply() 异步落盘、无 schema 版本号 崩溃即丢/解码失败即清空
引擎写入顺序是 先持久化 Assistant(toolCalls) → 执行工具 → 再写 ToolResult 进程死于工具执行期间留下悬空 toolCall → 重启后 LLM 请求被 400 拒绝
abort() 只置标志位,无持久化、无 Pause 语义 长任务无法断点续跑

2. 架构决策(D-1 ~ D-4)

# 决策 理由 / 否决项
D-1 文件式 JSON + 原子 rename:filesDir/taskstore/<taskId>.json,temp 写 → fsync → rename;schema 带 version + ignoreUnknownKeys;损坏文件移入 corrupt/(隔离而非删除) checkpoint 频率(每个 ToolCall 边界)远超 SharedPreferences 的 O(n) 模型;否决 SharedPreferences(非原子)、Room(第二数据库)、DataStore
D-2 TaskRuntime 作为叠加层(组合而非替换):AgentEngine 绑定保持 ApexAgentEngine 不动 审计修正:VM 有 15 处 (agentEngine as? ApexAgentEngine) cast,替换绑定会让全部 cast 静默失效;最终改由 AgentTaskStatusController 转发(3 处一行改动)
D-3 恢复触发 = ViewModel init 时确定性发现(纯文件扫描) 不复活 platform:persistence 死代码;不依赖后台任务
D-4 taskId 一等公民 + v1 单活跃执行 不先造第二套 Session 系统

3. 四层数据模型

层 类型 说明
Task AgentTask 单文件持久化:状态 / 步骤 / journal / 时间戳 / 配置快照
Execution 执行流 一次 execute = 一次执行,Channel + 镜像协程,单活跃互斥
Step TaskStepModel Plan / Spec 的步骤(状态 PENDING/RUNNING/DONE/FAILED/SKIPPED)
Operation ToolOperationRecord journal 条目(NOT_STARTED/RUNNING/SUCCEEDED/FAILED/UNKNOWN)
  • journal 上限:最近 200 条滚动保留(TaskStoreLimits.MAX_JOURNAL_SIZE);
  • operationId 格式 <taskId>-op<seq>,与 LLM callId 建立映射(恢复时判幂等 + 修补悬空历史);
  • 日志脱敏:journal 只存参数摘要(截断 512 字符)与输出摘要(截断 256 字符),不落 prompt 全文。

4. 持久层状态机(11 态)

PENDING / PLANNING / RUNNING / WAITING_USER / PAUSED / CANCELLING / RECOVERING / RETRYING / FAILED / COMPLETED / CANCELLED

迁移表(v1,逐条):

From 允许的 To
PENDING PLANNING, RUNNING, FAILED, CANCELLED
PLANNING RUNNING, WAITING_USER, PAUSED, FAILED, CANCELLED
RUNNING PLANNING, WAITING_USER, PAUSED, CANCELLING, RECOVERING, COMPLETED, FAILED
WAITING_USER RUNNING, PAUSED, CANCELLING, RECOVERING, FAILED
PAUSED RUNNING, CANCELLING, RECOVERING, FAILED
CANCELLING CANCELLED, FAILED
RECOVERING RUNNING, CANCELLING, FAILED
RETRYING RUNNING, CANCELLING, FAILED
FAILED RETRYING, CANCELLED
COMPLETED / CANCELLED 无出边(绝对终态)

三条不变量(有全矩阵测试锁定):

  1. 自环一律非法(isLegal(s, s) == false 对全部状态成立);
  2. RUNNING → CANCELLED 直达非法:取消必须经 CANCELLING 瞬态(abort 是协作式的,请求取消到收尾之间存在窗口);唯一例外是 FAILED → CANCELLED(放弃失败任务);
  3. PAUSED ≠ CANCELLED:PAUSED 可 resume 回 RUNNING,CANCELLED 重启后不自动继续。

与运行时状态机(编排器)的映射:

Planning → PLANNING
Acting / Observing / Responding → RUNNING
AwaitingUserInput / AwaitingPlanConfirmation / AwaitingSpecConfirmation → WAITING_USER
Finished.Completed → COMPLETED;Finished.Failed → FAILED;Finished.Aborted → PAUSED | CANCELLED(按用户意图)

5. Checkpoint:只在生命周期边界,绝不按 token

CheckpointBoundary 共 14 类:TASK_CREATED / PLAN_CONFIRMED / STEP_STARTED / TOOL_CALL_STARTED / TOOL_CALL_FINISHED / STEP_FINISHED / CONTEXT_COMPRESSED / WAITING_USER / ERROR / PAUSED / CANCELLED / COMPLETED / RECOVERED / RETRY_STARTED。

边界 落盘内容
TOOL_CALL_STARTED journal 追加 RUNNING + 幂等分类快照
TOOL_CALL_FINISHED journal 更新 SUCCEEDED / FAILED + 输出摘要
CONTEXT_COMPRESSED 压缩计数 + historyAnchor + Task 状态重注入
STEP_FINISHED 由步骤切换推导

Important

镜像 collector 跑在 TaskRuntime 自己的 scope(与 UI collect 生命周期解耦): 用户点取消、VM job 被取消,checkpoint 落盘仍然完整;abort/pause 的收尾事件不会因为 UI 停止消费而丢失。

6. 终态仲裁

引擎 finally 会无条件发 Complete(含失败场景),TaskRuntime 不信它, 由 finalizeStream 按标志统一裁决,优先级:

pause > cancel > failed > completed

finalize 的异常安全:finalize 失败不能吞掉 done.complete / tap.close,否则 pause/cancel 会永久挂起。

7. pause / resume / cancel / retry

操作 语义
pause() 置 pauseRequested → engine.abort()(协作式)→ 等流收尾 → PAUSED 落盘;对话历史不动。重启发现 PAUSED → 等用户显式续(不自动跑)
resume() 注入 [RESUME] 提示(告知中断点进度、勿重复已完成操作)→ 续跑;同一引擎 + 同一 history,上下文天然连续
cancel() 置 cancelRequested → abort → CANCELLING → CANCELLED 落盘;绝不自动继续
retry() FAILED → RETRYING → RUNNING,retryCount+1,上限 3(DEFAULT_RETRY_LIMIT);注入 [RETRY] 提示

并发互斥:AtomicBoolean.compareAndSet 占位 —— 并发两次 resume/retry/execute 只有一个成功, 另一个立即拒绝(无 check-then-act 竞态)。

8. 崩溃恢复流程

flowchart TD
    A["App 启动 → VM init"] --> B["TaskRuntime.discoverRecoverableTasks()"]
    B --> C["store.loadActiveTasks()<br/>(顺带清理 temp + 隔离损坏文件)"]
    C --> D["RUNNING/WAITING/PLANNING/RECOVERING → RECOVERING 并落盘<br/>PAUSED 保持不变"]
    D --> E["修补悬空 toolCall 历史"]
    E --> F["UI 横幅:任务标题 + 中断步骤"]
    F -->|继续| G["resumeFromCrash():RecoveryPolicy.planForTask<br/>→ 构造恢复提示 → RECOVERING→RUNNING"]
    F -->|取消| H["CANCELLED 终态"]
Loading

悬空 toolCall 修复(R-5):为"有 callId 但无配对 ToolResult"的调用追加合成结果:

⚠ Interrupted: outcome UNKNOWN ... Verify ...

既满足 OpenAI 兼容 API 的配对校验(否则整条历史 400 不可用),又明确告知 LLM"结果未知,先验证"。

Note

诚实声明:进程死亡用确定性模拟 TaskRuntime.simulateCrash()(SIGKILL 语义)—— 真机 E2E 未做(CI 无法复现真机进程死亡),不伪造 PASS。

9. 压缩兼容(N-9)

压缩(P7 三级)不会抹掉任务状态,靠三件事:

  1. Task 状态住在 TaskStore 独立文件,不在对话历史里;
  2. ContextCompressed 边界触发重注入:contextInjector 向引擎 history 追加 [TASK STATE] system 消息(标题 / 步骤进度 / 未完成步骤);
  3. checkpoint 记录 historyAnchor(压缩后持久化消息数)与 compressionCount。

10. 配置快照

TaskConfigSnapshot 在任务创建时刻从引擎快照:mode / thinkingLevel / maxIterations / maxContextTokens / 压缩参数 / temperature / reflectionRounds / enabledToolIds。 执行中途改设置不影响运行中任务;恢复与重试以快照为参照。

11. 可观测性

  • LlmRequestContext.taskId/stepId(此前恒 null)现在真实填充:execute 入口设 taskId、StepStart 设 stepId、finalize 清空;引擎全部 7 个构造点经 tagged() 包裹;
  • TaskRuntimeEvent(低频通道):StatusChanged / CheckpointSaved / Finished / RetryExhausted / RecoverableDiscovered;
  • AgentEvent sealed 层级冻结不扩展。

12. 相关页面

footer

🏠 返回首页 · 📚 文档索引 · ❓ FAQ · 🔧 故障排查 · 🗺️ 路线图 · 🐛 提 Issue

Android Guru Agent · v1.4.4 · Kotlin 2.0.21 · Compose · PRoot · Room

Clone this wiki locally