The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness
Your framework hands you a clean agent loop and nothing else; the reliability you actually want lives in a harness you have to build yourself, and you cannot build it until you have felt the naked loop break.

Most agent failures are not the model's fault. They live in the missing discipline layer around it. Here is what an AI agent with no harness looks like in the Claude Agent SDK and LangChain Deep Agents, and the three specific ways it breaks under real traffic.
Your agent did not hallucinate. It did exactly what it was told, with no one in the loop to stop it, and that gap is something you can actually fix.
In this article: You will learn why the runtime around a language model matters more than the model itself, what an agent stripped of every safety control actually looks like, and how to build that same naked agent twice: once in the Claude Agent SDK and once in LangChain Deep Agents. Then you will watch it break three concrete ways, each one pointing to a control a real AI agent harness has to supply. By the end you will know how to tell a model problem from a harness problem.
A real user typed "March 32nd" into a travel-booking agent. The model passed it straight to the flights API. Forty minutes and roughly fifty dollars of model calls later, someone was explaining a phantom booking to a customer who had already been told they were "all set."
The model did not fabricate anything. It dispatched exactly what it was told to. The harness had no opinion.
That sentence is the whole story. The model supplies intelligence. The harness supplies discipline. When a production agent misbehaves, the failure almost always lives in the discipline layer, not the intelligence layer. No stronger model and no cleverer prompt will fix a missing control. Before you can build any of those controls, you have to feel their absence. So this first part starts naked: the same fragile agent in two frameworks, with no harness at all.
The model is not the variable
There is benchmark-grade evidence that the runtime matters more than the upgrade you keep reaching for. A Princeton team held the model fixed at GPT-4 Turbo and changed only the interface code between the model and the codebase. The SWE-bench resolved-issue rate roughly tripled, from 3.8% to 12.47%, with no change to the model's weights. The paper, SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering, landed in 2024 and was accepted at NeurIPS that year. The lever was the interface, not the intelligence.
The discipline at work has an older name: mechanical sympathy, software written to work with the substrate it runs on instead of fighting it. Harness engineering applies that idea to a new substrate, the language model with its context window and its attention budget. The model sets the ceiling. The harness decides how reliably you reach it.
Hold onto the reframe, because it changes what you do when an agent fails:
- Before: the agent said something wrong, so you tweak the prompt, add caveats, try a stronger model, and hope.
- After: the agent said something wrong, so you name the specific missing control and move that rule out of the prompt and into the runtime.
Prompts are persuasion. Harnesses are enforcement. Everything that follows is enforcement.
What "naked" actually means
Underneath every agent framework is the same five-step loop. Send the model your tools. Get back a tool call. Run it. Send the result. Get the next move. Anthropic's tool-use docs, OpenAI's Responses API, and LangChain's agent runtime all reduce to it. The SDK changes; the shape does not.

Here is the bare shape: raw provider SDK, two travel tools, no safety net of any kind.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
TOOLS = [
{"name": "search_flights",
"description": "Search flights between two cities for a date.",
"input_schema": {"type": "object", "properties": {
"origin": {"type": "string"}, "destination": {"type": "string"},
"date": {"type": "string", "description": "YYYY-MM-DD"}},
"required": ["origin", "destination", "date"]}},
{"name": "book_flight",
"description": "Book a specific flight.",
"input_schema": {"type": "object", "properties": {
"flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
"required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
messages = [{"role": "user", "content": user_msg}]
while True: # ① no iteration cap
resp = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024,
tools=TOOLS, messages=messages,
)
if resp.stop_reason != "tool_use":
return resp.content[0].text
call = next(b for b in resp.content if b.type == "tool_use")
result = dispatch(call.name, call.input) # ② direct side effect, no check
messages.extend([ # ③ whole history, every turn
{"role": "assistant", "content": resp.content},
{"role": "user", "content": [{"type": "tool_result",
"tool_use_id": call.id, "content": result}]},
])
① while True: has no cap, so a faulty plan keeps calling the model until your budget
runs out; nothing in the loop counts iterations.
② dispatch sends the model's arguments straight to a side effect with no pre-flight
validation, so a structurally valid but impossible value reaches the real API.
③ The full message history is re-extended and resent on every turn, so a long run
silently inflates context until quality degrades.
Note: The full extracted listing at
code/harness_engineering/part-1-the-naked-agent/listings/01-naked-loop-raw-sdk.py
shows the dispatch stub and runnable entry point elided here.
Read it with three failure seams in mind. Every safety property you want lives outside this loop, in code you have not written. And here is the reframe most people miss: reaching for a framework does not close these seams. The Claude Agent SDK and Deep Agents give you a better-built version of this loop, with a real tool protocol and a clean execution path. They do not, by default, give you validators, memory you can reason about, or recovery. They hand you a loop. The harness is still yours to build.

Here is the same naked agent in both frameworks.
The naked agent in the Claude Agent SDK
The Agent SDK wraps the loop for you. You declare tools with the @tool decorator, expose them as an in-process MCP server, and call query(). What you do not see here is any control: no argument validation beyond the schema, no memory across calls, no recovery.
import asyncio
from claude_agent_sdk import (
query, ClaudeAgentOptions, tool,
create_sdk_mcp_server, AssistantMessage, ResultMessage,
)
@tool("search_flights", "Search flights between two cities for a date.",
{"origin": str, "destination": str, "date": str})
async def search_flights(args): # ① no check that date exists
hits = flights_api.search(**args)
return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
{"flight_id": str, "passenger_name": str})
async def book_flight(args): # ② destructive, ungated
confirmation = flights_api.book(**args)
return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
options = ClaudeAgentOptions(
mcp_servers={"travel": server},
allowed_tools=["mcp__travel__search_flights",
"mcp__travel__book_flight"],
)
async for msg in query(prompt="Rebook this customer for March 32nd.",
options=options):
if isinstance(msg, AssistantMessage):
for b in msg.content:
if hasattr(b, "text"):
print(b.text)
elif isinstance(msg, ResultMessage):
print("done:", msg.subtype) # ③ no state survives this run
① search_flights accepts whatever the model proposes within the schema, so an
impossible date passes straight through to the tool body.
② book_flight is a destructive action with nothing standing in front of it: the
decorator wires it to the loop with no approval gate or validator.
③ When the ResultMessage arrives and query() returns, the session ends and no
state from this run carries into the next call.
Note: The full extracted listing at
code/harness_engineering/part-1-the-naked-agent/listings/02-naked-agent-claude-sdk.py
shows the asyncio.run(main()) entry point elided here.
The SDK gave you a clean loop. It did not give you an opinion about bad dates, double charges, or what to remember.
The naked agent in LangChain Deep Agents
Deep Agents wraps the loop differently: you pass plain LangChain tools to create_deep_agent() and get back a compiled graph that you invoke. The story on the seams is the same. There is no validator, and with no checkpointer the graph forgets everything between calls.
from langchain.tools import tool
from deepagents import create_deep_agent
@tool
def search_flights(origin: str, destination: str, date: str) -> str:
"""Search flights between two cities for a date (YYYY-MM-DD)."""
return str(flights_api.search(origin, destination, date)) # ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
"""Book a specific flight."""
return flights_api.book(flight_id, passenger_name) # ② ungated side effect
agent = create_deep_agent( # ③ the loop, no controls
model="anthropic:claude-sonnet-4-6",
tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
"content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke and it starts from zero: no thread, no memory.
① The search_flights body calls the API with the raw date string; nothing checks
that the date is real before the call goes out.
② book_flight performs the destructive booking the moment the model selects it, with
no gate between the proposal and the side effect.
③ create_deep_agent returns a graph that runs the loop and nothing more: with no
checkpointer passed, the agent has no thread and no memory between invokes.
Note: The full extracted listing at code/harness_engineering/part-1-the-naked-agent/listings/03-naked-agent-deep-agents.py shows the complete runnable script.
Two frameworks, two clean loops, and zero discipline. That is what naked looks like in 2026.
Watch it break three ways
The naked agent is not a strawman. It is the shape of dozens of agent demos posted to GitHub every week. Here are the three failures it produces under real traffic. Each one maps to a control a real harness adds.
Failure 1: a malformed argument reaches a destructive call
The user says "March 32nd". The model, doing what models do, fills the date field with a string that is structurally valid and semantically impossible. The schema checks that date is a string. It does not check that the date exists. So book_flight runs against a phantom itinerary, and the model reports success.

book_flight(flight_id="AC-PHANTOM", passenger_name="J. Moffatt")
# -> "Booked." The action fired. Nothing in the loop asked whether it should.
There was no action boundary: no validator standing between the model's proposal and the side effect. That boundary is the job of tool contracts and validators.
Failure 2: context blows up and quality degrades silently
The naked loop resends the entire message history on every turn. A long rebooking conversation, with search results, policy text, and retries, climbs past fifty thousand tokens. Nothing trims it. Quality decays in the way the literature calls context rot, and the agent starts skipping steps it handled cleanly earlier in the same run. You get no error. You get a worse agent, quietly.

There was no working memory and no context assembly. Those are the jobs of memory and durable state and of context assembly.
Failure 3: a tool errors, the agent reports success
A tool returns an error payload. The model reads it and, more often than you would expect, composes a polite summary that elides the failure. As one field analysis of production failures put it, agents are confident liars: a database that returns zero rows is read as a factual answer, not as a miss. The hard problem was never picking the right tool. It is detecting the difference between a tool that succeeded and one that plausibly mimicked success.
There was no observation feedback loop. That is the job of observability and drift detection.
Read these as a diagnostic pattern, not a list of shame. In all three, the model did what models do. The system around it failed to catch it. Not one is a story about a model that was not smart enough.
The shape every part will follow
The rest of this series runs the same four-part move in every article. It is the working loop of harness engineering:
- Introduce the harness function and the failure it controls.
- Implement it in both frameworks as a focused snippet.
- Trigger the failure to prove the control is load-bearing.
- Inspect the harness response.
This article used that move three times without naming it: a naked seam, the failure it produces, and the control it points to. From here on, the series closes the seams one at a time.

| Part | Harness function | Closes the seam from |
|---|---|---|
| 1 | (control group) | this article |
| 2 | Tool contracts and validators | Failure 1 |
| 3 | Memory and durable state | Failure 2 |
| 4 | Context assembly | Failure 2 |
| 5 | Recovery: rollback, retry, replay | runaway loops |
| 6 | Orchestration and subagents | one agent doing too much |
| 7 | Human-in-the-loop and approvals | irreversible actions |
| 8 | Observability and drift detection | Failure 3 |
Do this today
You do not need the rest of the series to start feeling the difference. Open an editor and try these:
- Run the naked agent in either framework against
"Rebook this customer for March 32nd."Watch the impossible date flow straight through tobook_flight. - Add a five-iteration cap to the bare loop. Notice that this is the first harness control you have written, and that it lives in the runtime, not the prompt.
- Make
book_flightreturn{"is_error": true, ...}and observe whether the model's final summary admits the failure or smooths over it. - Pin one
claude-agent-sdkversion and onedeepagentsorlangchainversion before you run any of this; both APIs drift between releases, and the model identifiers (claude-sonnet-4-6,anthropic:claude-sonnet-4-6) are placeholders to set to whatever is current.
The model is the easy part
Both frameworks gave you a clean, well-built loop. Neither gave you a harness. That gap is not a flaw in the tools; it is the work. Claude Code, OpenCode, Codex, and Deep Agents are all agent harnesses, and the difference between a developer who fights them and one who gets reliable outcomes is whether they engineer the controls that the loop leaves out.
The model is the easy part. It has been for a while. The reliability you actually want is downstream of the discipline you build around it, one failure at a time. Next comes the action boundary, and the first rule you move out of the prompt and into the runtime.