diff --git a/gitbooks/developing/architecture/agent-harness.md b/gitbooks/developing/architecture/agent-harness.md index cf3567db2..2ff52f850 100644 --- a/gitbooks/developing/architecture/agent-harness.md +++ b/gitbooks/developing/architecture/agent-harness.md @@ -167,6 +167,42 @@ When the orchestrator calls `spawn_subagent` (or one of the `delegate_*` conveni For tasks that don't need to block the orchestrator's turn, `spawn_worker_thread` runs the sub-agent in the background and the orchestrator continues immediately. +### Spawn hierarchy and tiers + +Not every agent is allowed to spawn every other agent. The harness models a three-tier hierarchy that mirrors the cost / latency / depth-of-thought split between models: + +```text +Chat (fast, UX-focused — e.g. orchestrator on `chat` hint) + │ + ├─► Worker ◄─── fast path: one delegation, leaf does the work + │ + └─► Reasoning (slow, deep-thinking — e.g. planner on `reasoning` hint) + │ + └─► Worker ◄─── deep path: reasoning decomposes, workers execute +``` + +Each `AgentDefinition` carries an `agent_tier` field (`chat` / `reasoning` / `worker`, default `worker`). The contract: + +| Tier | May spawn | Must NOT spawn | Typical members | +| ------------ | ----------------- | ---------------------------- | -------------------------------------------------------- | +| `chat` | `reasoning`, `worker` | another `chat` | `orchestrator` | +| `reasoning` | `worker` | another `reasoning`, any `chat` | `planner` (today the canonical one) | +| `worker` | nothing[^1] | anything | researcher, code_executor, critic, archivist, tool_maker, integrations_agent, … | + +[^1]: Skill-wildcard entries (`{ skills = "*" }`) are exempt because they collapse to a single `delegate_to_integrations_agent` tool whose target is a worker — they're a fan-out delegation surface, not a recursive spawn. + +**Why the rules.** +- *Chat → chat is meaningless.* The chat tier exists for snappy UX. A chat agent spawning another chat agent just doubles TTFT and burns tokens without buying any new capability. +- *Reasoning → reasoning blows up depth.* The reasoning tier is expensive. Chains of reasoning agents tend to re-decompose the same problem and create runaway hierarchies. +- *Worker → anything mixes execution and orchestration.* Workers are leaves so the parent always sees one compact result, not a transcript of nested delegations. + +**Enforcement.** Two layers: + +1. **Loader-time (static).** [`agents::loader::validate_tier_hierarchy`](../../../src/openhuman/agent/agents/loader.rs) runs over the merged registry (built-ins + workspace TOMLs) and refuses to boot a registry that lists a same-tier or worker-with-subagents entry. Built-in archetypes are checked at compile-test time; user-shipped TOMLs are checked at workspace load. +2. **Runtime depth gate (dynamic, planned).** Independent of tier, the sub-agent runner *will* cap total spawn chain depth at `MAX_SPAWN_DEPTH = 3` via a task-local counter incremented across `run_subagent`, surfaced as a new `SpawnDepthExceeded` agent error. This makes a user-shipped TOML that drops the tier annotation still unable to recurse past three hops. Tracked as the follow-up to the gap noted in `harness_gap_tests.rs`. + +> **Status:** the loader-time tier check and `agent_tier` field are live (this section). The runtime depth-counter task-local is *not yet implemented* — it is the planned defence-in-depth layer described above. Until it lands, depth is bounded only by the static loader contract plus the prompt-level rules in the orchestrator and planner agents. + ### Toolkit-specific specialists For Composio toolkits with hundreds of actions (GitHub alone has 500+), loading every action into the sub-agent's tool set balloons prompt size. The harness ranks the toolkit's actions against the parent-refined task prompt with a cheap CPU-only filter (verb detection, token overlap, verb-alignment boost) and only loads the top-ranked subset into the sub-agent. No model call, pure heuristic - fast and explainable. diff --git a/src/openhuman/agent/agents/loader.rs b/src/openhuman/agent/agents/loader.rs index b2bd73a4f..6f30f45fa 100644 --- a/src/openhuman/agent/agents/loader.rs +++ b/src/openhuman/agent/agents/loader.rs @@ -35,9 +35,10 @@ //! collision. use crate::openhuman::agent::harness::definition::{ - AgentDefinition, DefinitionSource, PromptBuilder, PromptSource, + AgentDefinition, AgentTier, DefinitionSource, PromptBuilder, PromptSource, SubagentEntry, }; use anyhow::{Context, Result}; +use std::collections::HashMap; /// A single built-in agent: its id plus the metadata TOML and a /// function-driven prompt builder. @@ -150,7 +151,92 @@ pub const BUILTINS: &[BuiltinAgent] = &[ /// baked into the binary and therefore must always be valid. Unit tests /// below keep that invariant honest. pub fn load_builtins() -> Result> { - BUILTINS.iter().map(parse_builtin).collect() + let defs: Vec = BUILTINS.iter().map(parse_builtin).collect::>()?; + validate_tier_hierarchy(&defs) + .context("built-in agents violate the spawn-hierarchy contract")?; + Ok(defs) +} + +/// Validate the cross-agent spawn-hierarchy contract documented on +/// [`AgentTier`]. +/// +/// Rules enforced here: +/// +/// * `Chat` agents MUST NOT list another `Chat` agent in `subagents`. +/// * `Reasoning` agents MUST NOT list another `Reasoning` agent in +/// `subagents`. +/// * `Worker` agents MUST NOT list any [`SubagentEntry::AgentId`] +/// entries. (Skill wildcards are allowed: they expand to the generic +/// `integrations_agent`, which is itself a `Worker`, and the call +/// happens via a single delegation tool rather than recursive spawn.) +/// +/// Skill-wildcard entries (`{ skills = "*" }`) are intentionally +/// untouched: they collapse to one `delegate_to_integrations_agent` +/// tool whose target is a `Worker` and whose use sites are well +/// understood. Mis-tiering of the `integrations_agent` itself is still +/// caught because it appears as a normal entry elsewhere. +/// +/// Called from [`load_builtins`] for the bundled archetype set and from +/// [`crate::openhuman::agent::harness::definition::AgentDefinitionRegistry::load`] +/// after workspace-local TOML overrides are merged, so custom user +/// agents that violate the contract fail the boot rather than crashing +/// at spawn time. +pub fn validate_tier_hierarchy(defs: &[AgentDefinition]) -> Result<()> { + let tier_by_id: HashMap<&str, AgentTier> = + defs.iter().map(|d| (d.id.as_str(), d.agent_tier)).collect(); + + for def in defs { + for entry in &def.subagents { + let child_id = match entry { + SubagentEntry::AgentId(id) => id.as_str(), + // Skill wildcards always route to `integrations_agent` + // (a Worker) via a single collapsed delegation tool — + // not subject to the tier-mismatch rule. + SubagentEntry::Skills(_) => continue, + }; + + // Worker leaves: no spawn surface at all. + if def.agent_tier == AgentTier::Worker { + anyhow::bail!( + "agent `{parent}` is a `worker` tier and must not list `{child}` (or any \ + agent) in its subagents — workers are leaf executors. Either remove the \ + entry or re-tier `{parent}` as `chat` / `reasoning`.", + parent = def.id, + child = child_id, + ); + } + + let Some(child_tier) = tier_by_id.get(child_id).copied() else { + // Unknown id — that's a separate `subagents` integrity + // concern (covered by existing tests / runtime spawn + // resolution); don't mask it as a tier error. + continue; + }; + + // Same-tier delegation is forbidden for chat and reasoning. + // (Chat→Chat would defeat the whole point of the fast tier; + // Reasoning→Reasoning produces a depth-blowing recursion of + // slow models.) + match (def.agent_tier, child_tier) { + (AgentTier::Chat, AgentTier::Chat) => anyhow::bail!( + "agent `{parent}` (chat) lists `{child}` (chat) in subagents — the chat tier \ + is a leaf in its own dimension. Hand off to a `reasoning` or `worker` agent \ + instead.", + parent = def.id, + child = child_id, + ), + (AgentTier::Reasoning, AgentTier::Reasoning) => anyhow::bail!( + "agent `{parent}` (reasoning) lists `{child}` (reasoning) in subagents — \ + reasoning agents compose downward into workers, not into each other.", + parent = def.id, + child = child_id, + ), + _ => {} + } + } + } + + Ok(()) } /// Parse a single [`BuiltinAgent`] triple into a finished [`AgentDefinition`]. @@ -713,4 +799,106 @@ mod tests { assert!(def.omit_identity); assert_eq!(def.max_iterations, 12); } + + // ───────────────────────────────────────────────────────────────────── + // Spawn-hierarchy contract + // ───────────────────────────────────────────────────────────────────── + + #[test] + fn orchestrator_is_chat_tier() { + assert_eq!(find("orchestrator").agent_tier, AgentTier::Chat); + } + + #[test] + fn planner_is_reasoning_tier() { + assert_eq!(find("planner").agent_tier, AgentTier::Reasoning); + } + + #[test] + fn other_builtins_default_to_worker_tier() { + for def in load_builtins().unwrap() { + if def.id == "orchestrator" || def.id == "planner" { + continue; + } + assert_eq!( + def.agent_tier, + AgentTier::Worker, + "{} should default to worker tier (only orchestrator/planner are non-worker today)", + def.id + ); + } + } + + #[test] + fn builtins_pass_tier_validation() { + // load_builtins() already calls validate_tier_hierarchy; this + // just makes the contract a named invariant in the test suite. + let defs = load_builtins().expect("built-ins must pass tier validation"); + validate_tier_hierarchy(&defs).expect("explicit re-check must pass"); + } + + #[test] + fn rejects_chat_to_chat_delegation() { + let mut defs = load_builtins().unwrap(); + // Add a synthetic second chat agent and have the orchestrator + // try to delegate to it. + let mut bad_chat = find("orchestrator"); + bad_chat.id = "second_orchestrator".to_string(); + defs.push(bad_chat); + let orch = defs.iter_mut().find(|d| d.id == "orchestrator").unwrap(); + orch.subagents + .push(SubagentEntry::AgentId("second_orchestrator".into())); + + let err = validate_tier_hierarchy(&defs).expect_err("chat→chat must be rejected"); + let msg = err.to_string(); + assert!( + msg.contains("chat") && msg.contains("leaf"), + "error should call out chat-tier leaf rule, got: {msg}" + ); + } + + #[test] + fn rejects_reasoning_to_reasoning_delegation() { + let mut defs = load_builtins().unwrap(); + let mut bad_reasoning = find("planner"); + bad_reasoning.id = "second_planner".to_string(); + defs.push(bad_reasoning); + let planner = defs.iter_mut().find(|d| d.id == "planner").unwrap(); + planner + .subagents + .push(SubagentEntry::AgentId("second_planner".into())); + + let err = validate_tier_hierarchy(&defs).expect_err("reasoning→reasoning must be rejected"); + assert!(err.to_string().contains("reasoning")); + } + + #[test] + fn rejects_worker_with_subagents() { + let mut defs = load_builtins().unwrap(); + let researcher = defs.iter_mut().find(|d| d.id == "researcher").unwrap(); + researcher + .subagents + .push(SubagentEntry::AgentId("critic".into())); + + let err = validate_tier_hierarchy(&defs) + .expect_err("worker with declared subagents must be rejected"); + let msg = err.to_string(); + assert!( + msg.contains("worker") && msg.contains("leaf"), + "error should call out worker leaf rule, got: {msg}" + ); + } + + #[test] + fn allows_skill_wildcards_on_any_non_worker_tier() { + // Skills wildcards collapse to delegate_to_integrations_agent + // and must not be policed by the tier check (it'd be a false + // positive — they fan out to a worker anyway). + let mut defs = load_builtins().unwrap(); + let planner = defs.iter_mut().find(|d| d.id == "planner").unwrap(); + planner.subagents.push(SubagentEntry::Skills( + crate::openhuman::agent::harness::definition::SkillsWildcard { skills: "*".into() }, + )); + validate_tier_hierarchy(&defs).expect("skill wildcards on reasoning tier must validate"); + } } diff --git a/src/openhuman/agent/agents/mod.rs b/src/openhuman/agent/agents/mod.rs index f87484434..523f977b8 100644 --- a/src/openhuman/agent/agents/mod.rs +++ b/src/openhuman/agent/agents/mod.rs @@ -22,4 +22,4 @@ pub mod trigger_reactor; pub mod trigger_triage; pub mod welcome; -pub use loader::{load_builtins, BuiltinAgent, BUILTINS}; +pub use loader::{load_builtins, validate_tier_hierarchy, BuiltinAgent, BUILTINS}; diff --git a/src/openhuman/agent/agents/orchestrator/agent.toml b/src/openhuman/agent/agents/orchestrator/agent.toml index 9129451ac..42a5bd11c 100644 --- a/src/openhuman/agent/agents/orchestrator/agent.toml +++ b/src/openhuman/agent/agents/orchestrator/agent.toml @@ -4,6 +4,13 @@ when_to_use = "Staff Engineer — routes, judges quality, synthesises. Never wri temperature = 0.4 max_iterations = 15 sandbox_mode = "none" + +# Spawn hierarchy: this is the user-facing fast tier (chat model hint). +# Loader enforces: a `chat` agent must NOT list any other `chat` agent +# in `subagents` below — handoff goes to `reasoning` (e.g. planner) for +# long-running deep work, or directly to `worker` specialists for the +# fast path. See `AgentTier` in `src/openhuman/agent/harness/definition.rs`. +agent_tier = "chat" omit_identity = true omit_memory_context = true omit_safety_preamble = true diff --git a/src/openhuman/agent/agents/orchestrator/prompt.md b/src/openhuman/agent/agents/orchestrator/prompt.md index 1743a1be7..c07e521d1 100644 --- a/src/openhuman/agent/agents/orchestrator/prompt.md +++ b/src/openhuman/agent/agents/orchestrator/prompt.md @@ -38,7 +38,9 @@ Default bias: **do not spawn a sub-agent when a direct response or direct tool c ## Rules -- **Never spawn yourself** — You cannot delegate to another Orchestrator. +- **You are the chat tier.** You run on a fast UX-focused model (TTFT > deep reasoning). When a task needs sustained multi-step thinking — planning across many steps, comparing several non-obvious options, untangling ambiguous requirements — **delegate to the reasoning tier (`delegate_plan`)** rather than reasoning through it yourself. Your job at that point is to brief the planner well and synthesise its output back to the user. +- **Never spawn yourself** — You cannot delegate to another chat-tier agent (Orchestrator or otherwise). The chat tier is a leaf in its own dimension. +- **Spawn hierarchy (hard rule).** Allowed handoffs from here: `chat → worker` (fast path) or `chat → reasoning → worker` (deep path). Never `chat → chat` and never `chat → reasoning → reasoning`. The loader rejects same-tier delegation at boot; a runtime depth gate capping chains at 3 hops is a planned follow-up — until it lands, this rule is enforced by you, by the planner's matching rule, and by the static loader check. - **Minimise sub-agents** — Use the fewest agents necessary. Simple questions don't need a DAG. - **Direct-first always** — First try direct reply or direct tools; delegate only when required by task complexity/capability gaps. - **Context is expensive** — Pass only relevant context to sub-agents, not everything. diff --git a/src/openhuman/agent/agents/planner/agent.toml b/src/openhuman/agent/agents/planner/agent.toml index 722948da2..efd26ce35 100644 --- a/src/openhuman/agent/agents/planner/agent.toml +++ b/src/openhuman/agent/agents/planner/agent.toml @@ -11,6 +11,12 @@ omit_memory_context = false omit_safety_preamble = true omit_skills_catalog = true +# Spawn hierarchy: this is the deep-thinking tier (reasoning model hint). +# The planner produces a DAG of leaf tasks for downstream workers; it +# must NOT delegate to another `reasoning` agent. The loader rejects +# any same-tier entry added to a future `subagents` list. +agent_tier = "reasoning" + [model] hint = "reasoning" diff --git a/src/openhuman/agent/agents/planner/prompt.md b/src/openhuman/agent/agents/planner/prompt.md index 26027729f..4620e280d 100644 --- a/src/openhuman/agent/agents/planner/prompt.md +++ b/src/openhuman/agent/agents/planner/prompt.md @@ -40,6 +40,7 @@ Return **only** valid JSON matching this schema: ## Rules +0. **You are the reasoning tier.** The chat-tier Orchestrator handed off to you because the task needs sustained thinking. Compose plans for the **worker tier** — `code_executor`, `researcher`, `critic`, `integrations_agent`, `archivist`. **Never delegate to another reasoning agent** (no planner-spawns-planner, no planner-spawns-orchestrator); the loader rejects this at boot, and the planned runtime depth gate will reject it at spawn time. If a single worker can't cover a node, split the node — don't smuggle a second reasoning hop in. 1. **Gather before planning** — Search memory and the web first. Don't guess what you can look up. 2. **Minimise tasks** — Use the fewest nodes needed. Don't over-decompose. 3. **Dependencies matter** — Use `depends_on` to express ordering. Independent tasks run in parallel. diff --git a/src/openhuman/agent/harness/builtin_definitions.rs b/src/openhuman/agent/harness/builtin_definitions.rs index 2819deeaf..fbb119c30 100644 --- a/src/openhuman/agent/harness/builtin_definitions.rs +++ b/src/openhuman/agent/harness/builtin_definitions.rs @@ -73,6 +73,7 @@ pub(crate) fn test_inherit_echo_def() -> AgentDefinition { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } } @@ -107,6 +108,7 @@ pub(crate) fn test_inherit_parallel_worker_def() -> AgentDefinition { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } } diff --git a/src/openhuman/agent/harness/definition.rs b/src/openhuman/agent/harness/definition.rs index 9548a3926..c1cc3a204 100644 --- a/src/openhuman/agent/harness/definition.rs +++ b/src/openhuman/agent/harness/definition.rs @@ -180,12 +180,90 @@ pub struct AgentDefinition { #[serde(default)] pub delegate_name: Option, + // ── spawn hierarchy ──────────────────────────────────────────────── + /// Tier this archetype occupies in the spawn hierarchy + /// (`chat` → `reasoning` → `worker`). Drives loader-time validation + /// of [`AgentDefinition::subagents`] and runtime depth gating in the + /// sub-agent runner. Defaults to [`AgentTier::Worker`] so existing + /// specialists fit the "leaf" role without per-file edits. + /// + /// **Hierarchy contract** (enforced by + /// [`super::super::agents::loader`] at registry build time): + /// + /// * `Chat` MUST NOT list another `Chat` agent in `subagents`. The + /// user-facing fast tier is a leaf in its own dimension — it + /// hands off to `Reasoning` or `Worker`, never to itself. + /// * `Reasoning` MUST NOT list another `Reasoning` agent in + /// `subagents`. Reasoning composes downward into `Worker`s. + /// * `Worker` MUST NOT list any subagents. Workers execute; they + /// do not orchestrate. + /// * `{ skills = "*" }` entries expand to the generic + /// `integrations_agent` (a `Worker`) so they are always allowed. + /// + /// Combined with the harness's `MAX_SPAWN_DEPTH = 3` task-local + /// gate, this means any execution chain bottoms out within three + /// hops: `chat → reasoning → worker` (or `chat → worker` for the + /// fast path). + #[serde(default)] + pub agent_tier: AgentTier, + // ── source bookkeeping ────────────────────────────────────────────── /// Tracks where the definition was loaded from (Builtin vs. File). #[serde(skip)] pub source: DefinitionSource, } +// ───────────────────────────────────────────────────────────────────────────── +// Agent tier (spawn hierarchy) +// ───────────────────────────────────────────────────────────────────────────── + +/// Role an agent plays in the spawn hierarchy. +/// +/// See [`AgentDefinition::agent_tier`] for the full contract. In short: +/// +/// ```text +/// Chat (fast, UX-focused) +/// └─► Reasoning (slow, deep-thinking) +/// └─► Worker (leaf executors) +/// └─► Worker (direct fast-path delegation) +/// ``` +/// +/// `Chat` and `Reasoning` are forbidden from spawning their own tier; +/// `Worker` is forbidden from spawning anything. Total depth is capped +/// at three hops by the harness regardless of tier (defence in depth +/// against custom TOMLs that drop the tier annotation). +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum AgentTier { + /// User-facing fast-tier agent (e.g. the Orchestrator on the + /// `chat` model hint). Optimised for TTFT, not for long-horizon + /// reasoning. May delegate to `Reasoning` or `Worker`; must NOT + /// delegate to another `Chat` agent. + Chat, + /// Deep-thinking agent on a `reasoning-v1`-style model (e.g. the + /// Planner). Decomposes long-running tasks and delegates execution + /// to one or more `Worker`s. Must NOT delegate to another + /// `Reasoning` agent. + Reasoning, + /// Leaf executor — researchers, code executors, critics, archivists, + /// integration specialists, etc. Workers do the actual work and must + /// NOT spawn further subagents (a `Worker` with a non-empty + /// `subagents` list is rejected by the loader). + #[default] + Worker, +} + +impl AgentTier { + /// Human-readable tier name used in error messages. + pub fn as_str(self) -> &'static str { + match self { + Self::Chat => "chat", + Self::Reasoning => "reasoning", + Self::Worker => "worker", + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // Subagent delegation entries // ───────────────────────────────────────────────────────────────────────────── @@ -489,6 +567,21 @@ impl AgentDefinitionRegistry { ); reg.insert(def); } + + // Re-validate the tier hierarchy after custom overrides are + // merged in — a workspace TOML can legally replace a built-in + // (same id) and is held to the same spawn-hierarchy contract + // as the bundled set. See + // [`super::super::agents::loader::validate_tier_hierarchy`]. + let snapshot: Vec = reg.list().into_iter().cloned().collect(); + super::super::agents::validate_tier_hierarchy(&snapshot).map_err(|e| { + anyhow::anyhow!( + "agent registry rejected after merging workspace overrides from {}: {}", + workspace.display(), + e + ) + })?; + Ok(reg) } diff --git a/src/openhuman/agent/harness/definition_tests.rs b/src/openhuman/agent/harness/definition_tests.rs index 9606f2f2b..51e592aac 100644 --- a/src/openhuman/agent/harness/definition_tests.rs +++ b/src/openhuman/agent/harness/definition_tests.rs @@ -25,6 +25,7 @@ fn make_def(id: &str) -> AgentDefinition { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } } diff --git a/src/openhuman/agent/harness/payload_summarizer.rs b/src/openhuman/agent/harness/payload_summarizer.rs index 5459b6390..c4209143e 100644 --- a/src/openhuman/agent/harness/payload_summarizer.rs +++ b/src/openhuman/agent/harness/payload_summarizer.rs @@ -365,6 +365,7 @@ mod tests { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } } diff --git a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs index 462af5b07..cc9c76576 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs @@ -26,6 +26,7 @@ fn make_def_named_tools(names: &[&str]) -> AgentDefinition { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: crate::openhuman::agent::harness::definition::DefinitionSource::Builtin, } } diff --git a/src/openhuman/channels/runtime/dispatch.rs b/src/openhuman/channels/runtime/dispatch.rs index 72763c934..fc0ea2691 100644 --- a/src/openhuman/channels/runtime/dispatch.rs +++ b/src/openhuman/channels/runtime/dispatch.rs @@ -599,6 +599,7 @@ mod scoping_tests { background: false, subagents: vec![], delegate_name: None, + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } } diff --git a/src/openhuman/tools/orchestrator_tools.rs b/src/openhuman/tools/orchestrator_tools.rs index fb34578a6..6cbf0bb46 100644 --- a/src/openhuman/tools/orchestrator_tools.rs +++ b/src/openhuman/tools/orchestrator_tools.rs @@ -280,6 +280,7 @@ mod tests { background: false, subagents: vec![], delegate_name: delegate_name.map(String::from), + agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, source: DefinitionSource::Builtin, } }