Skip to content
OpenAI Agents SDK

OpenAI Agents SDK

An agent in the OpenAI Agents SDK runs through Runner.run. The model loops over your function tools inside that call, and hands back a final output. So the kslate calls sit outside the run. You read the record, call Runner.run, and write what it returned.

Runner.run is a coroutine and decide is an ordinary function, so decide runs it with asyncio.run. The model here is Claude through the SDK’s LiteLLM adapter.

recipe.py
import asyncio
import os
from agents import Agent, Runner, function_tool, set_tracing_disabled
from agents.extensions.models.litellm_model import LitellmModel
from kslate import Agent as KslateAgent, Client, StalePremiseError, Write, run

RECORD = "incidents/inc-31"
client = Client("http://127.0.0.1:8080", api_key="rem-token")

set_tracing_disabled(True)  # the SDK traces to OpenAI by default


@function_tool
def restart_deployment(name: str) -> str:
    """Restart a Kubernetes deployment. Returns the kubectl output."""
    return f"deployment.apps/{name} restarted"


remediator = Agent(
    name="Remediator",
    instructions="You carry out one remediation step. Reply with one line saying what you did.",
    tools=[restart_deployment],
    model=LitellmModel(model="anthropic/claude-sonnet-5", api_key=os.environ["ANTHROPIC_API_KEY"]),
)


def decide(fields):
    plan = fields["planned_action"]["value"]
    print(f"carrying out: {plan}")
    result = asyncio.run(Runner.run(remediator, f"Carry out: {plan}"))
    return [Write(op="append", field="actions_taken", entry=result.final_output, evidence="kubectl output")]


try:
    run(KslateAgent(decide=decide), client, record=RECORD, max_attempts=1)
    print("write accepted")
except StalePremiseError as e:
    for m in e.moved:
        print(f"rejected: {m.field} moved v{m.your_version} -> v{m.current_version} by {m.current_author}")
    print("the tool already ran, so this does not re-decide")

Both libraries export a class called Agent, so one of them is renamed on import. run() sends the versions it read as based_on, and the gateway rejects the write if planned_action moved while the run was in progress.

Run it against the gateway from Install, with a plan already on the record:

carrying out: roll back api deployment
write accepted

The gateway rejects the write when the diagnoser replaces the plan while the run is in progress:

carrying out: restart payments-api pods
rejected: planned_action moved v1 -> v2 by apikey:diagnoser
the tool already ran, so this does not re-decide

Why max_attempts=1

restart_deployment acts inside the run, before any kslate write exists. If the gateway rejects the write and run() called decide again, the model would restart the deployment a second time.

max_attempts=1 calls decide once and raises the rejection instead. Handling rejections covers what to do with it. Leave max_attempts at its default only when running your tools twice is harmless.

Handoffs and the record

A handoff to another SDK agent happens inside Runner.run. The gateway sees the result of the whole run, written once by the identity this client holds. To attribute each agent’s writes separately, run each one with its own client, its own credential and its own role. Scope and identity has the shape of those roles.

Written against openai-agents 0.20.0 and kslate 0.9.0.