PythonTypeScriptLow abstraction — you write the loop

No framework (LLM SDK + application code)

Nothing extra — the provider SDK already gives you messages, tool definitions, and structured output; a 30-line loop in your own code is the entire agent runtime.

Architecture

  • Messages array is the state: system prompt, user turns, assistant tool calls, tool results. You own it, so you can trim, summarise, or persist it however you like.
  • Tool registry is a dict from tool name to a plain function plus its JSON schema. Validation is whatever you write (pydantic, zod, hand-rolled).
  • The loop: call the model, execute any tool calls, append results, repeat until the model stops or a budget (iterations, tokens, seconds) is exhausted.
  • Everything else is ordinary software: retries with backoff, idempotency keys, logging spans, approval gates, tests — the same code you would write for any I/O-bound service.

Best use cases

  • Production systems where you must understand every prompt and token that leaves the process.
  • Teams with solid software-engineering habits and one or two providers.
  • Learning: you cannot reason about a framework until you have written the loop it wraps.
  • Latency- and cost-sensitive paths where a framework's hidden prompt scaffolding is unacceptable.

Weaknesses

  • You re-implement provider-agnostic message formats, streaming, and tool-call parsing yourself if you switch vendors.
  • Checkpointing, human-in-the-loop resumption, and durable execution are yours to build — this is where teams underestimate the work.
  • No shared vocabulary with other teams; every codebase invents its own Agent class.
  • Integrations (vector stores, document loaders, observability exporters) are individually wired, not plugged in.

When NOT to use it

  • You need durable, resumable, multi-step workflows with checkpoints and time-travel debugging and do not want to build a state machine runtime.
  • You are prototyping across many providers and vector stores and want swappable adapters more than control.
  • The team has no appetite for owning the loop and would rather own a framework upgrade cadence.

Code example

Illustrative — APIs change between versions.

1import json
2
3TOOLS = {"get_weather": get_weather, "search_docs": search_docs} # plain functions
4SCHEMAS = [weather_schema, search_schema] # JSON Schema per tool
5
6def run_agent(user_msg: str, max_steps: int = 8) -> str:
7 messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": user_msg}]
8 for _ in range(max_steps): # hard iteration budget
9 reply = call_model(messages, tools=SCHEMAS) # your SDK wrapper; shape is provider-specific
10 messages.append(reply.as_message())
11 if not reply.tool_calls: # model answered in prose -> done
12 return reply.text
13 for call in reply.tool_calls:
14 try:
15 result = TOOLS[call.name](**call.arguments)
16 except Exception as e: # errors go back to the model, not up the stack
17 result = {"error": type(e).__name__, "detail": str(e)[:200]}
18 messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
19 raise RuntimeError("step budget exhausted")

Alternatives

Related lessons