Deep Agent state cumbersome

Hi Team,

I’m working on a project where I need to retrieve participants from a support ticket as well as from several other data sources. The challenge is that the required information is spread across multiple systems, so it has to be orchestrated through map/reduce-style operations.

My Deep Agent design is largely prompt-driven. The main conversational Deep Agent is intentionally very lightweight: almost all of the orchestration logic lives in AGENTS.md and SKILL.md files. I split the workflow into multiple subagents, each with its own SKILL.md.

My use case is roughly 60–70% deterministic and 30–40% LLM-driven.

I rely on the LLM for tasks such as:

  • generating the execution plan,
  • extracting participants from unstructured text,
  • composing the final response.

Everything else is deterministic and mainly consists of calling APIs and processing structured data.

After reviewing several Deep Agent examples shared by my team, I noticed that almost everything is prompt-based, including the agent’s state, which is maintained in the conversation context. The main agent delegates work to subagents, each subagent performs API calls or LLM reasoning, returns its result, and the main agent orchestrates the overall workflow. As a consequence, the shared state also lives in the conversation context.

What I’m struggling with is that there doesn’t seem to be an equivalent of the strongly typed shared state you would normally build with LangGraph.

I experimented with storing the intermediate state in the filesystem, for example:

/work/<ticket_number>/
    participants.json
    emails.json
    ...

This approach works technically, but I’m concerned about isolation. What guarantees are there that an LLM won’t accidentally read files belonging to another ticket if they’re accessible through the filesystem?

In this workflow, one subagent produces part of the state and another consumes it. Since map/reduce operations don’t naturally fit a traditional LangGraph execution graph, it feels like additional orchestration logic is needed to implement them cleanly.

I also built an alternative solution based on compiled LangGraph graphs with a Pydantic state model. From a technical perspective, this approach works much better because it provides explicit state management. However, the architecture team considers it too rigid and prefers a pure Deep Agent approach.

This leads me to a few questions:

  • Am I using Deep Agents in the wrong way for this type of orchestration?
  • Is there a recommended pattern for sharing structured state across multiple cooperating subagents?
  • Does Deep Agent support true parallel execution through prompting? If so, what is the recommended approach to execute multiple subagents concurrently to reduce latency?
  • Are there any reference implementations or GitHub repositories demonstrating a Deep Agent orchestrating multiple subagents, where each contributes a portion of the overall state and the main agent aggregates the final result?

I’d really appreciate any guidance, best practices, or examples.

Thanks in advance!

PS
I looked at deep agent documentation and I’ve already built my deep agent, however, the prompt driven approach doesn’t satisfy me with data reliability and performance are really bad (but today everything is sequence and I am trying to figure out how to do parallelism of subagents).

Hi @sasadangelo — a few current deepagents features map directly onto what you’re hitting, and importantly you don’t have to abandon typed state to go “pure Deep Agent.”

1. Typed shared state (instead of stuffing everything into conversation context or loose JSON files)

Deep Agents supports a strongly-typed state_schema — the equivalent of the LangGraph/Pydantic state you’re used to. Subclass DeepAgentState (this preserves the built-in DeltaChannel reducer on messages, keeping checkpoint growth linear) and pass it in:

from typing import TypedDict
from deepagents import create_deep_agent
from deepagents.graph import DeepAgentState

class Participant(TypedDict):
    name: str
    role: str
    source: str

class TicketState(DeepAgentState):
    ticket_number: str
    participants: list[Participant]

agent = create_deep_agent(model=..., state_schema=TicketState)

Tools read/write that state through the injected ToolRuntime (runtime.state["participants"]) instead of parsing free text. Rule of thumb from the docs: mutable data checkpointed with the thread → state_schema; immutable per-run inputs (ticket ID, credentials) → runtime context. Declarative SubAgent specs automatically inherit the parent state_schema, so every subagent sees the same typed fields. (Caveat: pre-compiled CompiledSubAgent and remote AsyncSubAgent specs don’t inherit it — compile those with a compatible schema.)

Docs: Custom state schema

2. Parallelism (your performance problem) → Dynamic Subagents

This is the big one for you, and it’s brand new. Regular subagents are dispatched one tool call at a time, which is why everything runs sequentially. Dynamic subagents let the agent write short orchestration code (in a sandboxed QuickJS interpreter) that dispatches subagents via a built-in task() global — so you get real concurrency with plain Promise.all, plus looping/branching/conditional logic:

const sources = ["ticket", "crm", "directory", "email"];
const results = await Promise.all(sources.map(src =>
  task({ description: `Extract participants from ${src}`, subagentType: "participant-extractor" })
));

That’s the “fanout and synthesize” pattern — a direct match for gathering participants across multiple data sources in parallel and then combining them. Because the fan-out is expressed in code rather than model-driven tool sequencing, you get deterministic coverage, which suits your mostly-deterministic workload. The “classify and act” pattern (route mixed inputs to specialists) also lines up well with ticket triage.

Setup:

from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware  # install: uv add "deepagents[quickjs]"

agent = create_deep_agent(
    model=...,
    middleware=[CodeInterpreterMiddleware()],
    subagents=[participant_extractor, ...],
)

Blog: Introducing Dynamic Subagents in Deep Agents · Docs: Subagents

(If you ever need genuinely remote or long-running background subagents rather than in-process fan-out, there’s also AsyncSubAgentMiddleware, added in v0.5 — but for your case dynamic subagents are the simpler, lower-infra answer.)

3. File isolation between tickets

Instead of hand-managing /work/<ticket_number>/… paths and hoping the LLM stays in its lane, swap the filesystem backend. A store-backed or composite backend namespaces storage per ticket so isolation is structural, not prompt-dependent. There’s also an enabled_tools allowlist on the filesystem middleware to restrict which file operations the agent can call at all.

Docs: Customize Deep Agents

Bottom line: you can keep your typed, structured modeling — express it as a DeepAgentState subclass, use dynamic subagents for parallel participant gathering, and a store/composite backend for per-ticket isolation. Hope that helps!

Hi @dariel.datoon Thank you.
You gave me a lot of insights. I was starting to explore a CompositeBackend with FilesystemBackend virtual=True for AGENTS.md and SKILL.md and StateBackend to have the JSON file as represantation of intermediate state but the possibility to use Pydantic is a game changer.

All this stuff is new to me so I need time to understand. I’ll be back as soon as I have more experience on it.

Can I have also map/reduce operations with pydantic in deep agents?

Yes — map/reduce works natively through agent state. That’s exactly what reducers are for.

Annotate a state key with operator.add and every subagent that writes to it appends instead of overwriting. That’s your reduce step, for free:

import operator
from typing import Annotated
from pydantic import BaseModel
from deepagents.graph import DeepAgentState

class Participant(BaseModel):
    name: str
    role: str
    source: str

class TicketState(DeepAgentState):
    ticket_number: str
    participants: Annotated[list[Participant], operator.add]  # ← fan-in

Now fan out extraction across your sources (map), and each subagent returns its slice into participants — LangGraph merges them concurrently, no clobbering. Need custom merge logic (e.g. dedup by email)? Swap operator.add for your own reducer function.

This is LangGraph’s Send-based map/reduce, and Deep Agents inherits it. Docs: Graph API — reducers & the Send API.

And yes, your CompositeBackend + StateBackend design is right — StateBackend scopes files to the thread, so per-ticket isolation comes free.

Hope that helps!