stateframesubframesagents

Subframes, not sub-agents: how StateFrame splits one task into parallel work

Most agent frameworks split work by spinning up more agents and passing notes between them. StateFrame 0.7 forks the task instead. Each piece works on files only it owns, hands back a sealed proposal, and nothing joins the main state until the engine has checked it.

When a task has two or three separate pieces, the obvious move is to put more workers on it. Almost every agent framework does that the same way: spin up sub-agents.

A sub-agent gets a brief written by the main agent. It goes off with its own chat history, does its thing, and writes a report. The main agent reads the report and decides what to believe.

I've run a lot of sub-agents. I run crews of them every day. And the problem is always in the hand-offs. The brief leaves out the one detail that mattered. The report says "done, all tests pass" when it means "I ran one test." The main agent believes it, because a summary is all it has. Every hand-off is a place where the truth gets a little blurrier, and none of it gets checked.

StateFrame was built on the idea that the model's memory isn't the source of truth. The engine's typed state is. So adding parallel work the sub-agent way would have meant throwing that idea out the window the moment a task got big enough to need it.

So 0.7 does it differently.

#Fork, work, join

A subframe is a fork of the task, not a new agent.

Blueprint drawing of a parent workbench on the left that splits a conveyor into three lanes. Each lane runs into its own sealed glass workcell holding a frozen copy of the workspace and a short stack of owned files. The lanes come back together at a single inspection gate with a glowing lens, and only one merged track continues past it to the right.
One task forks into subframes. Each works on a frozen snapshot and only the files it owns, then hands back a sealed proposal. Nothing joins the parent's state until the gate has checked it.

When the model decides a task splits cleanly, it calls one tool, parallel, with a plan. Each piece of the plan is a contract:

  • What it owns. The existing files this subframe may change, and any new files it must create. Two subframes can never own the same file.
  • What it promises. The interfaces it will provide, like a function and its signature, so other pieces can depend on them.
  • What it needs. Interfaces from other subframes it depends on. A consumer only starts after its producer has committed, and it gets the committed interface, never work in progress.
  • How to check it. The tests that judge this piece on its own.

Before anything launches, the engine checks the whole plan: paths, ownership, dependency cycles, sizes, and a scan for code that reaches across pieces without saying so. If it finds an undeclared dependency, it refuses the plan and tells the model why. It doesn't guess.

Then each subframe starts from a frozen snapshot of the parent's workspace. It's a fresh StateFrame run with a narrow tool menu: it can read and search, edit what it owns, create what it declared, run its checks, and say it's blocked. That's it. No shell, no deleting or moving files, no nested subframes.

When it's done, it seals a proposal and hands it back. The parent checks that proposal: did it stay in its lane, are its base files still current, does it provide what it promised, does it pass its check when combined with everything already committed? Only then does it become part of the parent's state, in one transaction.

After the last piece is in, the parent runs the final check on the whole thing.

#Why this beats hand-offs

There's no brief to get wrong. A subframe's input is its contract and the actual files, not a paragraph somebody wrote about the files.

There's no report to trust. A subframe doesn't tell the parent what it did. It hands over the actual change, and the engine checks it.

There's no drift. There's one authoritative state for the whole task, owned by the parent. Subframes never see each other's work in progress, and they can't quietly disagree about what's true.

And the receipts are all there. Every request every subframe sent is saved, just like any other StateFrame run, and the parent's results record says for each piece whether it committed, what it changed, how many requests it used, and why it stopped if it did.

Subframes per plan
6
Default maximum, configurable up to 1,024
Running at once
4
Default concurrency ceiling, or let the engine size it with --parallel auto
First-request budget per subframe
26,675
Tokens, 80% of the shipped 33,344-token prompt budget
Requests per subframe
12
Default cap, reserved up front so the parent never runs dry

#What it looks like

You turn it on with one flag:

Shell
stateframe run --repo examples/parallel_fixes \
  --task "Repair the pricing calculations and include tax in checkout." \
  --parallel 2

One caveat before you try that on 0.7.4: read the first item under "What's rough right now" below.

--parallel offers the tool. It doesn't force it. The model decides whether the task is worth splitting, and a run that uses zero subframes is a perfectly good outcome. If you want the engine to nudge, STATEFRAME_PARALLEL_SUGGEST=on lets it recommend a split when it finds at least two clearly separate Python source-and-test pairs that would strain a single frame.

#Being honest about speed

Subframes do not promise to make a run faster.

Each one needs its own workspace copy, its own requests and its own checks. How much that costs you depends on where the model runs. On a hosted API, the provider has plenty of capacity, so subframes really do run side by side. On one local GPU, they share the same server, and four subframes are four agents fighting over the same slots. Either way, a small task split three ways can easily take longer than doing it in one go, and the engine says so: plans where every piece owns one tiny file get flagged as a "tiny plan," a gentle hint to just do it in the parent.

Where they earn their keep is on work that's genuinely separate and too big to keep in one frame comfortably. Two modules, each with its own file and tests, each needing a real read, edit and check cycle. That's the shape it's built for.

I don't have a broad speed benchmark to show you yet, and I'm not going to invent one. The results file records wall time, estimated serial time and the ratio between them for every run, so the honest numbers will come from real runs, and I'll publish them when I have enough of them. The first real benchmark runs as soon as the remaining fix below ships.

#What's rough right now

  • Turning it on can stop a run. I found this one today on a live run with a local model on 0.7.3. With --parallel on, the run stopped with an engine error on its fourth request, before it had split anything. The fix didn't make it into 0.7.4, so until a later 0.7 patch ships, leave --parallel off.
  • Python is the first-class citizen. Python interfaces are checked statically. Other languages work, but their signatures can't be verified that way, so they need a passing check to prove the pieces fit.
  • Shared server, shared limits. Slot leases coordinate StateFrame runs with each other. They don't reserve hardware against anything else hitting the same server.
  • Disk and memory. Preparing subframes needs room for two copies of the repository plus a gigabyte, and at least 20% free memory. If that isn't there, the plan is refused with the reason.

The subframes guide has the whole contract, every refusal code and what to do about each one.

#Why I care about this one

Sub-agents are how I run most of my own work today, and I'm not throwing that away. But every bad night I've had with a crew came from the same place: somebody trusted a summary. A report said a thing was fixed and it wasn't. A brief left out the one constraint that mattered. Just this week, a crew got briefed from a stale task title instead of the actual task list, and spent its time doing the wrong work, very confidently.

Subframes are what parallel work looks like when you refuse to trust a summary. One task, one source of truth, and nothing gets in without being checked. If you try it on a real repository, I'd love to know where it splits well and where it shouldn't have tried. Open an issue on GitHub.

THE NEXT QUESTION IS YOURS

Know More.
Succeed More.

Bring the decision, the alternatives, and the constraints. Put Farside on the research behind your next move.

Create an account. Review your plan and the cost before research starts.