| name | orchestrator |
| description | In-harness queue orchestration system that implements the DELEGATE/HANDBACK protocol lifecycle. Manages queue state machine (7 states: incoming, claimed, processing, done, failed, crashed, retry-pending), polls queue for new tasks, spawns sub-agents via Agent tool, correlates HANDBACK results, recovers from crashes, invokes quality gates, and detects idle conditions. Core system that makes DELEGATE/HANDBACK actually work. |
| license | Proprietary |
| compatibility | agentic-engineers framework v5.10+ |
| metadata | {"author":"agentic-engineers","version":"1.0","category":"orchestration","role":"orchestrator","model":"claude-haiku-4.5","effort":"high","thinking":false} |
orchestrator
Overview
The Orchestrator skill is the in-harness queue management system that coordinates all DELEGATE/HANDBACK protocol flow. It aims to:
- Queue polling — continuously monitor
incoming/ for new DELEGATE blocks
- Task claiming — atomically move tasks from
incoming/ → processing/ with timestamps
- Sub-agent spawning — invoke the Agent tool with full DELEGATE context
- HANDBACK correlation — parse HANDBACK text, extract task_id, apply routing decisions
- Crash recovery — detect orphaned tasks in
processing/ by claimed_at timeout
- Quality gating — invoke quality-engineer validation before marking done
- Idle detection — implement 3-minute sleep, 3-cycle threshold, deep sleep
- State transitions — 7-state queue machine with atomic file moves
Architecture
Queue State Machine (7 States)
incoming/ → new DELEGATEs waiting for pickup
↓
claimed/ → task claimed by orchestrator (timestamp: claimed_at)
↓
processing/ → sub-agent is executing the task
├─→ done/ (success)
├─→ failed/ (error)
└─→ crashed/ (orphaned / timeout after 600s)
retry-pending/ → task queued for retry (after crash recovery)
Queue Path Structure
All paths are per-session, per-harness (via queue-isolation):
~/.agentic-engineers/artifacts/{session_id}/{harness}/queue/
incoming/ – new DELEGATE files
processing/ – tasks being executed (with claimed_at)
done/ – completed tasks (with HANDBACK files)
failed/ – failed tasks (with error context)
Queue File Naming
DELEGATE: {task_id}.yaml
HANDBACK: {task_id}-HANDBACK-{role}.yaml
Metadata: {task_id}.meta.json (claimed_at, retry_count, last_error)
Core Methods
poll_queue()
Purpose: Main polling loop — read incoming/, validate, claim, spawn.
Behavior:
- Read all
.yaml files from incoming/
- Validate each DELEGATE format (handoff_type, task_id, scope, plan, success_criteria, agent)
- For each valid DELEGATE:
- Call
claim_task(task_id) → atomic move to processing/
- Call
spawn_sub_agent(delegate_dict) → invoke Agent tool
- Call
handle_handback(task_id, handback_text) → wait for HANDBACK, route result
- Continue until
incoming/ is empty
- Increment
clean_poll_count
- If
clean_poll_count >= 3 → call run_idle_loop()
Returns: (processed_count: int, failed_count: int)
claim_task(task_id: str) -> dict
Purpose: Atomically move task from incoming/ → processing/ with claimed_at timestamp.
Behavior:
- Read
incoming/{task_id}.yaml (DELEGATE)
- Create
processing/{task_id}.meta.json with:
{
"task_id": "...",
"claimed_at": "ISO8601 timestamp",
"retry_count": 0,
"last_error": null
}
- Move
incoming/{task_id}.yaml → processing/{task_id}.yaml
- Return the parsed DELEGATE dict
Exceptions: FileNotFoundError if task doesn't exist, IOError on move failure.
spawn_sub_agent(delegate: dict) -> str
Purpose: Invoke Agent tool with full DELEGATE context.
Behavior:
- Serialize DELEGATE as YAML
- Call Agent tool with:
- Role:
delegate['agent']
- Model: from routing table (engineer→haiku, senior-engineer→sonnet, etc.)
- Input: Full DELEGATE YAML as context
- Wait for subprocess to complete (via AgentInvoker timeout)
- Capture stdout/stderr
- Return combined output (contains HANDBACK YAML block)
Returns: output_text: str (may contain HANDBACK block or error message)
handle_handback(task_id: str, handback_text: str) -> dict
Purpose: Parse HANDBACK from Agent output, apply routing, invoke QE gate, transition state.
Behavior:
- Parse HANDBACK YAML block from
handback_text
- Extract task_id, status, output, metrics, confidence
- Validate HANDBACK schema (required fields)
- Move file from
processing/{task_id}.yaml → temp location
- Apply routing decision:
- If
status == 'success' → invoke invoke_qe_gate(task_id, handback)
- If
status == 'failure' → move to failed/{task_id}-HANDBACK.yaml
- If
status == 'escalate' → escalation chaining (C2c): synthesize a follow-on
{task_id}-escalated-to-{role} DELEGATE into incoming/ for the escalation
target (output.escalate_to, default lead-engineer) and archive the original
task to done/ with escalation audit metadata. There is no escalation/ state
directory in the queue protocol.
- Write
done/{task_id}-HANDBACK.yaml (or failed/)
- Increment metrics (tokens, cost, duration)
- Return parsed HANDBACK dict
Exceptions: ValueError if HANDBACK is malformed.
recover_crashed_tasks()
Purpose: Scan processing/ for orphaned tasks (claimed_at + 600s deadline).
Behavior:
- List all
.meta.json files in processing/
- For each file:
- Parse
claimed_at timestamp
- If
now - claimed_at > 600s (10 minutes):
- Increment
retry_count
- If
retry_count >= 3 → move to failed/
- Else → move to
retry-pending/, reschedule
- Write recovery log with timestamps and retry decisions
Returns: (recovered_count: int, failed_count: int)
wake_timer() -> Dict[str, Any]
Purpose: Wake-timer mechanism to detect and recover stalled tasks (no heartbeat for N seconds).
Behavior:
- Detect tasks in processing/ without recent heartbeat (> heartbeat_interval seconds)
- For each stalled task:
- Increment retry_count
- If retry_count >= retry_max_attempts → escalate to manual review
- Else → move to retry-pending/ with exponential backoff
- Log detection and recovery metrics
- Capture span for observability
Thresholds (from SPEC queue SLA design):
- Heartbeat interval: config.heartbeat_interval (default 30 seconds, configurable)
- Stale (WARN): config.stale_threshold_sec (300 seconds since last_heartbeat)
- Crash (ESCALATE): config.crash_threshold_sec (600 seconds since claimed_at, LOCKED)
Returns:
{
'stalled_detected': int,
'recovered': int,
'escalated': int,
'wake_reason': str
}
run_idle_loop() -> Dict[str, Any]
Purpose: Implement intelligent idle detection with 3-minute polling sleep and deep sleep after 3 consecutive clean polls.
Behavior:
-
Normal polling (clean_poll_count < 3):
- Sleep for
POLL_INTERVAL_SEC (180 seconds / 3 minutes)
- Return:
{'work_processed': 0, 'idle_entered': False, 'wake_reason': 'normal'}
-
Idle detected (clean_poll_count >= 3):
- Log "Queue idle ({IDLE_THRESHOLD_POLLS} clean polls), entering deep sleep"
- Call
_deep_sleep() to block until woken
- Reset
clean_poll_count = 0
- Return:
{'work_processed': 0, 'idle_entered': True, 'wake_reason': <wake_type>}
Returns:
{
'work_processed': int,
'idle_entered': bool,
'wake_reason': str
}
Wake Reasons:
'normal' — completed POLL_INTERVAL_SEC sleep (normal polling cycle)
'timeout' — deep sleep duration (DEEP_SLEEP_SEC) completed without event
'file_event' — new file detected in incoming/ directory (wakes early)
'signal' — received SIGUSR1 signal (external wake)
_deep_sleep() -> str
Purpose: Enter deep sleep and block until woken by file system event or signal.
Behavior:
- Setup SIGUSR1 signal handler
- Attempt to use inotify (Linux) to watch
incoming/ for new files
- If available: wait for file creation event with timeout
- If not available: fall back to
_deep_sleep_polling()
- On wake: return wake_reason and restore signal handler
Returns: 'file_event' | 'signal' | 'timeout'
_deep_sleep_polling() -> str
Purpose: Fallback deep sleep implementation using polling and signal handling.
Behavior:
- Get initial file count in
incoming/
- Setup SIGUSR1 signal handler
- Loop with ~10-second poll interval:
- Check if new files have appeared in
incoming/
- If found: return
'file_event'
- If SIGUSR1 received: return
'signal'
- If DEEP_SLEEP_SEC timeout reached: return
'timeout'
- Restore signal handler
Returns: 'file_event' | 'signal' | 'timeout'
invoke_qe_gate(task_id: str, handback: dict) -> bool
Purpose: Invoke Quality Engineer validation before marking done.
Behavior:
- Create QE DELEGATE:
handoff_type: DELEGATE
agent: quality-engineer
task_id: {task_id}-qe-gate
scope: "Validate {original_task_id} HANDBACK against success criteria"
context:
- original_task_id: {task_id}
- handback: {serialized HANDBACK}
- success_criteria: from original DELEGATE
- Spawn quality-engineer via Agent tool
- Wait for QE HANDBACK with approval/rejection
- If approved → return True (proceed to done/)
- If rejected → return False (move to failed/)
capture_span(method_name: str, **attrs) -> None
Purpose: Write structured SPAN file for observability.
Behavior:
- Create structured span dict:
{
"span_name": "orchestrator-{method_name}",
"span_id": "{uuid}",
"trace_id": "{session_id}",
"start_time": "ISO8601",
"end_time": "ISO8601",
"duration_ms": int,
"attributes": {
...attrs
}
}
- Write to
{queue_root}/spans/{task_id}-{method}.span.json
- Log span in metrics registry
Configuration
| Parameter | Type | Default | Description |
|---|
poll_interval_fast | int | 30 | Polling interval when tasks are processing (seconds) |
poll_interval_idle | int | 180 | Polling interval when queue is idle (seconds) |
heartbeat_interval | int | 30 | Expected interval between heartbeats (seconds, configurable) |
heartbeat_timeout_sec | int | 120 | Max time without task update before stalled (seconds) |
task_deadline_sec | int | 600 | Max claimed time before crash recovery (seconds) |
stale_threshold_sec | int | 300 | Threshold for WARN status (seconds since last_heartbeat) |
crash_threshold_sec | int | 600 | Threshold for ESCALATE (seconds since claimed_at, LOCKED) |
idle_threshold_polls | int | 3 | Clean polls before deep sleep |
idle_sleep_sec | int | 600 | Deep sleep duration (seconds) |
retry_max_attempts | int | 3 | Max retries for crashed/stalled tasks |
retry_backoff_multiplier | float | 1.5 | Exponential backoff multiplier for retries |
Dependencies
- queue-management — for all YAML queue writes (via QueueOperations)
- queue-isolation — for session/harness scoped queue paths
- orchestrator.py — routing table (agent→model mapping)
- Agent tool — sub-agent invocation
- quality-engineer — QE gating
Integration Points
- Orchestrator.py routing — import ENGINEER_CONFIG, SENIOR_ENGINEER_CONFIG, etc.
- QueueOperations.enqueue() / move_task() — never write queue files directly
- Agent tool invocation — pass full DELEGATE YAML, capture HANDBACK
- Model Engineer feedback — cost/token metrics from HANDBACK
- Escalation chain — when status='escalate', create escalation DELEGATE
Examples
Minimal Orchestrator Usage with Idle Loop
from src.skills.orchestrator import OrchestratorSkill
skill = OrchestratorSkill()
while True:
processed, failed = skill.poll_queue()
result = skill.run_idle_loop()
if result['idle_entered']:
print(f"Woken from deep sleep: {result['wake_reason']}")
Orchestrator Agent Loop Pattern
skill = OrchestratorSkill(session_id="my-session", harness="claude")
for cycle in range(100):
processed, failed = skill.poll_queue()
result = skill.run_idle_loop()
logger.info(
f"Poll cycle {cycle}: "
f"processed={processed}, failed={failed}, "
f"idle_entered={result['idle_entered']}, "
f"wake_reason={result['wake_reason']}"
)
With Crash Recovery
skill = OrchestratorSkill()
recovered, newly_failed = skill.recover_crashed_tasks()
print(f"Recovered {recovered} tasks, failed {newly_failed}")
while True:
processed, failed = skill.poll_queue()
result = skill.run_idle_loop()
Manual Deep Sleep with Signal Handling
import signal
import os
skill = OrchestratorSkill()
skill.clean_poll_count = 3
result = skill.run_idle_loop()
print(f"Wake reason: {result['wake_reason']}")
Testing
Unit tests cover:
- State machine transitions (incoming → claimed → processing → done)
- HANDBACK parsing and routing
- Crash recovery and timeout detection
- Idle detection and sleep threshold
- Atomic moves and metadata writes
- Quality gate invocation
- Escalation chain delegation
Run tests:
pytest src/skills/orchestrator/tests/
Observability
All operations write structured SPAN files to the queue's spans/ directory for:
- Latency measurement
- Error tracking
- Cost aggregation
- Task dependency visualization
Self-Improvement
We aim for orchestrator to feel like a knowledgeable colleague rather than a rulebook. If any section felt prescriptive rather than guiding, a tone_note in your feedback helps us improve it.
This skill participates in the framework's continuous improvement cycle
(see skill-improvement-feedback).
When you use orchestrator during a task, include a skill_feedback entry
in your HANDBACK to help improve it over time:
skill_feedback:
- skill_name: orchestrator
effectiveness_score: 0.85
clarity_score: 0.90
coverage_gaps:
- "Specific scenario the skill did not address"
improvement_suggestions:
- "Concrete change that would have helped"
usage_context: "One sentence on how you used this skill"
Positive feedback is as valuable as critical feedback. Three or more
feedback items for this skill automatically trigger an improvement task.