Skip to main content

recursive/hooks/
mod.rs

1//! Lifecycle hooks for the agent loop.
2//!
3//! Hooks are callbacks invoked at well-defined points during an agent run.
4//! They allow consumers to observe, log, gate, or transform behaviour without
5//! modifying the agent loop itself.
6//!
7//! # Hook points
8//!
9//! - `SessionStart` — at the top of an agent run, before any LLM call.
10//! - `PreToolCall` — before each tool dispatch (after the permission hook).
11//! - `PostToolCall` — after each tool returns.
12//! - `PreCompact` — before compaction fires.
13//! - `PostCompact` — after compaction completes.
14//! - `SessionEnd` — at terminal finishes (currently only on the legacy
15//!   `Agent` path; the `AgentRuntime` does not dispatch this yet).
16//!
17//! # Usage
18//!
19//! ```ignore
20//! use recursive::hooks::{Hook, HookEvent, HookAction, HookRegistry};
21//!
22//! struct MyHook;
23//! impl Hook for MyHook {
24//!     fn on_event(&self, event: HookEvent) -> HookAction {
25//!         match event {
26//!             HookEvent::PreToolCall { name, .. } => {
27//!                 eprintln!("about to call {name}");
28//!                 HookAction::Continue
29//!             }
30//!             _ => HookAction::Continue,
31//!         }
32//!     }
33//! }
34//!
35//! let mut registry = HookRegistry::new();
36//! registry.register(Arc::new(MyHook));
37//! ```
38
39pub mod config;
40pub mod external;
41
42pub use config::{load_hooks_config, HookCommand, HookCommandType, HookMatcher, HooksConfig};
43pub use external::{ExternalHookRunner, HookResult, PermissionDecision};
44
45use std::sync::Arc;
46
47use serde_json::Value;
48
49use crate::runtime::RuntimeOutcome;
50use std::collections::HashMap;
51use std::time::Instant;
52
53/// Action a hook can request in response to an event.
54#[derive(Debug, Clone)]
55pub enum HookAction {
56    /// Proceed normally.
57    Continue,
58    /// Skip this tool call (PreToolCall only; treated as Continue for other events).
59    Skip,
60    /// Abort with an error message (PreToolCall only; treated as Continue for other events).
61    Error(String),
62}
63
64/// Events emitted at lifecycle points during an agent run.
65///
66/// This enum is `#[non_exhaustive]` — new variants may be added in future
67/// releases without a breaking change.
68#[derive(Debug, Clone)]
69#[non_exhaustive]
70pub enum HookEvent<'a> {
71    /// Fired at the start of an agent run, before any LLM call.
72    SessionStart {
73        /// The goal text passed to `AgentRuntime::run()`.
74        goal: &'a str,
75    },
76    /// Fired before a tool is dispatched (after the permission hook).
77    PreToolCall {
78        /// Name of the tool about to be called.
79        name: &'a str,
80        /// Arguments that will be passed to the tool.
81        args: &'a Value,
82    },
83    /// Fired after a tool returns.
84    PostToolCall {
85        /// Name of the tool that was called.
86        name: &'a str,
87        /// Arguments that were passed to the tool.
88        args: &'a Value,
89        /// The result string returned by the tool (or error message).
90        result: &'a str,
91        /// Wall-clock duration of the tool execution in milliseconds.
92        duration_ms: u64,
93    },
94    /// Fired before compaction is attempted.
95    PreCompact {
96        /// Current transcript length in characters.
97        transcript_len: usize,
98    },
99    /// Fired after compaction completes.
100    PostCompact {
101        /// Number of messages removed during compaction.
102        removed: usize,
103        /// Character count of the summary message added.
104        summary_chars: usize,
105    },
106    /// Fired at the end of an agent run, before returning.
107    ///
108    /// Currently only dispatched by the legacy `Agent` path (deleted in
109    /// Goal 219). The `AgentRuntime` does not yet dispatch `SessionEnd`.
110    SessionEnd {
111        /// The outcome that will be returned.
112        outcome: &'a RuntimeOutcome,
113    },
114    /// Fired after the user submits a message, before the LLM processes it.
115    UserPromptSubmit {
116        /// The user's input content.
117        content: &'a str,
118    },
119    /// Fired when the agent completes normally (`NoMoreToolCalls`).
120    Stop {
121        /// The final outcome.
122        outcome: &'a RuntimeOutcome,
123    },
124    /// Fired before a sub-agent is dispatched.
125    SubagentStart {
126        /// The sub-agent's goal text.
127        goal: &'a str,
128        /// Nesting depth (0 = top-level).
129        depth: usize,
130    },
131    /// Fired after a sub-agent completes.
132    SubagentStop {
133        /// The sub-agent's outcome.
134        outcome: &'a RuntimeOutcome,
135        /// Nesting depth.
136        depth: usize,
137    },
138    /// Fired when a tool call returns an error (before `PostToolCall`).
139    PostToolCallFailure {
140        /// Name of the tool that failed.
141        name: &'a str,
142        /// Arguments that were passed to the tool.
143        args: &'a Value,
144        /// The error message.
145        error: &'a str,
146    },
147    /// Fired when a permission check denies a tool call.
148    PermissionDenied {
149        /// Name of the tool that was denied.
150        tool_name: &'a str,
151        /// Human-readable reason for the denial.
152        reason: &'a str,
153    },
154    /// Fired when the agent emits a notification to the user.
155    Notification {
156        /// Notification content.
157        message: &'a str,
158    },
159    /// Fired once at the very beginning, before `SessionStart`, for one-time setup.
160    Setup,
161}
162
163/// A lifecycle hook that can observe and influence agent behaviour.
164pub trait Hook: Send + Sync {
165    /// Called when a lifecycle event occurs.
166    ///
167    /// Return `HookAction::Continue` to proceed normally.
168    /// Return `HookAction::Skip` or `HookAction::Error` from a `PreToolCall`
169    /// event to prevent tool execution. For all other event types, `Skip`
170    /// and `Error` are treated as `Continue`.
171    fn on_event(&self, event: HookEvent) -> HookAction;
172}
173
174/// A registry of hooks that dispatches events to all registered hooks in order.
175///
176/// Hooks are stored as `Arc<dyn Hook>` and dispatched sequentially. If any
177/// hook returns `HookAction::Skip` or `HookAction::Error` from a `PreToolCall`
178/// event, the first non-`Continue` action is returned and remaining hooks
179/// are not called for that event.
180#[derive(Clone, Default)]
181pub struct HookRegistry {
182    hooks: Vec<Arc<dyn Hook>>,
183}
184
185impl HookRegistry {
186    /// Create an empty hook registry.
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Register a hook. Hooks fire in registration order.
192    pub fn register(&mut self, hook: Arc<dyn Hook>) {
193        self.hooks.push(hook);
194    }
195
196    /// Dispatch an event to all registered hooks.
197    ///
198    /// Returns the first non-`Continue` action, or `Continue` if all hooks
199    /// agree. For non-`PreToolCall` events, `Skip` and `Error` are converted
200    /// to `Continue`.
201    pub fn dispatch(&self, event: HookEvent) -> HookAction {
202        let is_pre_tool = matches!(event, HookEvent::PreToolCall { .. });
203        for hook in &self.hooks {
204            match hook.on_event(event.clone()) {
205                HookAction::Continue => continue,
206                HookAction::Skip if is_pre_tool => return HookAction::Skip,
207                HookAction::Error(msg) if is_pre_tool => return HookAction::Error(msg),
208                // Non-PreToolCall events: Skip/Error treated as Continue
209                _ => continue,
210            }
211        }
212        HookAction::Continue
213    }
214
215    /// Returns true if hooks of the given type would be dispatched.
216    /// Currently always true when hooks are registered; reserved for future filtering.
217    pub fn has_hooks(&self) -> bool {
218        !self.hooks.is_empty()
219    }
220
221    /// Returns true if no hooks are registered.
222    pub fn is_empty(&self) -> bool {
223        self.hooks.is_empty()
224    }
225
226    /// Returns the number of registered hooks.
227    pub fn len(&self) -> usize {
228        self.hooks.len()
229    }
230}
231
232/// A hook that prints tool call timing information to stderr.
233///
234/// On `PostToolCall` events, prints `[hook] {name} took {duration_ms}ms`.
235/// All other events return `HookAction::Continue`.
236pub struct ToolTimingHook {
237    start_times: std::sync::Mutex<HashMap<String, Instant>>,
238}
239
240impl ToolTimingHook {
241    pub fn new() -> Self {
242        Self {
243            start_times: std::sync::Mutex::new(HashMap::new()),
244        }
245    }
246}
247
248impl Default for ToolTimingHook {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl Hook for ToolTimingHook {
255    fn on_event(&self, event: HookEvent) -> HookAction {
256        match event {
257            HookEvent::PreToolCall { name, .. } => {
258                let mut map = self.start_times.lock().unwrap();
259                map.insert(name.to_string(), Instant::now());
260                HookAction::Continue
261            }
262            HookEvent::PostToolCall {
263                name, duration_ms, ..
264            } => {
265                eprintln!("[hook] {name} took {duration_ms}ms");
266                HookAction::Continue
267            }
268            _ => HookAction::Continue,
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use std::sync::atomic::{AtomicUsize, Ordering};
277
278    struct SkipHook;
279
280    impl Hook for SkipHook {
281        fn on_event(&self, event: HookEvent) -> HookAction {
282            match event {
283                HookEvent::PreToolCall { .. } => HookAction::Skip,
284                _ => HookAction::Continue,
285            }
286        }
287    }
288
289    struct ErrorHook;
290
291    impl Hook for ErrorHook {
292        fn on_event(&self, event: HookEvent) -> HookAction {
293            match event {
294                HookEvent::PreToolCall { .. } => HookAction::Error("nope".into()),
295                _ => HookAction::Continue,
296            }
297        }
298    }
299
300    #[test]
301    fn empty_registry_returns_continue() {
302        let reg = HookRegistry::new();
303        let action = reg.dispatch(HookEvent::SessionStart { goal: "test" });
304        assert!(matches!(action, HookAction::Continue));
305    }
306
307    #[test]
308    fn session_start_fires_with_correct_goal() {
309        let captured = Arc::new(std::sync::Mutex::new(String::new()));
310        let c = captured.clone();
311        struct GoalCapture(Arc<std::sync::Mutex<String>>);
312        impl Hook for GoalCapture {
313            fn on_event(&self, event: HookEvent) -> HookAction {
314                if let HookEvent::SessionStart { goal } = event {
315                    *self.0.lock().unwrap() = goal.to_string();
316                }
317                HookAction::Continue
318            }
319        }
320        let mut reg = HookRegistry::new();
321        reg.register(Arc::new(GoalCapture(c)));
322        reg.dispatch(HookEvent::SessionStart { goal: "my goal" });
323        assert_eq!(*captured.lock().unwrap(), "my goal");
324    }
325
326    #[test]
327    fn pre_tool_call_skip_prevents_execution() {
328        let mut reg = HookRegistry::new();
329        reg.register(Arc::new(SkipHook));
330        let action = reg.dispatch(HookEvent::PreToolCall {
331            name: "write_file",
332            args: &serde_json::json!({"path": "foo.txt"}),
333        });
334        assert!(matches!(action, HookAction::Skip));
335    }
336
337    #[test]
338    fn pre_tool_call_error_returns_message() {
339        let mut reg = HookRegistry::new();
340        reg.register(Arc::new(ErrorHook));
341        let action = reg.dispatch(HookEvent::PreToolCall {
342            name: "write_file",
343            args: &serde_json::json!({"path": "foo.txt"}),
344        });
345        assert!(matches!(action, HookAction::Error(ref msg) if msg == "nope"));
346    }
347
348    #[test]
349    fn skip_and_error_on_non_pre_tool_are_continue() {
350        let mut reg = HookRegistry::new();
351        reg.register(Arc::new(SkipHook));
352        reg.register(Arc::new(ErrorHook));
353        // SessionStart — SkipHook returns Continue, ErrorHook returns Continue
354        let action = reg.dispatch(HookEvent::SessionStart { goal: "test" });
355        assert!(matches!(action, HookAction::Continue));
356        // PostToolCall — same
357        let action = reg.dispatch(HookEvent::PostToolCall {
358            name: "read_file",
359            args: &serde_json::json!({"path": "foo.txt"}),
360            result: "ok",
361            duration_ms: 5,
362        });
363        assert!(matches!(action, HookAction::Continue));
364    }
365
366    #[test]
367    fn multiple_hooks_fire_in_order() {
368        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
369        let o1 = order.clone();
370        let o2 = order.clone();
371
372        struct OrdHook(usize, Arc<std::sync::Mutex<Vec<usize>>>);
373        impl Hook for OrdHook {
374            fn on_event(&self, _event: HookEvent) -> HookAction {
375                self.1.lock().unwrap().push(self.0);
376                HookAction::Continue
377            }
378        }
379
380        let mut reg = HookRegistry::new();
381        reg.register(Arc::new(OrdHook(1, o1)));
382        reg.register(Arc::new(OrdHook(2, o2)));
383        reg.dispatch(HookEvent::SessionStart { goal: "test" });
384        assert_eq!(*order.lock().unwrap(), vec![1, 2]);
385    }
386
387    #[test]
388    fn first_skip_short_circuits_remaining_hooks() {
389        let count = Arc::new(AtomicUsize::new(0));
390        let c1 = count.clone();
391        let c2 = count.clone();
392
393        struct FirstSkip(Arc<AtomicUsize>);
394        impl Hook for FirstSkip {
395            fn on_event(&self, event: HookEvent) -> HookAction {
396                match event {
397                    HookEvent::PreToolCall { .. } => HookAction::Skip,
398                    _ => {
399                        self.0.fetch_add(1, Ordering::SeqCst);
400                        HookAction::Continue
401                    }
402                }
403            }
404        }
405
406        struct SecondCounter(Arc<AtomicUsize>);
407        impl Hook for SecondCounter {
408            fn on_event(&self, _event: HookEvent) -> HookAction {
409                self.0.fetch_add(1, Ordering::SeqCst);
410                HookAction::Continue
411            }
412        }
413
414        let mut reg = HookRegistry::new();
415        reg.register(Arc::new(FirstSkip(c1)));
416        reg.register(Arc::new(SecondCounter(c2.clone())));
417        let action = reg.dispatch(HookEvent::PreToolCall {
418            name: "write_file",
419            args: &serde_json::json!({"path": "foo.txt"}),
420        });
421        assert!(matches!(action, HookAction::Skip));
422        // SecondCounter should NOT have been called
423        assert_eq!(c2.load(Ordering::SeqCst), 0);
424    }
425
426    #[test]
427    fn post_tool_call_receives_correct_fields() {
428        let captured = Arc::new(std::sync::Mutex::new(None::<(String, String, u64)>));
429        let c = captured.clone();
430        struct CaptureHook(Arc<std::sync::Mutex<Option<(String, String, u64)>>>);
431        impl Hook for CaptureHook {
432            fn on_event(&self, event: HookEvent) -> HookAction {
433                if let HookEvent::PostToolCall {
434                    name,
435                    result,
436                    duration_ms,
437                    ..
438                } = event
439                {
440                    *self.0.lock().unwrap() =
441                        Some((name.to_string(), result.to_string(), duration_ms));
442                }
443                HookAction::Continue
444            }
445        }
446        let mut reg = HookRegistry::new();
447        reg.register(Arc::new(CaptureHook(c)));
448        reg.dispatch(HookEvent::PostToolCall {
449            name: "read_file",
450            args: &serde_json::json!({"path": "foo.txt"}),
451            result: "file contents",
452            duration_ms: 42,
453        });
454        let captured = captured.lock().unwrap().clone().unwrap();
455        assert_eq!(captured.0, "read_file");
456        assert_eq!(captured.1, "file contents");
457        assert_eq!(captured.2, 42);
458    }
459
460    #[test]
461    fn session_end_receives_outcome() {
462        let captured = Arc::new(std::sync::Mutex::new(None));
463        let c = captured.clone();
464        struct CaptureOutcome(Arc<std::sync::Mutex<Option<RuntimeOutcome>>>);
465        impl Hook for CaptureOutcome {
466            fn on_event(&self, event: HookEvent) -> HookAction {
467                if let HookEvent::SessionEnd { outcome } = event {
468                    *self.0.lock().unwrap() = Some(outcome.clone());
469                }
470                HookAction::Continue
471            }
472        }
473        let outcome = RuntimeOutcome {
474            final_text: Some("done".into()),
475            finish_reason: crate::agent::FinishReason::NoMoreToolCalls,
476            total_usage: crate::llm::TokenUsage::default(),
477            steps: 3,
478            llm_latency_ms: 100,
479            checkpoint_id: None,
480        };
481        let mut reg = HookRegistry::new();
482        reg.register(Arc::new(CaptureOutcome(c)));
483        reg.dispatch(HookEvent::SessionEnd { outcome: &outcome });
484        let captured = captured.lock().unwrap().take().unwrap();
485        assert_eq!(captured.final_text.as_deref(), Some("done"));
486        assert_eq!(captured.steps, 3);
487    }
488
489    #[test]
490    fn pre_compact_receives_transcript_len() {
491        let captured = Arc::new(std::sync::Mutex::new(0usize));
492        let c = captured.clone();
493        struct CaptureLen(Arc<std::sync::Mutex<usize>>);
494        impl Hook for CaptureLen {
495            fn on_event(&self, event: HookEvent) -> HookAction {
496                if let HookEvent::PreCompact { transcript_len } = event {
497                    *self.0.lock().unwrap() = transcript_len;
498                }
499                HookAction::Continue
500            }
501        }
502        let mut reg = HookRegistry::new();
503        reg.register(Arc::new(CaptureLen(c)));
504        reg.dispatch(HookEvent::PreCompact {
505            transcript_len: 5000,
506        });
507        assert_eq!(*captured.lock().unwrap(), 5000);
508    }
509
510    #[test]
511    fn post_compact_receives_removed_and_summary_chars() {
512        let captured = Arc::new(std::sync::Mutex::new((0usize, 0usize)));
513        let c = captured.clone();
514        struct CaptureCompact(Arc<std::sync::Mutex<(usize, usize)>>);
515        impl Hook for CaptureCompact {
516            fn on_event(&self, event: HookEvent) -> HookAction {
517                if let HookEvent::PostCompact {
518                    removed,
519                    summary_chars,
520                } = event
521                {
522                    *self.0.lock().unwrap() = (removed, summary_chars);
523                }
524                HookAction::Continue
525            }
526        }
527        let mut reg = HookRegistry::new();
528        reg.register(Arc::new(CaptureCompact(c)));
529        reg.dispatch(HookEvent::PostCompact {
530            removed: 10,
531            summary_chars: 200,
532        });
533        assert_eq!(captured.lock().unwrap().0, 10);
534        assert_eq!(captured.lock().unwrap().1, 200);
535    }
536
537    #[test]
538    fn hook_event_is_non_exhaustive() {
539        // Compile-time check: HookEvent is #[non_exhaustive]
540        let _ = HookEvent::SessionStart { goal: "test" };
541    }
542
543    // ── Goal 204 new event tests ──────────────────────────────────
544
545    #[test]
546    fn new_events_compile_and_dispatch() {
547        let reg = HookRegistry::new();
548        let outcome = RuntimeOutcome {
549            final_text: None,
550            finish_reason: crate::agent::FinishReason::NoMoreToolCalls,
551            total_usage: crate::llm::TokenUsage::default(),
552            steps: 0,
553            llm_latency_ms: 0,
554            checkpoint_id: None,
555        };
556        let empty_args = serde_json::json!({});
557
558        let action = reg.dispatch(HookEvent::UserPromptSubmit { content: "hello" });
559        assert!(matches!(action, HookAction::Continue));
560        let action = reg.dispatch(HookEvent::Stop { outcome: &outcome });
561        assert!(matches!(action, HookAction::Continue));
562        let action = reg.dispatch(HookEvent::SubagentStart {
563            goal: "sub-goal",
564            depth: 1,
565        });
566        assert!(matches!(action, HookAction::Continue));
567        let action = reg.dispatch(HookEvent::SubagentStop {
568            outcome: &outcome,
569            depth: 1,
570        });
571        assert!(matches!(action, HookAction::Continue));
572        let action = reg.dispatch(HookEvent::PostToolCallFailure {
573            name: "run_shell",
574            args: &empty_args,
575            error: "err",
576        });
577        assert!(matches!(action, HookAction::Continue));
578        let action = reg.dispatch(HookEvent::PermissionDenied {
579            tool_name: "write_file",
580            reason: "not allowed",
581        });
582        assert!(matches!(action, HookAction::Continue));
583        let action = reg.dispatch(HookEvent::Notification { message: "hi" });
584        assert!(matches!(action, HookAction::Continue));
585        let action = reg.dispatch(HookEvent::Setup);
586        assert!(matches!(action, HookAction::Continue));
587    }
588
589    #[test]
590    fn post_tool_call_failure_dispatched_to_hook() {
591        let captured = Arc::new(std::sync::Mutex::new(None::<(String, String)>));
592        let c = captured.clone();
593        struct CaptureFailure(Arc<std::sync::Mutex<Option<(String, String)>>>);
594        impl Hook for CaptureFailure {
595            fn on_event(&self, event: HookEvent) -> HookAction {
596                if let HookEvent::PostToolCallFailure { name, error, .. } = event {
597                    *self.0.lock().unwrap() = Some((name.to_string(), error.to_string()));
598                }
599                HookAction::Continue
600            }
601        }
602        let mut reg = HookRegistry::new();
603        reg.register(Arc::new(CaptureFailure(c)));
604        let args = serde_json::json!({"command": "rm -rf /"});
605        reg.dispatch(HookEvent::PostToolCallFailure {
606            name: "run_shell",
607            args: &args,
608            error: "permission denied",
609        });
610        let cap = captured.lock().unwrap().clone().unwrap();
611        assert_eq!(cap.0, "run_shell");
612        assert_eq!(cap.1, "permission denied");
613    }
614
615    #[test]
616    fn user_prompt_submit_received_by_hook() {
617        let captured = Arc::new(std::sync::Mutex::new(String::new()));
618        let c = captured.clone();
619        struct CapturePrompt(Arc<std::sync::Mutex<String>>);
620        impl Hook for CapturePrompt {
621            fn on_event(&self, event: HookEvent) -> HookAction {
622                if let HookEvent::UserPromptSubmit { content } = event {
623                    *self.0.lock().unwrap() = content.to_string();
624                }
625                HookAction::Continue
626            }
627        }
628        let mut reg = HookRegistry::new();
629        reg.register(Arc::new(CapturePrompt(c)));
630        reg.dispatch(HookEvent::UserPromptSubmit {
631            content: "test prompt",
632        });
633        assert_eq!(*captured.lock().unwrap(), "test prompt");
634    }
635
636    #[test]
637    fn tool_timing_hook_prints_to_stderr_on_post_tool_call() {
638        // Capture stderr by redirecting
639        let hook = ToolTimingHook::new();
640        let action = hook.on_event(HookEvent::PostToolCall {
641            name: "read_file",
642            args: &serde_json::json!({"path": "foo.txt"}),
643            result: "ok",
644            duration_ms: 42,
645        });
646        assert!(matches!(action, HookAction::Continue));
647    }
648
649    #[test]
650    fn tool_timing_hook_returns_continue_for_non_tool_events() {
651        let hook = ToolTimingHook::new();
652        let action = hook.on_event(HookEvent::SessionStart { goal: "test" });
653        assert!(matches!(action, HookAction::Continue));
654
655        let action = hook.on_event(HookEvent::PreCompact {
656            transcript_len: 100,
657        });
658        assert!(matches!(action, HookAction::Continue));
659
660        let action = hook.on_event(HookEvent::PostCompact {
661            removed: 5,
662            summary_chars: 50,
663        });
664        assert!(matches!(action, HookAction::Continue));
665
666        let outcome = RuntimeOutcome {
667            final_text: Some("done".into()),
668            finish_reason: crate::agent::FinishReason::NoMoreToolCalls,
669            total_usage: crate::llm::TokenUsage::default(),
670            steps: 1,
671            llm_latency_ms: 0,
672            checkpoint_id: None,
673        };
674        let action = hook.on_event(HookEvent::SessionEnd { outcome: &outcome });
675        assert!(matches!(action, HookAction::Continue));
676    }
677
678    #[test]
679    fn tool_timing_hook_pre_tool_call_returns_continue() {
680        let hook = ToolTimingHook::new();
681        let action = hook.on_event(HookEvent::PreToolCall {
682            name: "write_file",
683            args: &serde_json::json!({"path": "foo.txt"}),
684        });
685        assert!(matches!(action, HookAction::Continue));
686    }
687}