Configure Stop Policies
Use this when you need to control when an agent run terminates based on round count, token usage, elapsed time, or error frequency.
Prerequisites
Section titled “Prerequisites”awakencrate added toCargo.toml- Familiarity with
PluginandAgentRuntimeBuilder
Overview
Section titled “Overview”Stop policies evaluate after each inference step and decide whether the run should continue or terminate. The system provides four built-in policies and a trait for custom implementations.
Built-in policies:
| Policy | Triggers when |
|---|---|
MaxRoundsPolicy | Step count exceeds max rounds |
TokenBudgetPolicy | Total tokens (input + output) exceed max_total |
TimeoutPolicy | Elapsed wall time exceeds max_ms milliseconds |
ConsecutiveErrorsPolicy | Consecutive inference errors reach max |
- Create policies programmatically.
use std::sync::Arc;use awaken::policies::{ MaxRoundsPolicy, TokenBudgetPolicy, TimeoutPolicy, ConsecutiveErrorsPolicy, StopPolicy,};
let policies: Vec<Arc<dyn StopPolicy>> = vec![ Arc::new(MaxRoundsPolicy::new(25)), Arc::new(TokenBudgetPolicy::new(100_000)), Arc::new(TimeoutPolicy::new(300_000)), // 5 minutes in ms Arc::new(ConsecutiveErrorsPolicy::new(3)),];- Register a
StopConditionPluginwith the runtime builder.
use std::sync::Arc;use awaken::policies::StopConditionPlugin;use awaken::engine::GenaiExecutor;use awaken::registry_spec::ModelSpec;use awaken::AgentRuntimeBuilder;
let mut spec = spec;spec.plugin_ids.push("stop-condition".into());
let runtime = AgentRuntimeBuilder::new() .with_plugin("stop-condition", Arc::new(StopConditionPlugin::new(policies))) .with_agent_spec(spec) .with_provider("anthropic", Arc::new(GenaiExecutor::new())) .with_model(ModelSpec::new("claude-sonnet", "anthropic", "claude-sonnet-4-20250514")) .build()?;For the common case of limiting rounds only, use the convenience wrapper:
use std::sync::Arc;use awaken::policies::MaxRoundsPlugin;use awaken::engine::GenaiExecutor;use awaken::registry_spec::ModelSpec;use awaken::AgentRuntimeBuilder;
let mut spec = spec;spec.plugin_ids.push("stop-condition:max-rounds".into());
let runtime = AgentRuntimeBuilder::new() .with_plugin("stop-condition:max-rounds", Arc::new(MaxRoundsPlugin::new(10))) .with_agent_spec(spec) .with_provider("anthropic", Arc::new(GenaiExecutor::new())) .with_model(ModelSpec::new("claude-sonnet", "anthropic", "claude-sonnet-4-20250514")) .build()?;Custom stop-condition plugins must be listed in AgentSpec.plugin_ids. The
built-in AgentSpec.max_rounds guard is still injected automatically; use these
plugins when you need additional policy types.
- Use declarative
StopConditionSpecvalues.
The policies_from_specs function converts declarative specs into policy instances. This is useful when loading configuration from JSON or YAML.
use awaken::contract::lifecycle::StopConditionSpec;use awaken::policies::{policies_from_specs, StopConditionPlugin};
let specs = vec![ StopConditionSpec::MaxRounds { rounds: 10 }, StopConditionSpec::Timeout { seconds: 300 }, StopConditionSpec::TokenBudget { max_total: 100_000 }, StopConditionSpec::ConsecutiveErrors { max: 3 },];
let policies = policies_from_specs(&specs);let plugin = StopConditionPlugin::new(policies);The full set of StopConditionSpec variants:
pub enum StopConditionSpec { MaxRounds { rounds: usize }, Timeout { seconds: u64 }, TokenBudget { max_total: usize }, ConsecutiveErrors { max: usize }, StopOnTool { tool_name: String }, ContentMatch { pattern: String }, LoopDetection { window: usize },}All variants above are backed by policies_from_specs; invalid ContentMatch regex patterns fail closed by stopping with content_match_invalid_regex.
The StopPolicy Trait
Section titled “The StopPolicy Trait”Implement StopPolicy to create custom stop conditions:
use awaken::policies::{StopPolicy, StopDecision, StopPolicyStats};
pub struct MyCustomPolicy { pub threshold: u64,}
impl StopPolicy for MyCustomPolicy { fn id(&self) -> &str { "my_custom" }
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision { if stats.total_output_tokens > self.threshold { StopDecision::Stop { code: "my_custom".into(), detail: format!("output tokens {} exceeded {}", stats.total_output_tokens, self.threshold), } } else { StopDecision::Continue } }}The trait requires Send + Sync + 'static. Evaluation must be synchronous — it is pure computation on the provided stats.
StopPolicyStats
Section titled “StopPolicyStats”Every policy receives a StopPolicyStats snapshot with fields populated by the internal StopConditionHook:
| Field | Type | Description |
|---|---|---|
step_count | u32 | Number of inference steps completed so far |
total_input_tokens | u64 | Cumulative prompt tokens across all steps |
total_output_tokens | u64 | Cumulative completion tokens across all steps |
elapsed_ms | u64 | Wall time since the first step, in milliseconds |
consecutive_errors | u32 | Current streak of consecutive inference errors (resets on success) |
last_tool_names | Vec<String> | Tool names called in the most recent inference response |
last_response_text | String | Text content of the most recent inference response |
StopDecision
Section titled “StopDecision”pub enum StopDecision { Continue, Stop { code: String, detail: String },}When any policy returns StopDecision::Stop, the hook converts it to TerminationReason::Stopped with the given code and detail, then updates the run lifecycle to Done. The agent loop exits after the current step. Policies are evaluated in order; the first Stop wins.
How Stop Policies Interact with the Agent Loop
Section titled “How Stop Policies Interact with the Agent Loop”- The
StopConditionPluginregisters aPhaseHookonPhase::AfterInference. - After each LLM inference, the hook increments
step_count, accumulates token usage, and tracks consecutive errors inStopConditionStatsState. - The hook builds a
StopPolicyStatssnapshot and callsevaluateon each registered policy. - If any policy returns
Stop, the hook emits aRunLifecycleUpdate::Donestate command with the stop code, which terminates the run. - If all policies return
Continue, the agent loop proceeds to the next step.
A policy with max or max_total set to 0 is treated as disabled and always returns Continue.
Common Errors
Section titled “Common Errors”| Error | Cause | Fix |
|---|---|---|
| Run never stops | No stop policy registered and LLM keeps calling tools | Register at least MaxRoundsPolicy or MaxRoundsPlugin |
StateError::KeyAlreadyRegistered | Both StopConditionPlugin and MaxRoundsPlugin registered | Use only one; they share the same state key |
| Timeout fires too early | TimeoutPolicy takes milliseconds, StopConditionSpec::Timeout takes seconds | When using TimeoutPolicy::new() directly, pass milliseconds |
Key Files
Section titled “Key Files”crates/awaken-runtime/src/policies/mod.rs— module root and public exportscrates/awaken-runtime/src/policies/policy.rs—StopPolicytrait, built-in policies,policies_from_specscrates/awaken-runtime/src/policies/plugin.rs—StopConditionPluginandMaxRoundsPlugincrates/awaken-runtime/src/policies/state.rs—StopConditionStatsStateand its state keycrates/awaken-runtime/src/policies/hook.rs— internalStopConditionHookthat drives evaluationcrates/awaken-runtime-contract/src/contract/lifecycle.rs—StopConditionSpecenum