{"record":{"id":"7216bffc4324da7e","repo":"tinyhumansai/openhuman","slug":"agent-parent-ptier-lists-child-ctier","errorCode":null,"errorMessage":"agent `{parent}` ({ptier}) lists `{child}` ({ctier}) in subagents — {reason}","messagePattern":"agent `(.+?)` \\((.+?)\\) lists `(.+?)` \\((.+?)\\) in subagents — (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/agent/registry/agents/loader.rs","lineNumber":433,"sourceCode":"                );\n            }\n\n            let Some(child_tier) = tier_by_id.get(child_id).copied() else {\n                // Unknown id — that's a separate `subagents` integrity\n                // concern (covered by existing tests / runtime spawn\n                // resolution); don't mask it as a tier error.\n                continue;\n            };\n\n            // Same-tier delegation is forbidden for chat and reasoning.\n            // (Chat→Chat would defeat the whole point of the fast tier;\n            // Reasoning→Reasoning produces a depth-blowing recursion of\n            // slow models.) The pair-rule lives in `validate_tier_transition`\n            // (the single source of truth shared with the runtime spawn gate\n            // in `run_subagent`); here we wrap its reason with the offending\n            // agent ids + tiers for a boot-time-friendly diagnostic.\n            if let Err(reason) = validate_tier_transition(def.agent_tier, child_tier) {\n                anyhow::bail!(\n                    \"agent `{parent}` ({ptier}) lists `{child}` ({ctier}) in subagents — {reason}\",\n                    parent = def.id,\n                    ptier = def.agent_tier.as_str(),\n                    child = child_id,\n                    ctier = child_tier.as_str(),\n                );\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Parse a single [`BuiltinAgent`] triple into a finished [`AgentDefinition`].\nfn parse_builtin(b: &BuiltinAgent) -> Result<AgentDefinition> {\n    // The TOML ships without `system_prompt` — serde falls back to\n    // `defaults::empty_inline_prompt` — and the loader injects the\n    // rendered sibling `prompt.md` immediately below.","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/agent/registry/agents/loader.rs#L415-L451","documentation":"Boot-time validation in the builtin-agent registry loader (`validate_tier_hierarchy`). Every agent definition's `subagents` list is resolved and each pair is checked against `validate_tier_transition` (src/openhuman/agent/harness/definition.rs:381). Forbidden pairs: Worker→anything (workers are leaf executors), Chat→Chat (would defeat the fast tier), and Reasoning→Reasoning (a depth-blowing recursion of slow models). The bail wraps the rule's reason with the offending parent/child ids and tiers for a boot-friendly diagnostic; upward hops like reasoning→chat are intentionally legal.","triggerScenarios":"Loading agent definitions (core boot or registry reload) where an agent TOML lists a subagent whose `agent_tier` forms a forbidden pair with the parent: any subagent under a `worker`-tier agent, a `chat`-tier agent listing another `chat`-tier agent, or a `reasoning`-tier agent listing another `reasoning`-tier agent. Also fires when a `subagents` id resolves to a different agent than intended (e.g. after a rename or tier change elsewhere).","commonSituations":"Editing agent.toml (orchestrator or custom agents) to add subagents; duplicating an existing agent entry and forgetting to change `agent_tier`; adding a new builtin chat agent and pointing an existing chat parent at it; renames that make a stale subagents id resolve to an agent with an incompatible tier.","solutions":["Change the child's `agent_tier` so the pair is legal: chat parents delegate to `reasoning` or `worker` children, reasoning parents compose downward into `worker` (or the intentional `chat`) children, and workers have no subagents at all.","Remove the offending id from the parent's `subagents` list if the delegation is not actually needed.","If the delegation is genuinely required and same-tier is intentional, change the rule itself in `validate_tier_transition` (the single source of truth shared with the runtime spawn gate in `run_subagent`) — bypassing the loader check is pointless because the runtime gate enforces the same function."],"exampleFix":"# before (agent.toml) — chat-tier parent lists a chat-tier child\n[id]\nagent_tier = \"chat\"\nsubagents = [\"quick_chat_agent\"]\n\n# after — delegate downward instead\n[id]\nagent_tier = \"chat\"\nsubagents = [\"research_agent\"]   # agent_tier = \"reasoning\"","handlingStrategy":"validation","validationCode":"// Pre-validate every declared subagents pair before shipping agent TOMLs\nuse openhuman::agent::harness::definition::{validate_tier_transition, AgentTier};\n\nfn hierarchy_is_valid(pairs: &[(String, AgentTier, String, AgentTier)]) -> Result<(), String> {\n    for (parent, ptier, child, ctier) in pairs {\n        validate_tier_transition(*ptier, *ctier)\n            .map_err(|reason| format!(\"`{parent}` -> `{child}`: {reason}\"))?;\n    }\n    Ok(())\n}","typeGuard":"fn allows_subagent(parent: AgentTier, child: AgentTier) -> bool {\n    validate_tier_transition(parent, child).is_ok()\n}","tryCatchPattern":null,"preventionTips":["Run agent-TOML tier validation in CI so a bad subagents pair fails the build instead of boot","Keep worker-tier agents leaf — never add subagents to them","Remember upward hops like reasoning→chat are intentional and legal; only worker-parent and same-tier chat/reasoning pairs are rejected","Change the rule in validate_tier_transition itself — the runtime spawn gate in run_subagent enforces the same function, so bypassing the loader gains nothing"],"tags":["agent-registry","tier-validation","subagents","boot-failure","toml-config"],"backgroundTag":"invalid-config-value","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}