codewhale_core/engine/thread/executor.rs
1//! Turn executor — the `monitor_turn` / `handle_deepseek_turn` leg
2//! (issue #5261 / #3313).
3//!
4//! This will own `handle_deepseek_turn`, the steer/subagent drains,
5//! `refresh_system_prompt()`, `should_compact`/`compact_messages_safe`,
6//! `MessageRequest` build, parallel tool exec, `StuckGuard`/
7//! `ReadRepeatGuard`/`ToolCallBudget`, and stream retry budget. The move
8//! is file-by-file from `crates/tui/src/core/engine/turn_loop.rs`
9//! (5,706 lines) so the diff stays reviewable. Until the move lands this
10//! file carries the executor type and the `execpolicy` gate that guarantees
11//! approvals route through the turn context identically in both modes.
12
13use codewhale_execpolicy::ExecPolicyEngine;
14use codewhale_protocol::ids::{SessionId, ThreadId};
15
16/// Per-turn execution context. The `execpolicy` engine is the sole authority
17/// for approvals; both TUI and headless construct it from the same
18/// `permissions.toml` / `ConfigStore` so the gate never diverges.
19#[derive(Debug)]
20pub struct TurnExecutor {
21 pub thread_id: ThreadId,
22 pub session_id: SessionId,
23 pub exec_policy: ExecPolicyEngine,
24 pub max_steps: u32,
25}
26
27impl TurnExecutor {
28 #[must_use]
29 pub fn new(
30 thread_id: ThreadId,
31 session_id: SessionId,
32 exec_policy: ExecPolicyEngine,
33 max_steps: u32,
34 ) -> Self {
35 Self {
36 thread_id,
37 session_id,
38 exec_policy,
39 max_steps,
40 }
41 }
42
43 #[must_use]
44 pub fn can_execute(&self, step: u32) -> bool {
45 step < self.max_steps
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn executor_respects_max_steps() {
55 let ex = TurnExecutor::new(
56 ThreadId::new(),
57 SessionId::new(),
58 ExecPolicyEngine::new(vec![], vec![]),
59 2,
60 );
61 assert!(ex.can_execute(0));
62 assert!(ex.can_execute(1));
63 assert!(!ex.can_execute(2));
64 }
65}