Skip to main content

AgentConfig

Struct AgentConfig 

Source
#[non_exhaustive]
pub struct AgentConfig { pub max_iterations: usize, pub max_parallel_tools: usize, pub tool_timeout_ms: u64, pub context_budget_chars: usize, pub max_repeated_batch_steps: Option<usize>, pub max_stagnation_steps: Option<usize>, pub prune_keep_tool_messages: usize, pub prune_keep_tail_messages: usize, pub observation_tools: Vec<String>, pub max_observation_steps: Option<usize>, }
Expand description

Configuration that governs a single agentic loop run.

All fields have sensible defaults via Default. These used to mirror TypeScript constants in the frontend’s own agent loop; that loop is backend-driven now, and streamAgentChat.ts records these fields as deliberately absent from it, so nothing in the frontend’s loop competes with the values here. What the settings UI still carries are the ceilings and floors it clamps user input to (MAX_PARALLEL_TOOLS_CEILING, TOOL_TIMEOUT_MS_FLOOR and friends), and its own copies of the two defaults a person can edit — MAX_TOOL_ITERATIONS, pinned against this file by settingsBounds.test.ts, and MAX_STAGNATION_STEPS, pinned by settingsParity.test.ts.

§Serialisation

AgentConfig is intentionally not Deserialize. External callers (HTTP, future config files) must go through a dedicated DTO that exposes only the safe subset of fields. This prevents accidental exposure of internal tuning knobs (pruning parameters, strike limits, etc.) to untrusted callers.

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§max_iterations: usize

Maximum number of LLM→tool→LLM iterations before the loop is aborted.

§max_parallel_tools: usize

Maximum number of tool calls that may be executed in parallel per iteration.

Dual-purpose: this value is used both as the Semaphore concurrency cap in tool_execution (limiting simultaneous in-flight calls) and as an upper bound on the batch size the model may request in a single turn. If the model emits more tool calls than this limit, the loop terminates with AgentError::ParallelToolLimitExceeded rather than silently serialising them. Setting this to 1 means the model may only request one tool call per turn; two calls in a single response will abort the loop, not run them sequentially.

§tool_timeout_ms: u64

Per-tool execution timeout in milliseconds.

§context_budget_chars: usize

Maximum total character budget across all messages before context pruning is applied.

§max_repeated_batch_steps: Option<usize>

Maximum number of times the same tool-call batch signature may repeat back to back and answered the same way before the loop is declared stuck and aborted with crate::ports::AgentError::LoopDetected.

Consecutive, not session-wide: a batch with a different signature resets the run. So does an answer that differs from the previous occurrence’s — the same call with a different result is progress that happens to look alike, and is not a strike. Contrast Self::max_stagnation_steps below, which is windowed and reads nothing but prose turns. See the loop_detection module docs for why the three differ.

Set to None to disable loop detection entirely (useful in tests that deliberately repeat the same tool call).

§max_stagnation_steps: Option<usize>

Occurrence limit for identical assistant text within a sliding window, before the loop is considered stagnant and aborted with crate::ports::AgentError::StagnationDetected.

Semantics: Occurrences of the same response text are counted within the last max_stagnation_steps × 4 recorded turns. The error fires when that count exceeds max_stagnation_steps. With the default value of 5, stagnation triggers on the sixth identical occurrence. With max_stagnation_steps = 0, the error fires on the very first occurrence of any repeated text.

Only turns that called no tools are recorded: narration alongside a tool call is not stagnation, and the loop detector judges that work. A → B → A → B oscillation in the prose is still caught. See ADR 0011.

Set to None to disable stagnation detection entirely (useful in tests that return a fixed LLM response across many iterations).

§prune_keep_tool_messages: usize

Number of most-recent tool-result messages preserved during the first pass of context pruning.

Not exposed as a user-facing option because the value is calibrated to balance context retention against token budget; changing it independently of context_budget_chars can produce incoherent conversation histories.

§prune_keep_tail_messages: usize

Number of non-system messages retained during the emergency tail-prune pass (second pass of context pruning).

Same rationale as Self::prune_keep_tool_messages.

§observation_tools: Vec<String>

Substring patterns used to classify tools as exploratory.

“Exploratory” tools are those that drive progress by repeatedly querying or traversing a stateful source — page snapshots, navigation, clicks, file reads, directory listings, API pagination calls, etc. Their repeated invocation with identical arguments is a legitimate ReAct pattern, not a stuck loop.

A tool call whose lowercased name contains any pattern in this list as a substring is classified as exploratory, so a short pattern like "read" also covers thread_create. When every call in a batch matches, Self::max_observation_steps is applied as the loop detection threshold instead of Self::max_repeated_batch_steps.

Matching semantics — substring rather than exact string — are intentional: MCP servers routinely prepend namespace prefixes to tool names (e.g. playwright_mcp_browser_snapshot), so exact matching would require users to enumerate every vendor variant. The pattern "navigate" matches browser_navigate, db_navigate, fs_navigate, etc.

BYO-MCP: users connecting custom MCP servers should extend or replace this list via AgentConfig::from_user_params to include their own exploratory tool name fragments (e.g. "get_dom", "fetch_page", "list_dir").

An empty list means no tools are ever classified as exploratory; the standard Self::max_repeated_batch_steps threshold applies to all batches.

Defaults cover both agent families gglib serves: browser/GUI tools (snapshot, screenshot, read_page, navigate, click) and the read-only tools coding agents repeat constantly (read_file, list_dir, grep_search, search_files, …). See the Default impl for why every entry is a full tool name rather than a fragment.

§max_observation_steps: Option<usize>

Maximum number of times an exploratory-tool-only batch may repeat back to back and be answered the same way before loop detection fires.

Applied instead of Self::max_repeated_batch_steps when every tool call in the current batch matches a pattern in Self::observation_tools. A higher value (default: 15) gives the agent room to browse multiple pages, walk directory trees, or paginate through API results while still aborting an agent that repeats one identical batch back to back before it exhausts the token budget.

Mixed batches (at least one non-exploratory tool alongside an exploratory one) always fall back to Self::max_repeated_batch_steps — the conservative choice.

Clamped to MAX_OBSERVATION_STEPS_CEILING when supplied via AgentConfig::from_user_params to prevent API callers from providing a value large enough to neutralise the guard.

§It is also the ceiling on rescued repeats

A repeat whose answer changed is not a strike — an agent polling a build for output repeats a batch every turn and is working. That reset would, on its own, exempt any tool whose output carries a clock or a counter, so a batch may be carried by changing answers only while its run stays inside this allowance — unless it is exploratory and free to repeat. The number is reused rather than invented: an exploratory batch already gets it because repeating a call which changes nothing is free, and there is no measurement behind a second number.

So this field is read twice for a mutating batch, and for an exploratory one that costs something elsewhere — never as a strike threshold, always as the ceiling on a moving answer. See ADR 0010.

Set to None to disable the elevated threshold entirely; exploratory batches then use Self::max_repeated_batch_steps like any other batch, and the rescue ceiling collapses onto that too — which leaves the verdict exactly as it was before it read answers at all.

Default: Some(15).

Implementations§

Source§

impl AgentConfig

Source

pub fn from_user_params( max_iterations: Option<usize>, max_parallel_tools: Option<usize>, tool_timeout_ms: Option<u64>, observation_tools: Option<Vec<String>>, max_observation_steps: Option<usize>, max_stagnation_steps: Option<usize>, ) -> Result<Self, AgentConfigError>

Build an AgentConfig from user-supplied overrides.

Each Some numeric value is clamped to the safe [floor, ceiling] range before assignment; None fields retain their Default values. The result is validated before returning.

This is the single entry-point for HTTP, Tauri, and CLI callers, eliminating duplicated clamping logic at every call site.

§Observation-tool parameters
  • observation_tools: Some(vec)replaces the default pattern list entirely. Pass the complete list you want, including any defaults you wish to preserve. Some(vec![]) disables observation classification (standard threshold applies to all batches). None keeps the built-in defaults, which cover browser tools and the read-only tools coding agents repeat (see AgentConfig::default).

  • max_observation_steps: Some(n) — clamped to [1, MAX_OBSERVATION_STEPS_CEILING]. None keeps the built-in default of Some(15).

§Stagnation parameter

max_stagnation_steps: Some(n) — clamped to [1, MAX_STAGNATION_STEPS_CEILING]; None keeps the built-in default of Some(DEFAULT_MAX_STAGNATION_STEPS). The floor is 1 because a value of 0 would abort on the first occurrence of any response. Callers pass the persisted Settings::max_stagnation_steps here; it is deliberately not exposed per-request.

§Errors

Returns Err(AgentConfigError) if the clamped config violates any invariant (defense-in-depth — should never happen given the clamping).

Source

pub fn validated(self) -> Result<Self, AgentConfigError>

Validate all fields that could cause the agent loop to malfunction.

Call this after constructing an AgentConfig from untrusted input. The Default implementation is always valid; this acts as a safety net for values assembled by HTTP DTOs or CLI argument parsing.

§Errors

Returns Err(AgentConfigError) if any field violates its invariant.

Trait Implementations§

Source§

impl Clone for AgentConfig

Source§

fn clone(&self) -> AgentConfig

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AgentConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for AgentConfig

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Serialize for AgentConfig

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more