子代理会话、执行租约与可关闭资源路由

Core Matrix 的运行时资源层由三个互锁概念组成:子代理会话 (SubagentSession) 实现代理任务的分层委派与结果聚合,执行租约 (ExecutionLease) 提供分布式独占控制以防止并发冲突,可关闭资源路由 (ClosableResourceRouting) 则统一 AgentTaskRunProcessRunSubagentSession 三类运行时资源的关闭协议。三者协同构成一个从「资源创建 → 独占占用 → 优雅/强制关闭 → 上游状态协调」的完整生命周期管理框架,是理解系统如何安全回收运行时资源的关键入口。

Sources: subagent_session.rb, execution_lease.rb, closable_runtime_resource.rb

领域模型关系总览

系统中的三类可关闭资源共享同一套关闭状态机(由 ClosableRuntimeResource concern 统一注入),但各自承载不同的运行时语义:AgentTaskRun 代表工作流 DAG 中的任务执行单元,ProcessRun 代表后台进程(如 background_service),SubagentSession 则桥接父对话与子对话,实现嵌套的代理委派。

erDiagram Conversation ||--o{ SubagentSession : "owns (owner_conversation)" Conversation ||--o| SubagentSession : "hosted on (conversation, 1:1)" SubagentSession ||--o{ SubagentSession : "parent → children" SubagentSession ||--o{ AgentTaskRun : "subagent_step tasks" SubagentSession ||--o| ExecutionLease : "leased_resource (polymorphic)" AgentTaskRun ||--o| ExecutionLease : "leased_resource (polymorphic)" ProcessRun ||--o| ExecutionLease : "leased_resource (polymorphic)" ExecutionLease }o--|| WorkflowRun : "belongs to" ExecutionLease }o--|| WorkflowNode : "belongs to" AgentTaskRun }o--|| WorkflowNode : "belongs to" ProcessRun }o--|| WorkflowNode : "belongs to" Conversation ||--o{ ConversationCloseOperation : "close operations"

上图揭示了几个关键结构约束:每个子代理会话通过 conversation_id 唯一索引绑定到一个子对话,同时通过 owner_conversation_id 关联到其拥有者对话。SubagentSession 递归引用自身形成树状嵌套结构(parent_subagent_session_id),depth 字段记录嵌套层级。ExecutionLease 通过多态关联 (leased_resource) 持有三种资源类型的独占锁,数据库层面以条件唯一索引 (leased_resource_type, leased_resource_id) WHERE released_at IS NULL 保证同一时刻每个活跃资源最多只有一个未释放的租约。

Sources: schema.rb, schema.rb, conversation.rb

ClosableRuntimeResource:统一的关闭状态机

ClosableRuntimeResource 是一个 ActiveRecord concern,为所有可关闭资源注入一套五态关闭状态机和配套验证规则。其核心设计原则是关闭元数据的生命周期配对严格性——状态的每一次推进都必须携带完整的审计字段。

stateDiagram-v2 [*] --> open : 资源创建 open --> requested : 请求关闭 requested --> acknowledged : 代理确认 acknowledged --> closed : 成功终止 acknowledged --> failed : 关闭失败 requested --> closed : 成功终止(跳过确认) requested --> failed : 关闭失败(跳过确认) open --> closed : 直接成功关闭 open --> failed : 直接失败关闭 closed --> [*] failed --> [*]

状态推进时的字段约束由 close_lifecycle_pairings 校验器严格执行:

状态 必须存在的字段 必须为空的字段
open 全部关闭元数据
requested close_reason_kind, close_requested_at close_outcome_kind
acknowledged 上述 + close_acknowledged_at close_outcome_kind
closed / failed 上述 + close_outcome_kind, close_outcome_payload

这套严格的配对规则确保关闭过程可审计、可追溯。close_grace_deadline_atclose_force_deadline_at 为可选字段,用于控制优雅关闭→强制关闭的升级时间窗口(默认 grace 为 30 秒,force 为 60 秒)。

Sources: closable_runtime_resource.rb, request_resource_closes.rb

SubagentSession:分层代理委派的会话管理

核心职责与数据结构

SubagentSession 将一个子对话conversation,类型为 forkaddressability: agent_addressable)绑定到其拥有者对话owner_conversation),形成一个具有明确所有权边界的委派单元。其关键字段包括:

字段 用途
scope turn(绑定到特定轮次)或 conversation(绑定到整个对话)
profile_key 使用的运行时 profile(如 mainresearcher
depth 嵌套层级(根为 0,每层 +1,由验证器保证 depth == parent.depth + 1
observed_status 代理报告的运行状态:idle / running / waiting / completed / failed / interrupted

SubagentSession 通过 DERIVED_CLOSE_STATUS_BY_CLOSE_STATE 将内部关闭状态映射为面向消费者的 derived_close_statusopen"open"requested/acknowledged"close_requested"closed/failed"closed"。这种双重状态模型(关闭状态 + 观测状态)使得系统可以同时表达「内核视角的关闭进度」和「代理视角的执行状态」。

Sources: subagent_session.rb

Spawn 流程:从委派意图到完整执行上下文

SubagentSessions::Spawn 是子代理会话的创建入口,其执行过程在单个事务内完成所有资源的原子化初始化:

flowchart TD A[调用 Spawn] --> B[验证 origin_turn 归属] B --> C[验证 subagent_spawn 工具可见性] C --> D[创建子对话 fork] D --> E[创建 SubagentSession 记录] E --> F[创建子对话的代理轮次] F --> G[创建子工作流 + AgentTaskRun] G --> H[返回序列化结果] style A fill:#e8f4fd style H fill:#d4edda

整个流程在 Conversations::WithMutableStateLock 保护下执行,确保父对话处于可变状态(retained + active + 非 closing)。Spawn 的关键步骤包括:

  1. Profile 解析:若未指定 profile_key,从 runtime capability contract 中查找标记为 default_subagent_profile: true 的 profile;若无标记,则选择非交互式 profile 的第一个键。若指定了 DEFAULT_SUBAGENT_PROFILE_ALIAS(即 default),则自动解析为上述默认 profile。

  2. 深度继承:若父对话自身也是子代理会话的宿主,则 depth = parent.depth + 1,否则 depth = 0

  3. 工作流创建:通过 Workflows::CreateForTurn 为子对话创建独立工作流,root_node_type: "agent_task_run"decision_source: "system",关联的 origin_turn 指向父对话中触发 spawn 的原始轮次。

Sources: spawn.rb, spawn.rb

消息投递与会话可变性保护

SubagentSessions::SendMessage 实现了从外部向子代理会话投递消息的能力。投递方分为三类:owner_agent(来自父对话的代理)、subagent_self(子代理自身)、system(系统消息)。每种发送方都有严格的身份校验——owner_agent 必须匹配 subagent_session.owner_conversationsubagent_self 必须匹配子对话本身。

消息投递前会执行双重保护:第一层通过 WithMutableStateLock 确保对话处于活跃可变状态,第二层通过 validate_session_mutable! 确保子代理会话的关闭状态为 open(一旦进入 requested/acknowledged/closed/failed 即拒绝投递)。此外还通过 ValidateAddressability 校验对话的 addressabilityagent_addressable(仅允许代理类型的发送方投递)。

Sources: send_message.rb, validate_addressability.rb

OwnedTree:递归子代理树的遍历

SubagentSessions::OwnedTree 实现了一种广度优先遍历算法,用于收集某个对话拥有的全部子代理会话(包括多层嵌套)。算法使用 frontier_owner_ids 作为广度优先队列,逐层展开子代理会话并记录到 collected 数组中,同时通过 seen_session_ids 集合防止循环引用。

# OwnedTree 的核心 BFS 遍历(伪代码)
frontier = [owner_conversation.id]
sessions = []
while frontier.any?
  batch = SubagentSession.where(owner_conversation_id: frontier)
  frontier = []
  batch.each do |session|
    sessions << session unless seen[session.id]
    frontier << session.conversation_id  # 子对话可能拥有自己的子代理
  end
end

该遍历在 Conversations::ProgressCloseRequestsBlockerSnapshotQuery 中被广泛使用——关闭对话时需要遍历整个子代理树来发出关闭请求,阻塞快照查询需要统计所有运行中的子代理数量。

Sources: owned_tree.rb, progress_close_requests.rb, blocker_snapshot_query.rb

Wait 机制:同步等待子代理终止

SubagentSessions::Wait 提供了一个基于轮询的同步等待原语,在指定的超时时间内持续 reload 子代理会话状态直到达到终止条件。终止判定包含两个维度:关闭状态closedfailed)和观测状态completedfailedinterrupted)。

该机制主要在代理程序本地使用(如 fenix 代理在执行 subagent_wait_all 工具调用时的阻塞等待)。返回结果包含 timed_out 标志、derived_close_statusobserved_statusclose_state,使调用方可以区分「正常完成但超时」和「实际已完成」的语义。

Sources: wait.rb

ExecutionLease:分布式独占控制原语

租约模型与字段语义

ExecutionLease 为运行时资源提供独占占用保证,防止同一资源被多个代理程序版本 (AgentProgramVersion) 或执行会话 (ExecutionSession) 并发操作。其核心字段语义如下:

字段 说明
holder_key 持有者标识(通常为 AgentProgramVersion.public_id
leased_resource 多态关联,指向 AgentTaskRun/ProcessRun/SubagentSession
acquired_at 租约获取时间
last_heartbeat_at 最后心跳时间
heartbeat_timeout_seconds 心跳超时阈值(秒)
released_at 释放时间(NULL 表示活跃)
release_reason 释放原因(resource_closedresource_close_failedheartbeat_timeout
metadata 附带元数据(JSONB)

active? 判定简单而精确:released_at.blank?stale? 判定则通过 last_heartbeat_at < now - heartbeat_timeout_seconds 实现超时检测。数据库层面的条件唯一索引 idx_execution_leases_active_resource 确保同一资源不会有多个活跃租约。

Sources: execution_lease.rb, schema.rb

Acquire / Heartbeat / Release 三段式协议

租约的生命周期遵循严格的三段式协议,每一步都包含持有者身份校验:

AcquireLeases::Acquire)在事务内先以 SELECT ... FOR UPDATE 锁定已有活跃租约,若存在且未过期则抛出 LeaseConflictError;若存在但已过期(stale?),先自动释放旧租约(release_reason: heartbeat_timeout)再创建新租约。

HeartbeatLeases::Heartbeat)在行锁保护下校验 holder_key 一致性和租约活跃状态,然后检测是否超时——若已超时则直接释放租约并抛出 StaleLeaseError,否则更新 last_heartbeat_at

ReleaseLeases::Release)在行锁保护下校验持有者身份和活跃状态后,设置 released_atrelease_reason

三段式协议保证了:即使持有者进程意外终止(心跳超时),新请求者也能通过 stale 检测安全接管资源。ApplyCloseOutcome 在关闭资源时通过 release_resource_lease! 自动释放关联租约(包含 ArgumentError 的容错处理,以应对租约已被提前释放的情况)。

Sources: acquire.rb, heartbeat.rb, release.rb, apply_close_outcome.rb

可关闭资源路由:关闭协议的统一分发层

ClosableResourceRegistry:类型注册表

ClosableResourceRegistry 维护了系统支持的三种可关闭资源类型的注册表:

RESOURCE_TYPES = {
  "AgentTaskRun" => AgentTaskRun,
  "ProcessRun"   => ProcessRun,
  "SubagentSession" => SubagentSession,
}.freeze

该注册表为关闭协议提供了类型安全的查找机制(find / find!)和类型兼容性校验(supported?),确保只有注册过的资源类型才能进入关闭流程。

Sources: closable_resource_registry.rb

ClosableResourceRouting:上下文解析路由

ClosableResourceRouting 是一套纯函数模块,负责从任意可关闭资源中解析出执行运行时所属对话所属轮次所属代理程序。其解析策略因资源类型而异:

资源类型 对话解析 轮次解析 代理程序解析
SubagentSession owner_conversation origin_turn turn.agent_program_version.agent_program
AgentTaskRun conversation turn agent_program
ProcessRun conversation turn turn.agent_program_version.agent_program

该路由模块在 CreateResourceCloseRequest 中用于确定关闭请求的目标投递端点,在 ApplyCloseOutcome 中用于确定关闭后需要协调的上游对话和轮次。

Sources: closable_resource_routing.rb

关闭协议的完整生命周期

sequenceDiagram participant Kernel as Core Matrix 内核 participant Mailbox as 邮箱系统 participant Agent as 代理程序 (Fenix) Kernel->>Kernel: CreateResourceCloseRequest Note right of Kernel: 资源.close_state → requested
创建 mailbox_item (queued) Kernel->>Mailbox: PublishPending Mailbox->>Agent: Poll → 投递关闭请求 Agent->>Mailbox: Report("resource_close_acknowledged") Note right of Kernel: 资源.close_state → acknowledged Agent->>Mailbox: Report("resource_closed" / "resource_close_failed") Mailbox->>Kernel: HandleCloseReport → ApplyCloseOutcome Note right of Kernel: 释放租约 → 协调轮次/工作流 →
协调 ConversationCloseOperation

关闭协议的关键步骤说明:

  1. 创建关闭请求CreateResourceCloseRequest):将资源状态推进到 requested,创建邮箱条目(item_type: resource_close_request),设置 grace_deadline_atforce_deadline_at。邮箱条目携带完整的关闭上下文(资源类型/ID、严格级别、截止时间)。

  2. 代理确认HandleCloseReporthandle_resource_close_acknowledged!):代理程序收到关闭请求后发送确认,资源状态推进到 acknowledged,邮箱条目状态更新为 acked

  3. 终态报告HandleCloseReporthandle_terminal_close_report!):代理完成清理后发送终态报告(resource_closedresource_close_failed),触发 ApplyCloseOutcome 执行完整的善后协调。

  4. 超时升级ProgressCloseRequest):内核定时推进关闭请求,检查 grace/force 截止时间。Grace 截止后,请求从 graceful 升级为 forced(重新排队);force 截止后,内核直接以 timed_out_forced 终态关闭资源。

Sources: create_resource_close_request.rb, handle_close_report.rb, progress_close_request.rb

ApplyCloseOutcome:关闭终态的善后协调

ApplyCloseOutcome 是关闭协议中最复杂的协调器,在资源达到终态(closedfailed)后执行四个层级的善后操作:

flowchart TD A[ApplyCloseOutcome.call] --> B{close_state?} B -->|closed| C[terminalize_closed_resource!] B -->|failed| D[terminalize_failed_resource!] C --> E[release_resource_lease!] D --> E E --> F[reconcile_turn_interrupt!] F --> G[reconcile_turn_pause!] G --> H[reconcile_close_operation!] H --> I[返回更新后的资源] style A fill:#e8f4fd style I fill:#d4edda

资源终态化(按类型)

SubagentSession 终态化最为简洁——仅更新 observed_status:关闭成功时设为 completed(除非是 turn_interrupt/turn_pause 请求则设为 interrupted),关闭失败时设为 failed

AgentTaskRun 终态化最为复杂——更新 lifecycle_statecanceled/interrupted/failed)、终态化所有运行中的 ToolInvocation(设置 status 和错误信息)、终态化所有运行中的 CommandRun、协调工作流节点状态、协调工作流运行状态、协调轮次状态。对于 turn_pause 请求的特殊处理:工作流节点被重置为 queued(而非终止),以便暂停恢复后重新执行。

ProcessRun 终态化涉及进程运行时的特殊语义——关闭成功时设置 lifecycle_state: "stopped"(若 close_outcome_kind: "residual_abandoned" 则设为 "lost"),关闭失败时设为 "lost",同时广播进程终止事件。

关闭协调的对话范围

conversations_for_close_reconciliation 收集需要协调的所有对话。对于 SubagentSession,需要协调两个对话owner_conversation(父对话,通过 ClosableResourceRouting 解析)和 conversation(子对话自身)。这意味着子代理的关闭会同时推进父对话和子对话的关闭操作状态。

Sources: apply_close_outcome.rb, apply_close_outcome.rb

子代理屏障:工作流中的并行等待语义

wait_all 屏障机制

当工作流中的代理任务通过 wait_transition_requested 提交一个包含 subagent_spawn 意图的批量操作时,Workflows::HandleWaitTransitionRequest 会执行以下流程:

  1. 调用 IntentBatchMaterialization 将批量意图转化为 DAG 节点
  2. 对每个 subagent_spawn 节点调用 SubagentSessions::Spawn,创建子代理会话及其完整的执行上下文
  3. 若该 stage 的 completion_barrierwait_all,工作流进入 waiting 状态,wait_reason_kind: "subagent_barrier"
  4. 工作流记录阻塞资源信息,包含所有子代理会话 ID、批次 ID、屏障 artifact 键等
flowchart LR subgraph "父对话工作流" A[agent_turn_step] --> B[subagent_alpha] A --> C[subagent_beta] B --> D[agent_step_2] C --> D end subgraph "子对话 alpha" E[SubagentSession.alpha] --> F[子 AgentTaskRun] end subgraph "子对话 beta" G[SubagentSession.beta] --> H[子 AgentTaskRun] end B -.->|"spawn"| E C -.->|"spawn"| G

屏障解析与工作流恢复

WorkflowWaitSnapshot.resolved_for? 中对 subagent_barrier 的解析逻辑为:检查所有 subagent_session_ids 对应的会话是否都已达到终端状态(terminal_for_wait? 判定:关闭终态 或 观测状态为 completed/failed/interrupted)。只有全部子代理都终止后,屏障才被解除。

Workflows::ResumeAfterWaitResolution 在屏障解除后执行恢复:将工作流状态重置为 ready,收集屏障涉及的子代理节点作为 predecessor_nodes,然后调用 ReEnterAgent 创建后续执行步骤。验收场景 subagent_wait_all_validation 完整验证了这一流程——两个并行子代理全部完成后,DAG 中出现连接到后续节点 agent_step_2 的边,后续 AgentTaskRun 被创建为 queued 状态。

Sources: handle_wait_transition_request.rb, workflow_wait_snapshot.rb, resume_after_wait_resolution.rb, subagent_wait_all_validation.rb

ConversationCloseOperation:对话级关闭的编排器

当对话请求归档或删除时(Conversations::RequestClose),系统创建 ConversationCloseOperation 来编排所有运行时资源的有序关闭。该操作的生命周期包括五个阶段:

阶段 lifecycle_state 含义
请求 requested 关闭操作已创建
静默 quiescing 正在等待活跃资源完成
处置 disposing 尚有尾部资源或依赖阻塞
降级 degraded 部分资源关闭失败
完成 completed 所有资源成功关闭

ReconcileCloseOperation 每次被调用时(通常由资源关闭的善后流程触发),会执行 ProgressCloseRequests 推进所有待关闭资源的状态,然后通过 BlockerSnapshotQuery 重新计算阻塞快照,据此决定关闭操作的生命周期状态转换。

阻塞快照将阻塞因素分为三个层次:

  • 主线阻塞 (mainline):活跃轮次、活跃工作流、活跃任务、阻塞式交互、运行中子代理——所有计数必须归零
  • 尾部阻塞 (tail):运行中后台进程、游离工具进程、降级关闭——用于判断是否进入 disposing/degraded
  • 依赖阻塞 (dependency):后代对话的 lineage 阻塞、根 lineage store 阻塞、变量/导入来源阻塞

对话关闭时,RequestClose 会遍历 OwnedTree 获取全部子代理会话(包括多层嵌套),对每个仍处于 open 状态的会话调用 SubagentSessions::RequestClose。这确保了对话关闭的级联传播——父对话的关闭会递归触发所有子代理会话的关闭请求。

Sources: conversation_close_operation.rb, request_close.rb, reconcile_close_operation.rb, conversation_blocker_snapshot.rb, blocker_snapshot_query.rb

三层协作的完整生命周期

以下时序图展示了一个典型的子代理会话从创建到关闭的完整生命周期:

sequenceDiagram participant Parent as 父对话工作流 participant Spawn as SubagentSessions::Spawn participant Child as 子代理会话/子对话 participant Lease as ExecutionLease participant Agent as 代理程序 participant Close as ApplyCloseOutcome Parent->>Spawn: HandleWaitTransitionRequest (wait_all) Spawn->>Child: 创建子对话 + SubagentSession + AgentTaskRun Spawn->>Lease: Acquire (holder: deployment) Parent->>Parent: 工作流进入 waiting (subagent_barrier) loop 执行阶段 Agent->>Lease: Heartbeat Agent->>Child: SendMessage (optional) end Agent->>Parent: 执行完成报告 Parent->>Parent: ResumeAfterWaitResolution (屏障解除) Note over Child: 对话关闭触发 Parent->>Child: SubagentSessions::RequestClose Child->>Agent: 邮箱投递关闭请求 Agent->>Child: acknowledged Agent->>Child: resource_closed Child->>Close: HandleCloseReport → ApplyCloseOutcome Close->>Lease: Release (reason: resource_closed) Close->>Close: 协调子对话 + 父对话的 ConversationCloseOperation

整个流程体现了三层协作的核心设计理念:SubagentSession 管理语义层面的代理委派关系,ExecutionLease 管理物理层面的独占占用,ClosableResourceRouting 统一关闭协议的分发和协调。三者共同确保运行时资源从创建到回收的每个阶段都有明确的所有权边界和状态转换规则。

Sources: apply_close_outcome.rb, handle_close_report.rb, workflow_wait_snapshot.rb

延伸阅读