How Strix Multi-Agent Orchestration Works: Dynamic Graph-Based Coordination

Strix coordinates multiple agents through a dynamic, in-memory graph where a root agent spawns sub-agents via LLM-invoked tools that mutate shared state, enabling parallel security scanning with structured inter-agent messaging.

The open-source security scanning framework Strix (usestrix/strix) implements a sophisticated multi-agent architecture that allows autonomous delegation of sub-tasks. Unlike static pipelines, Strix's multi-agent orchestration builds a runtime graph where agents communicate through tool-based mutations, enabling dynamic reconnaissance and vulnerability assessment workflows.

Core Architecture Components

The Agent Graph

At the heart of Strix's orchestration lies _agent_graph, a module-level dictionary defined in strix/tools/agents_graph/agents_graph_actions.py. This structure stores nodes representing agents and edges representing delegations or message flows. Each node contains critical metadata including the agent's ID, name, task description, current status, timestamps, and the latest state dump.

The graph serves as the single source of truth for the entire scan lifecycle. When the root agent initializes, it populates the first node; subsequent sub-agents register themselves automatically upon creation via the create_agent tool.

Root Agent and Sub-Agents

Root Agent: When StrixAgent.__init__ detects no parent ID in strix/agents/StrixAgent/strix_agent.py (lines 13-18), it automatically appends "root_agent" to the default_skills list. This designation grants exclusive access to the finish_scan tool and the authority to spawn child agents.

Sub-Agents: Created through the create_agent tool (lines 87-120 in agents_graph_actions.py), these instances inherit their parent's LLM configuration optionally and execute in isolated daemon threads with separate asyncio event loops. The tool constructs a fresh AgentState object, initializes a new StrixAgent, and launches it via _run_agent_in_thread (lines 124-160).

Tool-Based Orchestration Flow

The entire coordination mechanism operates through five primary tools registered in strix/tools/registry.py:

  1. create_agent: Spawns new agents with inherited or fresh contexts
  2. send_message_to_agent: Creates edges in the graph and entries in _agent_messages
  3. wait_for_message: Pauses execution until a response arrives or timeout elapses
  4. agent_finish: Terminates sub-agents and reports back to parents via structured XML
  5. finish_scan: Root-only finalization that validates no active agents remain before writing results

Step-by-Step Execution

Initialization: The user triggers a scan via CLI, instantiating the root StrixAgent. The agent enters its agent_loop, processing LLM responses and invoking tools based on reasoning.

Delegation: When the LLM determines a sub-task is needed (e.g., subdomain enumeration), it outputs a tool call:

{
    "tool_name": "create_agent",
    "args": {
        "task": "Enumerate all subdomains for example.com",
        "name": "SubdomainEnumerator",
        "inherit_context": true,
        "skills": "dns_bruteforce,sublist3r"
    }
}

The tool constructs the agent state:


# From agents_graph_actions.py

state = AgentState(
    task=args["task"],
    agent_name=args["name"],
    parent_id=agent_state.agent_id,
    max_iterations=300,
    waiting_timeout=600,
)
agent = StrixAgent({"llm_config": llm_config, "state": state})
thread = threading.Thread(target=_run_agent_in_thread,
                          args=(agent, state, inherited_messages),
                          daemon=True)
thread.start()

Inter-Agent Communication: Sub-agents exchange data through send_message_to_agent (lines 86-133). This tool creates a message entry in _agent_messages and records the relationship as a graph edge. When the receiver's loop calls _check_agent_messages (lines 47-88 in base_agent.py), the message is injected into the conversation history as a special XML block, prompting the LLM to react.

Waiting and Resuming: Agents can pause execution using wait_for_message (lines 71-108). This tool marks the node status as "waiting" and invokes AgentState.enter_waiting_state. The agent resumes when a new message arrives or the timeout expires, enabling synchronous-style coordination across asynchronous threads.

Completion: Sub-agents call agent_finish (lines 58-115), which updates the graph status to "completed", sends a structured report to the parent agent, and removes the thread from _running_agents. The root agent ultimately invokes finish_scan from finish_actions.py, which validates no active agents remain via _check_active_agents (lines 17-74) before finalizing the telemetry tracer.

State Management and Thread Safety

Each agent maintains an AgentState object (strix/agents/state.py) tracking messages, actions, observations, errors, iteration counters, and sandbox information. The system guarantees consistency through three mechanisms:

  • GIL-Protected Mutations: The _agent_graph is only modified while holding Python's Global Interpreter Lock, ensuring atomic updates across threads
  • Explicit Status Fields: Nodes maintain status strings (running, waiting, stopping, completed, error) that the TUI monitors for live orchestration views
  • Isolated Event Loops: Each agent runs in its own thread with a dedicated asyncio loop, preventing blocking operations from stalling the graph

Practical Implementation Examples

Creating a Sub-Agent with Context Inheritance

When the root agent detects a complex task, it delegates to a specialized sub-agent:


# LLM tool invocation

create_agent(
    agent_state=current_state,
    task="Perform port scan on discovered subdomains",
    name="PortScanner",
    inherit_context=True,
    skills="nmap,masscan"
)

The new agent appears in the graph:

{
  "id": "agent_9f2a3c1e",
  "name": "PortScanner",
  "task": "Perform port scan on discovered subdomains",
  "status": "running",
  "parent_id": "agent_7b1d4e9a",
  "created_at": "2024-01-15T10:30:00Z"
}

Sending Results Between Peers

A sub-agent completing enumeration can notify a vulnerability scanner:


# In agents_graph_actions.py

send_message_to_agent(
    agent_state=enumerator_state,
    target_agent_id="agent_3c7d5f8b",
    message="Found 42 subdomains: [list attached]",
    message_type="information",
    priority="high"
)

The receiving agent's next iteration receives:

<message from="PortScanner" type="information" priority="high">
Found 42 subdomains: [list attached]
</message>

Finalizing the Scan

Only the root agent can execute the completion sequence:


# From finish_actions.py

finish_scan(
    agent_state=root_state,
    executive_summary="Assessment identified 3 critical vulnerabilities",
    methodology="Automated scanning with manual validation",
    technical_analysis="Detailed CVE analysis...",
    recommendations="Immediate patching required"
)

Summary

  • Dynamic Graph Structure: Strix maintains runtime agent relationships in _agent_graph, enabling non-linear, adaptive security workflows
  • Tool-Driven Coordination: All orchestration occurs through LLM-invoked tools (create_agent, send_message_to_agent, finish_scan) that mutate shared state in agents_graph_actions.py
  • Thread-Isolated Execution: Each agent operates in a daemon thread with its own asyncio loop, communicating via message queues and graph edges
  • Hierarchical Control: The root agent (identified by default_skills=["root_agent"] in strix_agent.py) exclusively manages scan finalization while sub-agents handle delegated reconnaissance tasks
  • Structured Messaging: Inter-agent communication uses XML-injected messages processed through _check_agent_messages in base_agent.py

Frequently Asked Questions

How does Strix prevent race conditions when multiple agents access the shared graph?

Strix relies on Python's Global Interpreter Lock (GIL) to ensure atomic mutations of the _agent_graph dictionary. Since all graph updates in agents_graph_actions.py occur within standard Python execution contexts, the GIL prevents concurrent writes. Additionally, each agent maintains its own AgentState instance, minimizing shared mutable state beyond the orchestration graph itself.

Can sub-agents create their own child agents?

Yes. The create_agent tool is available to any agent with appropriate skills. When a sub-agent invokes this tool, the new agent receives the invoking agent's ID as its parent_id, creating a hierarchical tree in the graph. The system supports unlimited depth, though practical limits are enforced by the root agent's max_iterations constraints and the _check_active_agents validation in finish_actions.py.

What happens if a sub-agent encounters an error or timeout?

The AgentState tracks errors in its errors list. If an agent exceeds its max_iterations or encounters an exception, the agent_loop in base_agent.py transitions the node status to "error" in the graph. The parent agent can detect this through wait_for_message timeouts or by querying the graph, allowing for error handling strategies like retrying the task with a new agent or adjusting the scan scope.

How does the orchestration system integrate with the TUI for real-time monitoring?

The terminal user interface reads the _agent_graph dictionary directly, polling node statuses (running, waiting, completed) to render live views of the agent hierarchy. Since the graph maintains timestamps and state dumps for each node, the UI can display task progress, message history, and error counts without blocking the underlying agent threads.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →