OpenAI Agents SDK, few abstractions, plenty of control
The library grew out of an experiment called Swarm and kept its main quality: very few concepts to learn. Agent, tool, handoff, guardrail, and session. Those five things support a fairly complex system, and each takes minutes to understand.
That sets it apart from libraries introducing their own flow model, a state graph, or an intermediate layer over model vendors. Here you write ordinary Python or TypeScript, and the library adds the call loop and observability.
Agent and tools
An agent is a model with instructions and a set of tools. A tool is a function whose argument schema comes from type annotations.
pip install openai-agentsfrom agents import Agent, Runner, function_tool
@function_tool
def check_status(number: str) -> str:
"""Returns the status of the order with the given number.
Call this when a customer asks about their order.
"""
return db.status(number)
agent = Agent(
name="Support",
instructions="You answer questions about orders. No preamble.",
tools=[check_status],
)
result = Runner.run_sync(agent, "Where is order 1841?")
print(result.final_output)The function docstring is not a comment for a developer here but a description for the model. A sentence saying when to call the tool improves accuracy more than any change to the system instruction. It is the cheapest optimisation available.
The TypeScript version carries the same set of concepts, describing arguments through a schema rather than annotations. A team working on the front end can therefore keep agents in the same language as the rest of the application.
Handoffs
This mechanism is the library's heart and its most distinguishing feature. Instead of one agent with twenty tools you build several specialised ones and let them pass the conversation between themselves.
returns_agent = Agent(
name="Returns",
instructions="You handle returns and complaints.",
tools=[file_return, check_policy],
)
billing_agent = Agent(
name="Billing",
instructions="You handle invoices and payments.",
tools=[fetch_invoice, explain_charge],
)
front_desk = Agent(
name="Front desk",
instructions="You route the conversation to the right department.",
handoffs=[returns_agent, billing_agent],
)Technically a handoff is a tool call, so it appears in the trace like any other action. That matters for diagnosis, since a conversation that landed in the wrong department has a visible moment where the decision happened.
The advantage of the split is a shorter instruction and a smaller tool set per agent. A model choosing among four tools errs less often than one choosing among twenty, and an instruction covering one area is more precise than one trying to cover everything.
The trap is overdoing it the other way. Eight agents passing a conversation in circles is a system nobody will follow. Three to five roles suffice for most applications.
Guardrails, or input and output control
A guardrail is a separate check running in parallel with the agent, halting work when a condition fails.
from agents import input_guardrail, GuardrailFunctionOutput
@input_guardrail
async def orders_only(ctx, agent, user_input: str):
verdict = await Runner.run(classifier, user_input)
return GuardrailFunctionOutput(
output_info=verdict.final_output,
tripwire_triggered=not verdict.final_output.about_orders,
)The point is economic and practical at once. A cheap check with a light model rejects out of scope requests before the costlier agent with tool access starts. You save tokens and shrink the surface where things can go wrong.
An output guardrail works the same way on responses. Typical uses are checking that a response holds no personal data, keeps to company policy, and promises nothing it must not promise.
Remember that a guardrail is not a safeguard in the systems security sense. It protects against typical misbehaviour rather than against a deliberate attack. Keep enforcing permissions in tool code by checking who is calling the operation.
If the requirements run further than that, covering prompt injection detection and personal data leaks at company policy level, a guardrail you wrote yourself will not cover it and you need a separate product, Prompt Security for instance. Note, though, that the project is no longer standalone: SentinelOne acquired it in September 2025, so instead of buying a plan you face a sales conversation with a security vendor.
An agent as a tool, the second route
A handoff passes the conversation wholesale with no way back. That is unwelcome when you only want a second opinion and then to resume your own thread. The second mechanism serves that: an agent exposed as a tool.
translator = Agent(name="Translator", instructions="You translate text into Polish.")
main = Agent(
name="Editor",
instructions="You prepare replies for customers.",
tools=[translator.as_tool(
tool_name="translate",
tool_description="Translates the given text into Polish.",
)],
)The difference is fundamental for the flow. On a handoff another agent takes control and finishes the conversation. With an agent as a tool the result returns to the calling agent, which carries the thread on.
The selection rule is simple. If the matter belongs to another department and should end there, use a handoff. If you need a partial answer to assemble your own, use an agent as a tool.
The latter arrangement also carries a cost advantage. A specialised agent can run on a cheaper model, since its task is narrow, while the main agent runs on a stronger one. Splitting into steps lets you match model to difficulty rather than paying the top rate for everything.
Pausing for a human decision
Not every action should execute by itself. A refund, a message to a customer, or a change to system data are operations worth asking a human about.
The library lets you mark a tool as requiring approval. A run then stops at the call site, reports a pending decision, and you resume it after approving or rejecting.
In practice that mechanism solves the most common rollout problem. Teams fear releasing an agent with access to irreversible operations, so they either withhold such tools or do not deploy at all. Approving selected actions lets you start with an arrangement where the agent proposes and a human approves, then lift restrictions gradually as it proves correct.
Mark operations by consequence rather than by difficulty. Reading customer data can be technically hard and entirely safe, while a one line refund call is a simple operation with serious consequences.
Streaming responses
An agent making three tool calls answers after several seconds, and the user watches a blank screen throughout. Streaming lets you show progress as it happens.
result = Runner.run_streamed(agent, "Where is order 1841?")
async for event in result.stream_events():
if event.type == "raw_response_event":
show_chunk(event.data)Streaming tool call events matters as much as streaming text. A message saying "checking order status" shown mid run changes how the interface feels more than shaving a second off the response.
Sessions and conversation memory
Without a session every run starts from nothing and you pass history by hand. A session does that for you, storing the exchange under an identifier.
from agents import SQLiteSession
session = SQLiteSession("customer-1841", "conversations.db")
Runner.run_sync(agent, "Where is my order?", session=session)
Runner.run_sync(agent, "And when will it arrive?", session=session)The second question works because the agent sees the previous exchange. Without a session it would be incomprehensible.
The limitation is the same as everywhere: history grows and at some point costs more than the answer itself. For long or returning conversations it is worth adding a memory layer such as Mem0, which extracts facts rather than storing everything.
Tracing
The library collects a trace of every run: model calls, tool calls, handoffs, and guardrails. It works by default with no configuration and is one of the stronger arguments for this option.
Diagnosing an agent without a trace comes down to guessing why it did something odd. With a trace you see exactly which tool it called, with what arguments, and what came back.
Traces can also be routed to external observability systems, Langfuse for instance, if you want everything in one place alongside the rest of the application.
One thing needs deciding before rollout, since the default behaviour surprises people: traces contain conversation content and land on the model vendor's tracing dashboard rather than on your own disk. With sensitive data you have three ways out: disable capture of the content itself, disable tracing entirely through an environment variable or a run setting, or attach your own trace processor and route traces solely to your own infrastructure. A separate case is organisations on zero data retention with the vendor, where tracing is not available at all.
From prototype to production
An agent working across ten hand checked questions and an agent serving traffic are two different things. A few steps separate one from the other.
The first is limits. Set a maximum step count per run and a timeout. Without them a loop where the agent calls a tool, gets an error, and retries will burn a day's budget in a quarter of an hour.
The second is tool error handling. A function raising an exception aborts the run, though the better behaviour is usually returning the model a message it can act on. The sentence "no order found with that number" lets the agent ask the customer, while an exception ends the conversation in an error.
The third is a set of test cases. Gather thirty real conversations, mark the expected outcome, and run them after every instruction change. That is the only way to notice a fix helping one case while breaking three others.
The fourth is the cost of one run. Sum the tokens from the usage field in the result across ten real conversations and multiply by expected volume. That number says more than any estimate, since it reflects your tools and your instructions.
The fifth is a plan for when the agent does not know. The answer "I do not have that information, passing you to a consultant" beats invented content, but a model rarely picks it unprompted. Write it explicitly in the instruction and check it against the test cases.
Instructions that actually work
An agent instruction is not a job description but a set of decision rules. That difference translates directly into behaviour.
An instruction saying "you are a helpful customer service assistant" changes nothing, since the model tries to be helpful anyway. An instruction saying "on a question about order status always call the tool, never guess from the conversation" changes a specific behaviour in a specific situation.
The second principle concerns boundaries. State plainly what the agent does not do and what it should do instead. The sentence "you do not promise delivery dates, you quote only the date from the system" closes a whole category of problems.
The third concerns length. A three page instruction works worse than a half page one, since the model loses parts from the middle. If the rules are many, that usually signals one agent doing too much and worth splitting.
Against the alternatives
| Tool | Strength | Weakness | Pick it when |
|---|---|---|---|
| OpenAI Agents SDK | Few concepts, handoffs, tracing with no setup | Oriented towards one vendor | A team of roles covering different areas |
| LangGraph | Full control over flow, durable state, pauses | More work on simple cases | Process with branching and a human decision |
| PydanticAI | Typed output, tests without a model | Fewer ready integrations | Result feeding straight into code |
| CrewAI | Legible role split, fast start | Less control over detail | Prototype of an agent team |
The library supports models beyond the default ones through an interface compatible with that vendor's format. Note, though, that some features are built for one ecosystem and are thinner with external models.
A practical note to close the comparison: the choice of library rarely decides success. What determines an agent's quality is the tool descriptions, the boundaries in the instructions, and whether you have a set of cases to measure changes against. Those three carry across libraries almost unchanged, so the work put into them is not lost in a later migration.
Common mistakes
The first is tool descriptions written for a developer. The model reads the same text and needs a sentence about when to call the tool, not a description of the implementation.
The second is one agent holding every tool. At twenty functions selection accuracy drops, and the instruction grows to a size where the model loses parts of it.
The third is no cap on step count. An agent handing off in a loop or calling the same tool repeatedly will burn as many tokens as you allow.
The fourth is treating a guardrail as a security mechanism. A model based check can be worked around, so enforce permissions in code by checking the caller's identity.
The fifth is passing the user identifier in tool arguments. The model can supply somebody else's, so identity should come from run context rather than from the schema.
The sixth is having no test set. An instruction change that subjectively improves one conversation usually breaks another, and without a few dozen recorded cases nobody notices.
FAQ
How does this differ from Swarm?
Swarm was an educational experiment with no production support. This library is its successor intended for production use, adding guardrails, sessions, and tracing. The concepts stayed the same, so migration is straightforward.
Does it work with models other than OpenAI?
Yes, through an interface compatible with that vendor's format, so you can connect local models served through Ollama among others. Some features are built for one ecosystem, though, so check what exactly works with external models.
OpenAI Agents SDK or LangGraph?
Choose this library when the problem splits naturally into roles passing a conversation and you want a fast start. Choose LangGraph when the flow has explicit stages, branches, and points where the process pauses for a human decision.
Is there a TypeScript version?
Yes, with the same set of concepts: tools, handoffs, guardrails, sessions, tracing, and pausing for a human decision. A team working in Next.js can therefore keep agents in the same language as the rest of the code.
What does it cost?
The library itself is free and open source. You pay for model calls, with guardrails and classifiers being extra calls, usually on a cheaper model. Price one full conversation across ten real cases before the process goes permanent.
Documentation sits on the project site, and the TypeScript version in the GitHub repository.