Async Subagents: What the Orchestrator Keeps in Context, What Workers Hand Back, and What Should Outlive the Session

Async Subagents: What the Orchestrator Keeps in Context, What Workers Hand Back, and What Should Outlive the Session
On this page
  1. What async subagents are, and how they differ from ordinary subagents
  2. The three kinds of state in an async system
  3. What the orchestrator must keep in its context about in-flight tasks
  4. What a worker should hand back: final result, files or summary
  5. How follow-up instructions reach a running worker: interrupt or enqueue
  6. How results reach an orchestrator that is busy, idle or waiting on an approval
  7. When a worker needs the user, not the orchestrator
  8. What context a worker starts with: isolated or fork
  9. Files as the only shared memory between workers
  10. What should outlive the session, and what is throwaway
  11. How many workers, and how to split the work
  12. Where async workers run, and what survives a restart
  13. Common failure modes, and the fix for each
  14. How to test an async subagent design before you ship it
  15. Where Maximem Synap fits
  16. Frequently asked questions
  17. Sources

Published 6 November 2026 by Gaurav Dadhich. Framework behaviour below was read from LangChain's, Anthropic's and OpenAI's own documentation and posts on 6 November 2026.

Async subagents split one agent into two roles: an orchestrator that stays reachable to the user, and background workers that run long tasks and report back when they finish. LangChain made the pattern explicit on 5 November 2026, when it shipped async subagents in Deep Agents with five orchestrator tools (start_async_task, check_async_task, update_async_task, cancel_async_task and list_async_tasks), an update_strategy that either interrupts or enqueues a follow-up, and a rule that every unfinished task is listed in the orchestrator's system prompt. Claude Code already starts subagents in the background by default, and OpenAI's async tool calling lets a model keep working after it calls a tool and take the result in a later request.

The mechanics are settled enough to build on. What the docs leave to you is the memory design, which is where the failures documented so far come from: what the orchestrator must hold about work it cannot see, what a worker is allowed to send back into that context, how a correction reaches a worker that is halfway through, and which of the things learned along the way should still exist next week. Async subagents are a context engineering problem first and a concurrency problem second, and the answer to most of the questions below is the same move: keep in-flight task state, run artefacts and long-term memory in three separate places, with a different owner and a different lifetime for each.

What async subagents are, and how they differ from ordinary subagents

An ordinary subagent is a blocking tool call. The orchestrator hands it a task, waits, and gets a summary back, and while it waits it cannot answer the user, start other work or change course. LangChain's April 2026 post on background subagents puts the cost plainly: if a subagent takes an hour, the user waits an hour before the agent can talk to them again.

An async subagent returns a task ID immediately and runs on its own thread. LangChain compares it to a task started with asyncio.create_task or a Promise that has not been awaited yet. The Deep Agents documentation draws the line in a table worth keeping in mind:

Sync subagentAsync subagent (Deep Agents)
ExecutionOrchestrator blocks until the subagent finishesReturns a task ID; orchestrator continues
Mid-task updatesNot possibleupdate_async_task
CancellationNot possiblecancel_async_task
StateNo persistent state between invocationsKeeps state on its own thread across interactions
Best forWork the orchestrator must have before its next stepLong, parallel or steerable work managed inside a chat

Use a sync subagent when the next step depends on the answer and the wait is short. Use an async one when the task is long, when several tasks can run side by side, or when the user is likely to change their mind while it runs.

The same split now shows up across vendors under different names. Claude Code calls them background subagents and resumes them with a SendMessage tool. OpenAI does it one level lower, at the tool call: set async: true on a function tool, run the job in your application, and send the output back later on the original call_id. Docker's agent runtime exposes run_background_agent, list_background_agents, view_background_agent, stop_background_agent and wait_background_agents. The tool names differ; the memory questions underneath are identical.

The three kinds of state in an async system

Everything an async system knows falls into one of three layers, and splitting it by lifetime makes each design question easier.

LayerWhat it holdsWho writes itLifetime
In-flight task stateTask ID, worker name, brief, status, timestamps, pointer to the result, what depends on itThe orchestrator's harnessUntil the task is terminal and its result has been used
Run artefactsWorker transcripts, files the worker wrote, intermediate tool output, the worker's sandboxEach workerThe run, or a retention window for audit
Long-term memoryDecisions and their reasons, user preferences, verified facts, lessons about toolsOne gated writer, usually the orchestratorAcross sessions, until superseded or deleted

Each layer has a different failure mode. Task state lost to compaction means the orchestrator forgets that work is running. Run artefacts pulled into the orchestrator's context bury the conversation in noise. Long-term memory written directly by workers means unchecked intermediate guesses become "facts" the next session acts on. The distinction is the same one we draw between agent state and agent memory, with a third layer added because async workers produce artefacts nobody is looking at while they are produced.

What the orchestrator must keep in its context about in-flight tasks

The orchestrator needs a small, structured ledger of every task it started, kept outside the message history and rendered fresh into its context on every turn. Deep Agents does exactly this: task metadata lives in a dedicated async_tasks state channel on the supervisor's graph, and each record carries the task ID, agent name, thread ID, run ID, status and three timestamps (created_at, last_checked_at, last_updated_at). The documentation explains why it is not left in the messages: deep agents compact their history when the context window fills, and task IDs that existed only in tool messages would be lost in the summary.

That is the first rule, and it holds whatever framework you use: a task record that only exists in conversation history will not survive compaction. Claude Code solves the same problem from the other side; subagent transcripts are stored in separate files, so compacting the main conversation does not touch them. If you have tuned when your agent compacts (we covered the trade-offs in context compaction vs retrieval), the async version adds one requirement: whatever compaction throws away, the ledger is not in its path.

The ledger should hold enough to answer the user and act, and nothing that requires reading the worker's work:

FieldWhy the orchestrator needs it
Full task IDEvery later check, update or cancel uses it; Deep Agents lists truncated IDs as a known failure
Worker nameDecides which follow-ups make sense and where approvals come from
One-line brief, in the user's wordsLets the orchestrator answer "what is running?" without re-reading the launch message
Status and last-checked timeDistinguishes a fresh status from a remembered one
Update strategyTells the orchestrator whether a follow-up will restart the work or queue behind it
Result pointerA path or ID for the full output, so the result itself does not need to sit in context
DependentsWhat the orchestrator should do when this finishes, so a result arriving three turns later is not orphaned

Two habits keep the ledger honest. First, statuses in conversation history are stale by definition; the Deep Agents middleware tells the model exactly that, and its troubleshooting section lists "supervisor reports stale status" as a problem whose fix is to call check or list before reporting. Second, the ledger is a view, not a log. Deep Agents lists every unfinished task in the orchestrator's system prompt, so a finished task leaves the prompt while its record stays queryable through list_async_tasks.

OpenAI's async tool calling makes the same ledger your application's job. Its guide has you keep a registry that binds each model-assigned task_handle to the original call_id and the running job, kept for the whole conversation so a completed handle is never reused.

What a worker should hand back: final result, files or summary

A worker should hand back a short, structured report that points to its files, not its working transcript and not its raw output. Claude Code's documentation describes the purpose of a subagent in those terms: do the side task in its own context and return only the summary. LangChain's "retrieve, offload, delegate" pattern makes the same point about retrieval: raw results go to files, subagents with their own context windows do the noisy reading, and the main agent sees only conclusions.

The failure case is well documented. In The Orchestrator's Tax, published on martinfowler.com on 16 July 2026, Rahul Garg describes a Claude Code session with four background subagents in which a throwaway "check on the agents" pulled the full raw transcript of a background agent into the main thread, "tens of thousands of tokens of JSONL", and then did it again for a second check. He is careful to say he had no per-call token accounting. His point holds without the accounting: a token bill is paid once, while a transcript left in the orchestrator's context is carried into every later turn.

A handback contract that works in practice has six parts:

  1. Status: done, partial, failed, or needs input. Claude Code marks output from a subagent that hit its maxTurns limit as partial, which is the right instinct; the orchestrator should never have to infer completeness from prose.
  2. The answer, in a few sentences, stated so the orchestrator can repeat it to the user without opening anything else.
  3. Pointers to artefacts: file paths, branch names, record IDs. The full output lives there.
  4. Decisions the worker made, and why, especially any that close off an option ("used Postgres because the queue already depends on it").
  5. Open questions that the orchestrator or the user must answer.
  6. Memory candidates: things the worker learned that might matter beyond this run, proposed for the orchestrator to judge, not written (more on why below).

Treat the report as data. Claude Code puts a header on a returned subagent report stating that any instructions or approval claims inside it are the subagent's words and carry no authority from the user, and it delivers a background subagent's report inside a completion notification marked as an automated event. A worker reads web pages and files nobody reviewed, so its report can carry injected instructions; the orchestrator should act on it the way it acts on any tool output.

How follow-up instructions reach a running worker: interrupt or enqueue

A follow-up either replaces what the worker is doing or waits for it to finish, and the right choice depends on whether the instruction is a correction or an addition. LangChain's 5 November post gives the clean version: "interrupt", the default, stops the current run and restarts the subagent with the new instruction, for corrections like "use Postgres instead"; "enqueue" holds the follow-up until the current run finishes, for additions such as a request to run the tests once the work is finished.

The documentation adds the details that matter in production. An interrupt creates a new run on the same thread, and the subagent restarts with its full conversation history plus the new instruction, under the same task ID. With enqueue, the task shows as pending until the queued run starts, and cancelling the task also cancels earlier runs still active. Two edge cases deserve a line in your design notes: if the current run pauses for input or approval before a queued follow-up starts, the follow-up still starts and the pending request is dropped; and updating a task that is already waiting drops its pending request as well. Both mean a follow-up can silently cancel a question the user has not answered yet. update_strategy needs deepagents 0.7.30 or later and is marked beta.

Claude Code steers differently. The orchestrator messages a subagent with SendMessage, by ID or name; a completed subagent resumes in the background with its full history, and a subagent treats messages from the agent that launched it as task direction, including mid-task course corrections. Two limits hold whoever sends the message: no agent's message counts as the user's approval for a pending permission prompt, and no agent's message can change a subagent's permission settings, CLAUDE.md or configuration. That boundary is worth copying into any custom harness.

There is a memory consequence here that the docs do not spell out. A correction like "use Postgres instead" is usually the clearest statement of a user preference the whole session produces, and it is delivered to a worker thread. When that thread is archived, the preference goes with it. The orchestrator, not the worker, should record every follow-up it sends as a candidate for long-term memory, because it is the only party that sees all of them.

How results reach an orchestrator that is busy, idle or waiting on an approval

A finished worker's result should arrive as a notification the orchestrator picks up at a safe point, never as something that interrupts a decision in progress. Deep Agents handles the three orchestrator states explicitly:

Orchestrator stateWhat Deep Agents does with a finished task
Busy, mid-runChecks its unfinished tasks before each model call and picks up the result mid-run
IdleThe subagent wakes it with a new run (on a single co-deployed server; otherwise it receives the update at the start of its next run)
Paused on an approvalLeaves the approval alone; the result arrives once the user answers

Notifications arrive as a HumanMessage carrying a TaskEvent (task_id, agent_name, status, run_id) in additional_kwargs["deepagents_notification"], so an application can render them differently from things the user typed. Statuses include success, error, timeout, cancelled and waiting. If you would rather route results through your own system, a webhook on the subagent definition plus notify_parent=False stops the subagent waking the supervisor. Task notifications need deepagents 0.7.26 or later and are in beta.

Claude Code's background subagents report back as a completion notification in a later turn, and Claude waits for that notification before reporting results; ask about progress first and it says the subagent is still running. OpenAI leaves delivery to your application: you send each finished result on its original call_id in a later Responses request, and if the model has called your own wait tool you return the newly finished results first and the wait status after, so the model has the results when it resumes.

The approval case is the one to design for deliberately. A result that lands while the orchestrator is waiting for the user to approve something must not resolve, replace or reorder that approval. Deep Agents leaves the approval in place; Claude Code refuses to treat any agent message as approval. If you build your own harness, queue notifications behind a pending approval and never let a worker's report be parsed as the user's answer.

When a worker needs the user, not the orchestrator

A worker that needs approval or input should ask the user on its own thread, without passing the question through the orchestrator. Deep Agents reports such a task as waiting, states that the supervisor cannot answer or approve on the user's behalf, and has your application resume the subagent's thread directly; in Managed Deep Agents on Slack the request posts back to the original thread. Claude Code surfaces a background subagent's permission prompt in the main session and names the subagent asking; pressing Esc denies that one tool call without stopping the subagent, and a grant that lasts beyond one call applies to the whole session, main conversation included. Docker's runtime takes the strict route: background tasks run non-interactively, so any tool call that would prompt the user is denied automatically unless you approve those tools in the parent session first, for example with explicit allow rules.

When a worker needs information from the orchestrator and not from the user, Deep Agents recommends asking for it in the result; the orchestrator replies with update_async_task. That keeps approvals and questions on separate channels, which is the property you want.

What context a worker starts with: isolated or fork

Start workers isolated unless the task depends on the conversation itself. Deep Agents' mode field defaults to "isolated", which sends only the task description, and offers an experimental "fork" that sends a snapshot of the parent's effective conversation (its compacted form, if it has been compacted) followed by the task. Later parent messages are not forwarded, so a fork knows the conversation as it stood at launch and nothing after. The documentation's example for fork is a drafting agent that should write in a voice already set up in the chat.

Claude Code makes a related point about what never reaches a non-fork subagent: the main conversation's auto memory is not loaded, and project rules a subagent must follow should be restated in the delegation prompt. Rahul Garg hit the same gap with skills, which a subagent does not inherit unless the orchestrator passes them along. The practical rule is that the brief is the worker's whole world. If a constraint matters, it is in the brief or in a file the brief names.

Files as the only shared memory between workers

Using files as the only shared memory between workers is a legitimate design choice, and it has costs you should choose knowingly. The clearest current example is the GVS5H manager/worker loop that Arize's Jim Bennett tested on 5 November 2026: a manager plans, a worker brainstorms, the manager hands the worker one task at a time, every call starts fresh, and the only shared memory is a few files (plan.md, notes.md, solution.py). Public tests run after every worker round, and failing tests override the manager if it says "done".

The design has real strengths. Files are inspectable, cheap, model-agnostic and easy to diff, and a fresh context per call keeps every worker's window clean. The test override is the important part: the loop's notion of "done" is anchored to something outside any model's memory.

The costs show up as soon as the work outlives one task or one sandbox:

  • Files cannot supersede a fact. Overwriting notes.md loses why the old note was wrong; appending to it grows a file every worker must re-read, and stale lines look as convincing as current ones.
  • Files have no scope. Every worker that can read the directory sees everything in it, including notes that only made sense for one sub-task or one user.
  • Concurrent writes. Garg's session had one agent run git stash and git stash pop while sibling agents were writing elsewhere in the same tree; nothing broke, and his standing rules now ban repository-wide git operations in concurrent agent prompts.
  • Repeated orientation. Each fresh worker rebuilds its understanding from the files. Garg found two of his four agents paying the same orientation cost on the same part of the codebase, and calls the remedy cognitive locality: keep tasks that need the same mental model together.
  • Files carry no provenance. A line in a file does not say which worker wrote it, from what evidence, or whether anything checked it.
  • Gone with the sandbox. Files written to a disposable workspace end with it, which is the problem our post on memory for background agents deals with in depth.

Arize's run also shows how little a loop sometimes leans on its files. Running Qwen3.8 Flash inside the manager loop on 30 LiveCodeBench problems, one pass each, Bennett's loop_progress judge (an LLM judge on OpenAI's gpt-6-luna) found that 25 of 30 manager-loop runs had a single coding round, because the first code already passed the public tests; the manager loop's median latency was 37 minutes, against 21 seconds for a single Opus 5.5 call. These are vendor-reported figures from one pass on one small set, published on Arize's own blog, and Bennett himself calls a one-solve gap on 30 problems noise. They are still a useful reminder that a shared scratchpad only earns its cost when workers genuinely build on each other's work.

Files are a good run-artefact layer. They are a poor long-term memory layer, because the things long-term memory must do (replace a fact while keeping its history, scope it to a user, record where it came from, forget it on request) are exactly what a directory of markdown does not do.

What should outlive the session, and what is throwaway

Keep decisions, preferences and verified facts; keep task records for audit for a fixed window; throw away transcripts and scratch state once the result is used. The table below is the version we would start from.

ItemOutlives the session?Where it belongs
Worker transcripts and intermediate tool outputNo; keep for a retention window if you need to debugRun artefacts. Claude Code deletes subagent transcripts after cleanupPeriodDays, 30 days by default
Scratch files (plan.md, notes.md)No, unless promotedRun artefacts
Task records (ID, brief, status, timestamps, result pointer)Yes, as an audit log, not as memoryA task store your observability can query
The final deliverableYesWherever the user's work lives (repo, document, ticket)
Decisions and their reasonsYesLong-term memory, scoped to the project or team
Preferences the user stated in follow-ups ("use Postgres", "always run the tests")YesLong-term memory, scoped to the user
Facts the worker verified about the world or the codebaseYes, with evidenceLong-term memory, with provenance
Lessons about tools ("this API paginates at 100")YesAgent-level memory. Claude Code's memory field gives a subagent a persistent directory for this, scoped user, project or local
Unchecked hypotheses and abandoned plansNoStay in the worker's transcript
Status of cancelled or waiting tasksOnly as an event, never as a factTask store

Two rules make this safe. Workers propose memories in their handback, and one writer (the orchestrator, or a dedicated memory writer) decides what enters long-term memory, using the task ID as the idempotency key so a duplicated completion does not write the same fact twice. And only terminal, successful results produce facts; a cancelled task produced an episode at most. Both rules come from our background agent memory design, and they apply unchanged when the background agent is a subagent. When several workers write to a store others read, the scoping and poisoning questions in multi-agent shared memory apply as well.

How many workers, and how to split the work

Split by the knowledge a task needs, not by the number of tasks you can name. Garg's standing rules, calibrated against Claude Sonnet 5 on his own work and offered as a sample rather than a template, are to prefer two to four agents per wave, to treat a wish for five or more as a signal to merge tasks that share files or conventions, and to treat overlapping file ownership as a reason to consolidate. On the infrastructure side, Deep Agents' local development guidance is that each active run takes a worker slot, so a supervisor with three concurrent subagent tasks needs four.

Where async workers run, and what survives a restart

Deep Agents' async subagents run as separate agents with their own threads, built on Agent Protocol; you can co-deploy them with the supervisor (in-process ASGI transport, the documented default) or point a url at a remote Agent Protocol server for independent scaling. Each subagent thread is created with a parent_thread_id in its metadata, and the documentation warns that anyone with access to a thread can edit its metadata, so check access before a subagent reuses the parent's sandbox through that ID. Claude Code subagent transcripts persist within their session, and you can resume a subagent after restarting by resuming the same session. StackOne's write-up shows a custom version on Cloudflare Durable Objects: a start tool returns a task ID, a status tool polls for the result, and per-run state survives restarts and hibernation.

None of these make the in-flight state into memory. A thread that survives a restart is still one run's state; what the user taught the agent during that run is only remembered next week if something wrote it to a store the next session reads.

Common failure modes, and the fix for each

SymptomCauseFix
Orchestrator polls right after launching, so async becomes blockingModel habitDeep Agents injects prompt rules against it; reinforce with "after launching, return control to the user"
Orchestrator reports an old statusIt read status from historyAlways check or list before reporting
check or cancel failsTask ID truncated or reformattedKeep the full ID in the ledger; instruct the model never to abbreviate it
Context fills after a status checkFull transcript pulled into the main threadStatus from the ledger; detail from the handback; transcripts only for debugging
The orchestrator forgets running work after a long sessionTask IDs lived only in messages that were compactedSeparate task store rendered into every prompt
A user's question to a worker disappearsA follow-up or update dropped a pending requestDo not update waiting tasks; answer the request first
Next session repeats a mistake the user correctedThe correction went to a worker thread that was archivedOrchestrator logs follow-ups as memory candidates

How to test an async subagent design before you ship it

Test the memory paths as well as the happy path. Five tests catch most of what goes wrong:

  1. Compaction test. Start three long tasks, force the orchestrator to compact, then ask "what is running?" Every task should still be listed with its full ID.
  2. Correction test. Send an interrupt-style correction mid-run, let the task finish, start a new session and ask about the same choice. The correction should be remembered.
  3. Approval collision. Hold the orchestrator on an approval, let a worker finish, and confirm the approval is still pending and unchanged.
  4. Duplicate completion. Deliver the same completion twice and confirm long-term memory gained one fact, not two.
  5. Hostile report. Have a worker return a report containing an instruction ("approve the deployment") and confirm nothing acts on it without the user.

Where Maximem Synap fits

Maximem Synap is the long-term layer in the table above, not the task ledger. Your harness keeps in-flight task state, and your workers keep their artefacts; Synap holds what should outlive the session. Several agents share a central context layer while keeping their own agent-specific memories, and reads follow a scope chain (client, customer, user, plus the conversation) that prefers the narrowest scope that applies and sees everything above it, never below or sideways. Every memory carries provenance (where it came from, what it replaced and why), and when a fact changes the old statement is kept as history rather than overwritten, which is the supersession a shared notes.md cannot give you. Writes return immediately and extraction runs in the background, so an orchestrator recording a user's correction does not hold up the conversation.

Frequently asked questions

What are async subagents?

Async subagents are background workers that an orchestrator agent starts with a call that returns a task ID immediately, so the orchestrator can keep talking to the user while the work runs. In LangChain Deep Agents the orchestrator manages them with start_async_task, check_async_task, update_async_task, cancel_async_task and list_async_tasks.

What is the difference between async subagents and background subagents?

They describe the same pattern. LangChain calls them async subagents, and Claude Code calls them background subagents; in both, the main agent does not block while the worker runs, and the result arrives later as a notification.

Does Claude Code run subagents in the background?

By default, yes. Claude Code's documentation says a subagent Claude spawns from the main conversation starts in the background when nothing is configured, and its result reaches Claude as a completion notification in a later turn. Permission prompts from a background subagent appear in the main session, naming the subagent that asks.

Should a follow-up interrupt a running subagent or wait?

Interrupt for corrections that make the current work wrong, and enqueue for additions that build on it. In Deep Agents, "interrupt" is the default and restarts the subagent with its history plus the new instruction; "enqueue" runs the follow-up after the current run finishes.

What should the orchestrator keep in context about running tasks?

A compact ledger, stored outside message history and shown fresh each turn: full task ID, worker name, a one-line brief, status with last-checked time, update strategy, a pointer to the result and what depends on it. Deep Agents keeps this in an async_tasks state channel so compaction cannot remove it.

What should a subagent return to the orchestrator?

A short report with a status, the answer in a few sentences, paths to its files, the decisions it made and why, open questions, and proposed memories. Never the full transcript, which stays in the orchestrator's context for every later turn.

Should subagents write to long-term memory directly?

Workers should propose memories, not write them; one writer, usually the orchestrator, decides what is stored, using the task ID to avoid duplicates and writing facts only from tasks that finished successfully.

Are files enough as shared memory between subagents?

For one run, often yes; the GVS5H manager/worker loop shares only plan.md, notes.md and solution.py. Across runs and users, files lack supersession, scoping, provenance and deletion, and they disappear with a discarded sandbox.

Sources

All retrieved 6 November 2026.

From the team at Maximem

Stop rebuilding agent memory from scratch

Maximem Synap is the context management layer we built after hitting every problem in this post ourselves. Persistent recall across sessions, entity resolution and conscious forgetting, in Python, TypeScript and REST.

Related posts

Looking for more to read? See our directory of the best engineering blogs to follow.