双引擎架构——Engine v2 如何重新定义 Agent 执行模型

📑 目录

IronClaw 深度剖析(三):双引擎架构——Engine v2 如何重新定义 Agent 执行模型

系列导读:本文是 IronClaw 深度剖析系列的第三篇。在前两篇中,我们分别探讨了 IronClaw 的整体架构设计和权限系统。本篇将聚焦于 IronClaw 最核心的创新——Engine v2 统一执行引擎,解读它如何通过五个原语重新定义 Agent 的执行模型。


一、双引擎架构:新旧共存的智慧

IronClaw 的代码库中藏着一个罕见的架构决策:两个完整的 Agent 引擎并行运行。这不是技术债务的堆积,而是一次深思熟虑的渐进式架构升级。

Engine v1:传统 Agent 系统

位于 src/agent/ 的 v1 引擎是一个典型的传统 Agent 实现,包含约 15 个模块:

  • Agent 结构体作为核心入口
  • Session 管理交互式对话
  • Job 处理后台任务队列
  • Routine 处理定时和事件触发任务
  • run_agentic_loop() 驱动主循环
  • LoopDelegate 实现行为委托模式
// src/agent/mod.rs - Engine v1 模块结构
mod agent_loop;         // Agent 结构体 + AgentDeps
mod agentic_loop;       // 原始的 run_agentic_loop
mod router;             // 消息意图路由 (Chat/Tool/Research/...)
mod session;            // Session + Turn 管理
mod scheduler;          // Job 调度
mod routine;            // 定时/响应式任务
mod heartbeat;          // 主动后台执行
mod self_repair;        // 卡死任务恢复
mod compaction;         // 上下文压缩
mod cost_guard;         // Token 预算管理

Engine v2:统一执行引擎

位于 crates/ironclaw_engine/ 的 v2 引擎是一次彻底的重构,它将 v1 中约 10 个独立抽象(Session、Job、Routine、Channel、Tool、Skill、Hook、Observer、Extension、LoopDelegate)统一为 5 个核心原语:Thread、Step、Capability、MemoryDoc 和 Project。

桥接适配器:让两者共存

两个引擎通过一个桥接层(src/bridge/)实现共存:

┌─────────────────────────────────────────┐
│            Host Application             │
│  ┌──────────────┐    ┌───────────────┐  │
│  │   Engine v1  │◄──►│   Bridge      │  │
│  │  src/agent/  │    │  src/bridge/  │  │
│  └──────────────┘    └──────┬────────┘  │
│                             │            │
│                    ┌────────▼────────┐   │
│                    │   Engine v2     │   │
│                    │crates/ironclaw_ │   │
│                    │    engine/      │   │
│                    └─────────────────┘   │
└─────────────────────────────────────────┘

v2 引擎通过 ENGINE_V2=true 环境变量启用。同时,IRONCLAW_DISABLE_CODEACT 可以在 v2 中禁用 CodeAct 功能(来自 PR #3665),这种精细的开关控制体现了架构的灵活性。


二、五大核心原语:从十个抽象到一个统一模型

Engine v2 最核心的架构创新是将原本分散在 v1 中的众多概念统一为五个正交的原语。这种"做减法"的设计哲学,使得整个系统的认知复杂度大幅降低。

抽象映射全景图

graph TB
    subgraph "Engine v1 (Legacy)"
        V1_S[Session]
        V1_J[Job]
        V1_R[Routine]
        V1_SA[Sub-agent]
        V1_T[Tool]
        V1_SK[Skill]
        V1_H[Hook]
        V1_E[Extension]
        V1_W[Workspace]
        V1_M[Memory Blob]
    end

    subgraph "Engine v2 (Unified)"
        V2_Th[Thread
+ ThreadType] V2_St[Step] V2_C[Capability
+ ActionDef + Policy] V2_P[Project] V2_Md[MemoryDoc
+ DocType] end V1_S --> V2_Th V1_J --> V2_Th V1_R --> V2_Th V1_SA --> V2_Th V1_T --> V2_C V1_SK --> V2_C V1_H --> V2_C V1_E --> V2_C V1_W --> V2_P V1_M --> V2_Md style V2_Th fill:#4a90d9,color:#fff style V2_St fill:#4a90d9,color:#fff style V2_C fill:#4a90d9,color:#fff style V2_P fill:#4a90d9,color:#fff style V2_Md fill:#4a90d9,color:#fff

Thread:统一的工作单元

Thread 是 v2 中最核心的抽象,它将 v1 中分散的 Session(交互对话)、Job(后台任务)、Routine(定时任务)和 Sub-agent(子代理)统一为一个类型:

// crates/ironclaw_engine/src/types/thread.rs

/// Thread 统一了 v1 中 Session + Job + Routine + Sub-agent
pub struct Thread {
    pub id: ThreadId,
    pub project_id: ProjectId,
    pub thread_type: ThreadType,    // 区分不同用途
    pub state: ThreadState,         // 显式状态机
    pub parent_id: Option<ThreadId>, // 支持父子树
    pub capability_leases: Vec<LeaseId>,
    pub config: ThreadConfig,
}

pub enum ThreadType {
    Chat,        // 交互式对话(替代 Session)
    Background,  // 后台工作(替代 Job)
    Routine,     // 定时执行(替代 Routine)
    Sub,         // 委托推理(替代 Sub-agent)
}

pub enum ThreadState {
    Created,     // 已创建但未启动
    Running,     // 正在执行 Steps
    Waiting,     // 等待外部输入
    Suspended,   // 被系统暂停
    Completed,   // 执行完成
    Done,        // 完全结束(终态)
    Failed,      // 终态失败
}

状态转换规则

Created → Running → Waiting → Running (resume)
                    → Suspended → Running (resume)
                    → Completed → Done
                    → Failed

Step:显式的执行单元

v1 中的 Agent loop 迭代是隐式的——每次循环产生一组 tool call,但没有显式的数据结构来代表"一次 LLM 调用 + 其动作执行"。v2 引入了 Step 来显式建模这个过程:

// crates/ironclaw_engine/src/types/step.rs
pub struct Step {
    pub id: StepId,
    pub thread_id: ThreadId,
    pub sequence: usize,              // Thread 内的序号(1-indexed)
    pub status: StepStatus,           // Pending | LlmCalling | Executing | Completed | Failed
    pub execution_tier: ExecutionTier,
    pub llm_response: Option<LlmResponse>,
    pub action_calls: Vec<ActionCall>,
    pub action_results: Vec<ActionResult>,
    pub token_usage: TokenUsage,
    pub started_at: Option<DateTime<Utc>>,
    pub completed_at: Option<DateTime<Utc>>,
}

Capability:统一的效果模型

Capability 将 Tool、Skill、Hook 和 Extension 统一为一个"能力"概念,每个能力包含动作定义(ActionDef)和策略(Policy):

// crates/ironclaw_engine/src/types/capability.rs
pub struct Capability {
    pub id: CapabilityId,
    pub name: String,
    pub actions: Vec<ActionDef>,      // 该能力提供的动作
    pub policies: Vec<Policy>,        // 执行策略约束
}

pub struct ActionDef {
    pub name: String,
    pub description: String,
    pub parameters: JsonSchema,
    pub returns: JsonSchema,
}

MemoryDoc 与 Project:结构化知识管理

MemoryDoc 替代了 v1 中临时的内存 blob,提供了类型化的知识管理;Project 替代了扁平的 workspace,提供了作用域隔离。

v1 抽象v2 原语核心改进
Session + Job + Routine + Sub-agentThread + ThreadType单一生命周期管理,支持父子树
Tool + Skill + Hook + ExtensionCapability + ActionDef统一的效果模型,策略驱动
隐式 loop 迭代Step + 显式状态机可观测、可恢复的执行
扁平 workspaceProject + 作用域多租户内存隔离
临时 memory blobMemoryDoc + DocType结构化知识管理

三、Agent Loop 消息处理流程

Engine v2 的核心是 ExecutionLoop(位于 executor/loop_engine.rs,约 2143 行),它替代了 v1 的 run_agentic_loop()。这是一个自包含的循环,清晰地定义了从 Thread 创建到完成的完整生命周期。

执行流程全景

flowchart TD
    A[ThreadManager::spawn] --> B[ExecutionLoop::run]
    B --> C[Build Context
messages + actions + memory] C --> D[LLM call via
LlmBackend::complete] D --> E{Response type?} E -->|Text| F[Stream to user] E -->|ActionCalls| G[For each ActionCall] G --> H[GatePipeline.evaluate] H --> I{Decision?} I -->|Allow| J[EffectExecutor.execute] J --> K[Update Step
emit ThreadEvent] I -->|Pause| L[Store RunState
wait for resume] I -->|Deny| M[Record failure
continue] K --> N{More actions?} N -->|Yes| G N -->|No| O{Thread complete?} M --> N F --> O O -->|No| C O -->|Yes| P[Emit ThreadCompleted
return ThreadOutcome] style A fill:#5b8c5a,color:#fff style P fill:#5b8c5a,color:#fff style L fill:#c9a227,color:#000 style M fill:#c44,color:#fff

ExecutionLoop 的核心逻辑

// crates/ironclaw_engine/src/executor/loop_engine.rs (简化示意)
impl ExecutionLoop {
    pub async fn run(&mut self, thread_id: ThreadId) -> Result<ThreadOutcome, EngineError> {
        loop {
            // 1. 构建上下文:消息 + 能力动作 + 内存文档
            let context = self.build_context(thread_id).await?;
            
            // 2. 调用 LLM
            let llm_response = self.llm.complete(
                &context.messages,
                &context.available_actions,
                &context.config,
            ).await?;
            
            // 3. 创建新 Step
            let mut step = self.create_step(thread_id).await?;
            
            match llm_response {
                LlmResponse::Text(text) => {
                    // 纯文本响应,直接流式输出
                    self.stream_to_user(text).await?;
                    step.complete_text().await?;
                }
                LlmResponse::ActionCalls(calls) => {
                    // 需要执行动作
                    step.start_executing().await?;
                    
                    for call in calls {
                        // 4. 门控评估
                        let gate_ctx = GateContext::new(&call, &step);
                        match self.gate_pipeline.evaluate(&gate_ctx).await {
                            // 5a. 允许执行
                            GateDecision::Allow => {
                                let result = self.effector.execute(&call, &context).await?;
                                step.record_action_result(result).await?;
                            }
                            // 5b. 暂停等待
                            GateDecision::Pause { reason, resume_kind } => {
                                self.run_state.store_pause(thread_id, reason, resume_kind);
                                self.emit_event(EventKind::GatePaused).await?;
                                return Ok(ThreadOutcome::Paused);
                            }
                            // 5c. 拒绝执行
                            GateDecision::Deny { reason } => {
                                step.record_action_failure(&call, reason).await?;
                                self.emit_event(EventKind::GateDenied).await?;
                                continue;
                            }
                        }
                    }
                    step.complete().await?;
                }
            }
            
            // 6. 检查 Thread 是否完成
            if self.should_complete(thread_id).await? {
                self.emit_event(EventKind::ThreadCompleted).await?;
                return Ok(ThreadOutcome::Completed);
            }
        }
    }
}

这个循环相比 v1 的 run_agentic_loop() 有几个关键改进:

  1. 显式的 Step 管理:每次 LLM 调用及其动作执行都被封装为一个 Step,有明确的状态和生命周期
  2. 统一的 GatePipeline:所有动作执行前必须通过门控评估,不存在"绕过"路径
  3. 事件驱动:每个状态变更都产生事件,可被外部系统观测
  4. 可暂停恢复:Pause 决策将运行状态持久化,支持后续恢复

四、Gate Pipeline:执行门控系统

IronClaw 的安全模型建立在"fail-closed"原则之上——任何未明确允许的操作都被拒绝。GatePipeline 是这一原则的实现核心。

状态机设计

stateDiagram-v2
    [*] --> Evaluating : ActionCall received
    
    Evaluating --> Executing : GateDecision::Allow
    Executing --> Completed : ActionResult OK
    Executing --> Failed : ActionResult Err
    
    Evaluating --> Paused : GateDecision::Pause
    Paused --> Evaluating : ResumeKind::Approval
    Paused --> Evaluating : ResumeKind::Authentication
    Paused --> Evaluating : ResumeKind::External
    
    Evaluating --> Denied : GateDecision::Deny
    Denied --> NextAction : Record failure, continue
    
    Completed --> [*]
    Failed --> [*]
    NextAction --> Evaluating : More actions
    NextAction --> [*] : No more actions

    note right of Paused
        RunState persisted
        ThreadEvent::GatePaused emitted
        Thread enters Waiting state
    end note

核心枚举定义

// crates/ironclaw_engine/src/gate/mod.rs

/// 门控决策——注意:没有 None 变体,fail-closed 是设计 invariant
pub enum GateDecision {
    Allow,
    Pause { 
        reason: String, 
        resume_kind: ResumeKind 
    },
    Deny { 
        reason: String 
    },
}

/// 恢复类型——封闭枚举,所有暂停路径走同一套机制
pub enum ResumeKind {
    Approval { 
        allow_always: bool          // 用户批准
    },
    Authentication { 
        credential_name: String,    // OAuth/API Key
        instructions: String,
        auth_url: String,
    },
    External { 
        callback_id: Uuid            // Webhook 确认
    },
}

三个关键设计不变量

  1. 无 None 变体GateDecision 只有 AllowPauseDeny 三种可能,不存在"未决定"状态。这是 fail-closed 的根本保障。
  2. 封闭 ResumeKind:所有暂停都必须携带恢复类型,强制所有暂停路径走同一套恢复机制。
  3. 零拷贝热路径GateContext 对所有数据使用借用而非克隆,保证高频调用场景下的性能。

Pipeline 评估逻辑

// crates/ironclaw_engine/src/gate/pipeline.rs
pub struct GatePipeline {
    gates: Vec<Box<dyn ExecutionGate>>,
}

impl GatePipeline {
    /// 顺序评估所有 gate,第一个非-Allow 的决定获胜
    pub async fn evaluate(&self, ctx: &GateContext<'_>) -> GateDecision {
        for gate in &self.gates {
            match gate.evaluate(ctx).await {
                GateDecision::Allow => continue,
                decision => return decision,  // Pause 或 Deny 立即返回
            }
        }
        GateDecision::Allow  // 所有 gate 都通过
    }
}

工具分层(Tool Tier)

GatePipeline 与工具分层系统配合,对不同风险级别的操作施加不同的门控策略:

// crates/ironclaw_engine/src/gate/tool_tier.rs
pub enum ToolTier {
    Tier0,  // 结构化工具调用(JSON action)— 无需审批
    Tier1,  // CodeAct / 脚本执行 — 需要审批
    Tier2,  // 高风险操作(文件删除、网络请求等)— 严格审批
}
层级类型示例门控策略
Tier 0结构化调用读取文件、查询数据库自动允许(Capability lease 已授权)
Tier 1CodeAct 脚本Python 代码执行首次需用户批准,allow_always 可记住
Tier 2高风险操作删除文件、发起网络请求每次都需要明确审批

五、CodeAct 模式:当 Agent 开始写代码

CodeAct 是 Engine v2 中的一项重要能力,它允许 LLM 生成 Python 代码来执行复杂任务,而非仅仅调用预定义的工具。

ExecutionTier 分层

// crates/ironclaw_engine/src/types/step.rs
pub enum ExecutionTier {
    Structured,  // Tier 0: 结构化工具调用(JSON action calls)
    Scripting,   // Tier 1: 内嵌 Python via Monty(CodeAct/RLM 模式)
}

执行流程

CodeAct 的执行涉及多个组件的协作:

  1. LLM 生成 Python 代码:包含在 <code> 标签中
  2. 语法校验scripting::validate_python_syntax() 检查代码合法性
  3. Monty 沙箱执行:Pydantic 的 Python 沙箱环境隔离运行
  4. 结果捕获:执行结果封装为 ActionResult
  5. 事件发射ThreadEvent::ActionCompleted 被发出

Monty 是 v2 中唯一的 CodeAct/RLM 执行器:

# crates/ironclaw_engine/Cargo.toml
[dependencies]
monty = { git = "https://github.com/pydantic/monty.git", tag = "v0.0.16" }

安全措施

CodeAct 涉及执行 LLM 生成的任意代码,安全至关重要。IronClaw 采取了多层防护:

  1. 沙箱隔离:Monty 提供独立的 Python 执行环境
  2. fail-closed 门控:所有脚本执行必须通过 ToolTier::Tier1 的审批
  3. ReDoS 防护:在 executor/orchestrator.rs 中有这样一段注释:
// Security: `__regex_match__` 接受来自 Python orchestrator 的任意模式。
// 使用 `regex = "1"`(线性时间)而非 `fancy-regex`(ReDoS 风险)。
// 不要在没有 wall-clock budget 重新设计的情况下添加 `fancy-regex`。

这段代码使用线性时间的正则引擎,从根本上杜绝了正则表达式拒绝服务攻击的可能性。选择 "1" 版本而非功能更丰富的 fancy-regex,是安全优先于功能的典型架构决策。

禁用开关

// 来自 PR #3665 (commit cab708e)
// IRONCLAW_DISABLE_CODEACT=true 完全禁用 v2 的 CodeAct 功能

这个环境变量提供了一种紧急刹车机制,当发现 CodeAct 存在安全问题时可以立即全局禁用。


六、任务调度模型

v2 的任务调度采用三层架构,分别对应不同时间尺度的任务管理。

MissionManager:长期目标

Mission 代表需要持续执行的长期目标,按 cadence(节奏)生成 Thread:

// crates/ironclaw_engine/src/types/mission.rs
pub struct Mission {
    pub id: MissionId,
    pub project_id: ProjectId,
    pub description: String,
    pub cadence: MissionCadence,     // OneShot | Interval | Cron
    pub status: MissionStatus,       // Active | Paused | Completed | Failed
    pub next_run: Option<DateTime<Utc>>,
}

当 Mission 因为门控暂停时,MissionManager 会在门控解决后自动恢复(来自 PR #3166)。

ThreadManager:线程生命周期

// crates/ironclaw_engine/src/runtime/manager.rs
pub struct ThreadManager {
    threads: HashMap<ThreadId, Thread>,
    tree: ThreadTree,                 // 父子关系追踪
    lease_manager: LeaseManager,      // 能力租约管理
}

impl ThreadManager {
    pub async fn spawn(&mut self, config: ThreadConfig) -> ThreadId;
    pub async fn stop(&mut self, id: ThreadId) -> Result<ThreadOutcome>;
    pub async fn inject_message(&mut self, id: ThreadId, msg: ThreadMessage);
    pub async fn join(&mut self, id: ThreadId) -> Result<ThreadOutcome>;
}

Dispatcher:运行时分发

ironclaw_dispatcher crate 提供了组合式的运行时分发:

// crates/ironclaw_dispatcher/src/lib.rs
pub struct RuntimeAdapterRequest<'a, F, G> {
    pub package: &'a ExtensionPackage,
    pub descriptor: &'a CapabilityDescriptor,
    pub filesystem: &'a F,
    pub governor: &'a G,
    pub capability_id: &'a CapabilityId,
    pub runtime_kind: RuntimeKind,
    pub resource_reservation: Option<ResourceReservation>,
}

Dispatcher 的设计非常克制——它不解析扩展清单、不实现沙箱策略、不预留预算、不执行产品工作流。它只做一件事:将经过验证的扩展描述符连接到运行时 lane。


七、事件驱动架构:一切皆事件

Engine v2 是一个完全事件驱动的系统。ironclaw_events crate 提供了持久化的事件/审计基板。

事件类型

// crates/ironclaw_events/src/lib.rs
pub struct RuntimeEvent {
    pub id: Uuid,
    pub timestamp: chrono::DateTime<chrono::Utc>,
    pub kind: EventKind,
    pub payload: serde_json::Value,
}

pub trait EventSink: Send + Sync {
    async fn emit(&self, event: RuntimeEvent) -> Result<(), EventError>;
}

// crates/ironclaw_engine/src/types/event.rs
pub enum EventKind {
    // Thread 生命周期
    ThreadCreated,
    ThreadStarted,
    ThreadCompleted,
    ThreadFailed,
    // Step 生命周期
    StepStarted,
    StepCompleted,
    StepFailed,
    // LLM 交互
    LlmCalled,
    LlmResponded,
    // 动作执行
    ActionCalled,
    ActionCompleted,
    ActionFailed,
    // 门控事件
    GatePaused,
    GateResumed,
    GateDenied,
    // 知识与能力
    MemoryDocCreated,
    MemoryDocUpdated,
    CapabilityLeased,
    // ... 共 18 种变体
}

事件流架构

sequenceDiagram
    participant EL as ExecutionLoop
    participant TE as ThreadEvent
    participant ES as EventSink
    participant DS as Durable Storage
    participant SSE as SSE Stream
    participant UI as Debug Panel

    EL->>TE: Step 开始
    TE->>ES: emit(EventKind::StepStarted)
    ES->>DS: 持久化存储
    ES->>SSE: 实时推送
    SSE->>UI: 更新 Activity 面板

    EL->>EL: LLM 调用完成
    EL->>TE: 动作需要执行
    TE->>ES: emit(EventKind::ActionCalled)
    ES->>DS: 持久化存储

    EL->>EL: GatePipeline 评估
    alt Allow
        EL->>TE: 执行动作
        TE->>ES: emit(EventKind::ActionCompleted)
    else Pause
        TE->>ES: emit(EventKind::GatePaused)
        ES->>DS: 保存 RunState
        Note over EL,UI: 等待用户审批...
        UI->>EL: Resume signal
        TE->>ES: emit(EventKind::GateResumed)
    else Deny
        TE->>ES: emit(EventKind::GateDenied)
    end

    ES->>DS: 持久化存储
    ES->>SSE: 实时推送
    SSE->>UI: 更新状态显示

事件流的消费者

事件流同时服务于多个消费者:

  1. 持久化存储:所有事件被持久化,提供完整的审计追踪
  2. SSE 流:通过 Server-Sent Events 实时推送给前端
  3. 调试面板:Activity tab 实时显示执行状态
  4. Run State:从事件流中派生当前运行状态,回答"这个调用现在在等什么?"
// crates/ironclaw_run_state/src/lib.rs
pub enum RunStatus {
    Running,
    BlockedApproval,   // 等待用户审批
    BlockedAuth,       // 等待认证
    Completed,
    Failed,
}

值得注意的是,RunState 不是事件的简单累加,而是对"当前阻塞点"的建模。它与 append-only 的事件历史是互补关系:事件历史告诉你发生了什么,RunState 告诉你现在在等什么


八、总结:架构演进的方法论启示

Engine v2 的设计给我们提供了几个有价值的架构启示:

1. 统一比拆分更有力量

v1 将 Session、Job、Routine、Sub-agent 作为独立概念,每个都有自己的生命周期和管理逻辑。v2 用一个 Thread + ThreadType 就统一了全部。这不是简单的代码合并,而是找到了"工作单元"这个更本质的抽象。

2. 显式优于隐式

v1 的 loop 迭代是隐式的——你无法在运行时询问"当前执行到第几次迭代"。v2 的 Step 使每次执行显式化,带来了可观测性、可恢复性和可调试性的全面提升。

3. 安全是设计出来的,不是补丁

GatePipeline 的 fail-closed 设计(无 None 变体)、ToolTier 的分层策略、ReDoS 的线性时间正则——这些安全特性都是架构层面的设计决策,而非事后修补。

4. 渐进式迁移的策略智慧

通过 Bridge 模式让 v1 和 v2 共存,用环境变量控制启用/禁用,IronClaw 展示了如何在生产环境中进行重大架构升级而不中断服务。这比"大爆炸式重写"务实得多。

Engine v2 的五个原语——Thread、Step、Capability、MemoryDoc、Project——构成了 IronClaw Agent 执行的坚实基础。在这个基础上,GatePipeline 提供了安全保证,Event System 提供了可观测性,CodeAct 提供了灵活性。这套架构不仅解决了当前的问题,更为未来的扩展预留了清晰的演化路径。


关于本文:本文基于 IronClaw 开源代码库(https://github.com/nearai/ironclaw)的深度分析,分析日期为 2026-05-17。如有不准确之处,欢迎指正。