EQTY LabDocs

Deep Agents

Describe and route an agent built with Deep Agents: what Deep Agents adds, sub-agents, backends, skills and memory, and what's refused.

View as Markdown

guardian_agent.deepagents.create_deep_agent takes Deep Agents’ create_deep_agent arguments, plus name, the agent’s name as enrolled with viper agent add, and returns Deep Agents’ compiled agent, built with its models routed and each call checked. It needs the deepagents extra, with Deep Agents 0.7, and Python 3.11 or later: without Deep Agents installed, as on Python 3.10, the call raises ModuleNotFoundError, saying what’s missing (Choose an integration). This example uses the report-writer agent, enrolled as Before you start shows:

python
from langchain_openai import ChatOpenAI

from guardian_agent.deepagents import create_deep_agent
from guardian_agent.langchain import agent_of


def search_docs(query: str) -> list[str]:
    """Search the internal documentation."""
    return []


# Deep Agents' create_deep_agent, with one import changed and the agent's name added
writer = create_deep_agent(
    model=ChatOpenAI(model="gpt-4.1"),
    tools=[search_docs],
    system_prompt="You write reports from the internal docs.",
    subagents=[
        {
            "name": "critic",
            "description": "Checks a draft's claims against the docs.",
            "system_prompt": "You critique drafts and name each unsupported claim.",
        },
    ],
    name="report-writer",
)

# Each sub-agent is a definition of its own, general-purpose included
document = agent_of(writer).definition.document("report-writer")
print([subagent["name"] for subagent in document["subAgents"]])

result = writer.invoke(
    {"messages": [{"role": "user", "content": "Write up this week's incidents."}]}
)
print(result["messages"][-1].content)

agent_of(), from guardian_agent.langchain, returns the SDK’s agent, as for create_agent. guardian=, description=, version= and exclude= work as they do there (LangChain agents).

What the definition records

It describes the agent Deep Agents builds, not the arguments alone, recorded as create_agent records an agent (What the definition records), under the framework deepagents with its version:

  • What Deep Agents adds: its harness profile’s changes for your model (Deep Agents’ adjustments, per provider or model, to the prompt, the tools the model sees, middleware and the default sub-agent), the filesystem and task tools as they’re sent, the summarization model, and every middleware. There’s no execute tool, since a backend that runs commands is refused.
  • Each sub-agent, as a definition of its own, in subAgents: its model, prompt, tools and middleware, including the general-purpose sub-agent Deep Agents adds. The example prints ['critic', 'general-purpose'].

A sub-agent’s calls are signed as the agent and name the sub-agent (X-Viper-Subagent), so the proxy and the Guardian Platform can tell them apart (Agents and identities). Each call is checked before it’s sent, as for create_agent, sub-agents’ included (What each call is checked for).

  • Sub-agent names must be ones a definition takes: 1 to 64 lowercase letters, digits, ., _ and -, starting with a letter or digit (code-reviewer, not Code Reviewer). Another name raises ValueError.
  • In exclude, a sub-agent’s fields are under /subAgents/<n>, with sub-agents in order by name, as the definition lists them. The sub-agent records them as its own.
  • Size: Deep Agents’ tools are described in full, so each sub-agent adds about 14.8 KB to the definition. With no tools of your own and short prompts, 63 sub-agents of your own, beside general-purpose, come to about 960 KB, under the proxy’s 1 MiB limit, and a 64th makes 65 sub-agents, over the limit of 64, so create_deep_agent raises ga.DefinitionInvalid. Tools of your own, and sub-agents’ descriptions and prompts up to 16 KiB, add to each sub-agent that has them, so a definition can go over 1 MiB with fewer. A definition over it is refused when it registers, not when the agent is made: agent_of(agent).register() raises ga.DefinitionTooLarge, and the first invoke() fails with the provider client’s error, from which ga.proxy_error() returns it. Call register() at start-up to fail there. The limit can’t be raised: split sub-agents into agents of their own (Multiple agents).

Backends

Deep Agents’ StateBackend (its default), StoreBackend, FilesystemBackend(virtual_mode=True) and a CompositeBackend of them are recorded. With virtual_mode=True, a FilesystemBackend’s root directory is the agent’s /, a virtual root rather than a sandbox, as Deep Agents describes it. Each is recorded by what decides how they serve files: their type and settings, a composite’s routes, and a store’s namespace function, fingerprinted as code.

  • A filesystem’s root directory isn’t recorded, so the same agent gives the same definition on every machine. Keep it a project directory, since a definition can’t tell one root from another.
  • A subclass of your own is recorded with its code, and what its methods and properties capture, as your middleware is. One that holds its own root directory is refused, since that path is this machine’s.
  • What a backend holds is the agent’s to read and write as it runs, not part of its definition, apart from its skills and the memory it can’t change.

Skills and memory

Skills, and memory the agent can’t change, are part of what the agent is. When the agent is made, they’re read through your backend, their files are stored on the proxy, and each is recorded by the content ID the proxy gives them. It’s the same content ID Viper gives a coding agent’s skill of the same files, so the Agent Gateway checks both alike (Skills and instructions). This example reads the skill and notes Before you start shows, and uses the planner agent:

python
from deepagents.backends import FilesystemBackend
from deepagents.middleware.filesystem import FilesystemPermission
from langchain_openai import ChatOpenAI

from guardian_agent.deepagents import create_deep_agent
from guardian_agent.langchain import agent_of

# Skills, and memory the agent can't change, are read through the backend,
# stored on the proxy, and recorded by their content ID
planner = create_deep_agent(
    model=ChatOpenAI(model="gpt-4.1"),
    system_prompt="You plan the work and hand each step to a tool.",
    backend=FilesystemBackend(root_dir=".", virtual_mode=True),
    skills=["/skills/"],
    memory=["/docs/agent-notes.md"],
    # Denying writes makes the notes memory the agent can't change
    permissions=[FilesystemPermission(["write"], ["/docs/agent-notes.md"], "deny")],
    name="planner",
)

document = agent_of(planner).definition.document("planner")
for skill in document["skills"]:
    print("skill:", skill["name"], skill["cid"])
for memory in document["memory"]:
    print("memory:", memory["path"], memory.get("cid"))
  • Skills: each skill Deep Agents loads from each source (a directory with a SKILL.md), by the name in its SKILL.md, for the agent and each sub-agent that has skills. general-purpose loads the agent’s. What’s recorded is what Deep Agents’ own loader finds: a later source’s skill replaces an earlier one’s of the same name, and a skill whose SKILL.md it can’t read isn’t loaded or recorded.
  • Memory the agent can’t change: memory whose writes permissions denies, as above, and memory inside a skills source (a skill’s own file), whatever permissions says. It’s recorded as skills are.
  • Memory the agent writes as it runs is recorded by where it lives (filesystem, store or state) and whose it is, never by its contents, so what it learns never changes its definition. It’s shared on a FilesystemBackend, per namespace on a StoreBackend, and per thread on Deep Agents’ StateBackend, the default.

The agent reads its skills and fixed memory as they were when it was made. Editing a file reaches it only when it’s made again, and gives it a new definition. Writing to them, through any path to the same files, is refused. Deep Agents loads skills and memory once per thread. The SDK loads them again at the start of every run and checks them before each model call, so a checkpointed thread, or a run’s input, can’t hand the agent skills or memory its definition doesn’t record. A run resumed from a checkpoint starts past that load, so the check before each model call is what holds it to them.

Memory the agent writes usually lives in a store, one namespace per user. This example routes /memories/ to a StoreBackend whose namespace comes from the run’s context, beside the fixed notes:

python
from dataclasses import dataclass

from deepagents.backends import CompositeBackend, FilesystemBackend, StoreBackend
from deepagents.middleware.filesystem import FilesystemPermission
from langchain_openai import ChatOpenAI
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore

from guardian_agent.deepagents import create_deep_agent
from guardian_agent.langchain import agent_of


@dataclass
class Context:
    user_id: str


def user_memories(runtime: Runtime[Context]) -> tuple[str, ...]:
    """Whose learned memory loads: one per user, from the run's context."""
    return ("memories", runtime.context.user_id)


planner = create_deep_agent(
    model=ChatOpenAI(model="gpt-4.1"),
    system_prompt="You plan the work and remember what each user prefers.",
    backend=CompositeBackend(
        default=FilesystemBackend(root_dir=".", virtual_mode=True),
        # What the agent learns lives in the store, per user
        routes={"/memories/": StoreBackend(namespace=user_memories)},
    ),
    store=InMemoryStore(),
    context_schema=Context,
    memory=["/docs/agent-notes.md", "/memories/learned.md"],
    # The notes are fixed: stored, and recorded by content ID
    permissions=[FilesystemPermission(["write"], ["/docs/agent-notes.md"], "deny")],
    name="planner",
)

for memory in agent_of(planner).definition.document("planner")["memory"]:
    print(memory)

It prints the notes recorded by content ID, and the learned memory by where it lives (store) and its scope: a namespace, with the namespace function’s fingerprint, never what’s in it.

Where they can live:

  • Skills, and memory the agent can’t change, must be on a FilesystemBackend(virtual_mode=True), directly or as a composite’s route.
  • Memory the agent writes must be on Deep Agents’ own backends: one of your own that changes how files are routed or kept could keep it anywhere.
  • A StoreBackend’s namespace function is fingerprinted with the values it captures, so one that captures a secret is refused. A global it reads is seen by name only, so keep what decides the namespace in the function.
  • A StateBackend’s files live in the thread’s state: a run starts with the files it’s given (invoke({"messages": ..., "files": ...})) and, with a checkpointer, those the thread’s earlier runs left. None carry from one thread to the next.

With skills= or memory=, making the agent contacts the proxy: to read its limits, and to store the files of skills and fixed memory. Nothing is stored if the definition fails its checks. Storing comes before the definition is registered, so files can be stored for an agent the proxy then refuses.

What’s refused

These are refused when the agent is made, with TypeError, rather than left undescribed, for the agent and in each sub-agent’s spec, and where a harness profile adds them:

  • A SkillsMiddleware or MemoryMiddleware of your own: give their sources as skills= and memory=, which are read and recorded.

  • Skills, or memory the agent can’t change, on a backend that can’t be read when the agent is made: Deep Agents’ StateBackend, whose files arrive with each run, or a StoreBackend, read only as the agent runs.

  • Backends that could reach past the project:

    • a backend that runs commands (LocalShellBackend, or any sandbox);
    • a FilesystemBackend with virtual_mode=False, or rooted at the filesystem’s root, or at your home directory or above it, which could read the whole machine or your home;
    • a backend of a kind the SDK doesn’t describe;
    • a CompositeBackend whose routes were changed after it was made.

    This holds however the backend is given: backend=, a CompositeBackend route, or a FilesystemMiddleware of your own.

  • RubricMiddleware, which builds its grader’s model as the agent runs.

  • Middleware that calls OpenAI’s moderation endpoint or Decisions API (OpenAIModerationMiddleware, OpenAIAutoModeMiddleware, OpenAIModelRouterMiddleware), or that holds a provider’s client that isn’t routed, as for create_agent (Middleware and its models).

  • Sub-agents the SDK can’t describe or route: a CompiledSubAgent (a graph built elsewhere), a forked sub-agent (mode: "fork"), and an async sub-agent (one that runs elsewhere, or AsyncSubAgentMiddleware).

  • A SubAgentMiddleware of your own: give sub-agents through subagents=, which describes and routes each.

  • A tool that calls a model of its own, in the agent’s tools or a sub-agent’s, as for create_agent (Tools that call a model).

  • Models from more than one provider, such as a sub-agent on Anthropic under an OpenAI agent, or middleware with another provider’s model (One provider per agent). Leave model unset, which Deep Agents deprecates, and it’s an Anthropic model, so an OpenAI sub-agent is refused: give the agent its sub-agents’ provider’s model.

A sub-agent the task tool would build while the agent runs, for a per-call response schema, couldn’t be routed, so the run raises TypeError instead. Middleware that builds or looks up a model as it runs can’t be seen (Calls the SDK can’t see).

How it describes what Deep Agents builds

create_deep_agent builds the agent twice, and has Deep Agents’ own calls to LangChain’s create_agent handed to it while it does. At any other time they go to LangChain, unchanged, so Deep Agents used directly behaves as it always does. If Deep Agents builds the agent differently the second time, create_deep_agent raises RuntimeError rather than return an agent its definition might not describe.