# Deep Agents + ACP — Policy & Audit Install Guide

Put a LangChain Deep Agent under per-user policy, PII detection, and audit — main agent and subagents. One pip install, one import swap, every tool call checked, including inside task delegation.

# Deep Agents + ACP — Policy & Audit Install Guide

[Deep Agents](https://github.com/langchain-ai/deepagents) is LangChain's open-source harness for Claude Code-style agents: planning, a filesystem, subagent delegation, context management, memory, skills, and human-in-the-loop, on top of LangChain 1.x `create_agent`. MIT licensed, any model.

It takes the same `middleware=` list as `create_agent`, so `ACPMiddleware()` from [`acp-langchain`](/integrations/langgraph) already governs the main agent's tools: the filesystem tools, skill reads, the `task` delegation call, and your own. What it does not reach is the subagents. `acp-langchain` 0.3 closes that with one import swap. Same control model as Claude Code — same `/govern/tool-use` endpoint, same workspace policies.

> **Starter · 5-minute install.** `pip install "acp-langchain[deepagents]"`, import `create_deep_agent` from `acp_langchain.deepagents`, bind the JWT per request. See [the runnable starter](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/starters/deepagents), [the control model](/docs/governance-model), or the [frameworks index](/frameworks).

## Install

```bash
pip install "acp-langchain[deepagents]"
```

Needs `deepagents >= 0.7` and `langchain >= 1.3.3`.

## Minimal agent under policy, subagents included

```python
from fastapi import FastAPI, Header
from langchain.tools import tool
from acp_langchain import configure, set_context
from acp_langchain.deepagents import create_deep_agent   # not from deepagents

configure(base_url="https://api.agenticcontrolplane.com")
app = FastAPI()

@tool
def lookup_record(id: str) -> str:
    """Look up a record by ID."""
    return json.dumps(db.records.find_one({"id": id}))

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return mailer.send(to=to, subject=subject, body=body)

# Same signature as deepagents.create_deep_agent.
agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[lookup_record, send_email],
    subagents=[{
        "name": "researcher",
        "description": "Looks up records and summarises them.",
        "system_prompt": "Use lookup_record, then report.",
    }],
)

@app.post("/run")
def run(prompt: str, authorization: str = Header(...)):
    set_context(
        user_token=authorization.removeprefix("Bearer ").strip(),
        agent_name="my-deep-agent",
        agent_tier="background",
    )
    result = agent.invoke({"messages": [{"role": "user", "content": prompt}]})
    return {"result": result["messages"][-1].content}
```

No decorator on either tool, and nothing on the subagent spec. ACP is on the main agent, on `researcher`, and on the `general-purpose` subagent Deep Agents adds by itself. When the model delegates with `task`, the subagent's `lookup_record` call is checked and recorded exactly like the parent's would be, under the same session.

## Why the plain middleware line is not enough

`create_deep_agent` assembles a separate middleware stack for each declarative subagent. The parent's user-supplied middleware is merged in only for `mode="fork"` subagents. The auto-added `general-purpose` subagent inherits only parent middleware whose name matches one of its default slots, which `ACPMiddleware` never does.

So with `create_deep_agent(..., middleware=[ACPMiddleware()])` the trail looks like this: one governed `task` row, then silence while the subagent reads, writes, and calls tools with no policy check and no audit row. It looks covered and is not.

That is verified against deepagents 0.7.13, not inferred. [`tests/test_deepagents.py`](https://github.com/agentic-control-plane/acp-governance-sdks/blob/main/python/acp-langchain/tests/test_deepagents.py) drives a scripted model through main → `task` → subagent tool call with stock `create_deep_agent` and asserts ACP saw only `task`; the same script through the ACP wrapper asserts it saw `task` and the subagent's tool. No model key needed to run it.

If you build with `deepagents.create_deep_agent` directly, wrap the subagents list instead:

```python
from deepagents import create_deep_agent
from acp_langchain import ACPMiddleware
from acp_langchain.deepagents import govern_subagents

agent = create_deep_agent(
    model=..., tools=[...],
    middleware=[ACPMiddleware()],
    subagents=govern_subagents(my_subagents),
)
```

`govern_subagents` adds `ACPMiddleware` to every declarative spec and inserts a governed `general-purpose` spec (same name, description, and prompt as the stock one, so the model's view of `task` does not change) unless you supplied your own.

**Compiled and remote subagents.** A `CompiledSubAgent` (`runnable=`) or an `AsyncSubAgent` (`graph_id=`) is built elsewhere, so ACP cannot be injected here. They pass through unchanged and a `UserWarning` names each one. Register `ACPMiddleware` on those graphs where you build them.

## What happens on every tool call

1. `ACPMiddleware`'s tool-call wrap hook POSTs to ACP's `/govern/tool-use` with the tool name, args, and the user JWT bound by `set_context`.
2. **Deny** → the tool function is **never called**. The model receives `"tool_error: <reason>"` as the tool result, sees the denial, and adapts.
3. **Allow** → your function runs.
4. Post-audit: ACP scans the output for PII / secrets. `redact` → the redacted version replaces the original before the model sees it; `block` → the model sees `"[ACP] Blocked: <reason>"`. Audit row written, rooted in the end user's identity.

Filesystem tools (`read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep`), skill file reads, and `task` itself are tool calls, so they take the same path. Memory files loaded at startup are read outside any tool call and are not seen.

## What Deep Agents already ships, and what this adds

Deep Agents is not short on controls of its own. Be clear about the overlap before adding a layer.

| Already in Deep Agents / LangChain | What it is | What ACP adds |
|---|---|---|
| `interrupt_on` / `HumanInTheLoopMiddleware` | The pause. Approve, edit, or reject a proposed call. Inherited by subagents. | The decision behind the pause, from one workspace policy, with the approval on the record. An approved call still passes the ACP check at execution. |
| `permissions` (`FilesystemPermission`) | allow / deny / interrupt rules for filesystem paths, per agent, in code. Inherited by subagents. | Policy on every tool, not just filesystem, keyed on the end user, changed in the workspace without a redeploy. |
| `PIIMiddleware` | Redacts PII in the message stream of one agent. | PII and secret detection on tool input and output, recorded as findings on the audit row. |
| `ToolCallLimitMiddleware`, `ModelCallLimitMiddleware` | Caps per run, in code. | Rate limits per user and per tool across every run and every harness. |
| Tracing (LangSmith or your own) | Records what happened. | Records what was decided, and why, before it happened, in one trail alongside Claude Code, Cursor, Codex, and CrewAI. |

If you run one Deep Agent and nothing else, the native controls cover a lot. The gap they leave is that every rule lives in that agent's code, applies to that agent, and says nothing about which human triggered the call. ACP is the same control, applied from outside the harness, across all of them.

## Composes with Deep Agents' own controls

Keep `interrupt_on` and `permissions`. Wrap hooks nest, so the interrupt fires after the model proposes the call and ACP runs at execution:

```python
agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[lookup_record, send_email],
    interrupt_on={"send_email": True},      # the pause
    permissions=[FilesystemPermission(operations=["write"], paths=["/secrets/**"], mode="deny")],
)
```

Scope ACP when you need to:

```python
from acp_langchain import ACPMiddleware
create_deep_agent(..., acp=ACPMiddleware(exclude=["ls", "glob", "grep"]))
```

## Per-tier policy

`set_context(agent_tier="...")` controls the policy tier:

- **`interactive`** — human at the keyboard, permissive default.
- **`subagent`** — invoked by another agent, no human in the immediate loop.
- **`background`** — autonomous, most restrictive.
- **`api`** — programmatic call from your backend.

Subagent tool calls carry the tier and session bound on the request; contextvars carry through the `task` call.

## Price and meter the model calls

`ACPMiddleware` is the **interception plane** — what your agent *does*. The **proxy
plane** covers what it *spends*. Both run against the same gateway, and `init()`
wires them together:

```python
import acp_governance as acp

acp.init()            # call before you construct any model client
```

Model calls now land on the same trail as tool calls, carrying real cost,
tokens, and cache economics. Deep Agents resolves `model="anthropic:..."` /
`"openai:..."` strings through the provider clients `init()` configures, so
one call covers the main agent and every subagent.

`init()` is all-or-nothing per provider: it sets the base URL *and* the key, or
it leaves that provider completely alone and tells you which one it skipped.

**Your coverage:** interception ✓ (main agent + subagents) · proxy ✓ (model calls).

## FAQ

**Do I have to change my subagent specs?**
No. Pass them as you do today. The wrapper adds the middleware; your specs are not mutated.

**I disabled the general-purpose subagent through a profile.**
Pass `general_purpose=False` to `govern_subagents`, or supply your own spec with that name; the wrapper keeps yours and governs it.

**What does the model see on a denial inside a subagent?**
Same as the parent: `"tool_error: <reason>"` as the tool result. The subagent adapts and returns its final message to the parent as usual.

**Does this touch my LLM traffic?**
Not by default. Opt in with the proxy plane above.

**What if the ACP gateway is unreachable?**
Fail-open: the tool proceeds, marked `"fail-open"` in the reason. Policy checks are never a single point of failure for the agent. (5s timeout, configurable via `configure(timeout_s=...)`.)

**Which versions?**
`acp-langchain >= 0.3`, `deepagents >= 0.7`, `langchain >= 1.3.3`. The subagent behaviour described above is what deepagents 0.7.13 does; the characterisation test in the SDK repo will fail the day upstream starts propagating user middleware, which is the signal to drop the wrapper.

## Related

- [Deep Agents on GitHub](https://github.com/langchain-ai/deepagents)
- [`acp-langchain` on PyPI](https://pypi.org/project/acp-langchain)
- [`acp-governance` (core SDK)](https://pypi.org/project/acp-governance)
- [LangChain / LangGraph integration](/integrations/langgraph)
- [Pydantic AI integration](/integrations/pydantic-ai)
- [CrewAI integration](/integrations/crewai)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Add policy and audit to LangChain Deep Agents with Agentic Control Plane",
  "totalTime": "PT5M",
  "step": [
    {"@type": "HowToStep", "name": "Install acp-langchain with the deepagents extra", "text": "pip install \"acp-langchain[deepagents]\""},
    {"@type": "HowToStep", "name": "Swap the import", "text": "from acp_langchain.deepagents import create_deep_agent — same signature; ACP is registered on the main agent and every subagent."},
    {"@type": "HowToStep", "name": "Bind the user's JWT per request", "text": "Call set_context(user_token=...) at the start of each request."}
  ]
}
</script>
