Frameworksframeworksno frameworktool callingagent loopsimplicity

Option 0: No Framework

An LLM SDK plus ordinary application code covers most production agent systems; a tool-calling loop with a step limit is about forty lines, and you own every one of them.

Interview question
Progress

What “no framework” means

Option 0 is not “write everything from scratch”. It is: use the provider’s SDK (or a thin multi-provider client) for the model call, use your language’s normal tools for everything else, and write the agent loop yourself. Schemas come from Pydantic or Zod. Retries come from the HTTP client. Tracing comes from OpenTelemetry. State lives in your database. None of these are agent-specific, and all of them are already in your stack.

The result is a system your team can read top to bottom. Every prompt is a string in your repository. Every tool is a function with a signature. The loop is a while with a counter. When something goes wrong at 3 a.m., the trace points at a line you wrote.

The whole loop

The snippet below is a complete tool-calling agent: a registry of tools with JSON schemas, a bounded loop, argument validation, error results returned to the model, and a graceful stop. call_model(messages, tools) stands in for whatever chat-completions-style client you use; it takes the message list and the tool schemas and returns a response with optional tool calls. Nothing here is vendor-specific.

A bounded tool-calling loop against a generic chat-completions-style client.
1import json
2from dataclasses import dataclass, field
3
4@dataclass
5class ToolCall:
6 id: str; name: str; args: dict
7
8@dataclass
9class ModelResponse:
10 text: str = ""
11 tool_calls: list[ToolCall] = field(default_factory=list)
12
13def call_model(messages: list[dict], tools: list[dict]) -> ModelResponse:
14 """Stub: send messages + tool schemas to any chat-completions API and parse the reply."""
15 raise NotImplementedError
16
17def get_weather(city: str) -> dict:
18 return {"city": city, "temp_c": 21}
19
20TOOLS = {"get_weather": get_weather}
21SCHEMAS = [{
22 "name": "get_weather",
23 "description": "Current temperature for a city.",
24 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
25}]
26
27def run(user_input: str, max_steps: int = 8) -> str:
28 messages = [{"role": "system", "content": "Answer using tools when needed."},
29 {"role": "user", "content": user_input}]
30 for _ in range(max_steps):
31 resp = call_model(messages, SCHEMAS)
32 if not resp.tool_calls:
33 return resp.text
34 messages.append({"role": "assistant", "content": resp.text, "tool_calls": resp.tool_calls})
35 for tc in resp.tool_calls:
36 fn = TOOLS.get(tc.name)
37 try:
38 result = fn(**tc.args) if fn else {"error": f"unknown tool {tc.name}"}
39 except TypeError as e:
40 result = {"error": f"bad arguments: {e}"}
41 messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)})
42 return "Stopped: step limit reached before a final answer."

What this already covers

Add a few standard-library-level extras and this loop is production-shaped: a schema validator on tc.args (Argument Validation), a cost and token counter (Budgets, Limits and Termination), a retry wrapper around the HTTP call (Tool Errors, Retries and Timeouts), an OpenTelemetry span per iteration (Tracing Agents). Each is ten to twenty lines and independently testable.

Structured output is a tool with one required call. RAG is a retrieve() function called before the loop (Agent + RAG). A workflow is several such loops connected by ordinary control flow. A router is an if on a classifier result. Most systems described in Architecture Tradeoffs are combinations of this loop and plain code.

  • Single LLM call, structured output, tool calling, agent + RAG: fully covered.
  • Simple workflows and routers: covered with functions and if statements.
  • Human approval: a pending-actions table and a resume endpoint; a few dozen lines.
  • Evals: a script that runs run() over a dataset and scores results (Deterministic Evaluators).

When you outgrow it

The loop stops being enough when state must survive process restarts across many steps and branches, when several workflows share the same persistence, checkpointing and replay machinery, or when multiple teams need a common vocabulary for graphs with human-in-the-loop nodes. At that point you are building a workflow engine, and a mid-level runtime (Workflow State Graph, LangGraph, Google ADK, Microsoft Agent Framework) or a general durable-execution system is the right reach. The Framework Explorer at /agentic/frameworks covers the options.

Signals that you are there: you have written a checkpoint table and a resume function twice; you have a step_type enum with more than ten values; you need to visualise the graph for non-engineers. Signals that you are not: “the framework has a nice demo”, “everyone uses it”, or “we might need multi-agent later”. Reach for the abstraction when the pain is real, and keep the loop above as the reference for what the abstraction must not hide.

Key points

  • No framework means an LLM SDK plus normal code: schemas, retries, tracing and state come from tools you already have.
  • A complete bounded tool-calling loop is about forty lines and covers most production systems.
  • Structured output, RAG, routers and simple workflows are the same loop plus plain functions.
  • Outgrow it when you need durable, resumable, multi-branch state shared across workflows; then reach for a mid-level runtime.
  • Keep the plain loop as the reference: any framework you adopt must let you see and bound everything it does.

When to use — and when not to

Use it when
  • The first version of nearly every agent system.
  • Systems with strict security or compliance requirements where every prompt must be auditable.
  • Teams that already have solid HTTP, schema, tracing and persistence libraries.
Avoid it when
  • Long-running workflows with checkpointing, replay and many parallel branches: use a graph runtime.
  • A throwaway demo due tomorrow where a high-level framework’s defaults are good enough.
  • When your organisation has standardised on a runtime and consistency matters more than minimalism.

Failure modes

  • The loop has no step limit because it started as a script; see Budgets, Limits and Termination.
  • Tool errors raise exceptions instead of being returned to the model, so one bad call kills the run.
  • Persistence is bolted on late with a homegrown checkpoint format nobody documented.
  • Each team writes its own loop with slightly different retry and tracing conventions.
  • The team rewrites in a framework to “fix” a problem that was a missing tool description.

Tradeoffs

Complexity
low → high
Latency
low → high
Cost
low → high
Reliability
poor → strong
Debuggability
hard → easy

Lowest overhead and best visibility; reliability depends on you actually adding limits, retries and tracing.

Don't delegate understanding
The manifesto →