Skip to content

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.

  • awaken crate added to Cargo.toml
  • Familiarity with Plugin and AgentRuntimeBuilder

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:

PolicyTriggers when
MaxRoundsPolicyStep count exceeds max rounds
TokenBudgetPolicyTotal tokens (input + output) exceed max_total
TimeoutPolicyElapsed wall time exceeds max_ms milliseconds
ConsecutiveErrorsPolicyConsecutive inference errors reach max
  1. 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)),
];
  1. Register a StopConditionPlugin with 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.

  1. Use declarative StopConditionSpec values.

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.

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.

Every policy receives a StopPolicyStats snapshot with fields populated by the internal StopConditionHook:

FieldTypeDescription
step_countu32Number of inference steps completed so far
total_input_tokensu64Cumulative prompt tokens across all steps
total_output_tokensu64Cumulative completion tokens across all steps
elapsed_msu64Wall time since the first step, in milliseconds
consecutive_errorsu32Current streak of consecutive inference errors (resets on success)
last_tool_namesVec<String>Tool names called in the most recent inference response
last_response_textStringText content of the most recent inference response
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”
  1. The StopConditionPlugin registers a PhaseHook on Phase::AfterInference.
  2. After each LLM inference, the hook increments step_count, accumulates token usage, and tracks consecutive errors in StopConditionStatsState.
  3. The hook builds a StopPolicyStats snapshot and calls evaluate on each registered policy.
  4. If any policy returns Stop, the hook emits a RunLifecycleUpdate::Done state command with the stop code, which terminates the run.
  5. 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.

ErrorCauseFix
Run never stopsNo stop policy registered and LLM keeps calling toolsRegister at least MaxRoundsPolicy or MaxRoundsPlugin
StateError::KeyAlreadyRegisteredBoth StopConditionPlugin and MaxRoundsPlugin registeredUse only one; they share the same state key
Timeout fires too earlyTimeoutPolicy takes milliseconds, StopConditionSpec::Timeout takes secondsWhen using TimeoutPolicy::new() directly, pass milliseconds
  • crates/awaken-runtime/src/policies/mod.rs — module root and public exports
  • crates/awaken-runtime/src/policies/policy.rsStopPolicy trait, built-in policies, policies_from_specs
  • crates/awaken-runtime/src/policies/plugin.rsStopConditionPlugin and MaxRoundsPlugin
  • crates/awaken-runtime/src/policies/state.rsStopConditionStatsState and its state key
  • crates/awaken-runtime/src/policies/hook.rs — internal StopConditionHook that drives evaluation
  • crates/awaken-runtime-contract/src/contract/lifecycle.rsStopConditionSpec enum