
Agenthub
- 68 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
agenthub is a Claude skill that orchestrates multiple AI agents as a directed acyclic graph with dependency management and output merging.
About
agenthub is a Claude skill for orchestrating multiple AI agents as a directed acyclic graph. A developer uses it to decompose a complex task into sub-tasks, assign each to a specialized agent, define dependencies between them, and merge their outputs. It ships seven sub-skills (init, run, spawn, board, eval, merge, status) plus Python scripts for DAG analysis, board management, and result ranking.
- Orchestrates multiple AI agents as a directed acyclic graph (DAG) with typed dependencies
- Compound sub-skill architecture: init, run, spawn, board, eval, merge, status
- DAG analyzer validates for cycles, unreachable nodes, and bottlenecks
Agenthub by the numbers
- 68 all-time installs (skills.sh)
- Ranked #5,858 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
agenthub capabilities & compatibility
Free; local Python scripts, no API keys stated.
- Capabilities
- agent workflow designer · agent protocol · dag analyzer
- Use cases
- orchestration · planning
- Pricing
- Free
What agenthub says it does
Multi-agent DAG orchestration framework. Design, execute, and manage workflows
AgentHub provides patterns and tools for orchestrating multiple AI agents as a directed acyclic graph (DAG).
complex tasks decompose better than they scale
npx skills add https://github.com/borghei/claude-skills --skill agenthubAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Design and run multi-agent DAG workflows where specialized agents collaborate on sub-tasks and their outputs are merged.
Who is it for?
Developers building multi-agent systems that need parallel specialized agents with defined dependencies and a merge step.
Skip if: Simple single-agent tasks that fit in one context window.
When should I use this skill?
When a task requires multiple specialized agents working in concert, or when you need to parallelize AI work across sub-tasks.
What you get
A validated DAG of specialized agents that run in parallel where possible and merge into a coherent result.
- validated workflow DAG
- agent status board
- merged final result
By the numbers
- 7 sub-skills in skills/ directory
- 4 Python scripts (dag_analyzer, board_manager, result_ranker, session_manager)
- 7 agent states (PENDING, READY, RUNNING, COMPLETED, FAILED, SKIPPED, EVALUATING)
Files
AgentHub - Multi-Agent DAG Orchestration
Category: Engineering / AI Agents Maintainer: Claude Skills Team
Overview
AgentHub provides patterns and tools for orchestrating multiple AI agents as a directed acyclic graph (DAG). Instead of one agent doing everything sequentially, AgentHub lets you decompose complex tasks into sub-tasks, assign each to a specialized agent, define dependencies between them, and merge their outputs into a coherent result.
The core insight: complex tasks decompose better than they scale. A 10-step sequential task run by one agent hits context limits and quality degradation. Five parallel agents with clear scopes and a merge step produce better results faster.
Sub-Skills
This skill uses compound sub-skill architecture. Each sub-skill in skills/ handles a stage of the orchestration lifecycle:
| Sub-Skill | File | Purpose |
|---|---|---|
| Init | skills/init.md | Initialize a multi-agent workflow definition |
| Run | skills/run.md | Execute a defined workflow end-to-end |
| Spawn | skills/spawn.md | Spawn individual agents within a workflow |
| Board | skills/board.md | Dashboard showing agent status and progress |
| Eval | skills/eval.md | Evaluate agent outputs for quality and consistency |
| Merge | skills/merge.md | Merge outputs from multiple agents into final result |
| Status | skills/status.md | Show workflow execution status and health |
Sub-Skill Flow
Init ──> Run ──> Spawn (parallel) ──> Eval ──> Merge
│ │
Board ◄──── Status ◄───────────┘Lifecycle: Init defines the workflow DAG, Run orchestrates execution, Spawn creates individual agents, Board provides real-time visibility, Eval checks output quality, Merge combines results, and Status reports overall health.
Scripts
| Script | Purpose |
|---|---|
scripts/dag_analyzer.py | Analyze DAG definitions for cycles, unreachable nodes, and bottlenecks |
scripts/board_manager.py | Manage agent task boards with status tracking |
scripts/result_ranker.py | Rank and merge outputs from multiple agents |
scripts/session_manager.py | Manage orchestration sessions and state |
Core Concepts
Workflow DAG
A workflow is a directed acyclic graph where:
- Nodes are agent tasks with a defined scope, inputs, and expected outputs
- Edges are dependencies: agent B cannot start until agent A completes
- Root nodes have no dependencies and start immediately
- Terminal nodes have no dependents and feed into the merge step
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Research │────>│ Analysis │────>│ Merge │
│ Agent │ │ Agent │ │ Agent │
└──────────┘ └──────────┘ └──────────┘
▲
┌──────────┐ │
│ Data │──────────┘
│ Agent │
└──────────┘Workflow Definition Format
{
"name": "market-analysis",
"description": "Comprehensive market analysis for product launch",
"agents": {
"researcher": {
"task": "Research competitor landscape and market size",
"inputs": ["product_description"],
"outputs": ["competitor_list", "market_size"],
"dependencies": []
},
"data_collector": {
"task": "Collect pricing and feature data from competitors",
"inputs": ["competitor_list"],
"outputs": ["pricing_data", "feature_matrix"],
"dependencies": ["researcher"]
},
"analyst": {
"task": "Analyze positioning opportunities and pricing strategy",
"inputs": ["pricing_data", "feature_matrix", "market_size"],
"outputs": ["positioning_report", "pricing_recommendation"],
"dependencies": ["data_collector", "researcher"]
},
"writer": {
"task": "Write executive summary combining all findings",
"inputs": ["positioning_report", "pricing_recommendation"],
"outputs": ["executive_summary"],
"dependencies": ["analyst"]
}
},
"config": {
"max_parallel": 3,
"timeout_per_agent": 300,
"retry_on_failure": true,
"quality_threshold": 0.7
}
}Agent States
| State | Description |
|---|---|
PENDING | Waiting for dependencies to complete |
READY | All dependencies met, queued for execution |
RUNNING | Currently executing |
COMPLETED | Finished successfully |
FAILED | Failed after all retries |
SKIPPED | Skipped due to upstream failure |
EVALUATING | Output being evaluated for quality |
Execution Strategy
1. Topological sort the DAG to determine execution order 2. Identify parallel groups: nodes with no inter-dependencies run simultaneously 3. Execute root nodes first (no dependencies) 4. Chain results: completed node outputs become inputs for dependents 5. Evaluate outputs at quality gates 6. Merge terminal outputs into final result
Workflows
Workflow 1: Define and Validate
1. Define agents with tasks, inputs, outputs, dependencies
2. Run dag_analyzer.py to validate:
- No cycles in the dependency graph
- All referenced inputs are produced by upstream agents
- No unreachable nodes
- Critical path length is acceptable
3. Estimate execution time based on agent count and dependenciesWorkflow 2: Execute Orchestration
1. Load workflow definition
2. Initialize session (session_manager.py)
3. Topological sort to determine execution order
4. For each parallel group:
a. Spawn agents (up to max_parallel)
b. Monitor progress on board
c. Collect outputs on completion
d. Evaluate outputs against quality threshold
5. Pass outputs to downstream agents as inputs
6. Merge final outputs
7. Generate execution reportWorkflow 3: Evaluate and Iterate
1. Collect all agent outputs
2. Run quality evaluation (eval sub-skill)
3. Rank outputs by quality score (result_ranker.py)
4. If any output below threshold:
a. Retry the agent with adjusted instructions
b. Or flag for human review
5. Merge passing outputs into final resultCommon Patterns
Fan-Out / Fan-In
Multiple independent agents work in parallel, then a single agent merges results:
Task A ──┐
Task B ──┼──> Merge
Task C ──┘Pipeline
Sequential agents where each transforms the previous output:
Extract ──> Transform ──> Load ──> ValidateReducer
Multiple agents produce competing outputs, ranked and best one selected:
Agent 1 ──┐
Agent 2 ──┼──> Rank ──> Best Output
Agent 3 ──┘Validator Chain
Each agent validates the previous agent's work:
Generate ──> Review ──> Fix ──> ApproveBest Practices
1. Small, focused agent scopes -- each agent should have a single clear objective 2. Explicit inputs/outputs -- never rely on implicit shared state between agents 3. Quality gates between stages -- evaluate before passing outputs downstream 4. Timeout per agent -- prevent runaway agents from blocking the workflow 5. Retry with context -- when retrying a failed agent, include the failure reason 6. Merge strategy documented -- how competing or complementary outputs combine 7. Critical path awareness -- optimize the longest dependency chain first 8. Idempotent agents -- agents should produce the same output given the same input
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cycle in DAG | Agent A depends on B which depends on A | Run dag_analyzer.py before execution |
| Output format mismatch | Agent B expects JSON, Agent A produces markdown | Define explicit output schemas per agent |
| Single bottleneck agent | One agent depends on everything | Restructure DAG to parallelize dependencies |
| Lost context between agents | Outputs too terse for downstream use | Require structured output with context preservation |
| Quality degradation in merge | Naive concatenation loses coherence | Use a dedicated merge agent with synthesis instructions |
| Runaway execution time | No timeouts, retry loops | Set timeout_per_agent and max retries |
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Workflow hangs at agent N | Dependency not met or agent timeout | Check board for PENDING agents; verify upstream completed; check timeout config |
| Merged output is incoherent | No merge strategy defined | Use the merge sub-skill with explicit synthesis instructions |
| Agent produces wrong format | Input/output contract unclear | Define JSON schemas for agent inputs and outputs |
| DAG validation fails with cycle | Circular dependency in definition | Use dag_analyzer.py to identify the cycle; restructure the dependency chain |
| Quality eval fails everything | Threshold too strict for task complexity | Lower threshold or add a revision step before eval |
Success Criteria
- DAG validation passes on every workflow definition before execution
- Parallel execution utilization above 60% -- agents running in parallel most of the time
- Quality gate pass rate above 80% -- agent outputs meet threshold on first attempt
- End-to-end execution time within 2x critical path -- parallelization delivers real speedup
- Zero lost outputs -- every agent's output is captured and available for merge/review
- Merge coherence score above 0.7 -- final merged output reads as a unified deliverable
Scope and Limitations
This skill covers:
- Multi-agent workflow design with DAG dependency graphs
- Agent spawning, monitoring, and lifecycle management
- Output quality evaluation and ranking
- Result merging strategies for coherent final deliverables
This skill does NOT cover:
- Individual agent design or prompt engineering (see
agent-designer) - Agent memory and self-improvement (see
self-improving-agent) - Infrastructure for running agents (compute, scheduling, deployment)
- Real-time streaming communication between agents
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
agent-designer | Defines individual agent capabilities that become DAG nodes | Agent specs flow in; execution results flow back for agent tuning |
self-improving-agent | Each agent can use self-improvement patterns to get better | Session feedback from orchestration feeds into agent learning loops |
prompt-engineer-toolkit | Agent task prompts benefit from prompt engineering | Optimized prompts improve individual agent quality within the DAG |
context-engine | Manages what context each agent sees | Context retrieval provides relevant inputs to each spawned agent |
observability-designer | Monitors workflow execution and agent health | Agent state transitions and timing metrics feed into dashboards |
Multi-Agent Orchestration Patterns Reference
DAG Patterns
1. Fan-Out / Fan-In (Most Common)
┌── Agent A ──┐
Input ───┼── Agent B ──┼──> Merge ──> Output
└── Agent C ──┘When to use: Task decomposes into independent sub-tasks that can run in parallel, then need to be combined.
Examples:
- Research from multiple sources, then synthesize
- Generate multiple drafts, then pick the best
- Analyze different aspects of a dataset, then combine findings
Merge strategy: Synthesize (each agent contributes different aspects)
2. Pipeline (Sequential)
Extract ──> Transform ──> Enrich ──> Validate ──> OutputWhen to use: Each step depends on the previous step's output and transforms it further.
Examples:
- Data processing: extract -> clean -> transform -> load
- Content: research -> outline -> draft -> edit -> polish
- Code: spec -> implement -> test -> review -> deploy
Merge strategy: Chain (final agent output is the result)
3. Reducer (Competitive)
Agent 1 ──┐
Agent 2 ──┼──> Rank ──> Best Output
Agent 3 ──┘When to use: Multiple agents attempt the same task independently, and you pick the best result.
Examples:
- Multiple code solutions ranked by test pass rate
- Multiple summaries ranked by completeness
- Multiple designs ranked by criteria scores
Merge strategy: Rank-select (pick highest scoring output)
4. Validator Chain
Generate ──> Review ──> Fix ──> Re-Review ──> ApproveWhen to use: Quality is critical and each step validates/improves the previous step's work.
Examples:
- Code generation with review and fix cycle
- Document drafting with editorial review
- Test generation with coverage validation
Merge strategy: Chain (final validated output)
5. Map-Reduce
Split ──> [Agent per chunk] ──> Reduce ──> OutputWhen to use: Input is large and can be split into independent chunks processed in parallel.
Examples:
- Analyzing a large codebase file-by-file
- Processing multiple documents in a corpus
- Running tests across multiple environments
Merge strategy: Synthesize with deduplication
6. Diamond Dependency
Agent A ──> Agent B ──┐
└────> Agent C ──┼──> Agent DWhen to use: Two paths share a common ancestor and a common descendant.
Warning: Ensure Agent D correctly handles inputs from both B and C without duplication.
Agent Design Principles
Single Responsibility
Each agent should have ONE clear objective. If you cannot describe the agent's job in one sentence, split it.
Good:
- "Research competitor pricing data"
- "Generate TypeScript test code from spec"
- "Review code for security vulnerabilities"
Bad:
- "Research competitors, analyze pricing, and write a report" (3 agents)
- "Generate and review tests" (2 agents)
Explicit Contracts
Every agent must declare:
- Inputs: What data it needs (with types/schemas)
- Outputs: What data it produces (with types/schemas)
- Constraints: Time limit, quality threshold, format requirements
Idempotency
Given the same inputs, an agent should produce the same outputs. This enables:
- Safe retries on failure
- Caching of completed work
- Deterministic debugging
Context Isolation
Agents should not share mutable state. All data flows through declared inputs and outputs. This prevents:
- Race conditions in parallel execution
- Hidden dependencies between agents
- State corruption from failed agents
Quality Gate Patterns
Threshold Gate
Output Score >= 0.7 → PASS
Output Score >= 0.55 → REVISE (retry with feedback)
Output Score < 0.55 → FAILRubric Gate
Define specific criteria and grade each:
Completeness: 0-1 (all required fields present)
Accuracy: 0-1 (claims supported by evidence)
Format: pass/fail (matches expected schema)
Relevance: 0-1 (addresses the assigned task)Consensus Gate
Multiple evaluators must agree:
If 2/3 evaluators rate PASS → PASS
If 2/3 evaluators rate FAIL → FAIL
Otherwise → HUMAN_REVIEWFailure Handling
Retry with Context
When an agent fails, retry with additional context:
Retry 1: Original prompt + error message
Retry 2: Original prompt + error message + hint from evaluator
Retry 3: FAIL (escalate to human)Skip and Continue
If a non-critical agent fails, skip it and continue the workflow. Downstream agents must handle missing inputs gracefully.
Fallback Agent
If the primary agent fails, a simpler fallback agent takes over:
Primary Agent (complex) ──> FAIL ──> Fallback Agent (simple) ──> OutputCircuit Breaker
After N consecutive failures across agents, halt the workflow:
If 3+ agents fail → HALT workflow → notify humanScaling Considerations
| Agents | Max Parallel | Strategy |
|---|---|---|
| 2-4 | All parallel | Simple fan-out |
| 5-10 | 3-5 parallel | Batched execution with priority |
| 10-20 | 5-8 parallel | Phased execution with checkpoints |
| 20+ | Consider decomposing into sub-workflows | Hierarchical orchestration |
Metrics
| Metric | Definition | Target |
|---|---|---|
| Parallelization efficiency | parallel_time / sequential_time | > 0.6 |
| Quality gate pass rate | first_attempt_passes / total_attempts | > 0.8 |
| Workflow completion rate | completed_workflows / started_workflows | > 0.95 |
| Critical path ratio | critical_path_time / total_wall_time | < 0.7 |
| Retry rate | retried_agents / total_agents | < 0.15 |
#!/usr/bin/env python3
"""Manage agent task boards with status tracking for multi-agent workflows.
Provides a visual dashboard of agent states within an orchestration session.
Supports multiple view modes: board (kanban), timeline, summary.
Usage:
python board_manager.py --session session.json --view board
python board_manager.py --session session.json --view timeline
python board_manager.py --session session.json --agent researcher --detail
python board_manager.py --session session.json --json
Expected session.json format:
{
"session_id": "abc123",
"workflow_name": "market-analysis",
"started_at": "2026-04-02T10:30:00",
"agents": {
"researcher": {"state": "COMPLETED", "started_at": "...", "completed_at": "...", "duration_s": 90, "outputs": {...}},
"data_collector": {"state": "RUNNING", "started_at": "...", "duration_s": null},
"analyst": {"state": "PENDING", "dependencies": ["data_collector", "researcher"]},
"writer": {"state": "PENDING", "dependencies": ["analyst"]}
}
}
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
STATE_SYMBOLS = {
"COMPLETED": "[x]",
"RUNNING": "[>]",
"PENDING": "[ ]",
"FAILED": "[!]",
"SKIPPED": "[-]",
"READY": "[~]",
"EVALUATING": "[?]",
}
STATE_ORDER = {
"RUNNING": 0,
"READY": 1,
"EVALUATING": 2,
"PENDING": 3,
"COMPLETED": 4,
"FAILED": 5,
"SKIPPED": 6,
}
def load_session(path):
"""Load session state from JSON file."""
try:
with open(path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Error loading session: {e}", file=sys.stderr)
sys.exit(1)
def compute_elapsed(session):
"""Compute elapsed time since session start."""
started = session.get("started_at", "")
if not started:
return 0
try:
start_dt = datetime.fromisoformat(started)
return (datetime.now() - start_dt).total_seconds()
except ValueError:
return 0
def format_duration(seconds):
"""Format seconds as Xm Ys."""
if seconds is None or seconds == 0:
return "---"
minutes = int(seconds // 60)
secs = int(seconds % 60)
if minutes > 0:
return f"{minutes}m {secs:02d}s"
return f"{secs}s"
def group_by_state(agents):
"""Group agents by their current state."""
groups = {}
for agent_id, agent_data in agents.items():
state = agent_data.get("state", "PENDING")
if state not in groups:
groups[state] = []
groups[state].append({"id": agent_id, **agent_data})
return groups
def render_board_view(session):
"""Render kanban-style board view."""
agents = session.get("agents", {})
groups = group_by_state(agents)
elapsed = compute_elapsed(session)
lines = []
lines.append(f"AgentHub Board - Session: {session.get('session_id', '?')}")
lines.append(f"Workflow: {session.get('workflow_name', '?')}")
lines.append(f"Started: {session.get('started_at', '?')[:19]} | Elapsed: {format_duration(elapsed)}")
lines.append("")
# Column layout
column_order = ["RUNNING", "READY", "EVALUATING", "PENDING", "COMPLETED", "FAILED", "SKIPPED"]
active_columns = [s for s in column_order if s in groups]
if not active_columns:
lines.append(" No agents in session.")
return "\n".join(lines)
# Header
col_width = 22
header = ""
for state in active_columns:
count = len(groups[state])
header += f"{state} ({count})".ljust(col_width)
lines.append(header)
lines.append("─" * (col_width * len(active_columns)))
# Find max rows
max_rows = max(len(groups[s]) for s in active_columns)
for row in range(max_rows):
row_line = ""
for state in active_columns:
if row < len(groups[state]):
agent = groups[state][row]
symbol = STATE_SYMBOLS.get(state, "[?]")
duration = format_duration(agent.get("duration_s"))
entry = f"{symbol} {agent['id'][:14]}"
if state in ("COMPLETED", "RUNNING"):
entry += f" {duration}"
row_line += entry.ljust(col_width)
else:
row_line += " " * col_width
lines.append(row_line)
# Show pending dependencies
pending = groups.get("PENDING", [])
if pending:
lines.append("")
lines.append("WAITING ON:")
for agent in pending:
deps = agent.get("dependencies", [])
if deps:
lines.append(f" {agent['id']} <- {', '.join(deps)}")
return "\n".join(lines)
def render_timeline_view(session):
"""Render timeline/gantt-style view."""
agents = session.get("agents", {})
elapsed = compute_elapsed(session)
lines = []
lines.append(f"AgentHub Timeline - Session: {session.get('session_id', '?')}")
lines.append(f"Elapsed: {format_duration(elapsed)}")
lines.append("")
# Sort by start time, then by state
sorted_agents = sorted(
agents.items(),
key=lambda x: (
STATE_ORDER.get(x[1].get("state", "PENDING"), 9),
x[1].get("started_at", "z"),
),
)
max_name_len = max(len(aid) for aid, _ in sorted_agents) if sorted_agents else 10
bar_width = 40
for agent_id, agent_data in sorted_agents:
state = agent_data.get("state", "PENDING")
duration = agent_data.get("duration_s", 0) or 0
symbol = STATE_SYMBOLS.get(state, "[?]")
# Proportional bar
if elapsed > 0 and duration > 0:
bar_len = max(1, int(duration / max(elapsed, 1) * bar_width))
elif state == "RUNNING":
bar_len = max(1, int(bar_width * 0.3))
else:
bar_len = 0
bar_char = {"COMPLETED": "=", "RUNNING": ">", "FAILED": "x", "PENDING": ".", "READY": "~"}
char = bar_char.get(state, " ")
bar = char * bar_len
name = agent_id.ljust(max_name_len)
dur_str = format_duration(duration if duration else None)
lines.append(f" {symbol} {name} [{bar:<{bar_width}}] {dur_str}")
return "\n".join(lines)
def render_summary_view(session):
"""Render compact one-line-per-agent summary."""
agents = session.get("agents", {})
elapsed = compute_elapsed(session)
total = len(agents)
completed = sum(1 for a in agents.values() if a.get("state") == "COMPLETED")
failed = sum(1 for a in agents.values() if a.get("state") == "FAILED")
running = sum(1 for a in agents.values() if a.get("state") == "RUNNING")
progress_pct = int(completed / total * 100) if total else 0
bar_len = int(progress_pct / 5)
progress_bar = "█" * bar_len + "░" * (20 - bar_len)
lines = []
lines.append(f"Session: {session.get('session_id', '?')} | {session.get('workflow_name', '?')}")
lines.append(f"Progress: [{progress_bar}] {progress_pct}% ({completed}/{total})")
lines.append(f"Running: {running} | Failed: {failed} | Elapsed: {format_duration(elapsed)}")
lines.append("")
lines.append(f"{'Agent':<20} {'State':<12} {'Duration':<10}")
lines.append("─" * 42)
for agent_id, agent_data in sorted(agents.items(), key=lambda x: STATE_ORDER.get(x[1].get("state", "PENDING"), 9)):
state = agent_data.get("state", "PENDING")
duration = format_duration(agent_data.get("duration_s"))
lines.append(f"{agent_id:<20} {state:<12} {duration:<10}")
return "\n".join(lines)
def render_agent_detail(session, agent_id):
"""Render detailed view for a specific agent."""
agents = session.get("agents", {})
if agent_id not in agents:
return f"Agent '{agent_id}' not found in session."
agent = agents[agent_id]
lines = []
lines.append(f"Agent Detail: {agent_id}")
lines.append("=" * 50)
lines.append(f" State: {agent.get('state', 'PENDING')}")
lines.append(f" Started: {agent.get('started_at', '---')}")
lines.append(f" Completed: {agent.get('completed_at', '---')}")
lines.append(f" Duration: {format_duration(agent.get('duration_s'))}")
lines.append(f" Dependencies: {', '.join(agent.get('dependencies', [])) or 'none'}")
lines.append(f" Retries: {agent.get('retries', 0)}")
if agent.get("task"):
lines.append(f" Task: {agent['task'][:60]}")
if agent.get("error"):
lines.append(f" Error: {agent['error'][:80]}")
if agent.get("outputs"):
lines.append(f" Outputs: {json.dumps(agent['outputs'], indent=2)[:200]}")
if agent.get("eval_score") is not None:
lines.append(f" Eval score: {agent['eval_score']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Manage agent task boards with status tracking.",
epilog="Example: python board_manager.py --session session.json --view board",
)
parser.add_argument("--session", required=True, help="Path to session state JSON file")
parser.add_argument("--view", choices=["board", "timeline", "summary"], default="board", help="View mode")
parser.add_argument("--agent", help="Show detail for a specific agent")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
session_path = Path(args.session)
if not session_path.exists():
print(f"Error: Session file '{args.session}' not found.", file=sys.stderr)
sys.exit(1)
session = load_session(session_path)
if args.json_output:
agents = session.get("agents", {})
result = {
"session_id": session.get("session_id"),
"workflow_name": session.get("workflow_name"),
"elapsed_s": compute_elapsed(session),
"total_agents": len(agents),
"by_state": {},
"agents": agents,
}
for state in set(a.get("state", "PENDING") for a in agents.values()):
result["by_state"][state] = sum(1 for a in agents.values() if a.get("state") == state)
print(json.dumps(result, indent=2))
elif args.agent:
print(render_agent_detail(session, args.agent))
elif args.view == "board":
print(render_board_view(session))
elif args.view == "timeline":
print(render_timeline_view(session))
elif args.view == "summary":
print(render_summary_view(session))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Analyze multi-agent DAG workflow definitions for structural issues.
Validates workflow definitions by checking for cycles, unreachable nodes,
missing input/output references, and bottlenecks. Also computes critical
path length and parallelization potential.
Usage:
python dag_analyzer.py --workflow workflow.json --validate
python dag_analyzer.py --workflow workflow.json --critical-path
python dag_analyzer.py --workflow workflow.json --visualize
python dag_analyzer.py --workflow workflow.json --json
"""
import argparse
import json
import sys
from collections import defaultdict, deque
from pathlib import Path
def load_workflow(path):
"""Load workflow definition from JSON file."""
try:
with open(path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Error loading workflow: {e}", file=sys.stderr)
sys.exit(1)
def build_graph(workflow):
"""Build adjacency list and reverse graph from workflow agents."""
agents = workflow.get("agents", {})
graph = defaultdict(list) # agent -> list of dependents
reverse = defaultdict(list) # agent -> list of dependencies
all_nodes = set(agents.keys())
for agent_id, agent_def in agents.items():
deps = agent_def.get("dependencies", [])
for dep in deps:
graph[dep].append(agent_id)
reverse[agent_id].append(dep)
return graph, reverse, all_nodes
def detect_cycles(graph, all_nodes):
"""Detect cycles using DFS coloring (white/gray/black)."""
WHITE, GRAY, BLACK = 0, 1, 2
color = {n: WHITE for n in all_nodes}
cycles = []
def dfs(node, path):
color[node] = GRAY
path.append(node)
for neighbor in graph.get(node, []):
if color[neighbor] == GRAY:
# Found a cycle
cycle_start = path.index(neighbor)
cycles.append(path[cycle_start:] + [neighbor])
elif color[neighbor] == WHITE:
dfs(neighbor, path)
path.pop()
color[node] = BLACK
for node in all_nodes:
if color[node] == WHITE:
dfs(node, [])
return cycles
def topological_sort(graph, reverse, all_nodes):
"""Kahn's algorithm for topological sort."""
in_degree = {n: 0 for n in all_nodes}
for node in all_nodes:
for dep in reverse.get(node, []):
in_degree[node] = in_degree.get(node, 0)
in_degree[node] = len(reverse.get(node, []))
queue = deque([n for n in all_nodes if in_degree[n] == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for dependent in graph.get(node, []):
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
if len(order) != len(all_nodes):
return None # Cycle exists
return order
def find_roots_and_terminals(graph, reverse, all_nodes):
"""Identify root nodes (no deps) and terminal nodes (no dependents)."""
roots = [n for n in all_nodes if not reverse.get(n)]
terminals = [n for n in all_nodes if not graph.get(n)]
return sorted(roots), sorted(terminals)
def compute_critical_path(workflow, graph, reverse, all_nodes):
"""Compute the critical path (longest path through the DAG)."""
agents = workflow.get("agents", {})
config = workflow.get("config", {})
default_timeout = config.get("timeout_per_agent", 300)
# Estimate duration per agent (use timeout as upper bound)
durations = {}
for agent_id in all_nodes:
agent_config = agents.get(agent_id, {}).get("config", {})
durations[agent_id] = agent_config.get("timeout", default_timeout)
# Compute longest path from each node
longest_to = {n: 0 for n in all_nodes}
predecessor = {n: None for n in all_nodes}
topo = topological_sort(graph, reverse, all_nodes)
if topo is None:
return None, 0 # Cycle
for node in topo:
for dependent in graph.get(node, []):
new_dist = longest_to[node] + durations[node]
if new_dist > longest_to[dependent]:
longest_to[dependent] = new_dist
predecessor[dependent] = node
# Find the terminal with the longest path
terminals = [n for n in all_nodes if not graph.get(n)]
if not terminals:
return [], 0
end_node = max(terminals, key=lambda n: longest_to[n] + durations[n])
total_time = longest_to[end_node] + durations[end_node]
# Reconstruct path
path = [end_node]
current = end_node
while predecessor[current] is not None:
current = predecessor[current]
path.append(current)
path.reverse()
return path, total_time
def check_io_references(workflow):
"""Verify all input references resolve to upstream outputs."""
agents = workflow.get("agents", {})
issues = []
# Map each agent to its outputs
output_map = {}
for agent_id, agent_def in agents.items():
for output in agent_def.get("outputs", []):
output_map[output] = agent_id
# Check inputs
for agent_id, agent_def in agents.items():
deps = set(agent_def.get("dependencies", []))
for inp in agent_def.get("inputs", []):
if inp in output_map:
producer = output_map[inp]
# Check that the producer is an upstream dependency
if producer not in deps and producer != agent_id:
issues.append({
"type": "missing_dependency",
"agent": agent_id,
"input": inp,
"producer": producer,
"message": f"Agent '{agent_id}' uses input '{inp}' from '{producer}' but doesn't list it as a dependency",
})
# Inputs might be workflow-level inputs (not produced by agents)
# Check for unused outputs
all_inputs = set()
for agent_def in agents.values():
all_inputs.update(agent_def.get("inputs", []))
for output_name, producer in output_map.items():
if output_name not in all_inputs:
# Terminal output -- expected
if not any(
producer in agents[a].get("dependencies", [])
for a in agents
):
pass # Terminal agent output, this is fine
return issues
def compute_parallel_groups(graph, reverse, all_nodes):
"""Compute which agents can run in parallel at each level."""
topo = topological_sort(graph, reverse, all_nodes)
if topo is None:
return []
# Compute level (longest path from any root to this node)
level = {n: 0 for n in all_nodes}
for node in topo:
for dependent in graph.get(node, []):
level[dependent] = max(level[dependent], level[node] + 1)
# Group by level
groups = defaultdict(list)
for node in all_nodes:
groups[level[node]].append(node)
return [{"level": lvl, "agents": sorted(agents)} for lvl, agents in sorted(groups.items())]
def validate_workflow(workflow):
"""Run all validation checks on a workflow definition."""
agents = workflow.get("agents", {})
if not agents:
return {"valid": False, "errors": ["No agents defined in workflow"]}
graph, reverse, all_nodes = build_graph(workflow)
errors = []
warnings = []
# Check for unknown dependencies
for agent_id, agent_def in agents.items():
for dep in agent_def.get("dependencies", []):
if dep not in all_nodes:
errors.append(f"Agent '{agent_id}' depends on unknown agent '{dep}'")
# Cycle detection
cycles = detect_cycles(graph, all_nodes)
for cycle in cycles:
errors.append(f"Cycle detected: {' -> '.join(cycle)}")
# IO reference check
io_issues = check_io_references(workflow)
for issue in io_issues:
warnings.append(issue["message"])
# Root/terminal check
roots, terminals = find_roots_and_terminals(graph, reverse, all_nodes)
if not roots:
errors.append("No root agents found (all agents have dependencies)")
if not terminals:
warnings.append("No terminal agents found (all agents have dependents)")
# Unreachable check
if roots and not cycles:
reachable = set()
queue = deque(roots)
while queue:
node = queue.popleft()
if node in reachable:
continue
reachable.add(node)
queue.extend(graph.get(node, []))
unreachable = all_nodes - reachable
for node in unreachable:
warnings.append(f"Agent '{node}' is unreachable from root agents")
# Critical path
crit_path, crit_time = compute_critical_path(workflow, graph, reverse, all_nodes)
parallel_groups = compute_parallel_groups(graph, reverse, all_nodes)
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
"stats": {
"total_agents": len(all_nodes),
"root_agents": roots,
"terminal_agents": terminals,
"max_parallel": max(len(g["agents"]) for g in parallel_groups) if parallel_groups else 0,
"depth": len(parallel_groups),
"critical_path": crit_path if crit_path else [],
"critical_path_time_s": crit_time,
},
"parallel_groups": parallel_groups,
}
def visualize_dag(workflow):
"""Generate a text-based DAG visualization."""
graph, reverse, all_nodes = build_graph(workflow)
groups = compute_parallel_groups(graph, reverse, all_nodes)
lines = []
for group in groups:
level_agents = group["agents"]
level_line = " | ".join(f"[{a}]" for a in level_agents)
lines.append(f"Level {group['level']}: {level_line}")
# Show edges
for agent in level_agents:
dependents = graph.get(agent, [])
for dep in dependents:
lines.append(f" {agent} --> {dep}")
return "\n".join(lines)
def format_human(result, visualization=None):
"""Format validation result for human output."""
output = []
output.append("=" * 60)
output.append("DAG WORKFLOW ANALYZER")
output.append("=" * 60)
if not result["valid"]:
output.append("")
output.append("VALIDATION: FAILED")
output.append("-" * 60)
for err in result["errors"]:
output.append(f" [ERROR] {err}")
else:
output.append("")
output.append("VALIDATION: PASSED")
if result["warnings"]:
output.append("")
output.append("WARNINGS")
output.append("-" * 60)
for warn in result["warnings"]:
output.append(f" [WARN] {warn}")
stats = result["stats"]
output.append("")
output.append("STATISTICS")
output.append("-" * 60)
output.append(f" Total agents: {stats['total_agents']}")
output.append(f" Root agents: {', '.join(stats['root_agents'])}")
output.append(f" Terminal agents: {', '.join(stats['terminal_agents'])}")
output.append(f" Max parallelism: {stats['max_parallel']}")
output.append(f" DAG depth: {stats['depth']} levels")
if stats["critical_path"]:
output.append(f" Critical path: {' -> '.join(stats['critical_path'])}")
output.append(f" Est. time: {stats['critical_path_time_s']}s")
if result["parallel_groups"]:
output.append("")
output.append("EXECUTION GROUPS")
output.append("-" * 60)
for group in result["parallel_groups"]:
agents_str = ", ".join(group["agents"])
output.append(f" Level {group['level']}: [{agents_str}] ({len(group['agents'])} parallel)")
if visualization:
output.append("")
output.append("DAG VISUALIZATION")
output.append("-" * 60)
output.append(visualization)
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(
description="Analyze multi-agent DAG workflow definitions.",
epilog="Example: python dag_analyzer.py --workflow workflow.json --validate",
)
parser.add_argument("--workflow", required=True, help="Path to workflow JSON file")
parser.add_argument("--validate", action="store_true", help="Run full validation")
parser.add_argument("--critical-path", action="store_true", help="Show critical path only")
parser.add_argument("--visualize", action="store_true", help="Show DAG visualization")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
wf_path = Path(args.workflow)
if not wf_path.exists():
print(f"Error: Workflow file '{args.workflow}' not found.", file=sys.stderr)
sys.exit(1)
workflow = load_workflow(wf_path)
result = validate_workflow(workflow)
visualization = visualize_dag(workflow) if args.visualize else None
if args.json_output:
output = result
if visualization:
output["visualization"] = visualization
print(json.dumps(output, indent=2))
else:
print(format_human(result, visualization))
# Exit with error code if invalid
if not result["valid"]:
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Rank and merge outputs from multiple agents in a workflow.
Scores agent outputs on completeness, length, structure, and relevance.
Supports different merge strategies: synthesize (combine complementary),
rank-select (pick best), and chain (use final).
Usage:
python result_ranker.py --session session.json --list-outputs
python result_ranker.py --session session.json --rank
python result_ranker.py --session session.json --merge synthesize
python result_ranker.py --session session.json --merge rank-select --json
"""
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
def load_session(path):
"""Load session with agent outputs."""
try:
with open(path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Error loading session: {e}", file=sys.stderr)
sys.exit(1)
def extract_outputs(session):
"""Extract completed agent outputs from session."""
agents = session.get("agents", {})
outputs = []
for agent_id, agent_data in agents.items():
if agent_data.get("state") == "COMPLETED" and agent_data.get("outputs"):
outputs.append({
"agent_id": agent_id,
"state": agent_data["state"],
"outputs": agent_data["outputs"],
"duration_s": agent_data.get("duration_s", 0),
"eval_score": agent_data.get("eval_score"),
"task": agent_data.get("task", ""),
})
return outputs
def score_output(output):
"""Score an individual agent output on quality dimensions."""
scores = {}
output_data = output.get("outputs", {})
# Completeness: fraction of output fields that are non-empty
total_fields = len(output_data)
non_empty = sum(1 for v in output_data.values() if v)
scores["completeness"] = non_empty / total_fields if total_fields > 0 else 0
# Depth: total content length across all output fields
total_length = 0
for value in output_data.values():
if isinstance(value, str):
total_length += len(value)
elif isinstance(value, (list, dict)):
total_length += len(json.dumps(value))
# Normalize: 500 chars is minimal, 5000 is good, cap at 1.0
scores["depth"] = min(1.0, total_length / 5000) if total_length > 0 else 0
# Structure: presence of structured data (lists, dicts, headers)
has_structure = 0
for value in output_data.values():
if isinstance(value, (list, dict)):
has_structure += 1
elif isinstance(value, str):
if re.search(r"^#+\s|\n-\s|\n\d+\.\s", value):
has_structure += 1
scores["structure"] = has_structure / total_fields if total_fields > 0 else 0
# Use existing eval score if available
if output.get("eval_score") is not None:
scores["eval"] = output["eval_score"]
else:
scores["eval"] = None
# Composite score
weights = {"completeness": 0.3, "depth": 0.3, "structure": 0.2}
weighted_sum = sum(scores[k] * w for k, w in weights.items())
total_weight = sum(weights.values())
if scores["eval"] is not None:
weighted_sum += scores["eval"] * 0.2
total_weight += 0.2
scores["composite"] = round(weighted_sum / total_weight, 3) if total_weight > 0 else 0
return scores
def rank_outputs(outputs):
"""Rank outputs by quality score."""
ranked = []
for output in outputs:
scores = score_output(output)
ranked.append({
**output,
"scores": scores,
})
ranked.sort(key=lambda x: -x["scores"]["composite"])
return ranked
def merge_synthesize(ranked_outputs):
"""Synthesize complementary outputs into a unified result."""
sections = {}
for output in ranked_outputs:
agent_id = output["agent_id"]
task = output.get("task", agent_id)
output_data = output.get("outputs", {})
for key, value in output_data.items():
section_key = key
if section_key not in sections:
sections[section_key] = {
"content": value,
"source": agent_id,
"score": output["scores"]["composite"],
}
else:
# If existing has lower score, prefer new
if output["scores"]["composite"] > sections[section_key]["score"]:
sections[section_key] = {
"content": value,
"source": agent_id,
"score": output["scores"]["composite"],
}
merged = {}
attribution = {}
for key, section in sections.items():
merged[key] = section["content"]
attribution[key] = section["source"]
return {
"strategy": "synthesize",
"merged_output": merged,
"attribution": attribution,
"sections_count": len(merged),
"sources_count": len(set(attribution.values())),
}
def merge_rank_select(ranked_outputs):
"""Select the best output from competing agents."""
if not ranked_outputs:
return {"strategy": "rank-select", "selected": None, "reason": "No outputs available"}
best = ranked_outputs[0]
return {
"strategy": "rank-select",
"selected_agent": best["agent_id"],
"selected_output": best["outputs"],
"score": best["scores"]["composite"],
"runner_up": ranked_outputs[1]["agent_id"] if len(ranked_outputs) > 1 else None,
"runner_up_score": ranked_outputs[1]["scores"]["composite"] if len(ranked_outputs) > 1 else None,
"total_candidates": len(ranked_outputs),
}
def merge_chain(session):
"""Use the output of the last agent in the pipeline."""
agents = session.get("agents", {})
# Find terminal agent (no dependents)
all_deps = set()
for agent_data in agents.values():
all_deps.update(agent_data.get("dependencies", []))
terminal_agents = [
aid for aid in agents
if aid not in all_deps and agents[aid].get("state") == "COMPLETED"
]
if not terminal_agents:
return {"strategy": "chain", "error": "No completed terminal agent found"}
# If multiple terminals, pick the one with highest eval score
best_terminal = None
best_score = -1
for aid in terminal_agents:
score = agents[aid].get("eval_score", 0) or 0
if score > best_score:
best_score = score
best_terminal = aid
return {
"strategy": "chain",
"terminal_agent": best_terminal,
"output": agents[best_terminal].get("outputs", {}),
"eval_score": best_score,
}
def format_human(result, action):
"""Format result for human output."""
output = []
output.append("=" * 60)
output.append("RESULT RANKER")
output.append("=" * 60)
if action == "list":
output.append(f"\nAgent Outputs ({len(result['outputs'])} completed)")
output.append("-" * 60)
for o in result["outputs"]:
output_keys = list(o.get("outputs", {}).keys())
output.append(f" {o['agent_id']}")
output.append(f" State: {o['state']} | Duration: {o.get('duration_s', 0)}s")
output.append(f" Outputs: {', '.join(output_keys[:5])}")
if o.get("eval_score") is not None:
output.append(f" Eval: {o['eval_score']}")
elif action == "rank":
output.append(f"\nRanked Outputs ({len(result['ranked'])} agents)")
output.append("-" * 60)
output.append(f" {'Rank':<6} {'Agent':<20} {'Score':>8} {'Comp':>6} {'Depth':>6} {'Struct':>6}")
output.append(f" {'─' * 6} {'─' * 20} {'─' * 8} {'─' * 6} {'─' * 6} {'─' * 6}")
for i, r in enumerate(result["ranked"], 1):
s = r["scores"]
output.append(
f" {i:<6} {r['agent_id']:<20} {s['composite']:>8.3f} "
f"{s['completeness']:>5.2f} {s['depth']:>5.2f} {s['structure']:>5.2f}"
)
elif action == "merge":
merge_result = result["merge_result"]
strategy = merge_result.get("strategy", "?")
output.append(f"\nMerge Strategy: {strategy}")
output.append("-" * 60)
if strategy == "synthesize":
output.append(f" Sections: {merge_result['sections_count']}")
output.append(f" Sources: {merge_result['sources_count']} agents")
output.append("\n Attribution:")
for key, source in merge_result.get("attribution", {}).items():
output.append(f" {key} <- {source}")
elif strategy == "rank-select":
output.append(f" Selected: {merge_result.get('selected_agent', '?')} (score: {merge_result.get('score', 0):.3f})")
if merge_result.get("runner_up"):
output.append(f" Runner-up: {merge_result['runner_up']} (score: {merge_result['runner_up_score']:.3f})")
elif strategy == "chain":
output.append(f" Terminal agent: {merge_result.get('terminal_agent', '?')}")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(
description="Rank and merge outputs from multiple agents.",
epilog="Example: python result_ranker.py --session session.json --rank",
)
parser.add_argument("--session", required=True, help="Path to session JSON file")
parser.add_argument("--list-outputs", action="store_true", help="List all agent outputs")
parser.add_argument("--rank", action="store_true", help="Rank outputs by quality")
parser.add_argument("--merge", choices=["synthesize", "rank-select", "chain"], help="Merge strategy")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
session_path = Path(args.session)
if not session_path.exists():
print(f"Error: Session file '{args.session}' not found.", file=sys.stderr)
sys.exit(1)
session = load_session(session_path)
outputs = extract_outputs(session)
if args.list_outputs:
result = {"outputs": outputs}
action = "list"
elif args.rank:
ranked = rank_outputs(outputs)
result = {"ranked": ranked}
action = "rank"
elif args.merge:
ranked = rank_outputs(outputs)
if args.merge == "synthesize":
merge_result = merge_synthesize(ranked)
elif args.merge == "rank-select":
merge_result = merge_rank_select(ranked)
elif args.merge == "chain":
merge_result = merge_chain(session)
result = {"merge_result": merge_result}
action = "merge"
else:
parser.print_help()
sys.exit(1)
if args.json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(format_human(result, action))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Manage orchestration sessions for multi-agent workflows.
Creates, updates, and queries session state. A session tracks the execution
lifecycle of a workflow including agent states, outputs, timing, and history.
Usage:
python session_manager.py create --workflow workflow.json --output session.json
python session_manager.py status --session session.json
python session_manager.py update --session session.json --agent researcher --state COMPLETED --output-data '{"result": "..."}'
python session_manager.py history --session session.json
python session_manager.py list --dir ./sessions/
"""
import argparse
import json
import os
import sys
import uuid
from datetime import datetime
from pathlib import Path
def load_json(path):
"""Load JSON from file."""
try:
with open(path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Error loading {path}: {e}", file=sys.stderr)
sys.exit(1)
def save_json(data, path):
"""Save JSON to file."""
with open(path, "w") as f:
json.dump(data, f, indent=2, default=str)
def cmd_create(args):
"""Create a new orchestration session from a workflow definition."""
workflow = load_json(args.workflow)
agents_def = workflow.get("agents", {})
config = workflow.get("config", {})
session_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
agents = {}
for agent_id, agent_def in agents_def.items():
deps = agent_def.get("dependencies", [])
agents[agent_id] = {
"state": "READY" if not deps else "PENDING",
"task": agent_def.get("task", ""),
"inputs": agent_def.get("inputs", []),
"expected_outputs": agent_def.get("outputs", []),
"dependencies": deps,
"outputs": None,
"started_at": None,
"completed_at": None,
"duration_s": None,
"retries": 0,
"max_retries": agent_def.get("config", {}).get("retries", config.get("retry_on_failure", 1)),
"eval_score": None,
"error": None,
}
session = {
"session_id": session_id,
"workflow_name": workflow.get("name", "unnamed"),
"workflow_description": workflow.get("description", ""),
"created_at": now,
"started_at": now,
"completed_at": None,
"state": "RUNNING",
"config": config,
"agents": agents,
"history": [
{"timestamp": now, "event": "session_created", "detail": f"Workflow: {workflow.get('name', 'unnamed')}"}
],
}
output_path = args.output or f"session-{session_id}.json"
save_json(session, output_path)
return {
"action": "create",
"session_id": session_id,
"workflow_name": workflow.get("name"),
"agents_count": len(agents),
"ready_agents": sum(1 for a in agents.values() if a["state"] == "READY"),
"output_file": output_path,
}
def cmd_status(args):
"""Show current session status."""
session = load_json(args.session)
agents = session.get("agents", {})
by_state = {}
for agent_data in agents.values():
state = agent_data.get("state", "UNKNOWN")
by_state[state] = by_state.get(state, 0) + 1
total = len(agents)
completed = by_state.get("COMPLETED", 0)
failed = by_state.get("FAILED", 0)
running = by_state.get("RUNNING", 0)
# Determine overall health
if session.get("state") == "COMPLETED":
health = "COMPLETE"
elif failed > 0 and running == 0 and by_state.get("PENDING", 0) > 0:
health = "BLOCKED"
elif failed > 0:
health = "AT_RISK"
elif running > 0:
health = "RUNNING"
else:
health = "IDLE"
# Elapsed time
started = session.get("started_at", "")
elapsed = 0
if started:
try:
elapsed = (datetime.now() - datetime.fromisoformat(started)).total_seconds()
except ValueError:
pass
return {
"action": "status",
"session_id": session.get("session_id"),
"workflow_name": session.get("workflow_name"),
"state": session.get("state"),
"health": health,
"total_agents": total,
"by_state": by_state,
"progress_pct": round(completed / total * 100, 1) if total else 0,
"elapsed_s": round(elapsed),
"agents": {
aid: {
"state": adata.get("state"),
"duration_s": adata.get("duration_s"),
"eval_score": adata.get("eval_score"),
}
for aid, adata in agents.items()
},
}
def cmd_update(args):
"""Update an agent's state in the session."""
session = load_json(args.session)
agents = session.get("agents", {})
if args.agent not in agents:
return {"action": "update", "error": f"Agent '{args.agent}' not found"}
agent = agents[args.agent]
old_state = agent["state"]
now = datetime.now().isoformat()
# Update state
agent["state"] = args.state
if args.state == "RUNNING" and not agent.get("started_at"):
agent["started_at"] = now
elif args.state == "COMPLETED":
agent["completed_at"] = now
if agent.get("started_at"):
try:
started = datetime.fromisoformat(agent["started_at"])
agent["duration_s"] = round((datetime.now() - started).total_seconds(), 1)
except ValueError:
pass
elif args.state == "FAILED":
agent["completed_at"] = now
agent["error"] = args.error_msg or "Unknown error"
# Update outputs if provided
if args.output_data:
try:
agent["outputs"] = json.loads(args.output_data)
except json.JSONDecodeError:
agent["outputs"] = {"raw": args.output_data}
# Update eval score if provided
if args.eval_score is not None:
agent["eval_score"] = args.eval_score
# Check if dependents can now be set to READY
newly_ready = []
if args.state == "COMPLETED":
for other_id, other_agent in agents.items():
if other_agent["state"] == "PENDING":
deps = other_agent.get("dependencies", [])
all_met = all(
agents.get(d, {}).get("state") == "COMPLETED"
for d in deps
)
if all_met:
other_agent["state"] = "READY"
newly_ready.append(other_id)
# Check if all agents are done
all_done = all(
a["state"] in ("COMPLETED", "FAILED", "SKIPPED")
for a in agents.values()
)
if all_done:
session["state"] = "COMPLETED"
session["completed_at"] = now
# Log event
event = {
"timestamp": now,
"event": "agent_state_change",
"agent": args.agent,
"old_state": old_state,
"new_state": args.state,
}
if newly_ready:
event["newly_ready"] = newly_ready
session.setdefault("history", []).append(event)
save_json(session, args.session)
return {
"action": "update",
"agent": args.agent,
"old_state": old_state,
"new_state": args.state,
"newly_ready": newly_ready,
"session_complete": all_done,
}
def cmd_history(args):
"""Show session event history."""
session = load_json(args.session)
history = session.get("history", [])
return {
"action": "history",
"session_id": session.get("session_id"),
"event_count": len(history),
"events": history,
}
def cmd_list(args):
"""List all sessions in a directory."""
sessions_dir = Path(args.dir)
if not sessions_dir.is_dir():
return {"action": "list", "error": f"'{args.dir}' is not a directory"}
sessions = []
for jf in sorted(sessions_dir.glob("session-*.json")):
try:
data = json.loads(jf.read_text())
sessions.append({
"file": str(jf),
"session_id": data.get("session_id"),
"workflow_name": data.get("workflow_name"),
"state": data.get("state"),
"created_at": data.get("created_at"),
"agents_count": len(data.get("agents", {})),
})
except (json.JSONDecodeError, OSError):
continue
return {"action": "list", "count": len(sessions), "sessions": sessions}
def format_human(result):
"""Format result for human output."""
action = result.get("action", "unknown")
lines = []
if "error" in result:
return f"Error: {result['error']}"
if action == "create":
lines.append(f"Session Created: {result['session_id']}")
lines.append(f" Workflow: {result['workflow_name']}")
lines.append(f" Agents: {result['agents_count']} ({result['ready_agents']} ready)")
lines.append(f" File: {result['output_file']}")
elif action == "status":
lines.append(f"Session: {result['session_id']} ({result['workflow_name']})")
lines.append(f" Health: {result['health']}")
lines.append(f" Progress: {result['progress_pct']}% ({result['by_state']})")
lines.append(f" Elapsed: {result['elapsed_s']}s")
lines.append("")
for aid, adata in result.get("agents", {}).items():
dur = f"{adata['duration_s']}s" if adata.get("duration_s") else "---"
lines.append(f" {aid:<20} {adata['state']:<12} {dur}")
elif action == "update":
lines.append(f"Updated: {result['agent']} ({result['old_state']} -> {result['new_state']})")
if result.get("newly_ready"):
lines.append(f" Newly ready: {', '.join(result['newly_ready'])}")
if result.get("session_complete"):
lines.append(" Session is now COMPLETE")
elif action == "history":
lines.append(f"Session History ({result['event_count']} events)")
lines.append("-" * 50)
for evt in result.get("events", []):
ts = evt.get("timestamp", "")[:19]
event_type = evt.get("event", "")
detail = evt.get("detail", "")
agent = evt.get("agent", "")
if agent:
lines.append(f" {ts} {event_type}: {agent} ({evt.get('old_state', '')} -> {evt.get('new_state', '')})")
else:
lines.append(f" {ts} {event_type}: {detail}")
elif action == "list":
lines.append(f"Sessions ({result['count']} found)")
lines.append("-" * 60)
for s in result.get("sessions", []):
lines.append(f" {s['session_id']} {s['workflow_name']:<20} {s['state']:<12} {s['agents_count']} agents")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Manage orchestration sessions for multi-agent workflows.",
)
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
sub = parser.add_subparsers(dest="command")
p_create = sub.add_parser("create", help="Create a new session")
p_create.add_argument("--workflow", required=True, help="Path to workflow JSON")
p_create.add_argument("--output", help="Output session file path")
p_status = sub.add_parser("status", help="Show session status")
p_status.add_argument("--session", required=True, help="Path to session JSON")
p_update = sub.add_parser("update", help="Update agent state")
p_update.add_argument("--session", required=True, help="Path to session JSON")
p_update.add_argument("--agent", required=True, help="Agent ID to update")
p_update.add_argument("--state", required=True,
choices=["PENDING", "READY", "RUNNING", "COMPLETED", "FAILED", "SKIPPED", "EVALUATING"])
p_update.add_argument("--output-data", help="Agent output data (JSON string)")
p_update.add_argument("--eval-score", type=float, help="Evaluation score (0-1)")
p_update.add_argument("--error-msg", help="Error message (for FAILED state)")
p_history = sub.add_parser("history", help="Show session event history")
p_history.add_argument("--session", required=True, help="Path to session JSON")
p_list = sub.add_parser("list", help="List sessions in a directory")
p_list.add_argument("--dir", required=True, help="Directory containing session files")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
commands = {
"create": cmd_create,
"status": cmd_status,
"update": cmd_update,
"history": cmd_history,
"list": cmd_list,
}
result = commands[args.command](args)
if args.json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(format_human(result))
if __name__ == "__main__":
main()
Sub-Skill: Agent Board
Parent: agenthub Trigger: "show board", "agent dashboard", "workflow progress"
Purpose
Display a real-time dashboard of agent status within a running workflow. Shows which agents are pending, running, completed, or failed, along with timing and dependency information.
Workflow
Step 1: Load Session State
Read the current session from session_manager.py:
python scripts/board_manager.py --session <session-id> --view boardStep 2: Render Board
Display a visual board organized by execution stage:
AgentHub Board - Session: abc123
Workflow: market-analysis
Started: 2026-04-02 10:30:00 | Elapsed: 2m 15s
COMPLETED (2) RUNNING (1) PENDING (1)
─────────────── ─────────────── ───────────────
[x] researcher [>] data_collector [ ] analyst
1m 30s 0m 45s... waiting on:
2 outputs data_collector
QUEUE: analyst (ready when data_collector completes)
CRITICAL PATH: researcher -> data_collector -> analyst -> writer (est. 8m)Step 3: Show Details (Optional)
Drill into a specific agent:
python scripts/board_manager.py --session <session-id> --agent researcher --detailShows:
- Agent task description
- Input data received
- Output data produced
- Execution log/timeline
- Retries (if any)
Step 4: Refresh
Board auto-updates as agents change state. In non-interactive mode, produces a snapshot.
Display Modes
| Mode | Description |
|---|---|
board | Kanban-style columns by state |
timeline | Gantt-style timeline view |
graph | DAG visualization with state colors |
summary | One-line-per-agent compact view |
Inputs
| Input | Required | Description |
|---|---|---|
| Session ID | Yes | Active orchestration session |
| View mode | No | board, timeline, graph, or summary (default: board) |
| Agent ID | No | Specific agent to detail |
Outputs
- Visual board display
- Per-agent status details
- Timing estimates for remaining work
Sub-Skill: Evaluate Agent Output
Parent: agenthub Trigger: "evaluate output", "check agent quality", "grade results"
Purpose
Evaluate the quality of an agent's output against defined criteria. Quality gates prevent low-quality outputs from propagating to downstream agents.
Workflow
Step 1: Define Evaluation Criteria
Each agent can have evaluation criteria in its config:
{
"eval_criteria": {
"completeness": "All required output fields populated",
"accuracy": "Claims supported by evidence",
"format": "Output matches expected JSON schema",
"relevance": "Output addresses the assigned task"
},
"quality_threshold": 0.7
}Step 2: Run Evaluation
Score the output on each criterion (0.0 to 1.0):
| Criterion | Check | Score |
|---|---|---|
| Completeness | All declared outputs present and non-empty | 0.0 - 1.0 |
| Format compliance | Output matches expected schema | Pass/Fail |
| Length adequacy | Output has sufficient depth (not trivially short) | 0.0 - 1.0 |
| Relevance | Output content relates to the task description | 0.0 - 1.0 |
| Consistency | Output does not contradict inputs | 0.0 - 1.0 |
Composite score = weighted average of criteria scores.
Step 3: Gate Decision
| Composite Score | Decision |
|---|---|
| >= threshold | PASS -- output flows to downstream agents |
| >= threshold - 0.15 | REVISE -- retry with feedback on what to improve |
| < threshold - 0.15 | FAIL -- agent marked as failed |
Step 4: Revision Feedback (If REVISE)
Generate specific feedback for the agent retry:
- Which criteria scored low
- What was missing or incorrect
- Concrete improvement instructions
Step 5: Log Evaluation
Record the evaluation result in the session for audit:
- Per-criterion scores
- Composite score
- Decision (PASS/REVISE/FAIL)
- Feedback provided (if any)
Inputs
| Input | Required | Description |
|---|---|---|
| Agent output | Yes | The output to evaluate |
| Eval criteria | Yes | Criteria from agent config |
| Quality threshold | No | Override default threshold |
Outputs
- Per-criterion scores
- Composite quality score
- Gate decision (PASS / REVISE / FAIL)
- Revision feedback (if applicable)
Sub-Skill: Initialize Workflow
Parent: agenthub Trigger: "create multi-agent workflow", "design agent DAG", "initialize orchestration"
Purpose
Create a new multi-agent workflow definition. Guides the user through decomposing a complex task into agent nodes, defining dependencies, and validating the resulting DAG.
Workflow
Step 1: Decompose the Task
Break the overall objective into discrete sub-tasks:
- Each sub-task should be completable by a single agent in one session
- Sub-tasks should have clear inputs and outputs
- Identify which sub-tasks can run in parallel vs must be sequential
Step 2: Define Agents
For each sub-task, define an agent node:
{
"id": "agent-name",
"task": "Clear description of what this agent does",
"inputs": ["list of required inputs"],
"outputs": ["list of produced outputs"],
"dependencies": ["ids of agents that must complete first"],
"config": {
"timeout": 300,
"retries": 1,
"quality_threshold": 0.7
}
}Step 3: Build Dependency Graph
Map the edges:
- Root nodes: agents with no dependencies (start first)
- Terminal nodes: agents with no dependents (feed into merge)
- Ensure every output needed by a downstream agent is produced by an upstream agent
Step 4: Validate
Run dag_analyzer.py on the definition:
python scripts/dag_analyzer.py --workflow workflow.json --validateChecks:
- No cycles in the graph
- All input references resolve to upstream outputs
- No orphaned nodes (unreachable from root or terminal)
- Critical path length is reasonable
Step 5: Save Definition
Write the validated workflow definition to a JSON file for execution.
Inputs
| Input | Required | Description |
|---|---|---|
| Task description | Yes | The overall objective to decompose |
| Max parallel | No | Maximum concurrent agents (default: 3) |
| Timeout | No | Per-agent timeout in seconds (default: 300) |
Outputs
- Validated workflow definition (JSON)
- DAG visualization (text-based)
- Critical path analysis
- Estimated execution time
Sub-Skill: Merge Outputs
Parent: agenthub Trigger: "merge agent outputs", "combine results", "synthesize findings"
Purpose
Merge outputs from multiple agents into a coherent final deliverable. Handles different merge strategies depending on whether outputs are complementary (different aspects) or competing (same aspect, multiple attempts).
Workflow
Step 1: Identify Merge Strategy
| Pattern | Strategy | Description |
|---|---|---|
| Fan-in | Synthesize | Each agent covers different aspect; weave together |
| Reducer | Rank and select | Multiple agents did same task; pick best |
| Pipeline | Chain | Each agent transforms previous output; use final |
| Validator | Conditional | Use output only if validation passed |
Step 2: Collect Terminal Outputs
Gather all outputs from terminal nodes (agents with no dependents):
python scripts/result_ranker.py --session <session-id> --list-outputsStep 3: Apply Merge
Synthesize (fan-in):
- Organize outputs by topic/section
- Identify overlaps and contradictions
- Create a unified document with proper transitions
- Cite which agent produced each section
Rank and select (reducer):
- Score each output using eval criteria
- Select the highest-scoring output
- Optionally incorporate unique good ideas from runner-ups
Chain (pipeline):
- The last agent's output is the final result
- Verify it incorporates all upstream transformations
Step 4: Quality Check
Run a final eval on the merged output:
- Does it address the original objective?
- Is it internally consistent?
- Does it incorporate key findings from all agents?
Step 5: Format Final Output
Produce the deliverable in the requested format:
- Structured JSON for programmatic consumption
- Markdown document for human reading
- Summary + detailed sections for executive review
Inputs
| Input | Required | Description |
|---|---|---|
| Terminal outputs | Yes | Outputs from all terminal agents |
| Merge strategy | No | Auto-detected from DAG pattern |
| Output format | No | JSON, markdown, or structured (default: markdown) |
Outputs
- Merged final deliverable
- Merge report (what was combined, any conflicts resolved)
- Attribution (which agent contributed which section)
Sub-Skill: Execute Workflow
Parent: agenthub Trigger: "run workflow", "execute the agents", "start orchestration"
Purpose
Execute a validated workflow definition end-to-end. Manages the execution lifecycle including agent spawning, dependency resolution, output passing, and final merge.
Workflow
Step 1: Load and Validate
Load the workflow definition and run a final validation pass. Ensure all dependencies are satisfiable and configurations are complete.
Step 2: Initialize Session
Create an orchestration session using session_manager.py:
python scripts/session_manager.py create --workflow workflow.jsonThis creates a session record tracking:
- Session ID and start time
- Agent states (all start as PENDING)
- Output storage locations
- Execution log
Step 3: Execute Topological Order
Process agents in dependency order: 1. Identify all READY agents (dependencies met) 2. Spawn up to max_parallel agents simultaneously 3. Monitor running agents for completion or timeout 4. On completion: store output, update state, check dependents 5. On failure: retry if configured, or mark FAILED and SKIP dependents
Step 4: Pass Outputs
When an agent completes:
- Validate output format matches the declared schema
- Store output in session state
- Check if any PENDING agents now have all dependencies met
- Transition those agents to READY
Step 5: Final Merge
When all terminal agents complete:
- Collect their outputs
- Invoke the merge sub-skill (or merge agent if defined)
- Produce the final combined result
Step 6: Report
Generate an execution report:
- Total wall-clock time
- Per-agent execution times
- Parallelization efficiency
- Quality scores if eval was run
- Any failures or retries
Inputs
| Input | Required | Description |
|---|---|---|
| Workflow file | Yes | Path to validated workflow JSON |
| Input data | Yes | Initial inputs for root agents |
| Dry run | No | Simulate without actually executing |
Outputs
- Final merged result
- Per-agent outputs
- Execution report with timing and quality metrics
- Session record for audit trail
Sub-Skill: Spawn Agent
Parent: agenthub Trigger: "spawn agent", "create agent instance", "start sub-agent"
Purpose
Spawn an individual agent within a workflow. Prepares the agent's context, passes inputs from upstream agents, monitors execution, and captures outputs.
Workflow
Step 1: Prepare Context
Assemble the agent's execution context:
- Task description from the workflow definition
- Input data from upstream agent outputs
- Constraints: timeout, output format, quality expectations
- Relevant reference material (if specified)
Step 2: Configure Agent
Set agent parameters:
Agent ID: researcher-001
Task: Research competitor landscape
Inputs: { product_description: "..." }
Expected output: { competitor_list: [], market_size: {} }
Timeout: 300s
Retries: 1Step 3: Execute
Launch the agent and monitor:
- Start the agent with prepared context
- Track execution time
- Capture stdout/output as it streams
- Watch for timeout
Step 4: Capture Output
On completion:
- Parse agent output into structured format
- Validate output matches expected schema
- Store in session state keyed by agent ID
- Transition state to COMPLETED
On failure:
- Capture error message and stack trace
- If retries remain, re-spawn with error context
- If no retries, transition to FAILED
Step 5: Notify Orchestrator
Report back to the workflow engine:
- Agent state transition
- Output availability
- Duration and resource usage
- Quality signals (if evaluatable)
Inputs
| Input | Required | Description |
|---|---|---|
| Agent definition | Yes | From workflow definition |
| Upstream outputs | Yes | Data from completed dependencies |
| Session ID | Yes | Orchestration session reference |
Outputs
- Agent output in structured format
- Execution metadata (duration, retries, state)
- Error details (if failed)
Sub-Skill: Workflow Status
Parent: agenthub Trigger: "workflow status", "orchestration health", "how is the run going"
Purpose
Report the overall status and health of a workflow execution. Provides a compact summary of progress, timing, failures, and estimated completion.
Workflow
Step 1: Load Session
python scripts/session_manager.py status --session <session-id>Step 2: Compute Metrics
| Metric | Calculation |
|---|---|
| Progress | completed_agents / total_agents * 100% |
| Elapsed time | now - session_start |
| Est. remaining | critical_path_remaining * avg_agent_duration |
| Parallelization | time_agents_ran_parallel / total_agent_time |
| Failure rate | failed_agents / attempted_agents |
| Quality avg | mean(eval_scores) for completed agents |
Step 3: Determine Health
| Condition | Health Status |
|---|---|
| All agents on track, no failures | HEALTHY |
| Minor delays but progressing | DEGRADED |
| Agent failed, retrying | AT_RISK |
| Multiple failures, blocked | CRITICAL |
| All agents complete | COMPLETE |
Step 4: Display Status
Workflow Status: market-analysis
Session: abc123
Health: HEALTHY
Progress: ████████░░░░░░░░ 50% (2/4 agents)
Elapsed: 3m 15s
Est. remaining: 4m 00s
Agents:
researcher COMPLETED 1m 30s
data_collector COMPLETED 1m 45s
analyst RUNNING 0m 30s...
writer PENDING (waiting: analyst)
Quality: 0.85 avg (2 evaluated)
Failures: 0
Retries: 0Step 5: Recommendations
If health is not HEALTHY:
- Suggest timeout adjustments
- Identify bottleneck agents
- Recommend retry or skip strategies
Inputs
| Input | Required | Description |
|---|---|---|
| Session ID | Yes | Active orchestration session |
| Verbose | No | Show per-agent details (default: summary) |
Outputs
- Health status with color indicator
- Progress percentage and agent counts
- Timing breakdown (elapsed, estimated remaining)
- Quality metrics
- Actionable recommendations (if issues detected)
Related skills
FAQ
What are agenthub's sub-skills?
Init, Run, Spawn, Board, Eval, Merge, and Status, each handling one stage of the orchestration lifecycle.
How does it validate a workflow?
dag_analyzer.py checks the dependency graph for cycles, unreachable nodes, missing inputs, and critical-path length.