Skip to main content

evorule_cli/
executor.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! 同步反应器循环 —— CLI 执行核心
5//!
6//! # P0 修复
7//! 1. **FIFO 队列**:用 `VecDeque::pop_front()`,不能用 `Vec::pop()`。
8//!    tier0 `exec_push` 用 `new_queue.append(queue)` 把新指令前置(插队语义),
9//!    必须从前端取才能保证 push 的指令先执行。
10//! 2. **max_steps 上界**:先检后 pop(对齐 evorule-reactor reactor.rs BUG-3 修复)。
11//!    默认 10000,可 `--max-steps` 覆盖。超限发 `Fact::Error` + break。
12//! 3. **I/O 两阶段架构**:`pending_io: HashMap<FactId, JsonValue>` 缓存 orig 指令。
13//!    0.2.0 无 handler 时发 `Fact::Error` 退出,但架构正确——0.3.0 加 handler 时
14//!    只需在 IoRequest 分支注入 IoResponse + push_front(orig) 即可,循环主体不变。
15//!
16//! # 不引入 tokio runtime
17//! `execute_transition` 是同步纯函数,整个循环无 await。tokio 仅作为 evorule-reactor
18//! 编译依赖存在,CLI 不创建 runtime。
19//!
20//! # Fact 序列
21//! 执行产生 `Vec<Fact>`:
22//! 1. `Command`(初始指令)
23//! 2. 若干 `StateTransition`(每步执行)
24//! 3. 可选 `IoRequest` + `Error`(I/O 请求但无 handler)
25//! 4. 可选 `Error`(TCB 错误或 max_steps 超限)
26//! 5. `Stable`(稳定标记,始终发射)
27//!
28//! 最终 payload 经返回值直接交给调用方(CR-20260901-001:Stable 不再
29//! 内嵌全量快照,状态本体从执行器持有的 payload 返回,不经事实链)。
30
31use std::collections::{HashMap, VecDeque};
32
33use evorule_reactor::{Fact, FactId, FactIdGenerator, IoType};
34use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
35
36use crate::error::CliError;
37
38/// 默认 max_steps(与 evorule-reactor 默认 max_rounds 一致量级)
39pub const DEFAULT_MAX_STEPS: usize = 10000;
40
41/// 执行规则,产生 Fact 序列
42///
43/// # 参数
44/// - `core_eval`:transform 规则列表(由 `io_util::load_rules` 加载)
45/// - `initial_payload`:初始 payload
46/// - `initial_instruction`:初始指令(通常是 `{"type":"noop"}` 触发 transform 链)
47/// - `max_steps`:最大执行步数上界(先检后 pop)
48///
49/// # 返回
50/// `(Vec<Fact>, JsonValue)`:fact 序列(Command、若干 StateTransition、
51/// 可选 Error、结尾 Stable)+ 最终 payload(执行器持有,直接返回)
52///
53/// # 不变量
54/// - FIFO 队列:`VecDeque::pop_front`,不能用 `Vec::pop`
55/// - max_steps 先检后 pop:超限发 Error + break
56/// - I/O 两阶段:IoRequest 时缓存 orig 指令到 pending_io,0.2.0 无 handler 发 Error
57// 108 行: CLI 主循环 + I/O 两阶段 + max_steps 门禁 + 错误处理必须单函数原子语义
58// 拆函数会让 4 阶段 (loop / dispatch / pending_io / break) 状态传递出错
59#[allow(clippy::too_many_lines)]
60pub fn execute(
61    core_eval: &[JsonValue],
62    initial_payload: JsonValue,
63    initial_instruction: JsonValue,
64    max_steps: usize,
65) -> Result<(Vec<Fact>, JsonValue), CliError> {
66    let mut facts: Vec<Fact> = Vec::new();
67    let mut id_gen = FactIdGenerator::new();
68    let mut queue: VecDeque<JsonValue> = VecDeque::new();
69    queue.push_back(initial_instruction);
70    let mut payload = initial_payload;
71    let mut steps = 0;
72    // 会话版本号:对齐 reactor 语义,每条 StateTransition +1
73    let mut version: u64 = 0;
74    // 0.2.0 无 I/O handler,pending_io 仅缓存不消费(为 0.3.0 铺路)
75    let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
76
77    // 发射初始 Command fact
78    let cmd_id = id_gen.next_id();
79    let mut current_cause: FactId = cmd_id;
80    let cmd_instruction = queue.front().cloned().unwrap_or(JsonValue::Null);
81    facts.push(Fact::Command {
82        id: cmd_id,
83        instruction: cmd_instruction,
84    });
85
86    while !queue.is_empty() {
87        // max_steps 先检后 pop(对齐 evorule-reactor BUG-3 修复)
88        if steps >= max_steps {
89            let err_id = id_gen.next_id();
90            facts.push(Fact::Error {
91                id: err_id,
92                message: format!("max_steps exceeded: {}", steps),
93            });
94            tracing::warn!(steps, max_steps, "max_steps exceeded");
95            break;
96        }
97
98        // FIFO:pop_front(不能用 Vec::pop,那是 LIFO)
99        let instruction = match queue.pop_front() {
100            Some(i) => i,
101            None => break, // 逻辑不可达(while 条件已检查),防御性
102        };
103        steps += 1;
104
105        // 传当前 queue 快照给 execute_transition(供 core_eval 规则引用 __exec__.queue)
106        let queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
107        let result = execute_transition(core_eval, &instruction, &payload, &queue_snapshot);
108
109        match result {
110            Ok(TransitionResult::State {
111                new_payload,
112                new_queue,
113            }) => {
114                payload = new_payload;
115                queue = new_queue.into_iter().collect();
116                version += 1;
117                let id = id_gen.next_id();
118                let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
119                facts.push(Fact::StateTransition {
120                    id,
121                    cause: current_cause,
122                    new_payload: payload.clone(),
123                    new_queue: new_queue_snapshot,
124                });
125                current_cause = id;
126            }
127            Ok(TransitionResult::IoRequired { io_type, params }) => {
128                // v0.2.0:io_type 透传不校验(parse 已 deprecated,无条件接受)
129                let io_type = IoType::new(&io_type);
130                let req_id = id_gen.next_id();
131                // 缓存 orig 指令(0.3.0 加 handler 时用于 push_front 重执行)
132                pending_io.insert(req_id, instruction.clone());
133                facts.push(Fact::IoRequest {
134                    id: req_id,
135                    cause: current_cause,
136                    io_type: io_type.clone(),
137                    params,
138                });
139                // 0.2.0 无 I/O handler,发 Error 退出
140                let err_id = id_gen.next_id();
141                facts.push(Fact::Error {
142                    id: err_id,
143                    message: format!("no I/O handler for io_type={}", io_type.as_str()),
144                });
145                tracing::warn!(
146                    io_type = %io_type.as_str(),
147                    request_id = ?req_id,
148                    "I/O required but no handler available, stopping"
149                );
150                break;
151            }
152            Ok(TransitionResult::Ignored {
153                instruction_type,
154                reason,
155            }) => {
156                // v0.3.1:指令被静默忽略(无匹配 transform 规则或规则产生 noop 效果)。
157                // 与 reactor 行为一致:产生 Error 事实使系统显式感知此问题
158                let err_id = id_gen.next_id();
159                let msg = format!(
160                    "Instruction ignored by TCB: type={}, reason={}, instruction={:?}",
161                    instruction_type, reason, instruction
162                );
163                facts.push(Fact::Error {
164                    id: err_id,
165                    message: msg,
166                });
167                tracing::warn!(
168                    instruction_type = %instruction_type,
169                    reason = %reason,
170                    "TCB 静默忽略指令(无匹配规则或 noop 效果)"
171                );
172                break;
173            }
174            Err(e) => {
175                let err_id = id_gen.next_id();
176                let msg = format!("TCB error at step {}: {}", steps, e);
177                facts.push(Fact::Error {
178                    id: err_id,
179                    message: msg,
180                });
181                tracing::error!(step = steps, error = %e, "TCB execution error");
182                break;
183            }
184        }
185    }
186
187    // 始终发射 Stable(即使是 Error 退出,也标记当前版本稳定)。
188    // 最终 payload 经返回值直接交付(CR-20260901-001:不再内嵌快照)
189    let stable_id = id_gen.next_id();
190    facts.push(Fact::Stable {
191        id: stable_id,
192        version,
193    });
194
195    Ok((facts, payload))
196}
197
198#[cfg(test)]
199mod tests {
200    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
201    use super::*;
202    use evorule_tcb::JsonValue;
203
204    /// 构造 noop 指令
205    fn noop_instruction() -> JsonValue {
206        JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
207    }
208
209    /// 构造无条件 push 规则:push 一条 noop 到队列前端
210    fn push_noop_rule() -> JsonValue {
211        JsonValue::object_from_pairs(&[
212            ("type", JsonValue::string("push")),
213            (
214                "params",
215                JsonValue::object_from_pairs(&[(
216                    "instructions",
217                    JsonValue::array(vec![noop_instruction()]),
218                )]),
219            ),
220        ])
221    }
222
223    /// 构造无条件 io_request 规则
224    fn io_request_rule(io_type: &str) -> JsonValue {
225        JsonValue::object_from_pairs(&[
226            ("type", JsonValue::string("io_request")),
227            (
228                "params",
229                JsonValue::object_from_pairs(&[
230                    ("io_type", JsonValue::string(io_type)),
231                    ("url", JsonValue::string("http://example.com")),
232                ]),
233            ),
234        ])
235    }
236
237    #[test]
238    fn test_execute_empty_core_eval_noop() {
239        // v0.3.1:空 core_eval + noop 指令 → TCB 显式返回 `Ignored`(无匹配 transform 规则)
240        // cli executor 按 reactor 一致行为产生 `Error` 事实(不再静默失败)
241        let (facts, _) = execute(
242            &[],
243            JsonValue::empty_object(),
244            noop_instruction(),
245            DEFAULT_MAX_STEPS,
246        )
247        .unwrap();
248
249        // 应产生:Command + Error(ignored by TCB) + Stable
250        assert_eq!(facts.len(), 3, "expected Command + Error(ignored) + Stable");
251        assert!(matches!(facts[0], Fact::Command { .. }));
252        match &facts[1] {
253            Fact::Error { message, .. } => {
254                assert!(
255                    message.contains("ignored by TCB"),
256                    "expected TCB ignored error, got: {}",
257                    message
258                );
259            }
260            other => panic!("expected Error for Ignored, got {:?}", other),
261        }
262        assert!(matches!(facts[2], Fact::Stable { .. }));
263    }
264
265    #[test]
266    fn test_execute_max_steps_zero() {
267        // max_steps=0:立即发 Error,不执行任何指令
268        let (facts, _) = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
269
270        // 应产生:Command + Error + Stable
271        assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
272        assert!(matches!(facts[0], Fact::Command { .. }));
273        match &facts[1] {
274            Fact::Error { message, .. } => {
275                assert!(message.contains("max_steps"), "message: {}", message);
276            }
277            other => panic!("expected Error, got {:?}", other),
278        }
279        assert!(matches!(facts[2], Fact::Stable { .. }));
280    }
281
282    #[test]
283    fn test_execute_max_steps_exceeded_with_push() {
284        // core_eval 含 push 规则(无限循环),max_steps=3 限制
285        let core_eval = vec![push_noop_rule()];
286        let (facts, _) =
287            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
288
289        // 应产生:Command + 3×StateTransition + Error + Stable = 6
290        // (steps 0,1,2 各产生 StateTransition,step 3 触发 max_steps)
291        assert!(
292            facts.len() >= 4,
293            "expected at least Command + StateTransitions + Error + Stable, got {}",
294            facts.len()
295        );
296        // 最后第二个应为 Error
297        let last_idx = facts.len() - 2;
298        match &facts[last_idx] {
299            Fact::Error { message, .. } => {
300                assert!(message.contains("max_steps"), "message: {}", message);
301            }
302            other => panic!("expected Error at index {}, got {:?}", last_idx, other),
303        }
304        // 最后应为 Stable
305        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
306    }
307
308    #[test]
309    fn test_execute_push_produces_nonempty_queue() {
310        // core_eval 含 push 规则,max_steps=1 只执行一步
311        let core_eval = vec![push_noop_rule()];
312        let (facts, _) =
313            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
314
315        // 第一个 StateTransition 的 new_queue 应非空(push 生效)
316        let st = facts.iter().find_map(|f| {
317            if let Fact::StateTransition { new_queue, .. } = f {
318                Some(new_queue)
319            } else {
320                None
321            }
322        });
323        let new_queue = st.expect("should have StateTransition");
324        assert!(
325            !new_queue.is_empty(),
326            "push rule should produce non-empty new_queue"
327        );
328    }
329
330    #[test]
331    fn test_execute_io_request_produces_io_fact() {
332        // core_eval 含 io_request 规则
333        let core_eval = vec![io_request_rule("call_external")];
334        let (facts, _) = execute(
335            &core_eval,
336            JsonValue::empty_object(),
337            noop_instruction(),
338            DEFAULT_MAX_STEPS,
339        )
340        .unwrap();
341
342        // 应产生:Command + IoRequest + Error + Stable
343        let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
344        assert!(has_io_request, "should have IoRequest fact");
345
346        let has_error = facts.iter().any(
347            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
348        );
349        assert!(has_error, "should have Error fact about no I/O handler");
350
351        // 最后应为 Stable
352        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
353    }
354
355    #[test]
356    fn test_execute_unknown_io_type_produces_error() {
357        // v0.2.0:io_type 透传不校验,"unknown_io_type" 不再被 parse 拒绝,
358        // 而是透传后由 cli(无 handler)发 "no I/O handler for io_type=unknown_io_type" Error
359        let core_eval = vec![io_request_rule("unknown_io_type")];
360        let (facts, _) = execute(
361            &core_eval,
362            JsonValue::empty_object(),
363            noop_instruction(),
364            DEFAULT_MAX_STEPS,
365        )
366        .unwrap();
367
368        let has_error = facts.iter().any(
369            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
370        );
371        assert!(
372            has_error,
373            "should have Error (no I/O handler) for unknown io_type"
374        );
375    }
376
377    #[test]
378    fn test_execute_tcb_error_produces_error() {
379        // 构造会触发 TCB 错误的场景:core_eval 含非法规则(缺 params)
380        let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
381        let (facts, _) = execute(
382            &[bad_rule],
383            JsonValue::empty_object(),
384            noop_instruction(),
385            DEFAULT_MAX_STEPS,
386        )
387        .unwrap();
388
389        // 应产生 Error(TCB error)
390        let has_error = facts
391            .iter()
392            .any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
393        assert!(has_error, "should have TCB error fact");
394    }
395
396    #[test]
397    fn test_execute_fact_ids_monotonic() {
398        // 验证 FactId 单调递增
399        let (facts, _) = execute(
400            &[],
401            JsonValue::empty_object(),
402            noop_instruction(),
403            DEFAULT_MAX_STEPS,
404        )
405        .unwrap();
406
407        let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
408        for i in 1..ids.len() {
409            assert!(
410                ids[i] > ids[i - 1],
411                "FactIds must be monotonically increasing: {:?}",
412                ids
413            );
414        }
415    }
416
417    #[test]
418    fn test_execute_fifo_pop_front_semantics() {
419        // 验证 FIFO:push 两条指令后,按顺序执行
420        // core_eval: push [noop, noop](两条指令)
421        let push_two = JsonValue::object_from_pairs(&[
422            ("type", JsonValue::string("push")),
423            (
424                "params",
425                JsonValue::object_from_pairs(&[(
426                    "instructions",
427                    JsonValue::array(vec![noop_instruction(), noop_instruction()]),
428                )]),
429            ),
430        ]);
431        let core_eval = vec![push_two];
432        // max_steps=3:初始指令执行 push(step1),然后执行 push 的两条 noop(step2,3)
433        let (facts, _) =
434            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
435
436        // 验证:每次执行都通过 pop_front 取指令(FIFO)
437        // step1: pop_front 初始 noop → push [noop, noop] → queue=[noop, noop]
438        // step2: pop_front noop(第1条) → push [noop, noop] → queue=[noop, noop, noop]
439        // step3: pop_front noop → push → queue=[noop, noop, noop]
440        // max_steps=3 触发 Error
441        let state_transitions: Vec<_> = facts
442            .iter()
443            .filter(|f| matches!(f, Fact::StateTransition { .. }))
444            .collect();
445        assert!(
446            !state_transitions.is_empty(),
447            "should have at least one StateTransition"
448        );
449        // 第一个 StateTransition 的 new_queue 应有 2 条指令(push 的结果)
450        if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
451            assert_eq!(
452                new_queue.len(),
453                2,
454                "first push should produce 2 instructions in queue"
455            );
456        }
457    }
458}