Back to AI/ML Overview
Harness Engineering from First Principles

An agent is an HTTP call in a loop. Everything else is the harness.

The model is stateless. A runtime always runs the loop and carries or rebuilds the state. That runtime is the core of the harness, whether you operate it or a provider does. This page builds that idea from one request, with one small example carried through every section.

1
call to the model
30
lines for a whole agent
8
jobs of a harness
107
AI job postings counted
54
mention orchestration
17
name the top framework

🧭At a glance

The model
The model is stateless: it keeps nothing between two calls. Each call must be given everything it needs.
The call
A hosted model is commonly reached through an . Content goes in. Content and control signals come out.
The loop
The model asks for a tool. Code runs the tool and sends the result back. Repeat until the model stops asking. That loop is the agent.
The harness
The is the runtime around the loop. It has eight jobs, from building the context to scoring the results.
The scarce resource
Every model step consumes the again; caching can lower its price. What enters the context is the main design decision.
The decision
Frameworks and managed platforms are packaged versions of the same jobs. For each job, ask one question: who runs it?

Three meanings of the word harness

People mix these three in conversation. This page is about the first one.

Agent harness
The runtime around the model loop. The subject of this page.
Coding harness
A vendor-built agent harness for software work: Claude Code, Codex CLI, GitHub Copilot CLI, Antigravity CLI, Cursor, Grok Build.
Eval harness
The rig that runs test cases and scores them. An is an automated test of model or agent output.
💡Scope of this page
This page maps the runtime responsibilities of an agent harness. Deployment platforms, protocols and systems surround those responsibilities. They are placed on the map in section 7, but they are not themselves harnesses.

📊1. Why now: what 107 job postings mention

I went through the 107 AI job postings I saved for my own search. Not a market survey. I counted how many postings mention each capability and each product.

WHAT THE HARNESS DOESOrchestration54RAG / retrieval51Observability / tracing41Evaluation38Guardrails27MCP26NAMED FRAMEWORKSLangGraph17LangChain15CrewAI6LlamaIndex5LangSmith5
Orchestration
54 of 107 postings mention it.
Retrieval
51 of 107 mention retrieval or RAG.
Observability
41 of 107 mention observability or tracing.
Evaluation
38 of 107 mention evaluation in the AI sense.
Guardrails
27 of 107 mention guardrails.
Top framework
17 of 107 name LangGraph, the most-mentioned framework.
🔑What the count says
In this set of postings, the text describes the jobs of a harness more often than it names a framework. Each of the top four capabilities appears in 38 to 54 postings. The most-mentioned framework appears in 17. The word “harness” itself, in the agent or sense, appears in 9.
How I counted, and the limits of the count
The corpus
138 distinct postings that I saved. Three re-captures and staffing mirrors were removed first. It is my own search, so it is not a sample of the market.
AI roles
107 of the 138 are AI roles: the text mentions LLMs, generative AI or agents. All counts are out of 107.
Text that was read
Only the employer's text. For 52 roles that is the verbatim posting. For 55 roles it is a posting summary with my own notes removed.
Counting rule
One count per posting per term. A mention is not always a requirement.
Ambiguous words
“Evaluation” counts only in an AI sense. “Harness” the CI/CD product is excluded.
More counts
MCP 26. Multi-agent 26. Vector database 25. LangChain 15. Context engineering 7. Coding harnesses: GitHub Copilot 16, Claude Code 15, Cursor 9, Codex 8.

📨2. One call

Start with one request. A broker sends an email that asks for a homeowners insurance quote. Your program sends that email to a model.

Request: what your code sendssysteminstructionstoolsschemas the model may callmessageshistory + the new emailHTTPS POSTModelone inference stepResponse: what comes backcontenttext + tool_use blocksstop_reason"tool_use" | "end_turn"usageinput + output tokensThe model keeps nothing between calls
What goes in
Instructions, tool definitions and messages, written as . The size is counted in (pieces of words).
What comes out
Content, plus control signals: a stop reason, an optional tool request, and the token usage you are billed for.
What stays behind
Nothing. The model keeps no memory of this call. The next call starts from zero.
bash
#!/bin/sh
# One call to the Messages API. Content in; content and control signals out.
# The key comes from the environment. Never paste a real key into a file.
# The documented shape of the reply is in sample_response.json (illustrative).
curl https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 16000,
    "system": "You triage inbound homeowners quote emails for an insurance carrier.",
    "tools": [{
      "name": "verify_address",
      "description": "Check that a street address exists.",
      "input_schema": {
        "type": "object",
        "properties": {"address": {"type": "string", "description": "Street, city and state"}},
        "required": ["address"]
      }
    }],
    "messages": [
      {"role": "user", "content": "Please quote homeowners for 18 Gulf Shore Blvd, Naples FL, built 1998, 2,400 sq ft."}
    ]
  }'
↕ Scroll

curl_example.sh — one request to the Messages API. The key comes from the environment.

The response: sample_response.json (the documented shape)
json
{
  "_note": "ILLUSTRATIVE. Hand-written from the Messages API documentation to show the response shape for curl_example.sh. Not captured from a real call: ids, text and token counts are invented, and a real reply can carry extra blocks (for example a thinking block) and extra usage fields.",
  "id": "msg_01EXAMPLEEXAMPLEEXAMPLE",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-5-5",
  "content": [
    {
      "type": "text",
      "text": "I'll verify the address first."
    },
    {
      "type": "tool_use",
      "id": "toolu_01EXAMPLEEXAMPLEEXAMPLE",
      "name": "verify_address",
      "input": {
        "address": "18 Gulf Shore Blvd, Naples FL"
      }
    }
  ],
  "stop_reason": "tool_use",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 412,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 0,
    "output_tokens": 58
  }
}
↕ Scroll

sample_response.json — the documented response shape. Hand-written from the API documentation, not captured from a real call.

Read the stop reason
stop_reason is tool_use. The model did not finish. It asked your code to verify an address.
HTTP is the common case
Hosted models are commonly reached over HTTP. A model that runs on your own machine is a function call inside your process. The shape is the same: content in, content and signals out.
🔑Where does the state live?

The model keeps none. So a runtime must rebuild the context for every inference step (one run of the model). There are two ways.

Your code re-sends it. You keep the message history and send it with each request. The example on this page works this way.

A provider stores it. The OpenAI Responses API and Conversations API can keep the conversation on the server. Your next request refers to it by an id. The state still exists. A different runtime holds it.

What is, and is not, run on this page

The request and the response above follow the API reference. I did not run a live call while building this page. Every run shown here uses a scripted fake model that returns replies in the same response shape. The live path exists in the example and reads the key from the environment only when you ask for it.

🔁3. The loop

Tool use means the model asks the harness to act. The model cannot run anything. It returns a request. Your code runs the tool and sends the result back in the next call.

Call the modelstop_reason ?end_turnDonetool_useRun the toolverify_addressAppend the resulttool_resultcall againWho runs this loop?your codea provider's managed agent
Step 1: call
Send the messages so far. Append the reply to the history.
Step 2: check
If the stop reason is not a tool request, the agent is done. Return the text.
Step 3: act
Run each tool the model asked for. This is ordinary, deterministic code.
Step 4: return
Send the tool results back as the next message. Go to step 1.
python
def call_model(messages: list[dict]) -> dict:
    """One HTTPS POST. The model keeps nothing, so every call carries the full history."""
    body = {"model": "claude-opus-5-5", "max_tokens": 16000, "tools": TOOLS, "messages": messages}
    request = urllib.request.Request("https://api.anthropic.com/v1/messages", json.dumps(body).encode(), {
        "x-api-key": os.environ["ANTHROPIC_API_KEY"],
        "anthropic-version": "2026-10-21",
        "content-type": "application/json"})
    with urllib.request.urlopen(request, timeout=120) as reply:
        return json.load(reply)


def run_tool(name: str, args: dict) -> str:
    """The model asked; our code acts. Here there is only one tool."""
    zone = FLOOD_ZONES.get(args["address"].lower(), "X")
    return json.dumps({"outcome": "quote", "premium_band": "high" if zone == "AE" else "standard",
                       "reasons": [f"eligible; flood zone {zone}"]})


def agent(email: str, call=call_model) -> str:
    """The loop: call, run the tools the model asked for, send the results back, repeat."""
    messages = [{"role": "user", "content": email}]
    while True:  # no turn limit, no budget, no checkpoint: that is the harness's job
        response = call(messages)
        messages.append({"role": "assistant", "content": response["content"]})
        if response["stop_reason"] != "tool_use":
            return "".join(block["text"] for block in response["content"] if block["type"] == "text")
        results = [{"type": "tool_result", "tool_use_id": block["id"],
                    "content": run_tool(block["name"], block["input"])}
                   for block in response["content"] if block["type"] == "tool_use"]
        messages.append({"role": "user", "content": results})
↕ Scroll

agent_minimal.py — the whole agent in 30 lines: one HTTP call, one tool, one loop. No harness jobs.

🔑Who runs the loop?

Your code. The thirty lines above. You own every decision.

A library in your process. An agent or a framework runs the same loop for you.

A provider. A managed agent service runs the loop on its own servers, and may also run the tools there. Claude Managed Agents and the OpenAI Agents API work this way. Both are in beta.

Streaming does not change this. With the reply arrives in pieces. That changes the transport, not the loop.

⚠️What the thirty lines do not have
Read the comment on the loop: no turn limit, no budget, no checkpoint. There is also no permission check, no output check and no record of what happened. A library can give you this loop in an afternoon. The production controls around it (, permissions, cost limits, failure handling) are the engineering.

🧰4. The eight jobs of a harness

These eight jobs are a checklist for what a production agent needs around the loop. Later on this page, each framework, coding tool and managed platform covers some of them and leaves the rest to you.

1Contextwhat the model sees2Toolsschemas + a sandbox3Control flowstops, routes4Statecheckpoints5Identityroles + approval6Outputschema-checked7Reliabilityretry, budget8Observabilitytraces, evalsModelcallin a loop
💡About the example

All code on this page comes from one small example. It is a teaching miniature of an insurance-quote agent that I built. It is not production code.

The case. A broker emails a quote request for a house in Florida. The agent verifies the address, looks up the property and checks the underwriting rules. It returns a checked decision: quote, ask or human_review. Sending the reply email is a side effect behind a human approval gate.

The limits. About 450 lines of Python, standard library only. It runs against a scripted fake model. All vendor data is simulated in the file. The live call path follows the API reference but is not exercised here.

Three boundaries between the jobs

State vs reliability
State (4.4) owns checkpoints and resume. Reliability (4.7) owns retry policy and idempotency.
Tools vs identity
Tools (4.2) own the mechanics and the sandbox. Identity (4.5) owns who may run what, and approval.
Output vs reliability
Output contracts (4.6) own checking and repairing model output. Reliability (4.7) owns transport and tool retries.

🧱4.1 Context assembly

12345678Modelcall1 · Context
The problem
The email is two lines long. To decide, the model also needs the triage instructions, the Florida rules and the tool list. None of that is inside the model.
The mechanism
One function assembles every request: tool definitions, system prompt, role instructions, retrieved rules, then the history. Stable parts come first.
Without it
The model answers from general knowledge. Or it receives everything, and cost and error rate grow together.
The context window, top to bottomSystem promptsame every runRole instructionssame per roleTool definitionssame per roleRetrieved rulesper emailHistorygrows each turnNew emailnew every runcacheable prefixrebuilt each call
Code: harness.py — wrap_email and assemble_context
python
def wrap_email(email: str) -> dict:
    """Job 4.1: the email enters as the first user message, fenced so it reads as data."""
    return {"role": "user", "content": [{"type": "text", "text": f"New inbound email.\n<email>\n{email}\n</email>"}]}


def assemble_context(caller: Caller, state: dict, model: str) -> dict:
    """Job 4.1: build the full request body. The API renders tools, then system, then messages."""
    cache = {"type": "ephemeral"}  # a prompt-cache breakpoint: everything up to this block is the cached prefix
    system = [
        {"type": "text", "text": SYSTEM_PROMPT},                                         # 1. same for every run
        {"type": "text", "text": ROLE_INSTRUCTIONS[caller.role], "cache_control": cache},  # 2. same per role
        {"type": "text", "text": "Underwriting rules:\n" + retrieve_rules(caller, state["email"]),
         "cache_control": cache},                                                        # 3. same within a run
    ]
    return {"model": model, "max_tokens": MAX_TOKENS, "system": system,
            "tools": tool_schemas_for(caller),  # stable order, so the prefix stays byte-identical
            "messages": state["messages"]}      # 4. the history: it changes every step, so it comes last
↕ Scroll

harness.py — wrap_email and assemble_context. The email is fenced as data; the history comes last because it changes every step.

Depth: what counts as context, and why the order matters

Context is five things: instructions, tool definitions, retrieved data, conversation history and memory. Every inference step must rebuild it. Your code sends it, a provider-managed state refers to it, or a cache reuses it.

The order matters for (reusing an unchanged prefix at a lower price). A cache matches a prefix exactly. So the parts that never change go first, and the part that changes every step goes last.

Retrieval in the example is a keyword match over three documents in the file. A real system uses search, often (retrieve documents first, then ask the model). Instruction files such as CLAUDE.md and AGENTS.md are the same job inside a coding harness.

🔧4.2 Tools, and where they run

12345678Modelcall2 · Tools
The problem
The model cannot verify an address or look up a flood zone. It can only ask for it.
The mechanism
Schema in, deterministic code out. Each tool is a name, a description and a JSON Schema. The harness runs the code inside a boundary and returns text.
Without it
One wrong tool call reaches the host, the network or a real mailbox. The boundary sets the blast radius: the most damage one call can do.
Model asks for a tooltool_use: verify_addressinput: {"address": "..."}Harness checks the schemaTOOL_SCHEMASBoundary: allowlist + wait limitExecutor runs the toolrun_tool_guarded(name, args)MCPexternaltoolsResult returns as datatool_result
Code: harness.py — run_tool_guarded
python
def run_tool_guarded(name: str, args: dict, timeout_s: float = 2.0) -> tuple[str, bool]:
    """Job 4.2: run one tool behind two guards. Returns (result text, is_error).

    The guards are an allowlist and a wait limit. This is NOT a sandbox: the tool runs in our process,
    and a call that times out is abandoned, not killed. Real systems run tools in a container or microVM.
    """
    if name not in TOOL_ALLOWLIST:
        return f"blocked: {name} is not on the tool allowlist", True
    pool = ThreadPoolExecutor(max_workers=1)
    try:
        return json.dumps(pool.submit(TOOL_ALLOWLIST[name], **args).result(timeout=timeout_s)), False
    except TimeoutError:
        return f"{name} timed out after {timeout_s}s", True
    except Exception as error:  # e.g. the model sent wrong arguments; tell it, so it can correct itself
        return f"{name} failed: {error}", True
    finally:
        pool.shutdown(wait=False)
↕ Scroll

harness.py — run_tool_guarded. An error goes back to the model as text, so the model can correct itself.

Depth: MCP, and what the example simplifies

(Model Context Protocol) is a common protocol for exposing tools and data. Compatible clients can reuse the same server, within the features and transport each client supports. The current specification uses stateless, self-contained requests. Explanations that describe a session handshake are out of date.

In the example the tool runner has two guards: an allowlist and a wait limit. That shows where the boundary sits, but it is not a . The tool runs in the same process, and a call that times out is abandoned, not killed. A real system runs tools in a container or a microVM (a very small virtual machine), so a bad call cannot reach the host and can be stopped.

The send_reply tool is not on the tool allowlist. Sending email is a side effect. It goes through the approval gate in 4.5 and the send-once guard in 4.7.

🚦4.3 Control flow

12345678Modelcall3 · Control flow
The problem
Most emails need three tool calls. Some need a person. A confused run could call tools forever.
The mechanism
The loop has named stop conditions: done, token budget, max turns, waiting for approval, refusal, invalid output. One step is one model call or one batch of tool calls.
Without it
A run has no ceiling on turns or cost. When it stops, nobody can say why.
After each step: stop?end_turnmax_turnsbudgetStop the runstatus: stopped:...Route on the decisionaskmissing fieldsquotepremium bandhuman_reviewto a personOptional: delegate to a subagentits own context; it returns a summary
Code: harness.py — run
python
def run(db: sqlite3.Connection, model, state: dict, *, approve: bool = False,
        crash_after: int | None = None, verbose: bool = False) -> dict:
    """Job 4.3: the loop. One step is one model call or one batch of tool calls."""
    caller = Caller(**state["caller"])
    if state["status"] == "waiting_approval":
        state["status"] = "running"
    while state["status"] == "running":
        if over_budget(state):                                    # stop: token budget (4.7)
            state["status"] = "stopped:token_budget"
        elif pending_tool_calls(state):
            state["step"] += 1
            tool_step(db, state, caller, approve, verbose)        # may stop: waiting_approval (4.5)
        elif state["turn"] >= state["max_turns"]:                 # stop: max turns
            state["status"] = "stopped:max_turns"
        else:
            state["step"] += 1
            model_step(model, state, caller, verbose)             # may stop: done, refusal, invalid output
        if crash_after == state["step"]:
            raise SystemExit(f"simulated crash in step {crash_after}, before its checkpoint. "
                             f"Continue with: --resume {state['run_id']}")
        checkpoint(db, state)                                     # durability (4.4)
    return state
↕ Scroll

harness.py — run. Every exit from the loop is a named status, and every step ends with a checkpoint.

Depth: branching and delegation

Larger harnesses add branches and delegation. A is a second loop with its own context window for one sub-task. A graph runtime such as LangGraph makes each branch an explicit node. The example has one loop and no subagents.

Choosing which model to call is not control flow. It belongs to reliability and cost (4.7).

💾4.4 State, memory and durability

12345678Modelcall4 · State
The problem
The reply needs approval from an underwriter. The underwriter may answer in two days. No process stays alive that long.
The mechanism
After every step the harness saves the whole run state as one row in a database. This saved copy is a . Resume loads the newest row and enters the same loop.
Without it
A crash or a deploy loses the run. A human wait holds a process open. Repeated work costs tokens again.
Run stepscheckpoints tablestepstatus1running12running23running3crash4resume: newest row... NA human approval may wait two daysstatus: waiting_approvalThe row waits. No process has to.
Code: harness.py — checkpoint and load_checkpoint
python
def checkpoint(db: sqlite3.Connection, state: dict) -> None:
    """Job 4.4: save the whole run state after every step. The model remembers nothing; this row does."""
    db.execute("INSERT OR REPLACE INTO checkpoints VALUES (?, ?, ?, ?)",
               (state["run_id"], state["step"], state["status"], json.dumps(state)))
    db.commit()


def load_checkpoint(db: sqlite3.Connection, run_id: str) -> dict:
    """Job 4.4: resume = load the newest checkpoint and enter the same loop."""
    row = db.execute("SELECT state FROM checkpoints WHERE run_id = ? ORDER BY step DESC LIMIT 1", (run_id,)).fetchone()
    if not row:
        raise SystemExit(f"no checkpoint found for {run_id}")
    return json.loads(row[0])
↕ Scroll

harness.py — checkpoint and load_checkpoint. The model remembers nothing; this row does.

Depth: memory, and who can hold the state

Memory across runs is the same job with a longer life: facts that should survive after this run ends. The example keeps state for one run only.

A provider can hold this state for you: stored conversations, managed sessions or a durable execution service. The job does not go away. The owner changes.

🪪4.5 Identity, permissions and approval

12345678Modelcall5 · Identity
The problem
Three callers use the same agent: an underwriter, an intake service and a public broker portal. Only one may send email. One rule document holds an internal surcharge that brokers must not see.
The mechanism
The caller's role comes from your login system. It decides which tools the model sees, which documents may enter the context, and it is checked again before every tool call. Side effects wait for a person.
Without it
Private data leaks through the context. Text inside an email talks the model into acting. Nobody can say who approved what.
Callerrole: "intake_bot"Which documentsmay enter contextmay_read()Which toolsmay runtool_schemas_for()Agent loopEmail textuntrusted: data,not instructionsApproval gatea human approvesauthorize()send_replyside effect
Code: harness.py — tool_schemas_for, may_read, screen_untrusted and authorize
python
def tool_schemas_for(caller: Caller) -> list[dict]:
    """Job 4.5: the model only sees the tools this role may run."""
    return [schema for schema in TOOL_SCHEMAS if schema["name"] in ROLE_TOOLS[caller.role]]


def may_read(caller: Caller, doc: dict) -> bool:
    """Job 4.5: the role decides which documents may enter the context."""
    return caller.role in doc["roles"]


def screen_untrusted(text: str) -> list[str]:
    """Job 4.5: a simple prompt-injection check. Email text is data; flag text that reads like orders."""
    return [pattern for pattern in INJECTION_PATTERNS if re.search(pattern, text, re.IGNORECASE)]


def authorize(caller: Caller, tool: str, state: dict, approved: bool) -> str:
    """Job 4.5: the gate before every tool call. Returns "allow", "wait" or a denial reason."""
    if tool not in ROLE_TOOLS[caller.role]:  # checked again here: never trust the model to stay in its list
        return f"denied: role {caller.role} may not run {tool}"
    if tool in SIDE_EFFECT_TOOLS:
        if state["tainted"]:
            return "denied: the email contains instruction-like text; a human must review it"
        if not approved:
            return "wait"
    return "allow"
↕ Scroll

harness.py — tool_schemas_for, may_read, screen_untrusted and authorize. The role is never set by the model or by the email.

Depth: prompt injection, and an honest limit

Prompt injection is text from an outside sender that reads like an instruction. The email is untrusted input, so the harness treats it as data.

The injection check in the example is four regular expressions. It shows where the check sits. It does not show how to make the check complete. The real defence is structural: the email is fenced as data, a flagged run cannot send, and the decision is forced to human review.

The fake model is written to obey an injected instruction on purpose. That gives the harness something to stop.

📐4.6 Output contracts

12345678Modelcall6 · Output
The problem
The next system needs the decision to be one of three exact values. Text that looks like JSON is not enough.
The mechanism
The harness checks the final answer against a contract. If the check fails, it tells the model exactly what failed and allows one repair attempt. Then it stops with a named failure.
Without it
A parse error in production. Or worse: a wrong value flows on and nobody notices.
Model outputSchema checkvalidate_decision()validContinuedecision acceptedinvalidOne repair retryrepair_prompt()still invalidhuman_reviewValid JSON is not a completed side effectthe reply is sent later, by a tool
Code: harness.py — validate_decision and repair_prompt
python
def validate_decision(text: str) -> tuple[dict | None, list[str]]:
    """Job 4.6: check the final answer against the contract. Returns (decision, errors).

    Hand-written to show the idea. The API can also enforce a JSON Schema for you
    (structured outputs: output_config.format).
    """
    try:
        data = json.loads(text)
    except json.JSONDecodeError:
        return None, ["the reply is not a single JSON object"]
    if not isinstance(data, dict):
        return None, ["the reply is not a JSON object"]
    errors = []
    if data.get("decision") not in DECISIONS:
        errors.append(f"decision must be one of {DECISIONS}")
    for key in ("missing_fields", "reasons"):
        if not isinstance(data.get(key), list) or not all(isinstance(item, str) for item in data[key]):
            errors.append(f"{key} must be a list of strings")
    if data.get("decision") == "quote" and data.get("premium_band") not in BANDS:
        errors.append(f"a quote needs premium_band, one of {BANDS}")
    if extra := set(data) - {"decision", "premium_band", "missing_fields", "reasons"}:
        errors.append(f"unknown keys: {sorted(extra)}")
    return (None, errors) if errors else (data, [])


def repair_prompt(errors: list[str]) -> str:
    """Job 4.6: one repair retry. Tell the model exactly what failed."""
    return ("Your last reply did not match the output contract: " + "; ".join(errors)
            + ". Reply again with only the JSON object.")
↕ Scroll

harness.py — validate_decision and repair_prompt. Hand-written here to show the idea.

🔑A valid object is not a completed side effect
A valid decision object says what the model decided. It does not say the email was sent. The example tracks the two facts separately: one field for the decision, another for the reply status.
Depth: who can enforce the contract

The check here is written by hand. A model API can also enforce a JSON Schema for you, a feature usually called structured output. Either way the harness still decides what to do when the output is valid but wrong for the business.

🛟4.7 Reliability and cost

12345678Modelcall7 · Reliability
The problem
The API is busy and rejects a request. The process crashes right after the email is sent. A run keeps calling tools until the bill is large.
The mechanism
Retry with a growing wait. Fall back to a second model after two failures. Stop before the next call once recorded usage reaches a token budget. Record a key for each side effect so a retry or a resume cannot repeat it. That last property is .
Without it
The broker receives the same email twice. One provider incident stops all work. Nothing limits what a run spends.
Retry with backoff on 429 / 5xxwait 1swait 2s429429200Token budget42,000 / 60,000full = stopped:token_budgetIdempotency key: one send_replyfirst callretrysame keysentsuppressedPrompt cachemissall input at full pricehitprefix read from cachenew partPrimaryoverloadedFallback
Code: harness.py — pick_model and call_with_retry
python
def pick_model(failed_attempts: int) -> str:
    """Job 4.7: routing and fallback. After two failures, try the other model (its cache is separate)."""
    return PRIMARY_MODEL if failed_attempts < 2 else FALLBACK_MODEL


def call_with_retry(model, build_body) -> dict:
    """Job 4.7: retry 429 and 5xx with exponential backoff. Other errors are bugs; raise them."""
    for attempt in range(MAX_ATTEMPTS):
        try:
            return model.create(build_body(pick_model(attempt)))
        except urllib.error.HTTPError as error:
            retryable = error.code == 429 or error.code >= 500  # 529 = overloaded
            if not retryable or attempt == MAX_ATTEMPTS - 1:
                raise
            header = error.headers.get("retry-after") or ""  # seconds, or an HTTP date (then use backoff)
            wait = float(header) if header.replace(".", "", 1).isdigit() else BACKOFF_BASE_S * 2 ** attempt
            print(f"[retry] HTTP {error.code}, attempt {attempt + 1}, waiting {wait:.2f}s", file=sys.stderr)
            time.sleep(wait)
    raise AssertionError("unreachable")
↕ Scroll

harness.py — pick_model and call_with_retry. Rate limits and server errors are retried; other errors are bugs.

Code: harness.py — over_budget and send_once
python
def over_budget(state: dict) -> bool:
    """Job 4.7: token budget guard, fed by the usage numbers of every response."""
    return state["tokens"] >= state["token_budget"]


def send_once(db: sqlite3.Connection, run_id: str, email: dict) -> str:
    """Job 4.7: idempotency key. The same email in the same run is recorded once, so a resume cannot resend."""
    key = hashlib.sha256(json.dumps([run_id, email], sort_keys=True).encode()).hexdigest()[:16]
    inserted = db.execute("INSERT OR IGNORE INTO outbox VALUES (?, ?, ?)", (key, run_id, json.dumps(email)))
    db.commit()  # simulated send: a real system passes this key to a mail API that honours it, or uses an outbox worker
    return json.dumps({"status": "sent" if inserted.rowcount else "duplicate_suppressed", "idempotency_key": key})
↕ Scroll

harness.py — over_budget and send_once. The budget is a guardrail checked between calls, not a hard cap. The send is simulated: the key is recorded once, so a resume cannot resend.

Depth: caching and routing live here too

Prompt caching lowers the price of an unchanged prefix. It is not free: writing to the cache costs more than normal input. Section 5 has the arithmetic.

Model routing and fallback belong to this job. Note the comment in the code: the fallback model has its own separate cache, so the first fallback call cannot read the prefix that the primary model cached.

🔭4.8 Observability and evals

12345678Modelcall8 · Observability
The problem
An underwriter asks why one email went to human review. A teammate changes the prompt. Is the agent better or worse now?
The mechanism
Two loops. Traces record what happened: one line per model response and per tool call. Evals score whether it is good: a of emails runs through the same loop and gets a pass rate.
Without it
Without traces, the explanation of a decision is incomplete. Without evals, you cannot compare one change with another.
1Trace: what happened in a runturn 1: modelverify_addressturn 2: modellookup_propertyturn 3: modelbar length = latency; each span logs tokens2Evals: is it good?Golden setgolden.jsonlRunevals.pyScorepass / failGate in CIblock a dropnew failures
Code: harness.py — trace
python
def trace(state: dict, verbose: bool, **event) -> None:
    """Job 4.8: one JSON line per model response and per tool call. evals.py is the second half."""
    line = json.dumps({"run_id": state["run_id"], "step": state["step"], "turn": state["turn"], **event})
    with open(TRACE_FILE, "a") as file:
        file.write(line + "\n")
    if verbose:
        print(line, file=sys.stderr)

harness.py — trace. Step, stop reason, tokens, cache reads and latency, one JSON line each.

Code: evals.py — run_case and grade
python
def run_case(case: dict) -> dict:
    """Run one golden email through the real harness loop and return what happened."""
    db = harness.open_db(":memory:")  # a throwaway database: the outbox is not a real mailbox
    caller = harness.Caller(user="eval", role=case["role"])
    state = harness.start_run(db, case["email"], caller)
    state = harness.run(db, FakeModel(), state, approve=True)  # the eval approves, so only policy can block
    decision = state["decision"] or {}
    return {"status": state["status"], "decision": decision.get("decision"),
            "premium_band": decision.get("premium_band"), "missing_fields": decision.get("missing_fields"),
            "reply": state["reply"], "emails_sent": db.execute("SELECT COUNT(*) FROM outbox").fetchone()[0]}


def grade(expect: dict, actual: dict) -> list[str]:
    """Compare against the expected values. Returns a list of failures (empty = pass)."""
    failures = [f"{key}: expected {expect[key]!r}, got {actual[key]!r}" for key in expect if expect[key] != actual[key]]
    if actual["status"] != "done":
        failures.append(f"status: expected 'done', got {actual['status']!r}")
    if (actual["emails_sent"] == 1) != (expect["reply"] == "sent"):  # sent, but not expected to be (or the reverse)
        failures.append(f"outbox: {actual['emails_sent']} email(s) sent, expected reply {expect['reply']!r}")
    return failures
↕ Scroll

evals.py — run_case and grade. The script exits with an error below the minimum score, so a CI job can block the change.

Depth: what this eval does and does not prove

The golden set has five emails and runs on the fake model. It is free and gives the same result every time. It tests the harness: the gates, the policy and the send-once guard. It does not measure the quality of a real model.

A full eval adds a rubric (written scoring rules), a second model as a judge, and scoring of the traces. It runs in (the automated checks on every change).

🪟5. Context is the scarce resource

The model is stateless, so every step consumes the context again. A hub becomes slow and expensive when it puts every connected tool and document into every call.

Everythingconnected~160k tokensUnrelated docs~80kTool definitions~60kHistory ~20kcontext budget ~100kScoped by role +loaded on demand~12k tokensTools for role ~4kRules for email ~3kHistory + task ~5kIllustrative numbers, not a measurement
An assumed hub
30 connectors, 10 tools each, about 150 tokens per tool definition. These numbers are an illustration, not a measurement.
Per call
30 × 10 × 150 = 45,000 tokens of tool definitions, before the user has asked anything.
Per task
A 12-step task sends them 12 times: 540,000 input tokens for definitions alone.

Four levers, each with its cost

These are levers with trade-offs. They are not a proof that a broad hub is cheap.

1. Scope by role
What you gain
The underwriter agent sees four tools, not three hundred.
What it costs
You maintain a map from role to tools. A missing tool is a failed task.
2. Load on demand
What you gain
Only a short description sits in the context. The full instructions load when a task needs them.
What it costs
One extra step. The model must choose correctly from a short description.
3. Cache
What you gain
An unchanged prefix is read at a fraction of the normal input price.
What it costs
Writing to the cache costs more than normal input. Any change in the prefix is a miss. The cache expires.
4. Compact or delegate
What you gain
Old history becomes a short summary. A subagent does a sub-task in its own window.
What it costs
A summary loses detail. A subagent adds model calls.

Lever 4 uses (replacing long history with a summary) and subagents.

Cache arithmetic, with the write cost included

Anthropic publishes these multipliers: a cache write costs 1.25 times normal input with a five-minute lifetime, and a cache read costs 0.1 times. Take a stable prefix of 20,000 tokens.

Ten steps, no cache
10 × 20,000 = 200,000 token-units for the prefix.
Ten steps, cached
One write: 25,000. Nine reads: 9 × 2,000 = 18,000. Total 43,000.
One step, cached
One write: 25,000 against 20,000 without a cache. Caching a prefix you never reuse costs more.
⚠️The cache hit in the example is simulated
The example places the cache markers correctly. But its prompt is far below the minimum size a cache accepts, so a live call would not cache it. The fake model simulates the hit to show the usage fields. This is a real lesson: a small prompt gains nothing from caching.
How the three large providers differ on caching
Anthropic
Opt-in. You mark the cached prefix in the request. Writes cost 1.25 times input for five minutes or 2 times for one hour. Reads cost 0.1 times by default.
OpenAI
On by default for prefixes of 1,024 tokens or more. Cached input is billed at a reduced rate. On GPT-5.6 and later, cache writes cost extra.
Google (Gemini API)
Implicit caching is on by default for Gemini 2.5 and newer. Explicit caching means you create cache objects. The docs I read do not state a discount rate.

⌨️6. Coding harnesses implement the same jobs

A coding harness is a vendor-built agent harness for software work. Each one uses its own names. Under the names are the same jobs. This table translates between them.

Swipe sideways to see every column.
Feature, and what it meansClaude CodeCodex CLIGitHub Copilot CLIAntigravity CLI (replaces Gemini CLI)Cursor and Cursor CLIGrok Build
Project instructions file
A text file the harness loads into the context at the start of every session.
Job 1
CLAUDE.md (can also read AGENTS.md); auto memoryAGENTS.md.github/copilot-instructions.md, .github/instructions/**/*.instructions.md, AGENTS.md; also reads CLAUDE.md and GEMINI.mdRules: AGENTS.md, GEMINI.md, .agents/rules/*.md. Gemini CLI: GEMINI.mdRules: .cursor/rules/*.mdc, AGENTS.md, User Rules, Team RulesAGENTS.md, read from the working directory up to the repository root
Skills and custom commands
Instructions the harness loads only when a task needs them.
Job 1
Skills (SKILL.md), commands, pluginsSkills, slash commands, pluginsAgent skills (SKILL.md folders), custom agents, pluginsAgent Skills, Antigravity plugins. Gemini CLI custom commands import as skillsSkills, pluginsSkills (SKILL.md in .grok/skills/), shown as /<skill-name>; plugins and marketplaces
Hooks
Your own script that the harness runs at a fixed point in the loop, for example before a tool call.
Jobs 3 and 5
Hooks in settings; events such as PreToolUseHooks (hooks.json or [hooks] in config.toml): SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, StopHooks (.github/hooks/NAME.json): sessionStart, preToolUse, postToolUseJSON hooks (.agents/hooks.json, ~/.gemini/config/hooks.json): SessionStart, PreToolUse, PostToolUse, Stop. Gemini CLI: hooksHooks (.cursor/hooks.json): sessionStart, preToolUse, beforeShellExecution, afterFileEdit, stopHooks (.grok/hooks/); project hooks need /hooks-trust
Permission modes and sandbox
What the agent may do without asking, and the boundary its commands run inside.
Jobs 5 and 2
Modes default, acceptEdits, plan, auto, dontAsk, bypassPermissions; sandboxed Bash toolApproval policy on-request, never, granular. Sandbox modes read-only, workspace-write, danger-full-access--allow-tool, --allow-all-tools, --deny-tool; trusted directories; plan and autopilot modes; /sandbox enable--mode accept-edits|auto|plan, --sandbox. Gemini CLI: policy engine (allow, deny, ask_user), trusted folders, sandboxModes Agent, Plan, Ask; permissions.allow and deny in .cursor/cli.json; --sandbox enabled|disabledModes ask, auto, always-approve; plan mode /plan. Sandbox is off by default: --sandbox <profile> (workspace, read-only, strict), Landlock on Linux, Seatbelt on macOS
MCP
Connects outside tool servers through the Model Context Protocol.
Job 2
YesYesYes; ships GitHub's MCP server; repo file .mcp.jsonYes (both)YesYes (/mcps)
Subagents
A second agent loop with its own context window for one sub-task.
Jobs 3 and 1
Custom subagents; agent teamsSubagentsSubagents; custom agents (.github/agents/*.agent.md); /delegate, /fleetSubagents (/agents)Subagents (.cursor/agents/); Cloud Agents handoffSubagents (/tasks lists them)
Headless mode
Runs the same loop from a script with no person at the keyboard, for example in CI.
The loop
claude -pcodex execcopilot -pagy -p. Gemini CLI: gemini -pagent -pgrok -p
How to read it
Each row is one feature, defined in one line, with the harness job it serves. Read across a row to translate a feature from one tool to another.
A name that changed
Antigravity CLI replaced Gemini CLI for free-tier and Google AI Pro and Ultra users in June 2026. Gemini CLI stays open source for Gemini Code Assist and paid API-key users.

In 2026 the vendors use the word themselves

These products call themselves a harness in their own documentation. Each entry says what the vendor means by it.

create_agent
A minimal, configurable agent harness built on .
Deep Agents
A batteries-included harness: context compression, a virtual filesystem, subagents.
Microsoft
Agent Harness (in Agent Framework)
An opinionated agent with planning, compaction, file memory, tool approval and OpenTelemetry.
Microsoft
Microsoft Copilot harness (in Foundry, preview)
An optional harness inside Foundry Agent Service.
Pydantic
Harness
A capability library named in the documentation.
Strands
Strands harness
A ready-made harness you create with create_harness().
AWS
AgentCore Harness
A managed agent loop inside Bedrock AgentCore, powered by Strands Agents.
Anthropic
Claude Managed Agents (beta)
A pre-built, configurable agent harness that runs on Anthropic-managed infrastructure.
OpenAI
Agents API (beta)
A managed Codex harness: OpenAI runs sessions, compaction and recovery.
GitHub
Copilot CLI
Described as the same harness as the Copilot coding agent.
Where the two terms come from

OpenAI published a post titled “Harness engineering: leveraging Codex in an agent-first world”. Anthropic published “Effective context engineering for AI agents” in September 2025. I found no official source that says who used either term first, so this page does not say it.

🗺️7. The ecosystem map

Products overlap, so exclusive categories do not work. Each row has a primary role. Each cell says who does that job: the product, a partner, the vendor as a service, or you.

Built inThe product ships this itself.
IntegratesDone through a named partner product or plug-in.
You buildThe docs leave this to your code.
ManagedThe vendor runs it for you as a service.
Not its jobThis is not what the product is for.
— docs do not sayA dash, or small grey text: I looked in the official docs and they do not say. It does not mean the feature is absent.
Swipe sideways to see every column.
Product and primary role1 Context2 Tools + sandbox3 Control flow4 State + durability5 Identity + approval6 Output contract7 Reliability + cost8 Observability + evals
LangChain (create_agent)
Agent framework
Built inmiddleware; Deep Agents adds context compression
Built intools
Built inshell sandbox: DockerExecutionPolicy
Built inthe loop runs on LangGraph
Built inpersistence through LangGraph
Built inhuman-in-the-loop
identity: — docs do not say
Built inresponse_format: ToolStrategy, ProviderStrategy
Built inretry middleware, ModelFallbackMiddleware, call limits
IntegratesLangSmith
LlamaIndex
Agent framework (data-centric)
Built inretrieval over your data
Built intools
Integratessandbox: Azure Dynamic Sessions, AgentCore
Built inevent-driven Workflows
Built instate, memory, durable workflows
IntegratesDBOS
Built inhuman review step
identity: — docs do not say
Built instructured output, Pydantic programs
Built inworkflow retries
Built inTokenCountingHandler, token_budget
Built ininstrumentation, evaluation modules
CrewAI
Agent framework
Built inmemory, knowledge
Built intools, MCP
Integratessandbox: E2B, Daytona tools
Built inCrews + Flows
Built inmemory, checkpointing
Built inhuman input
identity: — docs do not say
Built inoutput_pydantic, output_json
Built inmax_rpm, max_retry_limit, tool cache
ManagedCrewAI AMP: tracing, observability
Pydantic AI
Agent framework (typed)
Built ininstructions, typed dependencies
Built intools, MCP
IntegratesPydantic AI Harness sandboxes: Docker, E2B, Modal
Built inloop; Pydantic Graph
IntegratesTemporal, DBOS, Prefect
Built intool approval
identity: — docs do not say
Built invalidated output types
Built infallback model, validation retries
Built inOpenTelemetry, Pydantic Evals
IntegratesLogfire
Microsoft Agent Framework
Agent framework
Built incontext providers; compaction in the Agent Harness
Built intools, MCP; shell tools are pre-release
Built inagents + graph workflows
Built insessions; workflow checkpointing, time-travel
Built intool approval
identity: — docs do not say
Built inresponse_format, RunAsync<T>
You buildretry and fallback written as middleware
Built inOpenTelemetry
Integratesevaluation services
LangGraph
Orchestration runtime
Built inruntime context, state, store
Built inToolNode (prebuilt)
sandbox: — docs do not say
Built ingraph runtime
Built incheckpointers (InMemorySaver, SqliteSaver, PostgresSaver); Store for cross-thread memory
Built inhuman-in-the-loop: pause, edit state, resume
identity: — docs do not say
IntegratesLangChain models: with_structured_output
Built indurable execution, RetryPolicy, TimeoutPolicy, node caching
IntegratesLangSmith
Claude Agent SDK
Agent SDK
Built inClaude Code context management, CLAUDE.md memory, skills
Built inbuilt-in tools, MCP, sandbox settings
You buildcontainer isolation is your deployment
Built inagent loop, subagents, hooks
Built insessions, external session storage, file checkpointing
Built inpermissions, approvals and user input
Built instructured outputs
Built incost tracking, max_budget_usd, fallback_model, API retries
Built inOpenTelemetry
evals: — docs do not say
OpenAI Agents SDK
Agent SDK
Built inlocal context, dynamic instructions, sessions
Built intools, MCP, hosted tools, sandbox agents
Built inloop, handoffs, agents as tools
Built insessions: SQLite, SQLAlchemy, Redis and others
Built inguardrails, human in the loop
You buildtools enforce authorization themselves
Built inoutput_type
Built inopt-in retries (ModelSettings.retry), usage tracking, max_turns
Built intracing
evals: — docs do not say
Google ADK
Agent SDK
Built incontext caching, context compaction
Built intools, MCP, OpenAPI
Built incode_executor; GKE Code Executor (gVisor)
Built ingraph Workflow Runtime, multi-agent
Built insessions, state, memory, resumable runs
Built intool confirmation
Built intool authentication: auth schemes, credentials
Built inoutput_schema
Built inretry in workflows, context caching
Built inevaluation; logging, metrics, traces
Strands Agents
Agent SDK
Built inharness: context management, caching
Built intools, MCP
Built inSandbox: Docker, SSH
Built inlifecycle controls, multi-agent patterns
Built inmemory, sessions
Built inguardrails
Built inHumanInTheLoop intervention, interrupts
Built in
Built inturn limits, token budgets, model portability
Built intracing, evals
Claude Managed Agents
Managed harness · Beta
Managedprompt caching, compaction
Managedcloud sandbox or self-hosted sandbox; Bash, files, web, MCP
ManagedAnthropic runs the loop
Managedstateful sessions, event history
Managedpermission policies, vaults
The official docs I read do not say
Managedprompt caching, session budgets
retries: — docs do not say
ManagedConsole session viewer, usage, OpenTelemetry tracing
OpenAI Agents API
Managed harness · Beta
Managedcontext compaction
ManagedOpenAI-hosted or self-hosted sandbox; MCP, web search, skills
Managedmanaged Codex harness, subagents
Managedsessions, recovery
Managedvaults, auto_review setting
The official docs I read do not say
Managedrecovery
cost: — docs do not say
Managedtracing on by default, OTLP export
Amazon Bedrock AgentCore
Managed agent platform
ManagedHarness: truncation, summarization, prompt caching
ManagedGateway, Code Interpreter, Browser; one microVM per session
ManagedHarness module: a managed agent loop
ManagedMemory: short and long term
ManagedIdentity, Policy
The official docs I read do not say
ManagedmaxIterations, timeoutSeconds, maxTokens
ManagedObservability, Evaluations, Optimization
Microsoft Foundry Agent Service
Managed agent platform
Managedautomatic truncation to the model context
ManagedToolboxes, code interpreter, MCP
ManagedAgent Runtime; optional Microsoft Copilot harness (preview)
Managedconversations, session state
ManagedEntra agent identity, RBAC, content filters
ManagedJSON Schema text format
You buildretry 429 errors with backoff
Managedtracing, metrics, evaluations
Agent Runtime (Gemini Enterprise Agent Platform)
Managed agent platform
The official docs I read do not say
Managedruntime
The official docs I read do not say
ManagedAgent Platform Sessions, Memory Bank
ManagedAgent Identity, Agent Gateway
The official docs I read do not sayThe official docs I read do not say
Managedagent evaluation, observability, trace viewer
OpenAI Responses API + Conversations API
Model API with optional state
Managedoptional server-side compaction
Managedbuilt-in tools; shell in hosted containers
The official docs I read do not say
Managedresponses stored 30 days by default; conversation objects persist
Not its job
Built inStructured Outputs: text.format json_schema
Built inprompt caching
IntegratesEvals API reads Responses data
Temporal
Durable execution
Not its job
Not its job
Built inWorkflows as code
Built indurable execution
Not its job
Not its job
Built inautomatic retries
Built inSDK metrics, OpenTelemetry tracing, Visibility
LangSmith (with Agent Server)
Tracing and evals (also deployment)
Not its job
Not its job
Not its job
ManagedAgent Server: threads, runs, durable execution
Built inAgent Server custom auth, access control
Not its job
Built inLLM Gateway; details not stated
Built intracing, dashboards, alerts, evaluation
Langfuse
Tracing and evals
Built inpartial: prompt management
Not its job
Not its job
Not its job
Not its job
Not its job
Built inAI Gateway with spend tracking
Built intracing, evals, datasets
Arize Phoenix
Tracing and evals
Built inpartial: prompt management, playground
Not its job
Not its job
Not its job
Not its job
Not its job
Not its job
Built intracing, evals, datasets and experiments
LiteLLM
Gateway
Not its job
Not its job
Not its job
Not its job
Built invirtual keys, guardrails
Not its job
Built inretry and fallback router, load balancing, caching, spend tracking
Integrateslogging to other tools
Inclusion rule
Every agent product named in my saved postings that I could check on official documentation, plus one current representative for a role the postings leave empty. It is not a logo wall.
Source rule
Each cell comes from the vendor's own docs or repository. Where the official docs do not say, the cell shows a grey dash or “docs do not say”. I did not fill any cell from memory.

Protocols and standards: the connectors, not the boxes

These three are not scored in the table. They are how the boxes connect.

MCP · protocol
Connects an agent to tools and data. The current specification uses stateless, self-contained requests. It is governed under the Linux Foundation.
A2A · protocol
(Agent2Agent) connects one agent to another agent. The specification is at version 1.0, under the Linux Foundation.
OpenTelemetry · emerging standard
defines names for traces of model calls and agent steps. These conventions are still in Development status.
Names and statuses that changed
AutoGen
Its README says it is archived and no longer maintained, with no bug or security fixes. Microsoft Agent Framework is the successor.
Semantic Kernel
Its README says Semantic Kernel is now Microsoft Agent Framework. Microsoft Agent Framework is the successor.
OpenAI Assistants API
Shut down on 26 August 2026. The Responses API and Conversations API replace it.
Google
Vertex AI Agent Engine became Agent Runtime, part of Gemini Enterprise Agent Platform, in April 2026.
LangChain, LangGraph, LangSmith
Three different things. LangChain is an agent framework. LangGraph is the stateful runtime under it. LangSmith is a tracing and eval platform that also offers deployment.
Left off the map
DSPy appears in one posting. I did not check it on official docs, so it has no row.

⚖️8. Build, adopt or combine

The first principle turns build-or-adopt into one question per job: who runs it? You do not choose one product for all eight jobs. You choose an owner for each job.

Who runs each job?Raw API + your loopyou run 812345678Agent SDK / frameworkyou run ~412345678Managed harnessyou run ~312345678you run itthe vendor runs itRough split. It varies by product.
Raw API
Who runs it
You run all eight jobs.
Choose it when
The loop is small, you need full control, or you are learning how it works.
What it costs
You write and maintain every job yourself.
Who runs it
The SDK runs the loop, the tools and the context inside your process.
Choose it when
You want a proven loop quickly and you will operate it yourself.
What it costs
You still own deployment, isolation and identity. You follow the SDK release cycle.
Framework and runtime
Who runs it
A graph runtime runs control flow and checkpoints. You operate it.
Choose it when
The work has many branches, long waits or several agents.
What it costs
A framework dependency and its concepts to learn.
Managed harness
Who runs it
The provider runs some combination of the loop, the execution environment and the session state. Check the boundary product by product.
Choose it when
You do not want to operate agent infrastructure.
What it costs
Less control, coupling to one vendor, and some of these services are still in beta.
🔑Most real systems combine

A common shape: an SDK for the loop, a durable execution service for long waits, a tracing tool for job 8, and your own code for identity.

In my experience three things stay with you whatever you adopt: the golden set, the permission model, and the decision about which data may enter the context. They describe your business, not the vendor's product.

🏢9. The harness for non-engineers

Many companies want one AI hub for people who are not engineers: finance, operations, support, sales. A hub is a harness plus three things around it.

Product ownerQADeveloperIdentitywho is asking, in which roleRole-scoped contextonly what this role needsHarnessthe loop + the eight jobsConnectors (MCP)ReposTicketsDocsGovernance and audit: every call logged
The harness
The runtime inside the hub. The same eight jobs.
Identity
Who is asking. It decides which data may enter the context and which tools may run (job 4.5).
Connectors
The systems the hub can reach, usually through MCP servers (job 4.2).
Governance
Approval rules, audit records, cost limits and evals, owned by named people (jobs 4.5, 4.7 and 4.8).
⚠️The honest objection: a hub connected to everything bloats
The objection is correct. Section 5 shows the arithmetic. The answer is the four levers: scope by role, load on demand, cache, and compact or delegate. Each lever has a cost, so a hub is a design that must be measured, not a free default.

For the people side of this, see the role-by-role adoption strategy and the full working version at enable.sammuthu.com.