mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(agent): apply iteration turn cap only to short-running agents (#3056)
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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': 'الأصل',
|
||||
|
||||
@@ -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': 'মূল',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'मूल',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '상위',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Родительский',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user