Date: 2026-03-25
Scope: Full codebase audit of atn/ runtime, focusing on orchestrator vs. child agent divergences
Goal: Produce a refactoring plan to make every agent, from root orchestrator to deepest sub-agent, behave identically
- Defined in
orchestrator/__init__.py:360viacreate_orchestrator_agent(). - Returns a pipeline-mode
AgentDefinitionwith a single cognitive step configured withtool_executors: "orchestrator". - Hardcoded ID:
ORCHESTRATOR_ID = "orchestrator"(orchestrator/__init__.py:14). - Registered via
runtime.setup_orchestrator()(runtime.py:2712), which also injects a status briefing and activates it. - Model aliases resolved inline (
orchestrator/__init__.py:382-388). - Provider is a fallback chain:
[primary, "claude_max", "anthropic", "gemini"](orchestrator/__init__.py:400-404). - System prompt is the 318-line
_DEFAULT_SYSTEM_PROMPTwith{user_md_path}substitution.
- Created via
_create_agent()inorchestrator/tools.py:894whenmode == "cognitive". - Returns a cognitive-mode
AgentDefinition(no steps list, usesAgentMode.COGNITIVE). - ID is auto-generated hierarchically via
runtime.generate_child_id(parent_id)→ e.g.,"orchestrator.1". parent_idderived fromcaller_id(injected via_caller_idinexecute_tool).- System prompt generated by
build_delegate_prompt()indelegate_prompts.py. - Also registered in
DelegateRegistryfor UI observability. - Provider stored in
defn.provider(string or list), resolved at execution time.
The orchestrator is AgentMode.PIPELINE with a cognitive step. Regular cognitive agents are AgentMode.COGNITIVE. This is the root structural divergence: it causes two completely different execution paths (_execute_pipeline vs _execute_cognitive_agent).
trigger_run()→defn.mode == PIPELINE→_execute_pipeline()(runtime.py:601).- Pipeline drains inbox, iterates through steps (just one cognitive step).
- The cognitive step goes to
CognitiveStepExecutor.execute()(steps/cognitive.py:108). - Because
tool_executors == "orchestrator"andprovider.supports_orchestrate, routes to_orchestrate()(steps/cognitive.py:268). _orchestrate()callsprovider.send_orchestrate()with a_tool_executorclosure.- The tool executor closure calls
execute_tool(name, input, runtime, caller_id=context.agent_id)(steps/cognitive.py:601). - Result recorded via
StepResultin the pipeline's step_results. - Conversation history managed via
runtime.conversation(the global orchestrator ConversationStore). - Session continuity: BridgeProvider
_session_idenables SDK resume across turns.
trigger_run()→defn.mode == COGNITIVE→_execute_cognitive_agent()(runtime.py:838).- This is a completely separate 320-line method that duplicates much of the pipeline logic.
- Resolves provider via
_resolve_provider_for_model()(runtime.py:1487). - Creates a new BridgeProvider instance per execution (not shared).
- Builds system prompt, drains inbox, builds user message, all inline.
- Creates its own
_tool_executorclosure (runtime.py:979) that routes throughexecute_tool. - Creates its own
_on_chunkcallback for output streaming (runtime.py:994). - Calls
sub_provider.send_orchestrate()directly (bypassing CognitiveStepExecutor entirely). - Handles result, token usage, delegate registry sync, parent notification, all inline.
- Conversation history managed via per-agent
ConversationStore(runtime.get_agent_conversation_store()). - History injected into system prompt as text (no SDK session resume for children).
| Concern | Orchestrator | Cognitive Agent |
|---|---|---|
| Entry point | _execute_pipeline → CognitiveStepExecutor → _orchestrate |
_execute_cognitive_agent (monolithic) |
| Provider lifecycle | Shared singleton in CognitiveStepExecutor._providers |
Fresh instance per execution in _active_providers |
| Tool executor | _route_tool_call in cognitive.py |
Inline closure in runtime.py |
| Streaming | Via _make_event_emitters in cognitive.py |
Inline _on_chunk in runtime.py |
| Conversation store | Global runtime.conversation |
Per-agent _agent_conversations[id] |
| Session resume | Via BridgeProvider _session_id |
History text appended to system prompt |
| Completion notification | Pipeline finally block emits events |
_on_agent_completed() posts inbox messages |
| Failure propagation | _notify_parent_of_failure() (only in pipeline finally) |
_notify_parent_of_failure() (in cognitive finally) |
| Delegate registry sync | Not registered in DelegateRegistry | Syncs status to DelegateRegistry |
| Token tracking | Via _accumulate_usage from step output |
Inline in _execute_cognitive_agent |
- LLM emits tool call → bridge relays to
_tool_executorin_orchestrate()(cognitive.py:302). _route_tool_call()checks connector prefix first, then callsexecute_tool()(cognitive.py:582-603).caller_id=context.agent_idis passed; this was recently fixed at line 601.execute_tool()injects_caller_idinto tool input dict (tools.py:2176).- Full orchestrator tool surface: 40+ tools (
_TOOLSintools.py:52-697).
- LLM emits tool call → bridge relays to
_tool_executorclosure in_execute_cognitive_agent()(runtime.py:979). - Shell tool check first (
_SHELL_TOOL_EXECUTORS), then connector prefix check, thenexecute_tool(). caller_id=defn.idis passed correctly.- Scoped tool surface: only
_DELEGATE_TOOL_NAMES(9 tools) via_get_delegate_tools()(tools.py:1853).
- Orchestrator:
_route_tool_call()→execute_tool()(clean abstraction in cognitive.py) - Child agents: inline closure in runtime.py with shell tool handling mixed in
- Non-bridge providers get
_SHELL_TOOLSappended for children but NOT for orchestrator (bridge providers have built-in file/bash tools)
- Provider chain defined at agent creation (
orchestrator/__init__.py:400):[primary, "claude_max", "anthropic", "gemini"]. CognitiveStepExecutor._resolve_provider_chain()picks the first available from the chain.- Fallback is automatic with retry logic in
_orchestrate()andexecute(). - Provider is a shared singleton registered in
cognitive._providers["claude_max"]. - Model can be changed via
set_orchestrator_model()which re-registers the agent.
- Model stored in
defn.provider/defn.cognitive_model. _resolve_provider_for_model()(runtime.py:1487) creates a new provider instance per execution:gemini-*→ newOpenAICompatibleProvidergpt-*/o1-*/o3-*→ newOpenAICompatibleProvider- Default → new
BridgeProvider(model=model_name)
- No fallback chain: if the selected provider fails, the agent fails.
- Provider stored in
_active_providers[agent_id]during execution, cleaned up after.
- Orchestrator has automatic provider fallback; children don't.
- Orchestrator shares a long-lived provider; children create disposable instances.
- Inbox drain happens in
_execute_pipeline()→self.inbox.drain(defn.id)(runtime.py:625). - Work messages fed to cognitive step as
{inbox}template substitution (cognitive.py:494-519). - Conversation history prepended to inbox text (for non-bridge providers) via
context.runtime.conversation.get_history_for_prompt(). - HIGH/URGENT messages wake orchestrator via
_inbox_watcher_loop().
- Inbox drain in
_execute_cognitive_agent()(runtime.py:910). - Messages assembled into
user_messageinline. - Conversation history appended to system prompt (not user message) via sliding window.
- Same watcher loop triggers wake on HIGH/URGENT.
- Orchestrator: history in user message (via
{inbox}substitution) or bridge SDK resume. - Children: history in system prompt (bloats system prompt over time).
- Receives
child_completedWORK messages in inbox from_on_agent_completed()(runtime.py:1290). - If actively running, receives direct injection via
send_user_message()on its BridgeProvider. - Receives
child_errorALERT messages from_notify_parent_of_failure()(runtime.py:1227).
- Same
_on_agent_completed()mechanism works identically. - Same
_notify_parent_of_failure()mechanism works identically. - Provider lookup checks both
resolved_parentand rawparent_id(handles "orch" alias).
Divergence: Minimal. Notification flow is one of the best-unified areas. The "orch" vs "orchestrator" alias handling (_resolve_parent_agent_id) is a wart but functional.
- Can have heartbeat set via
update_agenttool. - Scheduled via
_heartbeat_table/_schedule_tablejust like any agent. - Planning review timer is orchestrator-specific (
_post_planning_review(),runtime.py:1672).
- Heartbeat configured at creation via
create_agent(not yet exposed in schema, butHeartbeatConfigexists in models). _scheduler_loop()handles heartbeat identically for all agents.- Heartbeat posts WORK message to agent's own inbox, triggering a new execution.
Divergence: Minimal. Scheduling is well-unified. Planning review is orchestrator-specific by design.
- Uses
runtime.conversation, a globalConversationStoreatdata_dir/conversations/. - History recorded by
_orchestrate():runtime.conversation.add_assistant_turn()(cognitive.py:367). - User messages recorded externally (by CLI/WebSocket handler posting to inbox).
- Supports session archival and reset via
new_conversation().
- Each agent gets its own
ConversationStoreatdata_dir/agents/<id>/conversations/. - Created lazily via
get_agent_conversation_store()(runtime.py:2831). - User turn recorded in
_execute_cognitive_agent()before execution. - Assistant turn recorded in
_execute_cognitive_agent()after execution. - No session archival/reset mechanism.
Divergence: Separate persistence paths. Same ConversationStore class, but different locations, different recording points, and different lifecycle management.
- Uses the shared
BridgeProviderinstance fromcognitive._providers["claude_max"]. - Bridge subprocess is long-lived: spawned lazily on first request, kept alive.
- Session ID persists across orchestration turns (SDK resume).
_clear_bridge_session()resets session stats on conversation reset.- Interrupt via
interrupt_orchestrator()→cognitive._providers["claude_max"].interrupt().
- Each execution creates a new
BridgeProviderinstance (runtime.py:872). - Bridge subprocess spawned per execution, closed in
finallyblock (runtime.py:1141-1144). - No session resume: each execution is a fresh SDK session.
- Interrupt via
interrupt_delegate()→_active_providers[agent_id].interrupt().
- Orchestrator: persistent singleton process, session resume across turns.
- Children: ephemeral process per execution, no session resume.
- This means children pay cold-start cost on every execution (bun startup, SDK init).
The orchestrator is defined as mode=PIPELINE with one cognitive step. This was likely an early design choice when the framework only had pipeline mode. The cognitive step executor (CognitiveStepExecutor) handles the LLM call, but the surrounding pipeline machinery provides:
- Step iteration (only 1 step, so trivially wraps)
- Step-level event emission (STEP_STARTED, STEP_COMPLETED)
- Step result recording
- Token usage extraction from step output
- StepContext construction with all services
When mode=COGNITIVE was added for child agents, _execute_cognitive_agent() was written as a parallel implementation that bypasses the pipeline/step machinery entirely. It duplicates:
- Inbox draining
- Connector startup
- Provider setup
- Tool executor construction
- Streaming callback setup
- Result processing
- Token tracking
- Status bookkeeping
- Event emission
- Cleanup
This duplication is the core problem. The orchestrator's execution path goes through 3 clean layers (pipeline → step executor → _orchestrate()), while children use 1 monolithic method that copies logic from all 3.
| # | Divergence | Location | Impact |
|---|---|---|---|
| A1 | Two execution paths: pipeline+cognitive-step vs monolithic _execute_cognitive_agent |
runtime.py:598-601 (dispatch), runtime.py:838-1177 (cognitive path) |
Root cause of most other divergences. ~320 lines of duplicated logic. |
| A2 | Provider lifecycle: shared singleton vs ephemeral per-execution | runtime.py:872 (creates new), runtime.py:1141 (closes after) |
Children pay cold-start cost every execution. No session resume. |
| A3 | Tool routing: _route_tool_call() vs inline closure with shell-tool mixin |
cognitive.py:582 vs runtime.py:979 |
Two code paths doing the same thing. Shell tools only available in one. |
| A4 | Conversation history injection: user message vs system prompt | cognitive.py:513-518 vs runtime.py:937-974 |
Different approaches to the same problem. System prompt approach bloats over time. |
| A5 | Streaming callback: _make_event_emitters() vs inline _on_chunk |
cognitive.py:394 vs runtime.py:994 |
Children's output goes to delegate log; orchestrator's goes to step events. |
| A6 | Token tracking: _accumulate_usage() from step output vs inline |
runtime.py:3078 vs runtime.py:1044-1051 |
Different accumulation paths for the same data. |
| A7 | DelegateRegistry sync: orchestrator not registered, children are | tools.py:948-956 |
UI sees children in delegate tree but not orchestrator. |
| A8 | Provider fallback: orchestrator has chain, children don't | orchestrator/__init__.py:400 vs runtime.py:872 |
Children are fragile: single provider failure = agent failure. |
| A9 | Mode flag mismatch: orchestrator is PIPELINE, functionally acts as COGNITIVE | orchestrator/__init__.py:419 |
Confusing: snapshot shows mode: pipeline for the orchestrator. |
| A10 | Completion events: pipeline emits after finally, cognitive emits in finally | runtime.py:810-828 vs runtime.py:1159-1177 |
Different ordering of cleanup and event emission. |
| # | Divergence | Justification |
|---|---|---|
| N1 | Orchestrator cannot be unregistered (runtime.py:503) |
Root agent must always exist. |
| N2 | Full tool surface: orchestrator gets 40+ tools, children get 9 | Children shouldn't be able to manage connectors, providers, planning, etc. |
| N3 | Global conversation store: orchestrator conversation is the "chat" UI | User interacts with orchestrator via the main chat; agent conversations are separate windows. |
| N4 | Status briefing injection (runtime.py:2930) |
Only orchestrator needs initial fleet status. |
| N5 | Planning review messages (runtime.py:1672) |
Only orchestrator handles planning. |
| N6 | Model selector in UI: set_orchestrator_model() |
Orchestrator model is a global setting. Children have their own model config. |
| # | Capability | Current State |
|---|---|---|
| M1 | Provider fallback chain | Children get a single provider; should support fallback like orchestrator. |
| M2 | Session resume | Children's BridgeProvider is ephemeral; long-running agents should persist sessions. |
| M3 | Heartbeat in create_agent schema | HeartbeatConfig exists but create_agent tool schema doesn't expose it clearly. |
| M4 | Shell tools for bridge providers | Bridge children get SDK-native tools but can't use the simpler shell tool abstraction for non-bridge fallback. |
| M5 | Conversation reset | No way to reset a child agent's conversation without removing and recreating it. |
An agent at any level of the hierarchy MUST behave identically in terms of execution lifecycle, tool routing, inbox handling, and notification flow. The only differences are in configuration (tool surface, system prompt, provider) not in mechanism.
- R-EXEC-1: All cognitive agents (including orchestrator) MUST be dispatched through the same execution method.
- R-EXEC-2: The execution method MUST support both fresh and resumed sessions.
- R-EXEC-3: Completion and failure MUST trigger the same notification pathway for all agents.
- R-EXEC-4: Cleanup (provider close, status update, event emission) MUST follow the same order for all agents.
- R-TOOL-1: All cognitive agents MUST route tool calls through the same function.
- R-TOOL-2:
caller_idMUST be passed for every tool call from every agent. - R-TOOL-3: Tool surface MUST be configurable per agent (not per execution path).
- R-TOOL-4: Shell tools for non-bridge providers MUST be handled uniformly.
- R-PROV-1: Provider selection MUST support fallback chains for all agents.
- R-PROV-2: Provider lifecycle (shared vs ephemeral) MUST be a configuration choice, not a code path difference.
- R-PROV-3: Session resume MUST be available to any agent using BridgeProvider.
- R-INBOX-1: Inbox drain and message assembly MUST use the same code for all agents.
- R-INBOX-2: Conversation history injection MUST use the same strategy for all agents.
- R-INBOX-3: Parent notification on completion MUST work identically regardless of agent level.
- R-CONV-1: Every cognitive agent MUST have a ConversationStore.
- R-CONV-2: The orchestrator's ConversationStore MUST be a standard agent ConversationStore (possibly with additional UI integration).
- R-CONV-3: Session archival/reset MUST be available for any agent.
- R-REG-1: All cognitive agents MUST be registered with
mode=COGNITIVEin AgentDefinition. - R-REG-2: Tool surface MUST be specified in AgentDefinition configuration (not derived from execution path).
The refactoring has one central move: make the orchestrator a cognitive-mode agent and route all cognitive agents through a single unified execution path. The _execute_cognitive_agent() method becomes the ONE way any cognitive agent runs, with configuration (tool surface, system prompt, provider chain) driving the differences.
What: Change create_orchestrator_agent() to return mode=COGNITIVE instead of mode=PIPELINE with a cognitive step.
Files:
orchestrator/__init__.py: ChangeAgentDefinitionto useAgentMode.COGNITIVE, populateprovider,cognitive_model,system_prompt,max_turns, etc.runtime.py:598-601: The dispatch intrigger_run()will now route orchestrator to_execute_cognitive_agent().
Details:
- Move step config fields (
provider,model,system,max_turns,tool_executors) to cognitive-mode fields. - Provider fallback chain →
defn.provider(already supportsstr | list[str]). tool_executors: "orchestrator"→defn.tools = ["atn_full"](or similar flag).
Risk: Low. The change is in definition construction. The execution path change is the critical part (Step 2).
Dependencies: None (can be done first).
What: Enhance _execute_cognitive_agent() to handle orchestrator-specific needs, then remove the orchestrator's pipeline execution path.
Files:
runtime.py:838-1177: Enhance to support:- Provider fallback chain (from
defn.providerwhen it's a list) - Full vs scoped tool surface (from
defn.toolsconfig) - Shared vs ephemeral provider lifecycle (from new config flag)
- Orchestrator-specific conversation recording (to
runtime.conversation)
- Provider fallback chain (from
Key changes:
- Provider selection: Replace
_resolve_provider_for_model()with logic that also supports fallback chains. Whendefn.provideris a list, try each in order. - Tool surface: Replace hardcoded
_get_delegate_tools()with a configurable tool list:"atn_full"→ all orchestrator tools"atn_core"→ the current 9 delegate tools- Custom lists via
defn.tools
- Provider lifecycle: If
defn.id == ORCHESTRATOR_ID(or a newpersistent_session: boolflag), reuse provider from_active_providersinstead of creating a new one. - Conversation: Record to both per-agent store AND
runtime.conversationwhendefn.id == ORCHESTRATOR_ID.
Risk: Medium. This is the core change. Must preserve orchestrator's session resume, conversation continuity, and interrupt behavior. Integration test coverage needed.
Dependencies: Step 1 (orchestrator must be cognitive-mode).
What: Simplify _execute_pipeline() now that cognitive-mode agents never go through it.
Files:
runtime.py:613-833: Remove theif defn.mode == AgentMode.COGNITIVEbranch fromtrigger_run()(it's now the only path for cognitive agents). Pipeline execution remains for actual pipeline agents.steps/cognitive.py:_orchestrate()is still used by pipeline agents with cognitive steps, but no longer by the orchestrator.
Risk: Low. Pipeline agents with cognitive steps still work via CognitiveStepExecutor. The orchestrator just doesn't go through this path anymore.
Dependencies: Steps 1 and 2 (orchestrator must be fully migrated).
What: Consolidate the two tool executor closures into one shared function.
Files:
runtime.py:979-991: Extract_tool_executorclosure into a proper method.steps/cognitive.py:302-306:_route_tool_callalready does this; unify with the runtime closure.
Proposed function:
async def route_tool_call(
self, name: str, tool_input: dict, agent_id: str
) -> dict:
"""Universal tool router for all cognitive agents."""
# Shell tools (non-bridge providers)
if name in _SHELL_TOOL_EXECUTORS:
return await _SHELL_TOOL_EXECUTORS[name](tool_input)
# Connector tools (mcp_ prefix)
if self.connectors:
parsed = self.connectors.parse_tool_name(name)
if parsed:
cid, tool_name = parsed
return await self.connectors.call_tool(cid, tool_name, tool_input)
# Framework tools
return await execute_tool(name, tool_input, self, caller_id=agent_id)Move to runtime.py as a method on Runtime. Both execution paths call it.
Risk: Low. Pure refactor, same logic.
Dependencies: Step 2 (but can be done in parallel).
What: Support defn.provider as a fallback chain (list of strings) for cognitive agents.
Files:
runtime.py:866-872: Enhance_resolve_provider_for_model()to accept a list and try each.orchestrator/tools.py:924: When creating cognitive agents, optionally build a fallback chain.
Risk: Low. Additive feature, no breaking changes.
Dependencies: Step 2.
What: Remove all print(f"[DEBUG]...") statements.
Files and lines:
| File | Line | Statement |
|---|---|---|
runtime.py |
990 | print(f"[DEBUG] _tool_executor: ...") |
runtime.py |
1318 | print(f"[DEBUG] _on_agent_completed: ...") |
runtime.py |
1320 | print(f"[DEBUG] _on_agent_completed: NO parent_id...") |
orchestrator/tools.py |
939 | print(f"[DEBUG] create_agent: ...") |
orchestrator/tools.py |
2177 | print(f"[DEBUG] execute_tool: ...") |
Risk: None. These are development artifacts.
Dependencies: None (do anytime).
What: Remove or clean up code that becomes unnecessary after unification.
Candidates:
_execute_cognitive_agent()duplication: after Step 2, the old pipeline-as-orchestrator path is dead.interrupt_orchestrator()(runtime.py:1466-1481): special method to find orchestrator's provider. After unification,interrupt_delegate()works for all agents including orchestrator._get_bridge_provider()(runtime.py:1528-1545): special method with orchestrator vs delegate branching. After unification, just check_active_providers._clear_bridge_session()(runtime.py:2915-2929): reaches intocognitive._providers["claude_max"]. After unification, operates on_active_providers[ORCHESTRATOR_ID]._resolve_parent_agent_id()"orch" alias (runtime.py:1183-1194): the "orch" shorthand predates the unified agent registry. After cleanup, all agents use full IDs.DelegateRegistry.generate_child_id()duplication: bothDelegateRegistryandRuntimehavegenerate_child_id()with identical logic (agent_registry.py:98andruntime.py:1385). Consolidate to one.
Risk: Low. Each removal is small and testable.
Dependencies: Steps 1-4 (most dead code is created by the unification).
Phase A (Preparation, can be done immediately):
Step 6: Remove debug prints
Step 7 (partial): Remove generate_child_id duplication
Phase B (Core unification):
Step 1: Change orchestrator to cognitive mode
Step 2: Enhance _execute_cognitive_agent for all agents
Step 4: Unify tool routing
Phase C (Cleanup):
Step 3: Simplify pipeline execution
Step 5: Provider fallback for children
Step 7 (remainder): Dead code removal
Phase D (Enhancement):
M2: Session resume for persistent children
M3: Heartbeat in create_agent schema
M5: Conversation reset for any agent
| Step | Risk | Impact if wrong | Mitigation |
|---|---|---|---|
| 1 | LOW | Orchestrator won't start | Simple revert, just agent definition change |
| 2 | MEDIUM | Orchestrator or all cognitive agents break | Feature-flag: keep both paths, switch via config |
| 3 | LOW | Pipeline agents with cognitive steps break | Only touch dispatch, not pipeline logic |
| 4 | LOW | Tool calls fail | Keep old closures as fallback during transition |
| 5 | LOW | No downside; additive | n/a |
| 6 | NONE | n/a | n/a |
| 7 | LOW | Reference errors | Run full test suite after each removal |
| File | Lines | Role |
|---|---|---|
runtime.py |
3125 | Central runtime: execution, scheduling, providers, lifecycle |
orchestrator/tools.py |
2183 | Tool definitions + executors for LLM tool calls |
orchestrator/__init__.py |
433 | Orchestrator agent definition + system prompt |
steps/cognitive.py |
649 | CognitiveStepExecutor: LLM call abstraction |
providers/bridge.py |
842 | Claude Max bridge subprocess provider |
providers/base.py |
292 | Abstract provider interface |
models.py |
331 | Core data models (AgentDefinition, ExecutionRecord, etc.) |
agent_registry.py |
278 | DelegateRegistry: hierarchy tracking |
inbox.py |
87 | InboxManager: message queues |
conversation.py |
287 | ConversationStore: persistent conversation history |
delegate_prompts.py |
262 | System prompt builder for delegate sub-agents |
tool_registry.py |
389 | Unified tool registry (connectors + pipeline tools) |
steps/base.py |
50 | StepContext and StepExecutor base class |
See Step 6 above for the complete list (5 statements across 2 files).
The codebase uses both "orch" and "orchestrator" as IDs for the root agent:
ORCHESTRATOR_ID = "orchestrator"(the canonical ID in the agent registry)"orch"used as parent_id shorthand in delegate generation (generate_child_id("orch")→"orch.1")_resolve_parent_agent_id()maps"orch"→"orchestrator"at runtimeget_children()checks both mappings
This creates a split-brain: children's parent_id says "orch" but the registry key is "orchestrator". The resolution function papers over it, but it's a source of subtle bugs (e.g., provider lookup for parent injection checks both).
Recommendation: After unification, standardize on "orchestrator" everywhere. Change generate_child_id to use the full ORCHESTRATOR_ID for the root. Existing delegate IDs ("orch.1", "orch.1.2") keep working via a one-time migration or backward-compat alias.