# Sub-agents are just wrong

> Polylane's autofix used to be a workflow of multiple agents, each responsible for a single task, with an orchestrator managing state between them. We moved to a single agent which does the entire job, from evidence gathering, hypothesis testing to pull request.

Published: September 14, 2026
Authors: Aleksandr Diamond and Boris Tane
Canonical: https://polylane.com/blog/sub-agents-are-just-wrong/

Polylane's autofix agent was originally built as a workflow of multiple sub-agents, each responsible for a single task:
- a triage to sort through thousands of alerts and signals
- a coordinator to operate sub-agents
- up to fifteen sub-agents to investigate all possible hypotheses for the issue
- and a coding agent to ultimately submite a pull request

```mermaid
graph TB
    S["Alerts and signals"] --> T["Triage agent"]
    T -->|"confirmed issue"| C["Coordinator agent"]
    C -->|"hypotheses"| H["Up to 15 hypothesis sub-agents"]
    H -->|"verdicts"| C
    C -->|"plan"| A["Coding agent"]
    A --> PR["Pull request"]
    style C fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
    style H fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
```

A confirmed issue could lead to up to 18 agents and sub-agents for the investigation and the fix. Today the same work is done by a single agent.

The workflow was a sound design based on what we knew at the time: small prompts, small tool sets, an orchestrator to carry results between specialists. It was however extremely expensive and difficult to operate and reason about.

## What is an issue?

Polylane scans logs, metrics and traces from every cloud resource (Lambda function, Cloudflare Worker, Vercel project, etc.) on every connected provider. For each resource we store baselines across multiple horizons, such that we can capture seasonality in the data. We then assess the current state of the cloud resource and compare it with historical data. Values that break the baseline are recorded as issues.

Issues can also be created by alerts we receive from observability and error tracking solutions.

```mermaid
graph TB
    R["Cloud resource"] -->|"telemetry"| B["Compare with its baselines"]
    A["Alert from an observability<br/>or error tracking provider"] --> I
    B -->|"breaks the baseline"| I["Issue"]
    B -->|"within the baseline"| N["No issue"]
    style I fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
```

## How do we resolve issues?

To resolve an issue, we need to:

1. **Triage:** Gather data to confirm or reject the issue.
2. **Investigate:** Assess multiple hypotheses and determine the root cause.
3. **Open a pull request or escalate.** Either write a pull request to resolve the issue, escalate to an engineer if the fix is not a code change, or write a report highlighting the findings and stop.

The vast majority of runs terminate before a pull request or an escalation, when Polylane concludes the issue is either a false positive or benign.

```mermaid
graph TB
    I["Triage the issue"] --> V["Investigate the root cause"]
    V -->|"dismiss, fold, or reopen"| E["Report and stop"]
    V -->|"confirm"| X["Open a pull request"]
    V -->|"confirm, no code change"| Z["Escalate to an engineer"]
    style V fill:#fef3c7,stroke:#fcd34d,color:#78350f
    style X fill:#d1fae5,stroke:#6ee7b7,color:#065f46
```

## Sub-agents architecture

Our initial architecture was based on multiple sub-agents.

```mermaid
graph TB
    F["Finding"] --> T["Triage agent"]
    T -->|"confirm"| C["Orchestrator agent"]
    C -->|"hypotheses"| W["Fan-out workflow"]
    W --> H1["Hypothesis agent 1"]
    W --> H2["Hypothesis agent 2"]
    W --> H3["Hypothesis agent ..."]
    W --> H15["Hypothesis agent 15"]
    H1 --> AG["Vote and summarize"]
    H2 --> AG
    H3 --> AG
    H15 --> AG
    AG -->|"summary as a message"| C
    C -->|"confirmed hypothesis"| AF["Coding agent"]
    AF --> PR["Pull request"]
    style C fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
    style AG fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
```

We used an orchestrator agent to cordinate work between multiple sub-agents. Sub-agents were instanciated to investigate hypotheses, trying to prove or disprove each hypothesis with different starting points (for example starting by looking at the logs, or starting with the codebase, etc.).

The findings of the sub-agents were voted with simple arithmetic and summarised by another agent before being rolled up to the coordinator agent.

Each hypothesis's verdict was a confidence-weighted vote across its passes.

```ts
const CONFIDENCE_RANK = { definitive: 4, strong: 3, moderate: 2, weak: 1, speculative: 0 };

function aggregatePassVerdicts(passes: PassResult[]): Verdict {
  const weights: Record<string, number> = {};
  const counts: Record<string, number> = {};
  for (const p of passes) {
    weights[p.verdict] = (weights[p.verdict] ?? 0) + CONFIDENCE_RANK[p.confidence];
    counts[p.verdict] = (counts[p.verdict] ?? 0) + 1;
  }
  const totalWeight = Object.values(weights).reduce((a, b) => a + b, 0);
  const [winner, winnerWeight] = Object.entries(weights).sort((a, b) => b[1] - a[1])[0];

  const countMajority = counts[winner] > passes.length / 2;
  const weightMajority = winnerWeight > totalWeight / 2;
  if (!countMajority && !weightMajority) {
    return { verdict: "inconclusive", confidence: "weak", summary: `No consensus across ${passes.length} passes.` };
  }

  const majority = passes.filter((p) => p.verdict === winner);
  const confidence = majority.reduce((min, p) => (CONFIDENCE_RANK[p.confidence] < CONFIDENCE_RANK[min] ? p.confidence : min), majority[0].confidence);
  const lead = majority.reduce((best, p) => (CONFIDENCE_RANK[p.confidence] > CONFIDENCE_RANK[best.confidence] ? p : best));
  return { verdict: winner, confidence, summary: lead.summary };
}
```

Once all hypotheses were investigated, if a root cause was identified, the coordinator would either escalate to an engineer or write a plan for the fix. This plan would be handed to a coding sub-agent to implement the fix and submit the pull request.

The main issue in this architecture is the context loss between sub-agents. Each agent was passing up or down just fractions of their context, through summarization, and both upstream and downstream agents regularly had to redo the same work.

```mermaid
graph TB
    I["Issue"] --> O["Orchestrator<br/>splits the issue into briefs"]
    O -->|"brief A"| A["Sub-agent A<br/>investigates brief A"]
    O -->|"brief B"| B["Sub-agent B<br/>investigates brief B"]
    A -.->|"summary A"| M["Orchestrator, later<br/>writes the plan from the summaries"]
    B -.->|"summary B"| M
    M -.->|"plan"| F["Coding agent<br/>writes the fix"]
    F --> PR["Pull request"]
    style I fill:none,stroke:none
    style PR fill:none,stroke:none
    style O stroke:#4C8C57,color:#3d7046
    style A stroke:#3b7dd8,color:#2b5fa8
    style B stroke:#7c5cd6,color:#5b3fb0
    style M stroke:#a07d1c,color:#7a5f14
    style F stroke:#6b7280,color:#4b5563
    %% aside O right #4C8C57 Issue, alerts and history
    %% aside A left #3b7dd8 Brief A and its evidence
    %% aside B right #7c5cd6 Brief B and its evidence
    %% aside M right #a07d1c History and two summaries, no evidence
    %% aside F right #6b7280 The plan, nothing else
```

Moreover, the agent writing the fix only had the plan, lacking context on the original issue or the evidence collected in the investigation. This led to sub-par pull requests which often focused on symptoms rather than the root cause.

We chose this architecture primarily because when we first designed it in March 2026, frontier models were not always capable of running an investigation end-to-end, including writing the fix, and also plainly, our harness was not that good.

It became increasingly harder to build on this foundation because:

- **Debugging took many traces.** Reasoning around an end-to-end investigation required looking through multiple, sometimes dozens of traces
- **Evaluation was per stage.** Each sub-agent could be evaluated individually and pass, while the end result was poor, because the failures lived in the handoffs.

We iterated on this architecture for months, improving each agent individually and tweaking the prompts and the handoffs, but the results never matched the effort. It was expensive, hard to debug, and the pull requests were not good enough.

## Single agent

On Sept 3, we replaced the sub-agents workflow with a single agent doing everything from triage to pull request. The same agent collects the evidence, investigates all the hypotheses, and submits the pull request.

```mermaid
graph TB
    F["Issue"] --> A["Single agent"]
    A --> I["Triage"]
    I --> B["Investigate"]
    B --> V["One verdict"]
    V -->|"confirm"| X["Clone, edit, validate in the sandbox"]
    X --> PR["Open the pull request"]
    V -->|"dismiss, fold, or reopen"| E["Report and stop"]
    V -->|"confirm, no code change"| Z["Escalate to an engineer"]
    style A fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style PR fill:#d1fae5,stroke:#6ee7b7,color:#065f46
```

We deleted the triage agent, the coordinator, the hypothesis agents, and the autofix agent, along with the workflows that carried results between them and the gates that sat in front of the pull request. The operational benefits were immediate: one trace to evaluate, and no summaries between sub-agents.

More importantly, the quality of the pull requests improved, and the time between detecting an issue and opening the pull request that fixes it collapsed. Over the month around the cutover, the median went from 2.2 hours to 35 minutes and the p90 from nine days to under two hours. Under the pipeline a finding typically sat for multiple hours in a chain of workflows, each waiting on the last. Under the single agent, the pull request opens just minutes after the issue is detected.

**Figure 1. Hours from detecting an issue to opening its pull request**

Boundary at 3 Sep (3 Sep: one agent). The drawn axis is not to scale.

| Day | Daily median | p90 |
| --- | --- | --- |
| 14 Aug | 2.3 h | 2.7 h |
| 15 Aug | 5.6 h | 5.9 h |
| 16 Aug | 1.2 h | 4.2 d |
| 17 Aug | 38 min | 4.5 d |
| 18 Aug | 20 min | 27 min |
| 19 Aug | 49 min | 12.2 h |
| 20 Aug | 1 h | 35.9 h |
| 21 Aug | 44.2 h | 4.2 d |
| 22 Aug | 7.7 d | 10.4 d |
| 23 Aug | 32 min | 35.3 h |
| 24 Aug | 2.5 d | 5.6 d |
| 25 Aug | 1.5 h | 13.4 h |
| 26 Aug | 1.5 h | 2.1 h |
| 27 Aug | 1.9 h | 2.5 h |
| 28 Aug | 11.7 d | 14.1 d |
| 29 Aug | 26.1 h | 10.4 d |
| 30 Aug | 2 d | 2.8 d |
| 31 Aug | 9 d | 10.4 d |
| 1 Sep | 4.2 d | 4.2 d |
| 2 Sep | 6.7 d | 6.7 d |
| 3 Sep | 29 min | 2.6 d |
| 4 Sep | 23 min | 5.4 h |
| 5 Sep | 34 min | 3.4 d |
| 6 Sep | 34 min | 43.7 h |
| 7 Sep | 32 min | 4.5 h |
| 8 Sep | 41 min | 19.4 h |
| 9 Sep | 34 min | 1.2 h |
| 10 Sep | 34 min | 1.3 h |
| 11 Sep | 56 min | 2.1 h |
| 12 Sep | 35 min | 1.3 h |
| 13 Sep (partial day) | 30 min | 55 min |

We now also open far more pull requests. When we were using sub-agents, 0.6% of detected issues ended in a pull request. Under the single agent it is 4.2%, and rising. The difference is failure modes. A pipeline with multiple sub-agents has to survive every handoff: triage has to promote the finding, the coordinator has to produce hypotheses, the fan-out has to reach a verdict, etc. Each handoff is a potential failure mode.

**Figure 2. Share of detected issues that ended in a pull request**

Boundary at 3 Sep (3 Sep: one agent).

| Day | Share with a pull request |
| --- | --- |
| 14 Aug | 1.9% |
| 15 Aug | 1.1% |
| 16 Aug | 2.3% |
| 17 Aug | 1.5% |
| 18 Aug | 0.9% |
| 19 Aug | 0.7% |
| 20 Aug | 0.7% |
| 21 Aug | 0.8% |
| 22 Aug | 0.8% |
| 23 Aug | 1.3% |
| 24 Aug | 0.6% |
| 25 Aug | 1.4% |
| 26 Aug | 0.3% |
| 27 Aug | 0.7% |
| 28 Aug | 1% |
| 29 Aug | 1.5% |
| 30 Aug | 0.5% |
| 31 Aug | 0.2% |
| 1 Sep | 0.1% |
| 2 Sep | 0.1% |
| 3 Sep | 0.4% |
| 4 Sep | 2.4% |
| 5 Sep | 2.9% |
| 6 Sep | 1.4% |
| 7 Sep | 1.8% |
| 8 Sep | 4% |
| 9 Sep | 7.6% |
| 10 Sep | 5% |
| 11 Sep | 5.4% |
| 12 Sep | 9.1% |
| 13 Sep (partial day) | 8.2% |

Cost per pull request fell as well. Every run now starts on the stronger model, so a dismissed finding costs more than it did under the pipeline. But, the average cost per pull request went from $111 to about $18 in the first nine days of the single agent. This drop is not exclusively the result of the change in architecture, as we're actively iterating on every aspect of Polylane. Some of it is the ordinary drift of a system under constant iteration.

**Figure 3. Model spend per pull request opened**

Boundary at 3 Sep (3 Sep: one agent).

| Day | Spend per pull request |
| --- | --- |
| 14 Aug | $144 |
| 15 Aug | $270 |
| 16 Aug | $121 |
| 17 Aug | $120 |
| 18 Aug | $57 |
| 19 Aug | $157 |
| 20 Aug | $202 |
| 21 Aug | $211 |
| 22 Aug | $563 |
| 23 Aug | $205 |
| 24 Aug | $90 |
| 25 Aug | $43 |
| 26 Aug | $45 |
| 27 Aug | $58 |
| 28 Aug | $49 |
| 29 Aug | $32 |
| 30 Aug | $51 |
| 31 Aug | $77 |
| 1 Sep | $169 |
| 2 Sep | $93 |
| 3 Sep | $69 |
| 4 Sep | $25 |
| 5 Sep | $15 |
| 6 Sep | $23 |
| 7 Sep | $16 |
| 8 Sep | $26 |
| 9 Sep | $15 |
| 10 Sep | $17 |
| 11 Sep | $12 |
| 12 Sep | $3.46 |
| 13 Sep (partial day) | $2.88 |

**Every value behind the three charts, by UTC day**

| Day | Detected issues with a pull request | Median | p90 | Spend per pull request |
| --- | --- | --- | --- | --- |
| 14 Aug | 1.9% | 2.3 h | 2.7 h | $144 |
| 15 Aug | 1.1% | 5.6 h | 5.9 h | $270 |
| 16 Aug | 2.3% | 1.2 h | 4.2 d | $121 |
| 17 Aug | 1.5% | 38 min | 4.5 d | $120 |
| 18 Aug | 0.9% | 20 min | 27 min | $57 |
| 19 Aug | 0.7% | 49 min | 12.2 h | $157 |
| 20 Aug | 0.7% | 1 h | 35.9 h | $202 |
| 21 Aug | 0.8% | 44.2 h | 4.2 d | $211 |
| 22 Aug | 0.8% | 7.7 d | 10.4 d | $563 |
| 23 Aug | 1.3% | 32 min | 35.3 h | $205 |
| 24 Aug | 0.6% | 2.5 d | 5.6 d | $90 |
| 25 Aug | 1.4% | 1.5 h | 13.4 h | $43 |
| 26 Aug | 0.3% | 1.5 h | 2.1 h | $45 |
| 27 Aug | 0.7% | 1.9 h | 2.5 h | $58 |
| 28 Aug | 1% | 11.7 d | 14.1 d | $49 |
| 29 Aug | 1.5% | 26.1 h | 10.4 d | $32 |
| 30 Aug | 0.5% | 2 d | 2.8 d | $51 |
| 31 Aug | 0.2% | 9 d | 10.4 d | $77 |
| 1 Sep | 0.1% | 4.2 d | 4.2 d | $169 |
| 2 Sep | 0.1% | 6.7 d | 6.7 d | $93 |
| 3 Sep (one agent) | 0.4% | 29 min | 2.6 d | $69 |
| 4 Sep | 2.4% | 23 min | 5.4 h | $25 |
| 5 Sep | 2.9% | 34 min | 3.4 d | $15 |
| 6 Sep | 1.4% | 34 min | 43.7 h | $23 |
| 7 Sep | 1.8% | 32 min | 4.5 h | $16 |
| 8 Sep | 4% | 41 min | 19.4 h | $26 |
| 9 Sep | 7.6% | 34 min | 1.2 h | $15 |
| 10 Sep | 5% | 34 min | 1.3 h | $17 |
| 11 Sep | 5.4% | 56 min | 2.1 h | $12 |
| 12 Sep | 9.1% | 35 min | 1.3 h | $3.46 |
| 13 Sep (to 16:00) | 8.2% | 30 min | 55 min | $2.88 |

## Don't build sub-agents

- **Handoffs lose more than they save.** Every summary passed between agents is context the next agent will never have. The agent that gathers the evidence should be the agent that acts on it.
- **Evaluate the run, not the agents.** Per-agent evaluations pass while the system fails, because the failures live between the agents.
- **Keep one trace per run.** A wrong decision spread across a dozen traces takes an afternoon to explain. In one trace it takes a scroll.
