Building a production-ready AI agent
Everyone’s agent demo works. The loop calls the model, the model calls a tool, something useful comes out, the room nods. Production is where it dies.
Production asks questions the demo never faced. What happens to the conversation after five hundred turns? What happens when the process restarts while a human approval is pending? Which of the agents in the pipeline just spent forty cents on one tool call? What stops the model from pasting an internal credential into a customer email? None of these are answered by the model. All of them decide whether the thing ships. Some don’t even become visible until the second week of real traffic, when the first long conversation crosses a pricing cliff nobody knew existed.
And here’s the uncomfortable proportion at the center of it: the agent itself — the part everyone obsesses over — is about ten lines.
while True:
out = call_llm(agent, items) # 1. call the model
if out.is_final: # 2. plain text -> done
return out.text
results = run_tools(out.tool_calls) # 3. run the tools
items += out.tool_calls + results # 4. append, go again
That’s the whole thing. Call the model; if it answered in plain text, you’re done; if it asked for tools, run them, append the results, go again. Every agent framework on earth is selling you some version of this loop.
The model reasons. The harness makes it production-ready. The harness is my name for everything engineered around the loop — the systems that keep it capable, bounded, observable, and safe. In 2023 the scarce skill was prompt engineering. By 2024 it was context engineering. The unit of engineering now is the harness — and the gap between a bare model and a harnessed one widens every year, because better models don’t shrink the harness’s job. They raise what a good harness can extract.
What follows is the map of the harness I build and run in production: thirteen systems, four concerns, with the code.
The map: four concerns, thirteen systems
Every system in the harness answers to one of four concerns.
Capability is what the agent can do: its tools, the MCP servers that supply tools you didn’t write, the skills that package expertise into loadable folders, the sub-agents it delegates to, and the sandbox where heavy work happens.
Context is what it knows and sees: long-term memory that outlives any single conversation, and context management — the discipline that keeps the model’s working window small, cheap, and lossless all at once.
Safety & quality is what it’s allowed to do, and how you know it’s good: guardrails wrapped around the run, human-in-the-loop approval for the irreversible actions, and evaluation that catches quality regressions before users do.
Operations is how you watch it work: streaming that shows progress live, tracing that prices every step, and feedback that turns user judgment into signal.
Thirteen systems. None of them is exotic on its own; the engineering is that all of them are present and that they cooperate — the sandbox feeds the memory, the traces feed the eval, the eval feeds the self-improvement loop at the end of this article. The numbering isn’t priority order, it’s dependency order — later systems lean on earlier ones. The rest walks the map, top to bottom.
Part I — Capability
Tools: the typed baseline
The baseline unit of capability is a typed function tool.
@function_tool
async def list_integrations(
ctx: RunContextWrapper[AppContext],
name: str | None = None,
) -> str:
"""List published integrations."""
rows = await ctx.context.db.integrations(name)
return json.dumps(rows)
The decorator derives the JSON schema from the type hints and the docstring — nobody should write tool schemas by hand. The context wrapper injects per-request state: who’s asking, which tenant, a live database handle. That’s the detail worth copying — tools never read globals, so one tool definition serves every user concurrently. It runs in-process, no ceremony.
Tools are deliberately boring. A production agent’s problem is never “can I write a tool” — it’s what happens when you have sixty of them. Everything else in this part is about scaling past the flat list.
MCP: tools you don’t write
The second source of capability is tools you don’t write. MCP — the Model Context Protocol — lets any vendor ship a server exposing their tools, and wiring one in is a few lines:
jira = MCPServerStdio(
name="Jira MCP Server",
params={"command": "uvx", "args": ["mcp-atlassian"]},
)
# connected once at startup, reused across turns
# failed connect -> server disabled; the agent still boots
The lines that matter are the comments, because connection lifecycle is the actual engineering here, and it has two rules. First: connect once, at startup, and reuse the connection across turns — a per-call reconnect adds seconds to every tool use and will come to dominate your latency budget. Second, the production rule: when a server fails to connect, mark it disabled and boot anyway. An agent with sixty tools that loses its Jira integration should become an agent with fifty tools, not an outage. Fewer powers beats no agent, every time.
One more discipline: a server’s tool list is not your tool list. A vendor server happily exposes forty tools; allowlist the six your agent actually needs. Every extra definition is prompt space and decision surface, and models get measurably worse at choosing tools as the menu grows.
Skills: capability as a catalog
Past a few dozen tools, the flat list stops scaling — not because the model can’t choose between them, but because every registered tool definition is prompt space you pay for on every call, whether or not the task needs it. The answer is skills: capability that loads on demand.
A skill is a folder. The format is open (agentskills.io, originally Anthropic’s), and the whole contract is a SKILL.md with YAML frontmatter, plus whatever the skill needs alongside — scripts/ holds executables, references/ holds the depth:
system-debugger/ # a skill is a folder
├── SKILL.md
├── scripts/
└── references/
--- # SKILL.md frontmatter
name: system-debugger
description: Diagnose production issues from container logs.
Use when a job hangs, a queue is silent, or you need a root cause.
---
# then: workflow · grep references/ · report format
The mechanism that makes it scale: at startup, only the description — twenty-ish words per skill — enters the system prompt. That’s the catalog. The full body loads only when a task matches, and because the SDK I use has no native skill support, a matched skill runs as a scoped sub-agent: fresh context, given the skill body and the tools it declares, nothing else. Depth stays unbounded — a skill can carry a references/ folder of runbooks and grep it on demand — while the standing cost stays twenty words.
Which puts enormous weight on those twenty words. The description is a routing index entry; it alone decides whether the skill ever fires. Write it like an API contract, not marketing copy: what it does, then “Use when…” with the exact situations. A vague description is a skill that never runs.
Skills are also code, so they get treated like code: registered in a versioned registry and validated at boot — frontmatter schema, name uniqueness, description length. A malformed skill fails at deploy time, not at routing time at 2am.
Sub-agents: two ways to compose
Skills already smuggled in the next idea: sub-agents. There are exactly two ways to compose agents, and confusing them causes real design damage.
Agents-as-tools is the default. The orchestrator calls a specialist the way it calls any tool; the specialist runs, returns a result, and control comes back. The orchestrator owns the final answer — it can fan out to three specialists, merge their outputs, discard a bad one, and wrap the whole thing in its own guardrails.
Handoffs transfer control instead of returning it. Agent A stops being responsible; agent B owns the conversation from here on. In my harness this is rare — the one clean use is triage: a front-door agent that classifies the request and hands off to billing or support, permanently.
The rule: default to agents-as-tools, and reach for a handoff only when the specialist should genuinely own everything that follows. If the caller needs to see the result, it wasn’t a handoff. And budget honestly: every specialist is its own model loop, so a three-agent fan-out is three times the calls plus its own latency tail. Parallelize what you can; don’t spawn a sub-agent where a function would do.
There’s a quieter benefit hiding here. Every sub-agent starts with a fresh, scoped context — the specialist sees its task, not your entire conversation history. Composition is also context hygiene, which previews Part II: half of capability design turns out to be window management wearing a different hat.
Sandboxes: files, not context
The last capability system exists because of a category error: making the model’s context window do a filesystem’s job. Ask an agent to analyze a 40MB log file and the naive design pastes it into the conversation. Now every turn re-reads 40MB, the window blows out, and the agent gets dumber and more expensive at the same time.
The fix is a sandbox: an isolated Unix environment where heavy work happens. A manifest declares what goes in and what comes out:
manifest = Manifest(entries={
"logs.jsonl": File(content=raw_logs), # data in
"evidence": Dir(), # files out
})
agent = SandboxAgent(
name="Log analyst", model="gpt-5.5",
instructions="grep the logs; write findings to evidence/.",
default_manifest=manifest, capabilities=[Shell()],
)
result = await Runner.run(agent, task) # only a summary returns
The agent gets a shell. It greps, filters, runs scripts — the 40MB stays on disk where 40MB belongs, intermediate artifacts land in evidence/, and the only thing that returns to the conversation is the summary: what it found, and where it wrote the proof.
Files, not context. The prompt is for reasoning; the filesystem is for data. The isolation doubles as the safety boundary — no network egress, no credentials beyond what the manifest grants — which keeps “the agent runs arbitrary shell commands” a feature rather than an incident report. Once that split exists it pays for itself in unexpected places — Part II’s memory section is one of them.
Notice the shape of this whole part. Capability scales by indirection: tools are defined inline, servers are wired once, skills are cataloged in twenty words, specialists get fresh contexts, heavy work is exiled to a filesystem. Each step moves bulk further from the model’s window. That’s not an accident, and it’s not really about capability at all — it’s the first appearance of the constraint that runs this entire architecture. The window is the scarce resource. Part II manages it directly.
Part II — Context
Memory: two systems, chosen by failure mode
“Memory” means two different things on two timescales. Short-term memory is the context window itself — the live working set the model reasons over — and it’s deep enough to get the next section. This section is long-term memory: knowledge that outlives any single conversation.
The obvious way is vectors. An LLM watches the conversation and extracts durable facts — not raw transcripts, distilled statements like “prefers staging deploys on Fridays” — and mem0 embeds them into pgvector with text-embedding-3-small. Retrieval queries two pools:
# search — two reranked pools, awaited
personal = await memory.search(q, filters={"user_id": uid},
top_k=5, rerank=True)
system = await memory.search(q, filters={"user_id": "system"},
top_k=3, rerank=True)
# save — returns in ~1ms; embed + store run in the background
asyncio.create_task(do_save(text, uid))
return f"Remembered: {text[:100]}..."
Two pools, deliberately. The personal pool is keyed by the user’s id and private to them. The system pool is keyed "system" and shared — platform knowledge every user benefits from. A search hits both and reranks the union. And note the save path: it acknowledges in about a millisecond and runs extraction, embedding, and storage in a background task. Remembering costs an LLM call; a turn should never block on it.
The agent sees all of this as two ordinary tools — search when context is thin, save when the user states something durable — and the tool descriptions matter as much as the plumbing. “Save durable facts, not conversation summaries” is the difference between a memory and a landfill. The extractor also owns hygiene: a new fact is reconciled against the pools, so “prefers Friday deploys” updates the existing memory instead of accumulating ten near-duplicates — and anything secret-shaped is never stored at all.
The less obvious way is one Part I already built: the sandbox filesystem. An agent that writes findings to files can grep them back next week. That’s memory too — exact, lossless, navigable, fully auditable.
Why run both? Because every memory system fails, and the game is choosing where you can afford the failure. Embeddings are paraphrase-tolerant — ask “what does this user like about deploys” in words never stored, and they’ll still find it — but they’re lossy, and they fail silently: a fact that missed the top-k simply isn’t there, and nothing tells you. Grep is the opposite: byte-exact and lossless, and it fails loudly — on synonyms, on drift, on a query phrased differently than the file. So: embeddings for fuzzy recall and cross-conversation preferences, where missing one is cheap. Files-and-grep for IDs and findings you must recall exactly, where a silent miss is expensive. The principle generalizes well past memory: pick the system whose failure mode you can afford.
Context management: caching cuts cost, compaction cuts tokens
The context window is the short-term memory, and managing it starts with a distinction that sounds pedantic and turns out to be the whole game: caching cuts cost; compaction cuts token count. They are different axes. Prompt caching makes re-sent tokens cheaper — same window, discounted bill. Compaction makes the window itself smaller. Teams enable caching and think they’ve solved growth (the window still marches toward the pricing cliff), or build compaction and wonder why the bill barely moved (every turn still pays full freight on an uncached prefix). You need both, and conflating them is the most common context mistake I see.
The caching half is configuration more than architecture: a stable cache key per user, retention long enough (24 hours) that every turn reuses the shared prefix, responses stored server-side so there’s something to fold later.
The compaction half is four layers, escalating:
Masking runs on every model call. Any tool output over ~500 characters is replaced in the outgoing history by a short deterministic placeholder — the agent remembers that the tool ran, not its 14KB payload. No LLM involved. And the call/result pairing stays intact: delete items outright and the API rejects your history.
Between-turn compaction is the everyday path. After each turn I read the real input-token count from the last call — the number I was billed for, not an estimate — and at 200K I fold the history into one summary plus a verbatim tail of recent items. The threshold placement is the point: it sits at the money, just under the 272K pricing cliff where input starts billing at a multiple — nowhere near the window’s actual capacity.
The mid-flow guard exists because a single turn can run away: an agent looping on tools, each call carrying a bigger input than the last, can cross the cliff inside one turn. At ~900K — about 86% of the window — compaction fires mid-stream. Disruptive by design, deliberately rare.
Overflow is the last resort. If a call still fails on context length despite all of the above: force one compaction, retry exactly once, and if that fails too, return a clean “this conversation grew too large — resend your last message” instead of a stack trace.
Underneath all four: compaction never deletes. It flips an is_summarized flag — the model reads the rows where it’s false; exports and audits read every row. One table, two views, nothing ever lost.
Each of those paragraphs compresses production teeth — pairing rules, sub-agent token accounting, why the thresholds sit exactly where they do. I wrote the full deep dive in An unlimited context window (without an unlimited model).
That’s the context concern in full: memory decides what the agent knows; context management decides what it sees right now. Both exist to spend the window well. Which leaves the harder question — what is the agent allowed to do with all this capability — and that’s Part III.
Part III — Safety & quality
Guardrails: six hooks, not two
Most teams picture guardrails as two moderation endpoints — scan the input, scan the output — bolted onto an otherwise opaque run. That undersells what’s available. The run is not opaque: it exposes six lifecycle hooks, and a guardrail is nothing more than a check wrapped at one of them.
The two everyone builds first map to the ends. An input guardrail at on_agent_start rejects before the model generates its first token — the cheapest possible place to say no, because you pay for nothing downstream of it. An output guardrail at on_agent_end is the last gate before the user: where leaked credentials, PII, and policy violations get caught after generation but before anyone sees them.
The interesting one sits in the middle. on_tool_start gates a specific tool with full knowledge of its arguments — “this refund exceeds $500” is checkable there and nowhere earlier — and it’s where human-in-the-loop attaches, which gets its own section next. The remaining hooks (on_llm_start, on_llm_end, on_tool_end) do the quieter work: shaping what enters the model, validating what a tool returned before the model reasons over it.
The design rule that falls out: put each check at the hook where rejection is cheapest. Blocking a bad request costs a string comparison. Blocking a bad answer costs a full generation. Un-sending a bad email costs a career. Move every check as early as its information allows.
Two operating modes matter in practice. A blocking guardrail fails the run; a tripwire logs and lets it pass. New guardrails ship as tripwires first — you learn a check’s false-positive rate on real traffic before you allow it to say no to a customer.
Human-in-the-loop: approvals that survive a restart
Some tools should never run un-reviewed. Refunds over a threshold, deletes, anything that sends to a real customer — the agent can decide that it wants to act; a human decides whether it does.
The mechanism is small. A tool marks itself as needing approval — a boolean, or a predicate over the arguments — and the run pauses when the model tries to call it:
@function_tool(needs_approval=True) # or a predicate
async def cancel_order(order_id: int) -> str: ...
result = await Runner.run(agent, msg)
while result.interruptions: # paused on a tool
state = result.to_state() # serializable
for it in result.interruptions: # ToolApprovalItem
state.approve(it) if ok(it) else state.reject(it)
result = await Runner.run(agent, state) # resume
The run comes back not with an answer but with interruptions — the tool calls awaiting judgment. Approve and resume; reject, and the model is told no and reasons onward from there.
Everything above works in a demo. The line that makes it production is result.to_state(): the paused run serializes to a string. Persist it, and the approval no longer has to happen here, now, in this process. The reviewer can be paged and answer in an hour. The service can deploy twice in between. The verdict can arrive from another machine behind a queue. Without serialization, human-in-the-loop means a human staring at a terminal while the process holds its breath. With it, approval becomes a durable workflow like any other. One method is the difference between a feature you demo and a feature you ship.
In the UI, the paused run is just another phase in the streamed tree — an approval card where a tool row would be. And the wait costs nothing: a paused run is serialized state in a database, not a process holding a connection open.
Eval: a floor in CI, a ceiling in the loop
Nothing in this article survives contact with production unless you can measure it. “The agent seems better lately” is not an engineering statement. Quality has to be a system, and mine has two layers.
The first layer is eval in code — pytest for LLMs, and it literally runs under pytest:
def test_refund_answer(): # a regression test
case = LLMTestCase(
input="How do I request a refund?",
actual_output=run_agent("How do I request a refund?"),
expected_output=golden)
assert_test(case, [
AnswerRelevancyMetric(threshold=0.8),
GEval(name="correctness",
criteria="matches the golden answer?")])
A case is an input, the agent’s actual answer, and a golden expected answer; the metrics are LLM-judged with hard thresholds. DeepEval supplies the metrics, pytest supplies the workflow every team already has — and the point is where it runs: CI. Change a prompt, reorder skills, swap the model, and if refund answers regress, the build fails before the change ships. Exactly like a broken unit test, except the unit is behavior.
Two details earn their keep. Metrics are plural on purpose: relevancy alone rewards confident evasion, correctness alone punishes answers better than the golden, so hallucination and task-completion metrics cover what the first two miss. And LLM-judged tests are slow, mildly flaky, and cost real money — so the CI suite stays small and sharp, a few dozen cases on the flows that pay the bills. The thousand-case sweeps live elsewhere, off the critical path. Goldens are versioned like code, too: when a product decision changes the right answer, the golden changes in the same PR. The suite tests intent, not history.
Elsewhere is the second layer: an eval service running a permanent four-step loop. Dataset — curated goldens (input, expected output, tags), seeded by hand and grown from production traces and user feedback. Experiment — run the agent, or a variant, over the dataset; score every output into a pass rate. Compare — A against B: did the new prompt help, what broke, which cases are chronically weak. Optimize — treat the score as a reward, search prompt and skill changes that raise it, commit the winner.
Floor and ceiling. The CI layer means you never ship worse; the service layer means you keep shipping better. And that pass rate quietly becomes the most important number in the harness — the last section of this article turns it into an objective function.
Part IV — Operations
Streaming: one stream, two consumers
An agent turn can take ninety seconds — several model calls, three tools, a skill running in a sandbox. Nobody watches a spinner for ninety seconds. But streaming an agent is harder than streaming chat, because tokens aren’t the only thing happening: the interesting events are structural. Which phase are we in? Which tool just started? Did it succeed?
I emit one SSE stream with a small event vocabulary: run_start; step_start for each phase (a thinking phase, a skill run); reasoning_delta while the model thinks; tool_start and tool_complete around each call; content_delta as answer tokens arrive; run_complete. Every event carries an id and a parent id, and that small decision does the heavy lifting: the UI reconstructs a live tree from parentage alone — phases containing tools, tools with durations and checkmarks, the answer streaming in below. A skill run is always a phase boundary, never a leaf, so a running skill renders as a section the user watches fill in.
The same events serve a second consumer. Persisted alongside the message, they are the trace: reload the conversation a week later and the finished turn re-renders its tree from the stored events — exactly what you watched live, because it is the same data. The live view and the audit trail cannot disagree; they were never separate systems.
One hardening note from real networks: during a long tool call, nothing streams — and idle connections get killed by proxies somewhere around the 30–60 second mark. A keepalive event every 15 seconds keeps the pipe warm. Boring — and it separates “works on my machine” from works behind a corporate proxy. And because events are journaled server-side as they’re emitted, a mid-run reload isn’t a blank screen: the client reconnects and tails the journal from the last event it saw. Streams that can’t resume punish exactly the long runs that need streaming most. I wrote the full queue-and-journal design up in A run is not a request (agent runs on plain Postgres).
Tracing: cost per span
Streaming shows the user what’s happening. Tracing shows you. The wiring is four lines at startup:
import mlflow
mlflow.set_tracking_uri("http://mlflow-server:5000")
mlflow.set_experiment("agent-prod")
mlflow.openai.autolog() # every run becomes a trace
From then on, every run is a trace in MLflow: a span tree — agent at the root; model calls, tool executions, guardrail checks as children — each span carrying latency, token counts, and cost. A custom trace processor keeps spans clean (autolog’s defaults are noisy), and model calls route through a gateway so cost attribution is exact rather than estimated. Guardrail outcomes land in the same tree, so a blocked run and its reason are one query away.
Cost per span is the feature that changes team behavior. “The agent is expensive” is a mood. “The summarize-doc skill costs three cents a call and is 60% of turn latency” is a work item. When latency spikes, the span tree answers which tool; when the bill spikes, it answers which agent, which user, which day. Debugging an agent without traces is archaeology. With them, it’s reading. Traces are also where eval datasets come from — a weird production case is one click from becoming a golden.
Feedback: a thumb keyed to a trace
The last operations system is the smallest, and it closes the biggest loop. Every answer in the UI carries a thumbs up/down. The implementation detail that matters: the thumb is keyed by the same trace id as the run that produced the answer.
Un-keyed feedback is a sentiment dashboard — 4.2 stars, trending down, nobody knows why. Keyed feedback is a repro case: this exact answer, produced by this exact span tree — these skills, these tool calls, this context state — displeased this user. You can go read what the agent actually did.
And repro cases have a destination: the eval pipeline from Part III. Reviewed thumbs-down cases become new goldens in the dataset, so the chronically weak spots in the next experiment are literally last month’s complaints. Feedback → dataset → experiment → better agent → new feedback. The loop is closed. Volume turns out not to matter much — a few percent of turns get a thumb — because eval doesn’t need volume. It needs cases worth studying, and users flag those better than any sampler.
Which completes something worth pausing on. Operations was supposed to be the boring part — logs and dashboards. Instead, streaming produced a lossless record of every run, tracing priced it, and feedback attached human judgment to it. The harness now holds everything a learning system needs: behavior, cost, and a reward signal. That’s not an operations stack anymore. That’s a training setup pointed at itself — and it’s where this article has been heading all along.
Where this goes
Everything so far ships today. This last part is where it points — and the short version is that the harness wants to improve itself.
Take inventory. The eval service produces a score: a measurable statement of how good the agent is. Feedback streams in fresh signal keyed to real behavior. Memory and configuration are writable stores that change how the agent acts. Objective, signal, parameters — that is a reinforcement-learning setup, with one substitution that changes the economics: the parameters are the config, not the weights.
Written as an objective: score = w₁·accuracy + w₂·relevancy + w₃·safety + … — each dimension an eval metric, the weights encoding what your product actually cares about. “Training” is then a search over agent configs — the system prompt, the skill set, individual skill instructions — for changes that raise the score. Propose a variant, run it over the goldens, keep the winner, revert the losers.
The offline version I call dreaming. The agent replays its stored trajectories — every one preserved losslessly by the soft-delete archive from Part II — and distills what worked into durable memories and sharper skill instructions, written back to the memory store for tomorrow’s runs. I’ve since written up the full design: Self-improving agents: RL on the config, not the weights.
Why bother, when real RL exists? Because this variant has none of RL’s costs. No gradients, no training run, no GPU bill — and no lock-in to one model: swap the model and the learned config still applies. Every “update” is a diff you can read in review, audit later, and revert with git. RL on the config, not the weights: cheap, auditable, reversible. And it compounds with use, because every day of traffic grows the dataset and refreshes the signal. None of it runs unsupervised, either — a change proposed by the loop ships through the same review and CI evals as a change proposed by a human. Dreaming proposes; people and pipelines dispose. The loop gets a seat at the table, not the keys to the repo.
The second place this goes: once every capability lives in the harness, an individual agent stops being code. It collapses into one declarative file:
name: support-agent
model: default
prompt_file: prompt.md
skills: [knowledge-search, ticketing, summarize-doc]
guardrails:
input: [intent_safety]
output: [pii_leak]
mcp_servers: [jira, slack]
eval: # self-improvement — the dreaming loop
dataset: support-goldens
weights: {accuracy: 0.5, relevancy: 0.3, safety: 0.2}
improve: {dreaming: nightly}
That file belongs to the support team, not the platform team. One shared engine validates every profile at boot and runs them all — the support agent, the sales agent, the devops agent are the same engine with different files. Adding an agent is adding a file: no engine change, no redeploy. And each agent improves against its own goldens, on its own weights, at its own cadence. This is how a platform falls out of a harness: build the thirteen systems once, and every agent after the first is configuration.
What transfers
Strip the specifics, and four principles carry to any serious agent system:
-
The model is the small part. The loop is ten lines; the product is the thirteen systems around it. Budget your engineering accordingly — the teams that struggle are usually polishing prompts while the harness doesn’t exist yet.
-
Context is the scarce resource. Count how much of the harness is window management in disguise: skills exist to keep capability out of the prompt, sandboxes to keep data out of it, sub-agents to scope it, masking and compaction to shrink it, memory to outlive it. Design every system by asking what it puts in the window, and when.
-
The loop can improve itself without touching the weights. Eval gives an objective, feedback gives signal, config gives parameters. You don’t need a training run to have a learning system — you need those three wired together and the discipline to measure.
-
An agent is config; the engine is shared. Write the harness once. If adding your second agent is an engineering project, the harness isn’t finished.
The loop from the first page hasn’t changed — call the model, run the tools, go again. It was never going to change; it was finished before any of us arrived. The work — the part that decides whether your agent is a demo or a product — is everything you build around it.
The model reasons. The harness makes it production-ready.
Comments
Loading comments…