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

> Discover how Strix multi-agent orchestration enables dynamic coordination. Learn about its graph-based approach for parallel security scanning and structured messaging.

- Repository: [Strix/strix](https://github.com/usestrix/strix)
- Tags: internals
- Published: 2026-03-26

---

**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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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:

```python
{
    "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:

```python

# 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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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:

```python

# 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:

```json
{
  "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:

```python

# 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:

```xml
<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:

```python

# 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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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`](https://github.com/usestrix/strix/blob/main/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.