mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(agent): codify chat → reasoning → worker spawn hierarchy (#2026)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<Vec<AgentDefinition>> {
|
||||
BUILTINS.iter().map(parse_builtin).collect()
|
||||
let defs: Vec<AgentDefinition> = BUILTINS.iter().map(parse_builtin).collect::<Result<_>>()?;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,12 +180,90 @@ pub struct AgentDefinition {
|
||||
#[serde(default)]
|
||||
pub delegate_name: Option<String>,
|
||||
|
||||
// ── 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<AgentDefinition> = 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user