#[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
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.max_iterations: usizeMaximum number of LLM→tool→LLM iterations before the loop is aborted.
max_parallel_tools: usizeMaximum 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: u64Per-tool execution timeout in milliseconds.
context_budget_chars: usizeMaximum 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: usizeNumber 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: usizeNumber 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
impl AgentConfig
Sourcepub 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>
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).Nonekeeps the built-in defaults, which cover browser tools and the read-only tools coding agents repeat (seeAgentConfig::default). -
max_observation_steps: Some(n)— clamped to[1, MAX_OBSERVATION_STEPS_CEILING].Nonekeeps the built-in default ofSome(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).
Sourcepub fn validated(self) -> Result<Self, AgentConfigError>
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
impl Clone for AgentConfig
Source§fn clone(&self) -> AgentConfig
fn clone(&self) -> AgentConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more