Skip to main content

ferrin_core/generate_text/
stop_condition.rs

1//! Stop conditions of the generation loop.
2
3use std::sync::Arc;
4
5use ferrin_spec::BoxFuture;
6use ferrin_spec::ToolName;
7
8use super::StepResult;
9
10/// Decides after each step whether the loop stops.
11///
12/// Conditions configured on a call are combined with *any-of* semantics.
13/// Implemented for every `Fn(&[StepResult]) -> bool` closure.
14pub trait StopCondition: Send + Sync {
15    /// Returns `true` when the loop must stop after `steps`.
16    fn should_stop<'a>(&'a self, steps: &'a [StepResult]) -> BoxFuture<'a, bool>;
17}
18
19impl<F> StopCondition for F
20where
21    F: Fn(&[StepResult]) -> bool + Send + Sync,
22{
23    fn should_stop<'a>(&'a self, steps: &'a [StepResult]) -> BoxFuture<'a, bool> {
24        let result = self(steps);
25        Box::pin(async move { result })
26    }
27}
28
29/// Stops when the number of steps reaches `n`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct StepCount(u32);
32
33/// Stops when `n` steps have been completed.
34#[must_use]
35pub fn step_count(n: u32) -> StepCount {
36    StepCount(n)
37}
38
39impl StopCondition for StepCount {
40    fn should_stop<'a>(&'a self, steps: &'a [StepResult]) -> BoxFuture<'a, bool> {
41        let stop = u32::try_from(steps.len()).unwrap_or(u32::MAX) >= self.0;
42        Box::pin(async move { stop })
43    }
44}
45
46/// Stops when the last step called one of the listed tools.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct HasToolCall(Vec<ToolName>);
49
50/// Stops when the last step contains a call to `tool_name`.
51#[must_use]
52pub fn has_tool_call(tool_name: impl Into<ToolName>) -> HasToolCall {
53    HasToolCall(vec![tool_name.into()])
54}
55
56/// Stops when the last step contains a call to any of `tool_names`.
57#[must_use]
58pub fn has_any_tool_call(tool_names: impl IntoIterator<Item = impl Into<ToolName>>) -> HasToolCall {
59    HasToolCall(tool_names.into_iter().map(Into::into).collect())
60}
61
62impl StopCondition for HasToolCall {
63    fn should_stop<'a>(&'a self, steps: &'a [StepResult]) -> BoxFuture<'a, bool> {
64        let stop = steps.last().is_some_and(|step| {
65            step.tool_calls()
66                .any(|call| self.0.contains(&call.tool_name))
67        });
68        Box::pin(async move { stop })
69    }
70}
71
72/// Never stops on its own (the loop ends when no tool calls remain).
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct Never;
75
76/// A condition that never stops the loop.
77#[must_use]
78pub fn never() -> Never {
79    Never
80}
81
82impl StopCondition for Never {
83    fn should_stop<'a>(&'a self, _steps: &'a [StepResult]) -> BoxFuture<'a, bool> {
84        Box::pin(async { false })
85    }
86}
87
88/// Evaluates `conditions` with any-of semantics.
89pub(crate) async fn is_stop_condition_met(
90    conditions: &[Arc<dyn StopCondition>],
91    steps: &[StepResult],
92) -> bool {
93    for condition in conditions {
94        if condition.should_stop(steps).await {
95            return true;
96        }
97    }
98    false
99}