feat(agent): apply iteration turn cap only to short-running agents (#3056)

This commit is contained in:
Steven Enamakel
2026-05-30 21:27:40 -07:00
committed by GitHub
parent 18435f11d6
commit daa0a13f2e
47 changed files with 202 additions and 15 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ function activityForEntry(entry: ToolTimelineEntry): string {
if (subagent.childIteration) {
return subagent.childMaxIterations
? `Iteration ${subagent.childIteration}/${subagent.childMaxIterations}`
: `Iteration ${subagent.childIteration}`;
: `Step ${subagent.childIteration}`;
}
if (entry.detail?.trim()) {
+1
View File
@@ -2433,6 +2433,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.updateFailed': 'ولم يتمكن من استكمال المهمة؛ ولم يتم توفير التغييرات.',
'conversations.toolTimeline.turn': 'دور',
'conversations.toolTimeline.step': 'خطوة',
'conversations.toolTimeline.workerThread': 'محادثة عامل',
'conversations.subagent.viewProcessing': 'عرض المعالجة الكاملة',
'conversations.subagent.parent': 'الأصل',
+1
View File
@@ -2480,6 +2480,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.updateFailed': 'কাজটি সংরক্ষণ করা যায়নি।',
'conversations.toolTimeline.turn': 'টার্ন',
'conversations.toolTimeline.step': 'ধাপ',
'conversations.toolTimeline.workerThread': 'ওয়ার্কার থ্রেড',
'conversations.subagent.viewProcessing': 'সম্পূর্ণ প্রক্রিয়া দেখুন',
'conversations.subagent.parent': 'মূল',
+1
View File
@@ -2544,6 +2544,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
'Aufgabe konnte nicht aktualisiert werden; Änderungen wurden nicht gespeichert.',
'conversations.toolTimeline.turn': 'drehen',
'conversations.toolTimeline.step': 'Schritt',
'conversations.toolTimeline.workerThread': 'Worker-Thread',
'conversations.subagent.viewProcessing': 'Vollständige Verarbeitung anzeigen',
'conversations.subagent.parent': 'Übergeordnet',
+1
View File
@@ -2739,6 +2739,7 @@ const en: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
'conversations.toolTimeline.turn': 'turn',
'conversations.toolTimeline.step': 'step',
'conversations.toolTimeline.workerThread': 'worker thread',
'conversations.subagent.viewProcessing': 'View full processing',
'conversations.subagent.parent': 'Parent',
+1
View File
@@ -2527,6 +2527,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
'No se pudo actualizar la tarea; los cambios no se guardaron.',
'conversations.toolTimeline.turn': 'turno',
'conversations.toolTimeline.step': 'Paso',
'conversations.toolTimeline.workerThread': 'hilo de worker',
'conversations.subagent.viewProcessing': 'Ver procesamiento completo',
'conversations.subagent.parent': 'Principal',
+1
View File
@@ -2536,6 +2536,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
"Impossible de mettre à jour la tâche; les modifications n'ont pas été enregistrées.",
'conversations.toolTimeline.turn': 'tour',
'conversations.toolTimeline.step': 'Étape',
'conversations.toolTimeline.workerThread': 'fil worker',
'conversations.subagent.viewProcessing': 'Voir le traitement complet',
'conversations.subagent.parent': 'Parent',
+1
View File
@@ -2484,6 +2484,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.updateFailed': 'कार्य अद्यतन नहीं कर सका; परिवर्तन बचाया नहीं गया।',
'conversations.toolTimeline.turn': 'टर्न',
'conversations.toolTimeline.step': 'चरण',
'conversations.toolTimeline.workerThread': 'वर्कर थ्रेड',
'conversations.subagent.viewProcessing': 'पूरी प्रोसेसिंग देखें',
'conversations.subagent.parent': 'मूल',
+1
View File
@@ -2486,6 +2486,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Hapus',
'conversations.taskKanban.updateFailed': 'Tak bisa memutakhirkan tugas; perubahan tak disimpan.',
'conversations.toolTimeline.turn': 'giliran',
'conversations.toolTimeline.step': 'Langkah',
'conversations.toolTimeline.workerThread': 'thread worker',
'conversations.subagent.viewProcessing': 'Lihat proses lengkap',
'conversations.subagent.parent': 'Induk',
+1
View File
@@ -2518,6 +2518,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
"Impossibile aggiornare l'attività; le modifiche non sono state salvate.",
'conversations.toolTimeline.turn': 'turno',
'conversations.toolTimeline.step': 'Passo',
'conversations.toolTimeline.workerThread': 'thread worker',
'conversations.subagent.viewProcessing': 'Visualizza elaborazione completa',
'conversations.subagent.parent': 'Principale',
+1
View File
@@ -2461,6 +2461,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
'작업을 업데이트할 수 없어 변경 사항이 저장되지 않았습니다.',
'conversations.toolTimeline.turn': '턴',
'conversations.toolTimeline.step': '단계',
'conversations.toolTimeline.workerThread': '워커 스레드',
'conversations.subagent.viewProcessing': '전체 처리 과정 보기',
'conversations.subagent.parent': '상위',
+1
View File
@@ -2513,6 +2513,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
'Nie udało się zaktualizować zadania; zmian nie zapisano.',
'conversations.toolTimeline.turn': 'tura',
'conversations.toolTimeline.step': 'Krok',
'conversations.toolTimeline.workerThread': 'wątek workera',
'conversations.subagent.viewProcessing': 'Zobacz pełne przetwarzanie',
'conversations.subagent.parent': 'Nadrzędny',
+1
View File
@@ -2525,6 +2525,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.updateFailed':
'Não foi possível atualizar a tarefa; as alterações não foram salvas.',
'conversations.toolTimeline.turn': 'turno',
'conversations.toolTimeline.step': 'Passo',
'conversations.toolTimeline.workerThread': 'thread de worker',
'conversations.subagent.viewProcessing': 'Ver processamento completo',
'conversations.subagent.parent': 'Principal',
+1
View File
@@ -2498,6 +2498,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': 'Удалить',
'conversations.taskKanban.updateFailed': 'Не удалось обновить задачу; изменения не сохранились.',
'conversations.toolTimeline.turn': 'ход',
'conversations.toolTimeline.step': 'Шаг',
'conversations.toolTimeline.workerThread': 'чат воркера',
'conversations.subagent.viewProcessing': 'Посмотреть весь процесс',
'conversations.subagent.parent': 'Родительский',
+1
View File
@@ -2361,6 +2361,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.deleteCard': '删除',
'conversations.taskKanban.updateFailed': '无法更新任务;更改未保存。',
'conversations.toolTimeline.turn': '轮次',
'conversations.toolTimeline.step': '步骤',
'conversations.toolTimeline.workerThread': '工作线程',
'conversations.subagent.viewProcessing': '查看完整处理过程',
'conversations.subagent.parent': '父级',
@@ -197,10 +197,11 @@ export function SubagentDrawer({
<span className={`rounded-full px-1.5 py-0.5 ${tone.pill}`}>
{t(`conversations.subagent.${tone.label}`)}
</span>
{subagent.childIteration != null && subagent.childMaxIterations != null ? (
{subagent.childIteration != null ? (
<span>
{t('conversations.toolTimeline.turn')} {subagent.childIteration}/
{subagent.childMaxIterations}
{subagent.childMaxIterations != null
? `${t('conversations.toolTimeline.turn')} ${subagent.childIteration}/${subagent.childMaxIterations}`
: `${t('conversations.toolTimeline.step')} ${subagent.childIteration}`}
</span>
) : subagent.iterations != null ? (
<span>
@@ -51,10 +51,14 @@ export function SubagentActivityBlock({
const headerBits: string[] = [];
if (subagent.mode) headerBits.push(subagent.mode);
if (subagent.dedicatedThread) headerBits.push(t('conversations.toolTimeline.workerThread'));
if (subagent.childIteration != null && subagent.childMaxIterations != null) {
headerBits.push(
`${t('conversations.toolTimeline.turn')} ${subagent.childIteration}/${subagent.childMaxIterations}`
);
if (subagent.childIteration != null) {
if (subagent.childMaxIterations != null) {
headerBits.push(
`${t('conversations.toolTimeline.turn')} ${subagent.childIteration}/${subagent.childMaxIterations}`
);
} else {
headerBits.push(`${t('conversations.toolTimeline.step')} ${subagent.childIteration}`);
}
} else if (subagent.iterations != null) {
headerBits.push(
subagent.iterations === 1
@@ -36,6 +36,17 @@ describe('SubagentActivityBlock', () => {
expect(block.textContent).toContain('turn 2/5');
});
it('renders "step N" when childMaxIterations is null (extended policy)', () => {
renderInStore(
<SubagentActivityBlock
subagent={{ taskId: 't', agentId: 'code_executor', childIteration: 7, toolCalls: [] }}
/>
);
const block = screen.getByTestId('subagent-activity');
expect(block.textContent).toContain('step 7');
expect(block.textContent).not.toContain('/');
});
it('renders final-run statistics on a completed sub-agent', () => {
renderInStore(
<SubagentActivityBlock
@@ -67,6 +67,7 @@ pub(crate) fn test_inherit_echo_def() -> AgentDefinition {
skill_filter: None,
extra_tools: vec![],
max_iterations: 3,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -102,6 +103,7 @@ pub(crate) fn test_inherit_parallel_worker_def() -> AgentDefinition {
skill_filter: None,
extra_tools: vec![],
max_iterations: 6,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
+44
View File
@@ -25,6 +25,30 @@ use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Iteration-cap policy for a sub-agent.
///
/// Controls how the harness enforces [`AgentDefinition::max_iterations`]:
///
/// * **Strict** — hard-fail at `max_iterations` (the current default).
/// Right for short-running agents (summarizer, triage) where hitting
/// the cap signals a likely loop.
/// * **Extended** — the per-agent `max_iterations` is replaced at runtime
/// by a higher harness-wide constant
/// ([`EXTENDED_MAX_TOOL_ITERATIONS`](super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS))
/// so the agent can complete realistic multi-tool workflows. The
/// repeated-failure circuit breaker and cost budget still apply. The
/// UI omits the denominator ("step N" instead of "turn N/M") to avoid
/// a misleading terminal countdown.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum IterationPolicy {
/// Hard cap at `max_iterations`. Default for most agents.
#[default]
Strict,
/// Raised cap for multi-step specialists. Guards still apply.
Extended,
}
// ─────────────────────────────────────────────────────────────────────────────
// Agent definition
// ─────────────────────────────────────────────────────────────────────────────
@@ -127,6 +151,12 @@ pub struct AgentDefinition {
#[serde(default = "defaults::max_iterations")]
pub max_iterations: usize,
/// Iteration-cap policy. See [`IterationPolicy`] for semantics.
/// Defaults to [`IterationPolicy::Strict`]; long-running specialists
/// set `iteration_policy = "extended"` in their `agent.toml`.
#[serde(default)]
pub iteration_policy: IterationPolicy,
/// Maximum character length for this sub-agent's output before the
/// harness truncates it before feeding it back as a tool result to the
/// parent. `None` means no cap (the default for most agents). Set to
@@ -317,6 +347,20 @@ impl AgentDefinition {
pub fn display_name(&self) -> &str {
self.display_name.as_deref().unwrap_or(&self.id)
}
/// Effective iteration cap after applying [`IterationPolicy`].
///
/// * `Strict` → `self.max_iterations` unchanged.
/// * `Extended` → the higher of `self.max_iterations` and the
/// harness-wide [`EXTENDED_MAX_TOOL_ITERATIONS`](super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS).
pub fn effective_max_iterations(&self) -> usize {
match self.iteration_policy {
IterationPolicy::Strict => self.max_iterations,
IterationPolicy::Extended => self
.max_iterations
.max(super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS),
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -19,6 +19,7 @@ fn make_def(id: &str) -> AgentDefinition {
skill_filter: None,
extra_tools: vec![],
max_iterations: 8,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -178,3 +179,67 @@ fn skills_wildcard_only_star_matches_all() {
};
assert!(!specific.matches_all());
}
// ── iteration policy ─────────────────────────────────────────────
#[test]
fn strict_policy_returns_max_iterations_unchanged() {
let mut def = make_def("summarizer");
def.max_iterations = 2;
def.iteration_policy = IterationPolicy::Strict;
assert_eq!(def.effective_max_iterations(), 2);
}
#[test]
fn extended_policy_raises_cap_to_at_least_extended_constant() {
let mut def = make_def("code_executor");
def.max_iterations = 10;
def.iteration_policy = IterationPolicy::Extended;
assert_eq!(
def.effective_max_iterations(),
super::super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS
);
assert!(def.effective_max_iterations() > def.max_iterations);
}
#[test]
fn extended_policy_preserves_custom_cap_when_higher_than_constant() {
let mut def = make_def("custom_agent");
def.max_iterations = 100;
def.iteration_policy = IterationPolicy::Extended;
assert_eq!(def.effective_max_iterations(), 100);
}
#[test]
fn iteration_policy_defaults_to_strict() {
let def = make_def("test");
assert_eq!(def.iteration_policy, IterationPolicy::Strict);
}
#[test]
fn iteration_policy_parses_from_toml() {
let toml_src = r#"
id = "code_executor"
when_to_use = "Runs code"
max_iterations = 10
iteration_policy = "extended"
"#;
let def: AgentDefinition = toml::from_str(toml_src).expect("toml parse");
assert_eq!(def.iteration_policy, IterationPolicy::Extended);
assert_eq!(
def.effective_max_iterations(),
super::super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS
);
}
#[test]
fn iteration_policy_omitted_defaults_strict() {
let toml_src = r#"
id = "summarizer"
when_to_use = "Summarizes"
max_iterations = 1
"#;
let def: AgentDefinition = toml::from_str(toml_src).expect("toml parse");
assert_eq!(def.iteration_policy, IterationPolicy::Strict);
assert_eq!(def.effective_max_iterations(), 1);
}
@@ -184,6 +184,7 @@ pub(crate) struct SubagentProgress {
pub sink: Option<tokio::sync::mpsc::Sender<AgentProgress>>,
pub agent_id: String,
pub task_id: String,
pub extended_policy: bool,
}
#[async_trait]
@@ -196,6 +197,7 @@ impl ProgressReporter for SubagentProgress {
task_id: self.task_id.clone(),
iteration,
max_iterations,
extended_policy: self.extended_policy,
})
.await;
}
@@ -359,6 +359,7 @@ mod tests {
skill_filter: None,
extra_tools: vec![],
max_iterations: 1,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -23,7 +23,9 @@ use super::tool_prep::{
load_prompt_source, top_k_for_toolkit,
};
use super::types::{SubagentMode, SubagentRunError, SubagentRunOptions, SubagentRunOutcome};
use crate::openhuman::agent::harness::definition::{AgentDefinition, PromptSource};
use crate::openhuman::agent::harness::definition::{
AgentDefinition, IterationPolicy, PromptSource,
};
use crate::openhuman::agent::harness::{
current_spawn_depth, with_current_sandbox_mode, with_spawn_depth, MAX_SPAWN_DEPTH,
};
@@ -976,7 +978,8 @@ async fn run_typed_mode(
agent_id = %definition.id,
model = %model,
tool_count = allowed_names.len(),
max_iterations = definition.max_iterations,
max_iterations = definition.effective_max_iterations(),
iteration_policy = ?definition.iteration_policy,
"[subagent_runner:typed] resolved configuration"
);
@@ -1172,12 +1175,13 @@ async fn run_typed_mode(
lazy_resolver,
&model,
temperature,
definition.max_iterations,
definition.effective_max_iterations(),
task_id,
&definition.id,
options.worker_thread_id.clone(),
handoff_cache.as_deref(),
parent,
definition.iteration_policy == IterationPolicy::Extended,
)
.await?;
@@ -1231,6 +1235,7 @@ async fn run_inner_loop(
worker_thread_id: Option<String>,
handoff_cache: Option<&ResultHandoffCache>,
parent: &ParentExecutionContext,
extended_policy: bool,
) -> Result<(String, usize, AggregatedUsage), SubagentRunError> {
// An autonomous skill run (set via `with_autonomous_iter_cap`) lifts the
// per-agent cap so sub-agents run until done / the circuit breaker trips.
@@ -1333,6 +1338,7 @@ async fn run_inner_loop(
sink: parent.on_progress.clone(),
agent_id: agent_id.to_string(),
task_id: task_id.to_string(),
extended_policy,
};
let parser = super::super::engine::DefaultParser;
@@ -58,6 +58,7 @@ fn make_def_named_tools(names: &[&str]) -> AgentDefinition {
skill_filter: None,
extra_tools: vec![],
max_iterations: 5,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: crate::openhuman::agent::harness::definition::SandboxMode::None,
+7
View File
@@ -14,6 +14,13 @@ pub(crate) const STREAM_CHUNK_MIN_CHARS: usize = 80;
/// Used as a safe fallback when `max_tool_iterations` is unset or configured as zero.
pub(crate) const DEFAULT_MAX_TOOL_ITERATIONS: usize = 10;
/// Extended iteration cap for agents with `IterationPolicy::Extended`. These
/// are multi-step specialists (code executor, integrations, planner, …) whose
/// realistic workflows commonly exceed the default 10-iteration cap. The
/// repeated-failure circuit breaker and cost budget remain the primary runaway
/// guards; this value is intentionally generous to avoid premature stops.
pub(crate) const EXTENDED_MAX_TOOL_ITERATIONS: usize = 50;
/// Repeated-failure circuit breaker. The plain iteration cap lets an agent grind
/// the same dead-end (e.g. re-running `pip install` when there is no pip) until
/// `max_iterations`, then return an opaque `MaxIterationsExceeded` that the caller
+3
View File
@@ -113,6 +113,9 @@ pub enum AgentProgress {
iteration: u32,
/// Maximum iterations configured for this child run.
max_iterations: u32,
/// `true` when the agent uses [`IterationPolicy::Extended`](crate::openhuman::agent::harness::definition::IterationPolicy::Extended).
/// The UI uses this to show "step N" instead of "turn N/M".
extended_policy: bool,
},
/// A sub-agent is about to execute a tool. Distinct from
@@ -4,6 +4,7 @@ delegate_name = "run_code"
when_to_use = "Code-repo worker — owns the FULL lifecycle of any task scoped to a code repository: clone, navigate via `codegraph_search` / `grep` / `lsp`, read files, edit (`edit` / `apply_patch` / `file_write`), build, run tests/lint, and drive **local `git`** (branch / commit / push / diff) for the working tree. **GitHub state I/O (issues, PRs, comments, reviews, checks, labels) goes through `composio_execute` with the matching `GITHUB_*` tool — never `gh` CLI** (see code_executor prompt.md 'GitHub I/O' section for the full split rule and rationale). Use for ANY repo-scoped work — locating where to edit, investigating a bug, exploring a codebase, and any modify / build / test / git / push / PR step — not only for the literal 'writing code' moment. Keep the entire end-to-end flow inside one `delegate_run_code` call so the worker accumulates context across steps."
temperature = 0.4
max_iterations = 10
iteration_policy = "extended"
max_result_chars = 16000
sandbox_mode = "sandboxed"
omit_identity = true
@@ -3,6 +3,7 @@ display_name = "Integrations Agent"
when_to_use = "Service integration specialist — drives a SINGLE Composio toolkit per spawn (gmail, notion, github, slack, …). The `toolkit` argument is mandatory. Use when a task should be completed via a managed OAuth integration rather than raw HTTP / file I/O."
temperature = 0.4
max_iterations = 10
iteration_policy = "extended"
sandbox_mode = "none"
omit_identity = true
omit_memory_context = true
@@ -4,6 +4,7 @@ delegate_name = "setup_mcp_server"
when_to_use = "Walks the user through installing and connecting an MCP server end-to-end. Use when the user asks to add / install / set up an MCP server (e.g. \"set up the Notion MCP\", \"connect the GitHub MCP server\"). Owns the full flow: search registries → ask the user for any required secrets via a native dialog (raw values never enter the agent context) → test the connection → commit the install."
temperature = 0.3
max_iterations = 12
iteration_policy = "extended"
sandbox_mode = "none"
omit_identity = true
omit_memory_context = true
@@ -4,6 +4,7 @@ delegate_name = "plan"
when_to_use = "Architect — break a complex task into a small DAG of subtasks with explicit acceptance criteria. Reads memory and searches the web to ground plans in real context. Read-only; produces JSON, not code."
temperature = 0.4
max_iterations = 8
iteration_policy = "extended"
max_result_chars = 8000
sandbox_mode = "read_only"
omit_identity = true
@@ -4,6 +4,7 @@ delegate_name = "research"
when_to_use = "Web & docs crawler — reads real documentation, compresses to dense markdown. Use for any task that requires looking up external knowledge."
temperature = 0.4
max_iterations = 8
iteration_policy = "extended"
max_result_chars = 8000
sandbox_mode = "none"
omit_identity = true
@@ -4,6 +4,7 @@ delegate_name = "create_skill"
when_to_use = "JavaScript skill/runtime specialist — creates or updates OpenHuman SKILL.md packages, Node-backed JS helpers, and tool-facing JavaScript code meant to be executed by the core's Node runtime bridge or by other agents."
temperature = 0.3
max_iterations = 10
iteration_policy = "extended"
max_result_chars = 16000
sandbox_mode = "sandboxed"
omit_identity = true
@@ -3,6 +3,7 @@ display_name = "Tools Agent"
when_to_use = "Generalist for heavyweight ad-hoc execution that does NOT touch a code repository — host shell, HTTP/web fetch, web search, memory helpers, file READS (`file_read` / `grep` / `glob`). Lacks `edit` / `apply_patch` / `file_write` / `git_operations` / `codegraph_search` — do **not** use for any task scoped to a code repo (cloning, locating, modifying, building, testing, git, push, PR); those route to `delegate_run_code` end-to-end. Do not use for managed Composio OAuth integrations either — those route to `integrations_agent` with a `toolkit` argument."
temperature = 0.4
max_iterations = 10
iteration_policy = "extended"
sandbox_mode = "none"
omit_identity = true
omit_memory_context = true
+11 -4
View File
@@ -1228,21 +1228,28 @@ fn spawn_progress_bridge(
task_id,
iteration,
max_iterations,
extended_policy,
} => {
publish_web_channel_event(WebChannelEvent {
event: "subagent_iteration_start".to_string(),
client_id: client_id.clone(),
thread_id: thread_id.clone(),
request_id: request_id.clone(),
message: Some(format!(
"Sub-agent '{agent_id}' iteration {iteration}/{max_iterations}"
)),
message: Some(if extended_policy {
format!("Sub-agent '{agent_id}' step {iteration}")
} else {
format!("Sub-agent '{agent_id}' iteration {iteration}/{max_iterations}")
}),
tool_name: Some(agent_id),
skill_id: Some(task_id),
round: Some(round),
subagent: Some(SubagentProgressDetail {
child_iteration: Some(iteration),
child_max_iterations: Some(max_iterations),
child_max_iterations: if extended_policy {
None
} else {
Some(max_iterations)
},
..Default::default()
}),
..Default::default()
@@ -463,6 +463,7 @@ mod scoping_tests {
skill_filter: None,
extra_tools: vec![],
max_iterations: 8,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -274,6 +274,7 @@ mod tests {
skill_filter: None,
extra_tools: vec![],
max_iterations: 8,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -248,6 +248,7 @@ fn definition(max_iterations: usize) -> AgentDefinition {
skill_filter: None,
extra_tools: Vec::new(),
max_iterations,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -356,6 +356,7 @@ fn definition(max_result_chars: Option<usize>) -> AgentDefinition {
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 2,
iteration_policy: Default::default(),
max_result_chars,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
+1
View File
@@ -273,6 +273,7 @@ fn coverage_definition() -> AgentDefinition {
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 3,
iteration_policy: Default::default(),
max_result_chars: Some(18),
timeout_secs: None,
sandbox_mode: SandboxMode::ReadOnly,
@@ -289,6 +289,7 @@ fn integrations_definition() -> AgentDefinition {
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 4,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -254,6 +254,7 @@ fn definition(prompt: PromptSource) -> AgentDefinition {
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 3,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -1002,6 +1002,7 @@ fn definition(
skill_filter: None,
extra_tools: Vec::new(),
max_iterations,
iteration_policy: Default::default(),
max_result_chars,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
@@ -1570,6 +1570,7 @@ named = ["todo", "plan_exit"]
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 8,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,
+1
View File
@@ -3189,6 +3189,7 @@ fn turn_state_mirror_persists_progress_edges_from_public_events() {
task_id: "task-1".into(),
iteration: 1,
max_iterations: 3,
extended_policy: false,
}));
assert!(!mirror.observe(&AgentProgress::SubagentToolCallStarted {
agent_id: "researcher".into(),
@@ -392,6 +392,7 @@ fn agent_definition(id: &str, max_result_chars: Option<usize>) -> AgentDefinitio
skill_filter: None,
extra_tools: Vec::new(),
max_iterations: 2,
iteration_policy: Default::default(),
max_result_chars,
timeout_secs: None,
sandbox_mode: SandboxMode::ReadOnly,
@@ -288,6 +288,7 @@ fn coverage_agent_definition(
skill_filter: None,
extra_tools: vec![],
max_iterations: 8,
iteration_policy: Default::default(),
max_result_chars: None,
timeout_secs: None,
sandbox_mode: SandboxMode::None,