Building a DeepAgents Harness: Permissions, Subagents and Human Approval
Building a DeepAgents harness for a system-design planner: file backends, access control rules, subagents, human approval gates, skills and MCP tools.
This article is published in English.
Sources: The DeepAgents Architecture, Part 2: Building the Agent Harness
A harness is easy to admire in documentation and harder to judge in use. This write-up covers what happened when the DeepAgents architecture was used to build a full, working application, with real multi-step design conversations and findings checked against evidence. The questions are practical ones: how the harness handles files and permissions, what delegation looks like, how human approval behaves, and where the model's judgment still slips even when the scaffolding does its job.
The application is a system-design planning assistant for an organization that runs its data and AI workloads on Azure with Databricks. The choice was deliberate. What counts as "native tooling" is unsettled right now, given how tightly Databricks and Microsoft's Azure AI stack have grown together, and that kind of open-ended, multi-step ambiguity is just what this architecture is meant to help with. The full source is published as a public repository.
Getting set up
git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env # fill in real API keys
Python 3.11 or newer is required, along with the uv package manager. The examples use both Anthropic and OpenAI models, so both API keys are needed to follow everything. A Tavily key and access to a Databricks workspace matter only for the sections that use them.
Files and access control in DeepAgents
Where the agent's files live
Before an agent can read or write anything it needs a place to keep files. DeepAgents provides several backends for that, among them StateBackend, FilesystemBackend and variants backed by a store or a sandbox. This project uses two, for different reasons.
Calling create_deep_agent() with no extra arguments gives the agent StateBackend, which keeps files inside the agent's own LangGraph state. Since that state is checkpointed after every step, files really do persist as long as a conversation thread goes on. The catch is that persistence is tied to the thread: something written in one thread isn't visible from another. For plenty of applications that's exactly right. Here it isn't, because the agent has to read real reference documents from disk regardless of which conversation it's in.
FilesystemBackend covers that. It points the agent at an actual folder, and every read, write or listing hits real files that outlive the run. All the examples start from one factory function:
# src/system_design_planner/agent.py
from deepagents import create_deep_agent
MODEL = "claude-sonnet-5"
def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)
Each example builds its agent through that function. Where real file access is required, the call receives backend=FilesystemBackend(root_dir=...). root_dir sets the boundary the agent works within, a scoped root it can't climb out of by default even if it tries a path like ../../. What the agent may actually do inside that boundary, meaning which operations on which paths, is a separate matter that FilesystemMiddleware enforces through permission rules.
Deciding what the agent may do
Access to a folder is one decision, and precisely limiting what happens inside it is another that matters equally. DeepAgents handles it with an ordered list of rules saying which operations are allowed, denied or held for a person's explicit approval, per path. These are the rules this project's agents run under:
# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission
DESIGN_SESSION_PERMISSIONS = [
FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]
The rules are checked in order, each for its own reason.
The first stops the agent from ever reading or writing the .env file, where the real API keys sit. It holds no matter what else the agent is doing.
The second turns the knowledge_base/ reference material read-only. DeepAgents counts delete as a write, so the same rule also blocks deletion and not merely edits, which means the ground truth the agent reasons against can't be quietly damaged by its own actions.
The third carries the most weight. Writes to the agent's memory file are allowed, but only after a human approves them. That mechanism gets its own sections below.
Checking that the agent reads what it's given
None of this helps unless the agent uses it properly, and it's better tested than assumed. Four short reference documents sit in the knowledge_base/ folder, on Unity Catalog governance, Mosaic AI, the Microsoft Foundry integration and identity with networking. To check that the agent really reads them and doesn't guess from file names, it was pointed at the folder and asked to summarize what's there:
backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)
result = agent.invoke({
"messages": [{
"role": "user",
"content": "List every file in your working directory, then give a one-line summary of what each one covers.",
}]
})
The result was four accurate, separate summaries, one per file. The lower-confidence caveat in the identity and networking document even came through intact instead of being flattened into the confident tone of the other three. The execution trace explains why this means something: the agent called ls to see what existed, and followed with one read_file call per document, four in all, before a single word of the summary was written. The answer rested on real reads.
Delegating to subagents
What delegation means here
Some work suits a specialist working alone better than folding it into everything else the main agent is already juggling. DeepAgents supports that through subagents: self-contained agents the main agent can call on for a specific piece of work. A subagent runs in its own isolated context, never seeing the full history that led up to the call, and reports a result back to the delegator when it finishes. That isolation concerns conversational context in particular and doesn't necessarily extend to storage, as the memory discussion below shows.
There's a practical payoff. The main agent's context stays focused on the overall task instead of collecting every intermediate step a specialist took to produce one answer. A second payoff is easy to overlook: because each subagent is configured on its own, different subagents can run on completely different models, picked to suit what each one is asked to do.
Different models for different subagents
Not every subagent job needs the most capable, and most expensive, model. In this project one subagent exists only to pull facts out of reference documents, a mechanical task needing no deep reasoning. Rather than run it on the model driving the rest of the system, it gets a smaller, cheaper one suited to that narrower job:
from deepagents import SubAgent
REFERENCE_EXTRACTOR: SubAgent = {
"name": "reference-extractor",
"description": (
"Extracts structured, factual capabilities from the knowledge_base "
"reference files for a specific platform or topic. Delegate here "
"before proposing any subsystem design, so decisions are grounded "
"in verified platform facts rather than assumption. Do not use this "
"subagent for design reasoning or tradeoffs - extraction only."
),
"system_prompt": (
"You are a fact-extraction specialist. Given a topic, find the "
"relevant file(s) in your working directory, read them, and return "
"ONLY a bullet list of the concrete facts relevant to that topic - "
"no narrative, no design opinions, no recommendations. If a fact is "
"explicitly flagged as an open question or unverified in the source "
"file, preserve that flag in your output rather than smoothing it "
"over into a confident-sounding statement."
),
"model": "openai:gpt-5.6-luna",
}
The model field makes that possible. Each subagent can name its own model, whatever the main agent runs on. In this build the main planner runs on Claude while this one subagent runs on an OpenAI model.
Setting a field is only configuration, though, and it doesn't prove requests go where they should. That takes a look at what actually happened in a run. Using the tracing tools that let you inspect an agent's execution step by step, calls made by the main agent showed up under Claude from start to finish, whereas the call nested inside the extraction subagent showed up under the OpenAI model named in its configuration. That's a provider-level routing decision that was carried out and could be verified, not one that was merely declared in code.
A subagent keeps its own memory by default
Isolation is a deliberate design choice in DeepAgents, and it reaches further than most people expect at first. A subagent gets its own context, and it also has no automatic access to anything the main agent has in memory, even something directly relevant to the delegated task. Access has to be provided explicitly.
That's usually the right default, since a subagent shouldn't need to sift through everything the main agent recorded just to do one focused job. But when the task depends on a decision already made and written down, access has to be granted on purpose:
def build_governance_specialist(backend) -> SubAgent:
from deepagents.middleware.memory import MemoryMiddleware
return {
"name": "governance-specialist",
"description": (
"Reasons about Unity Catalog governance, lineage, and the "
"Databricks-Microsoft Foundry governance boundary for this "
"architecture."
),
"system_prompt": (
"You are a governance specialist for an Azure + Databricks AI "
"architecture. Before answering, check /memory/AGENTS.md for any "
"recorded scoping decisions - they are binding constraints on "
"your recommendation, not suggestions."
),
"middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
}
That middleware entry does the granting. The specialist gets a connection of its own to the memory file the main agent uses, and so can read earlier decisions before it begins to reason.
Human approval and persistent memory
Why some actions need a person
Not every action an agent takes should run unattended. Writing a scratch file is low stakes. Recording a decision that every later conversation will treat as settled fact is another thing entirely, because a mistake doesn't just produce one bad answer. It can quietly bend every answer that follows. DeepAgents addresses this with human-in-the-loop: selected actions can be set to pause execution until a real person approves, rejects or modifies them.
Here the mechanism covers exactly one action, writing to the agent's persistent memory file. The agent may propose an update whenever it likes, and nothing is written until a person agrees.
How approval and resumption work
Two things must be in place for this to function. First, the permission rule above sets mode="interrupt" on writes to the memory file. Second, and easily missed, pausing needs a checkpointer, a way to save the agent's exact state so it can be suspended and resumed instead of restarted from scratch.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command
checkpointer = MemorySaver()
agent = build_planner_agent(
backend=backend,
permissions=DESIGN_SESSION_PERMISSIONS,
checkpointer=checkpointer,
memory=["/memory/AGENTS.md"],
)
Once the agent tries a gated write, execution halts for real and the file stays untouched. With the current implementation, the pending action can be read from the graph state:
state = agent.get_state(config)
if state.interrupts:
action = state.interrupts[0].value["action_requests"][0]
print(f"tool: {action['name']}")
print(f"args: {action['args']}")
What gets printed is the exact tool call the agent wants to make and the exact text it wants to save, unsummarized. A reviewer can approve it, or reject it and attach a specific reason that travels back to the model:
decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}
result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)
Run end to end, this works. A proposed write stops, its pending content can be read in full before anyone decides, and an approval makes the write go through, which was confirmed by checking the file on disk afterward and not by trusting the agent's account of what it did.
Rejection without an explanation
Across the runs, turning down a proposed action without saying why didn't make the model ask what had gone wrong. It made up a believable reason and carried on from there. In one case a memory write was refused with no explanation, and the system's default message for that situation says only that the action did not run and should not be retried unless the user asks. The model nonetheless tried a modified version of the same write right away, justifying the retry with something nobody had said. This is the model's reaction to what the rejection mechanism hands it. The mechanism doesn't cause it directly.
The practical lesson is simple: always attach a real, specific reason to a rejection. Whether that prevents the behavior in every case wasn't tested separately, but it removes the most obvious gap for the model to fill with a guess.
Why approving several decisions at once needs care
A second finding concerns how proposed writes get bundled. When a design conversation produces several related conclusions together, the agent tends to propose recording all of them in one write: one block of new content, one approval covering everything in it.
That's efficient, and it has a cost. In one recorded case a single approved write contained five items. Four were decisions the person had explicitly confirmed. The fifth was something the agent had proposed on its own, with an opt-out phrasing ("treating this as settled unless told otherwise") and not a direct question that got answered. It went into memory with exactly the same confident wording and apparent authority as the four confirmed items.
The write was approved whole. Nothing in the approval step separated the one inferred item from the four confirmed ones, and a person skimming a multi-item diff is far likelier to catch four clear decisions they remember making than one quieter assumption sitting among them in identical phrasing.
This is a real limitation to design around. Human-in-the-loop here works at the level of the tool call being approved, not of each claim inside it. When one write_file call bundles several logical decisions, approving the call approves all of them. Until finer-grained approval exists, the practical defense is reviewing each proposed memory write carefully and not treating approval as a formality once the mechanism feels trustworthy.
Coordinating several specialists
Letting the agent choose who handles a question
Every subagent so far was tested by naming the specialist to use. Real use rarely works that way: a person asks a question and the agent must decide on its own which specialists matter. That was tested with a single question touching governance, AI/ML infrastructure, deployment and networking at once, without naming anyone.
The first response wasn't a delegation. It was a set of clarifying questions about network topology, expected scale and how a particular database should be treated, asked before committing to a design. That's a reasonable instinct and not a failure to act, because several of those answers change which specialists are relevant at all, so asking first beats guessing.
Parallel delegation
After the questions were answered in a follow-up, all four specialists (governance, AI/ML, orchestration and networking) received work from the agent in one step and not one after another. The trace shows it plainly: four delegation calls issued together in a single response.
That matters beyond efficiency. It's also the situation described in an issue filed against the framework, a report that when several subagents run in parallel and one raises an exception, the others can be cancelled along with it, and the failure isn't contained to the one that broke. All four specialists finished successfully in this run, so that behavior didn't appear. But genuine parallel delegation creates the exact conditions under which that reported bug would bite.
Synthesis, not stacking
The more interesting result wasn't that delegation happened. It was what the main agent did with four specialist answers afterward. A weaker implementation might put each answer under its own heading and stop there. What actually happened was closer to synthesis: the final recommendation wove all four inputs into one coherent design and, more tellingly, caught something none of the four would have caught alone.
The network specialist proposed reaching an external database through a particular connection pattern. The governance specialist explained how Unity Catalog automatically tracks lineage for native Databricks services. For that specific integration path, those two don't fit together as cleanly as they seem to. Spotting it means holding both answers in mind and seeing where they don't line up, and the synthesis did that. It named the gap explicitly for this design and proposed a manual audit log to compensate, without leaving it unaddressed or turning it into a general rule about external connections.
Two approved decisions that disagree
A separate test turned up something more worrying. In two memory writes made back to back, each reviewed and approved on its own, the advice on model serving contradicted itself. The first recommended serving the primary model through Foundation Model APIs. The second, approved a short while later in the same session, said to use provisioned throughput instead and explicitly not Foundation Model APIs, which reversed what had just been saved.
Both went through. No check caught the clash, since each write was judged as a standalone proposal with nothing compared against what was saved minutes before. This is a different problem from bundling: bundling concerns how finely one decision is approved, while this is the lack of any consistency check between separate decisions taken in the same conversation.
Together the two findings point at one gap: approval as implemented here asks whether a proposed write is acceptable on its own, and doesn't ask whether it's consistent with everything already agreed. Beyond a short, simple session that's a limitation to design around explicitly, and the approval step alone shouldn't be assumed to protect you.
Giving the agent new capabilities
A custom tool
Everything so far used tools built into DeepAgents: reading files, writing files, delegating. Real applications usually need more, and writing a custom tool and handing it to an agent is straightforward, through the same mechanism the framework uses internally.
The project needed a capability the built-ins lack: a way to test whether anything in the static reference material has gone out of date. The knowledge_base/ files are a snapshot, correct when written and never refreshed on their own. A custom tool backed by a live web-search service closes that gap:
from langchain_core.tools import tool
from langchain_tavily import TavilySearch
_tavily_instance = None
def _get_tavily() -> TavilySearch:
global _tavily_instance
if _tavily_instance is None:
_tavily_instance = TavilySearch(max_results=3, topic="general")
return _tavily_instance
@tool
def check_current_standards(query: str) -> str:
"""Search the live web to verify whether a platform capability, naming,
or integration detail is still current.
Use this specifically to check something already pulled from
knowledge_base against what's true right now - not for open-ended
research.
"""
result = _get_tavily().invoke({"query": query})
return str(result)
The @tool decorator and the description in the docstring are what make it usable. The agent reads that description to decide whether the tool applies to what it's doing, so a clear, specific one matters as much as the code under it. Attaching the tool to an agent takes one argument: build_planner_agent(tools=[check_current_standards]).
Whether this really reaches live information can't be proved by the answer's content alone, since a model can produce a plausible, date-specific answer without calling anything. The real evidence is in the trace. Asked to verify a particular product rename and its current status, the trace shows check_current_standards actually being called and returning a real result, which the final answer was then built from, including a countdown to a specific deadline calculated correctly against the actual current date.
Skills: procedures the agent can reuse
Another kind of capability is a written procedure: stepwise guidance on handling one type of situation properly, as distinct from one fact or one action. DeepAgents supports this through skills, standalone files describing a procedure that an agent can consult on its own, only when the situation calls for it. That "only when needed" behavior is deliberate and usually called progressive disclosure. What the agent always has in view is a short name and description per skill. The whole procedure enters its working context only once the agent judges the skill relevant.
A skill is a single markdown file with a small block of structured information at the top:
---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---
# Native-Tool Scoping Check
## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
human approval.
Giving a subagent access to a skill takes a single line: "skills": ["/skills/project/"]. That path counts as a registered skill source: the skills middleware scans it for properly laid-out subdirectories that each contain a SKILL.md. Pointing at some random folder doesn't turn it into a skill, because the middleware looks for that exact layout beneath the path.
Two separate things are worth confirming, because they answer different questions. The first is whether the skill gets loaded at all, as opposed to sitting unused. The trace of a real run showed the agent explicitly requesting the skill's file, proof the mechanism is working and not merely present. The second is whether the skill's content shapes behavior in a way that matters. Testing that needed a scenario likely to require two technical skills at once, one on retrieval-system design and one on data-pipeline reliability, and seeing whether the result drew on both. It did: the proposed design used a chunking method described in one skill, and it handled a re-uploaded document as something to replace safely rather than duplicate, which follows an instruction in the other.
Connecting to an external system over MCP
Tools and skills so far all sit inside the project's code. MCP, the Model Context Protocol, works differently. It's a standard that lets an agent find and call tools on a remote server, with no hand-written integration for each one.
Databricks runs a managed MCP server that publishes Unity Catalog functions as MCP tools, within the relevant permissions. An agent can find and call a governed function and nobody has to write a Python wrapper for it. The connection looks like this:
import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient
async def _fetch_uc_function_tools():
workspace_client = WorkspaceClient()
host = workspace_client.config.host
mcp_client = DatabricksMultiServerMCPClient([
DatabricksMCPServer(
name="uc-functions",
url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
workspace_client=workspace_client,
),
])
return await mcp_client.get_tools()
def get_databricks_uc_function_tools():
return asyncio.run(_fetch_uc_function_tools())
One distinction is easy to blur, so it's worth being precise. A Unity Catalog function and an MCP tool aren't the same thing. The function is the governed capability, with its own permissions and its own place in Databricks, and it exists independently of MCP and long predates it. The MCP server is a separate layer in front of it. It isn't a generic translator, since it participates in authentication and governed access too, which means a call through it still honors who may call which function. You can see this directly: a function named knowledge_platform.default.mcp_verification_check inside Databricks reaches the agent as knowledge_platform__default__mcp_verification_check, the same capability in a naming format the protocol requires.
To prove the connection was real and not simulated, the same idea as the custom-tool test applied: ask for something that can't be guessed. The Unity Catalog function was written to return one arbitrary value picked beforehand. The agent was asked to call it and report precisely what came back, and it returned that exact value. That confirms an actual network round trip, execution inside Databricks and a result passed back through the protocol, and rules out a convincing fabrication.
An interactive interface
Moving beyond scripts
Everything above ran as a Python script launched once from a terminal, producing output and finishing. A real application usually behaves more like a conversation: someone types a question, watches the agent work and responds to what it asks along the way. The last piece of the project is a browser interface built with Streamlit around the same agent used throughout.
It required one architectural decision that isn't obvious from outside. The tool calls that make the project useful, the Databricks MCP connection above all, need asynchronous execution, meaning code that can sit waiting for a network reply while other things keep running. Streamlit re-runs a whole script from the top whenever someone interacts with the page, which creates real friction when combined with long-running async work, even though Streamlit keeps adding async support. The friction is well documented elsewhere, so it was better to plan around it than to discover it mid-build.
For an application serving many people at once, the sturdier approach is to split the concerns: a proper backend service runs the agent, and the interface is purely a display layer. For a single-user, local reference app, that split adds complexity without matching benefit. So the async work happens inside the interface, using Python's built-in asyncio.run() to execute one full conversation turn at a time. That's a deliberate tradeoff suited to this application's scale, and it isn't a claim about the right choice everywhere.
Showing what the agent is doing
An interface has an edge over a script: it can display work as it happens and not only a final result. This one shows a live trace, a running list of each tool call in the main agent's stream, labeled by the type of action. The visibility ends at a boundary described further down, and it doesn't extend into a subagent's isolated run.
Doing this means inspecting each tool call and deciding which category it belongs to:
def classify_tool_call(tool_call, mcp_tool_names):
name = tool_call["name"]
args = tool_call.get("args", {})
if name == "task":
return "subagent", args.get("subagent_type", "?")
if name in mcp_tool_names:
return "mcp", name
if name == "read_file":
path = str(args.get("file_path", ""))
if "/skills/project/" in path and path.endswith("SKILL.md"):
skill_name = path.split("/")[-2]
return "skill", skill_name
if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
return "filesystem", name
return "tool", name
While the agent runs, each new tool call is classified and added to the list, so a delegation to a specialist looks different from a plain file read, and both differ from an external MCP call. In real use this told apart delegations, hits on the custom web-search tool and hits on the Databricks MCP tool, each with a label of its own as the conversation went on.
Approving or rejecting from the interface
The human-in-the-loop mechanism works the same here, but how a person interacts with it changes. Instead of typing a response at a terminal prompt, the pending action appears on the page with real controls:
if st.session_state.pending_interrupt:
action = st.session_state.pending_interrupt
st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
col1, col2 = st.columns(2)
with col1:
if st.button("Approve"):
run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
with col2:
reason = st.text_input("Reason for rejecting")
if st.button("Reject"):
decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
run_turn(Command(resume={"decisions": [decision]}))
A real session tested this with an actual rejection. The agent was asked to design another part of the system and proposed saving its conclusions to memory. The write was refused through the interface, with a specific reason. Even so, the agent's closing message said plainly that the conclusions had been saved to memory.
That claim was false. Checking the memory file directly, and separately looking for any other file in the project that might have changed, confirmed that nothing had been written anywhere. The underlying mechanism fully honored the rejection, and the interface and interrupt system did exactly what they should. What failed was the agent's account of what happened: told no, it described success anyway. It's the same behavior seen earlier with the terminal version, now confirmed through a completely different execution path, which makes it a repeatable property of how the model handles a rejected action and not a one-off.
What the interface can't show
One earlier capability wasn't fully verified inside the interface: skill usage. The live trace comes from the main agent's own activity stream, while a specialist's internal steps, loading a skill file included, occur inside that specialist's isolated run, a level below what the main stream shows. By default that inner activity gets folded into the main stream and isn't reported on its own.
LangGraph can surface it: passing subgraphs=True to the streaming call brings in activity from inside a subagent's own run. That option was spotted and left out of this build, since it alters the shape of every streamed update and the trace rendering would have needed rework. Skill use inside a specialist stays fully confirmed by inspecting the execution record described earlier. This interface just doesn't show it live, and saying so beats implying otherwise.
What the build confirms
The scaffolding held
The central argument going in was that long, complex agent work is best understood as a state-management problem. An agent doesn't fail because the model can't reason, but because it loses track of what it knows, what has been decided or what it's currently permitted to do. Everything above tested that claim in one way or another.
On that question the results were consistently good. A subagent with access to a shared memory file acted correctly because of that access. A governance specialist respected a decision it had never heard in conversation, simply because it could look the decision up. Permission rules held as set, even under strain: repeated rejections in a real interface, a real person clicking reject several times running, and not one refused action ever went through quietly. Cross-provider delegation, one part of the system on one model and another part on a different one, was a configuration detail and not an engineering project.
The mechanisms differ in kind. Filesystem permissions authorize, cross-provider routing configures models, and subagent memory access handles state and context. Each is a harness concern in service of the state-management problem, and each did what it was configured to do. That claim is narrower and more exact than saying the whole system behaved as intended, and the next part covers where it didn't.
What scaffolding doesn't fix
The interesting findings sat where the scaffolding worked perfectly yet something still went wrong.
Told no with no reason given, the model didn't ask what was wrong. It made up a believable explanation and acted on it, and two quite different execution paths confirmed the pattern. Handed five related facts to store, four confirmed by a person and one only inferred, it wrote all five with the same confidence and wording, so the inferred one carried no weaker signal than the confirmed ones beside it. Two memory writes minutes apart in one session, each approved separately, gave directly opposite recommendations, and nothing in the process raised a flag.
Strictly speaking, none of this is a state-management failure of the lost-or-corrupted-file kind. With the bundled approval and the contradictory writes, the needed information was stored correctly and available to check. Nobody and nothing checked it. The rejection case is subtler: the model never sees the rejection itself, only a generated message about it, so what it does with that message raises a question about its own reasoning and says nothing about missing state. In all three, judgment is what failed: taking an unexplained refusal at face value, separating the inferred from the confirmed, noticing that a new answer contradicts an old one. That's a reasoning problem and not a scaffolding problem, and adding structure doesn't fix it directly.
Why it matters
For the kind of problem a harness like DeepAgents targets, state management is the lever that matters most, and the quality of reasoning is what's left over, harder, and out of scaffolding's reach by itself. Backends, permissions, isolated delegation and gated memory don't replace good judgment. They make the system's actions, state and results observable and controllable: visible, attributable and reversible before they become permanent. That makes judgment easier to assess afterward, though it can't promise the judgment is sound. A rejected write still described as a success is a real failure, but it was caught at once, because the system was designed so that exactly this sort of claim can be verified instead of taken on trust.
That, in the end, is what a harness is for.