Blog
AI28 min czytania

OpenAI Agents SDK dla TypeScriptu — agenci, narzędzia i workflow głosowy

Framework OpenAI do budowania agentów w TypeScripcie: pętla wywołań modelu, narzędzia walidowane Zodem, przekazywanie rozmowy między agentami, bramki bezpieczeństwa i wstrzymanie przebiegu na zatwierdzenie przez człowieka. Pokazujemy pełny obraz API na konkretnym kodzie, trzy sposoby zszycia tego z aplikacją w Laravelu (włącznie z mintowaniem tokenów efemerycznych dla agenta głosowego w przeglądarce) oraz to, o czym trzeba wiedzieć przed produkcją: wersja 0.x, treść rozmów w panelu śledzenia i koszt niekontrolowanej pętli.

Napisanie „agenta" na jedno wywołanie modelu zajmuje kwadrans. Napisanie agenta, który realnie działa w produkcie, zajmuje kilka tygodni — i te tygodnie w większości nie idą na prompt. Idą na pętlę: model prosi o wywołanie funkcji, trzeba sparsować argumenty, zwalidować je, wykonać kod, dopisać wynik do historii, zapytać model ponownie, rozpoznać, że tym razem odpowiedź jest już finalna, i przy okazji nie dopuścić, żeby dwadzieścia obrotów tej pętli wypaliło budżet miesiąca w jednym żądaniu.

Potem dochodzi reszta. Historia rozmowy między żądaniami. Rozdzielenie zadań między kilku wyspecjalizowanych agentów, bo jeden prompt z czterdziestoma instrukcjami przestaje działać. Zatwierdzanie przez człowieka operacji, których nie chcemy oddać modelowi bez nadzoru — zwrot pieniędzy, wysłanie maila do klienta, usunięcie rekordu. Wgląd w to, co się w środku stało, gdy agent zrobił coś dziwnego trzy dni temu.

OpenAI Agents SDK dla JavaScriptu i TypeScriptu (github.com/openai/openai-agents-js) zamyka dokładnie te elementy. Jest to port SDK znanego wcześniej z Pythona, utrzymywany przez OpenAI, na licencji MIT — i, co istotne, niezależny od dostawcy modelu: działa z API OpenAI natywnie, a przez adapter również z modelami dostępnymi w Vercel AI SDK. Poniżej pełny obraz API na działającym kodzie, trzy realistyczne architektury zszycia tego z aplikacją w Laravelu i uczciwa lista rzeczy, które trzeba wiedzieć przed wdrożeniem.

Stan projektu

Dane z API GitHuba na 11 września 2026:

  • 3780 gwiazdek i 959 forków — nieproporcjonalnie dużo forków w stosunku do gwiazdek, co przy repozytorium z katalogiem examples/ zwykle oznacza, że ludzie realnie z tego kodu startują,
  • repozytorium założone 31 maja 2025, kod w TypeScripcie, monorepo na pnpm z pakietami @openai/agents, @openai/agents-realtime i @openai/agents-extensions,
  • najnowsze wydanie v0.17.2 z 8 września 2026, a przed nim v0.17.1 tego samego dnia, v0.17.0 z 19 sierpnia, v0.16.1 z 16 sierpnia i v0.16.0 z 15 sierpnia. Pięć wydań w niecały miesiąc,
  • ponad sto commitów od 10 sierpnia (tyle zwraca jedna strona API i lista jest ucięta) — projekt jest w fazie intensywnego rozwoju,
  • tylko 16 otwartych zgłoszeń, co przy tym tempie oznacza, że zgłoszenia są domykane, a nie odkładane,
  • licencja MIT.

Numer wersji jest tu najważniejszą informacją w całym akapicie i wrócimy do niego w rozdziale o ryzykach. To jest wciąż wersja 0.x i wydania minor przynoszą zmiany API.

Środowiska uruchomieniowe: Node.js 22 lub nowszy, Deno i Bun, a eksperymentalnie także Cloudflare Workers z włączonym nodejs_compat. Wymagany jest Zod w wersji 4 — SDK używa go do schematów narzędzi i wyjść strukturalnych.

Pierwszy agent i pętla, która za nim stoi

npm install @openai/agents zod
export OPENAI_API_KEY=sk-...

Minimalny agent to obiekt z nazwą i instrukcją, uruchamiany funkcją run:

import { Agent, run } from '@openai/agents';

const agent = new Agent({
  name: 'Assistant',
  instructions: 'You are a helpful assistant.',
});

const result = await run(agent, 'Write a haiku about recursion in programming.');
console.log(result.finalOutput);

Za tym jednym wywołaniem stoi pętla, którą warto znać, bo z niej wynikają wszystkie późniejsze decyzje projektowe. Runner:

  1. wywołuje model bieżącego agenta z bieżącym wejściem,
  2. sprawdza odpowiedź: jeśli to wynik finalny — zwraca go; jeśli to przekazanie (handoff) — przełącza agenta, zachowuje zgromadzoną historię i wraca do punktu 1; jeśli to wywołania narzędzi — wykonuje je, dopisuje wyniki do rozmowy i wraca do punktu 1,
  3. po osiągnięciu limitu maxTurns rzuca MaxTurnsExceededError.

Reguła rozpoznawania wyniku finalnego jest prosta i warto ją zapamiętać: odpowiedź tekstowa o oczekiwanym typie i brak wywołań narzędzi. Domyślny maxTurns to 10; można go podnieść albo wyłączyć, przekazując null. Wyłączanie go w kodzie, który obsługuje ruch użytkowników, jest tym samym co zdjęcie bezpiecznika — każdy obrót pętli to płatne wywołanie modelu.

Do jednorazowych skryptów wystarczy funkcja run(), która używa domyślnego runnera. W aplikacji obowiązuje inna zasada: Runner tworzy się raz, przy starcie procesu, i reużywa między żądaniami — instancja trzyma konfigurację globalną, dostawcę modeli i ustawienia śledzenia.

Narzędzia

SDK rozpoznaje siedem kategorii narzędzi. Trzy z nich pokrywają praktycznie wszystkie zastosowania biznesowe, o pozostałych warto po prostu wiedzieć, że istnieją.

Narzędzia funkcyjne

Najważniejsza kategoria: dowolna funkcja opakowana helperem tool(), ze schematem parametrów w Zodzie.

import { Agent, tool } from '@openai/agents';
import { z } from 'zod';

const getWeather = tool({
  name: 'get_weather',
  description: 'Return the weather for a given city.',
  parameters: z.object({ city: z.string() }),
  async execute({ city }) {
    return `The weather in ${city} is sunny.`;
  },
});

const agent = new Agent({
  name: 'Weather bot',
  instructions: 'You are a helpful weather bot.',
  model: 'gpt-4.1',
  tools: [getWeather],
});

Schemat walidacyjny automatycznie włącza tryb strict — argumenty niezgodne ze schematem wracają do modelu jako błąd, zamiast wpadać do naszego kodu. Poza podstawami tool() przyjmuje kilkanaście opcji i cztery z nich zmieniają jakość wdrożenia:

  • needsApproval — wymusza zatwierdzenie przez człowieka przed wykonaniem. Może być wartością logiczną albo funkcją decydującą na podstawie argumentów. Cały mechanizm opisujemy niżej,
  • isEnabled — warunkowo odsłania narzędzie w danym przebiegu; przyjmuje wartość logiczną albo predykat. Tak buduje się uprawnienia: agent obsługujący gościa nie widzi narzędzia do zwrotów,
  • timeoutMs i timeoutBehavior — limit czasu na wywołanie. Domyślne zachowanie (error_as_result) zwraca modelowi błąd jako wynik, wariant raise_exception rzuca ToolTimeoutError. Bez tego jedno zawieszone żądanie HTTP w narzędziu zawiesza cały przebieg,
  • errorFunction — przekształca wewnętrzny błąd w wynik widoczny dla modelu. Domyślny handler jest wyłączony, więc oryginalny wyjątek leci w górę. To dobra domyślna decyzja i zła niespodzianka, jeśli ktoś liczył na przeciwną.

Są też inputGuardrails i outputGuardrails na poziomie pojedynczego narzędzia oraz customDataExtractor, dopinający metadane widoczne wyłącznie dla naszego kodu, nie dla modelu.

Narzędzia hostowane po stronie OpenAI

Przy modelu korzystającym z Responses API dochodzi zestaw narzędzi wykonywanych na serwerach OpenAI, bez naszego kodu: web_search (wyszukiwanie w internecie), file_search (odpytywanie magazynów wektorowych), code_interpreter (uruchamianie kodu w piaskownicy), image_generation, tool_search (doczytywanie odroczonych narzędzi w trakcie przebiegu) i programmatic_tool_calling (model generuje JavaScript koordynujący inne narzędzia). SDK udostępnia do nich helpery z nazwami opcji w konwencji JavaScriptu:

import { Agent, webSearchTool, fileSearchTool } from '@openai/agents';

const agent = new Agent({
  name: 'Research assistant',
  instructions: 'Answer with sources.',
  tools: [
    webSearchTool({ searchContextSize: 'medium' }),
    fileSearchTool(['vs_abc123'], { maxNumResults: 5, includeSearchResults: true }),
  ],
});

Trzeba mieć świadomość konsekwencji: web_search i file_search to funkcje platformy OpenAI, więc korzystanie z nich wiąże agenta z tym dostawcą. Przy wymogu przenośności między modelami bezpieczniej jest napisać własne narzędzie funkcyjne uderzające we wybraną wyszukiwarkę.

Narzędzia wykonywane lokalnie

Trzecia kategoria to narzędzia wbudowane w SDK, ale wykonywane poza modelem — po naszej stronie. computerTool() wymaga implementacji interfejsu Computer i pozwala agentowi sterować komputerem; shellTool() uruchamia komendy powłoki lokalnie albo w hostowanym kontenerze; applyPatchTool() wymaga implementacji interfejsu Editor i służy do modyfikowania plików. Model prosi o wywołanie, wykonanie robi nasza aplikacja albo skonfigurowane środowisko.

Przy tych trzech needsApproval przestaje być opcją i staje się wymogiem higieny. Agent z dostępem do powłoki i bez bramki zatwierdzania to nie funkcja produktu, to incydent w oczekiwaniu.

Serwery MCP

Narzędzia można też podłączyć z serwera Model Context Protocol — lokalnego przez wejście standardowe albo zdalnego przez HTTP:

import { Agent, run, MCPServerStreamableHttp } from '@openai/agents';

const mcpServer = new MCPServerStreamableHttp({
  url: 'https://mcp.deepwiki.com/mcp',
  name: 'DeepWiki MCP Server',
});

const agent = new Agent({
  name: 'DeepWiki Assistant',
  instructions: 'Use the tools to respond to user requests.',
  mcpServers: [mcpServer],
});

try {
  await mcpServer.connect();
  const result = await run(agent, 'For the repository openai/codex, tell me the primary programming language.');
  console.log(result.finalOutput);
} finally {
  await mcpServer.close();
}

Wariant lokalny to MCPServerStdio z pełną komendą uruchomienia procesu. Na poziomie agenta jest jeszcze mcpConfig — sterujące trybem strict schematów, obsługą błędów i prefiksowaniem nazw narzędzi nazwą serwera. To ostatnie rozwiązuje realny problem kolizji, gdy dwa serwery MCP wystawiają narzędzie o tej samej nazwie.

Wyjście strukturalne

Domyślnie agent zwraca tekst. Właściwość outputType zmienia to na obiekt walidowany schematem:

import { Agent } from '@openai/agents';
import { z } from 'zod';

const CalendarEvent = z.object({
  name: z.string(),
  date: z.string(),
  participants: z.array(z.string()),
});

const extractor = new Agent({
  name: 'Calendar extractor',
  instructions: 'Extract calendar events from the supplied text.',
  outputType: CalendarEvent,
});

Przy ustawionym outputType SDK automatycznie włącza structured outputs po stronie API. Istotna różnica, której łatwo nie zauważyć: schemat Zoda waliduje wynik lokalnie i zachowuje wywnioskowany typ TypeScriptu, natomiast surowy JSON Schema opisuje wyłącznie kontrakt wobec modelu — sparsowany wynik pozostaje typu unknown. Dla kodu w TypeScripcie oznacza to jedno: Zod, nie surowy schemat.

To jest zarazem najprostszy sposób użycia tego SDK w istniejącej aplikacji, bez żadnej „agentowości": agent z outputType i bez narzędzi to typowany ekstraktor danych z tekstu. Klasyfikacja zgłoszeń, wyciąganie pozycji z faktury, normalizacja adresów — jedno wywołanie, wynik zwalidowany schematem, bez ręcznego parsowania JSON-a z odpowiedzi modelu.

Dwa wzorce orkiestracji

Gdy jeden agent przestaje wystarczać, SDK proponuje dwa wzorce i różnica między nimi jest architektoniczna, nie kosmetyczna.

Menedżer, czyli agenci jako narzędzia

Agent centralny zachowuje kontrolę nad rozmową i wywołuje wyspecjalizowanych agentów jak zwykłe narzędzia:

const bookingAgent = new Agent({
  name: 'Booking expert',
  instructions: 'Answer booking questions and modify reservations.',
});

const refundAgent = new Agent({
  name: 'Refund expert',
  instructions: 'Help customers process refunds and credits.',
});

const customerFacingAgent = new Agent({
  name: 'Customer-facing agent',
  instructions: 'Talk to the user directly. When they need booking or refund help, call the matching tool.',
  tools: [
    bookingAgent.asTool({
      toolName: 'booking_expert',
      toolDescription: 'Handles booking questions and requests.',
    }),
    refundAgent.asTool({
      toolName: 'refund_expert',
      toolDescription: 'Handles refund questions and requests.',
    }),
  ],
});

Zaleta: jedno miejsce, w którym da się wymusić bramki bezpieczeństwa, limity i logowanie. Agent nadrzędny nigdy nie oddaje kontroli, więc podsumowanie zawsze przechodzi przez jego prompt.

Przekazanie rozmowy

Wariant drugi: agent segregujący rozpoznaje temat i przekazuje rozmowę specjaliście, który od tego momentu jest jej właścicielem do wyprodukowania wyniku finalnego.

import { Agent, run } from '@openai/agents';

const historyTutorAgent = new Agent({
  name: 'History Tutor',
  instructions: 'You provide assistance with historical queries.',
});

const mathTutorAgent = new Agent({
  name: 'Math Tutor',
  instructions: 'You provide help with math problems. Explain your reasoning at each step.',
});

const triageAgent = Agent.create({
  name: 'Triage Agent',
  instructions: "You determine which agent to use based on the user's homework question",
  handoffs: [historyTutorAgent, mathTutorAgent],
});

const result = await run(triageAgent, 'What is the capital of France?');
console.log(result.finalOutput);
console.log(result.lastAgent?.name); // który agent odpowiedział

Zwróć uwagę na Agent.create(...) zamiast new Agent(...). To nie jest kwestia stylu: przy celach przekazania zwracających różne typy wyjścia statyczna metoda pozwala TypeScriptowi wywnioskować unię możliwych kształtów finalOutput w całym grafie przekazań i unika ostrzeżenia w czasie działania. Przy jednorodnych typach różnicy nie ma.

Samo przekazanie można dostosować helperem handoff() — z callbackiem wywoływanym w momencie przekazania, własnym typem wejścia i nadpisaną nazwą oraz opisem narzędzia:

import { z } from 'zod';
import { Agent, handoff, RunContext } from '@openai/agents';

const FooSchema = z.object({ foo: z.string() });

function onHandoff(ctx: RunContext, input?: { foo: string }) {
  console.log('Handoff called with:', input?.foo);
}

const handoffObj = handoff(agent, {
  onHandoff,
  inputType: FooSchema,
  toolNameOverride: 'custom_handoff_tool',
  toolDescriptionOverride: 'Custom description',
});

Wzorce są komplementarne. Menedżer daje kontrolę i jeden punkt egzekwowania reguł; przekazania pozwalają trzymać prompty krótkie, bo każdy agent odpowiada za jedną rzecz i nie musi wiedzieć o pozostałych.

Bramki bezpieczeństwa

Bramki (guardrails) to walidacja wejścia i wyjścia, konfigurowana tablicami inputGuardrails i outputGuardrails. Bramka może być zwykłą funkcją, ale najciekawszy wzorzec to bramka będąca osobnym, tanim agentem oceniającym wejście:

import { Agent, run, InputGuardrail, InputGuardrailTripwireTriggered } from '@openai/agents';
import { z } from 'zod';

const guardrailAgent = new Agent({
  name: 'Guardrail check',
  instructions: 'Check if the user is asking you to do their math homework.',
  outputType: z.object({
    isMathHomework: z.boolean(),
    reasoning: z.string(),
  }),
});

const mathGuardrail: InputGuardrail = {
  name: 'Math Homework Guardrail',
  // false blokuje model do zakończenia bramki
  runInParallel: false,
  execute: async ({ input, context }) => {
    const result = await run(guardrailAgent, input, { context });
    return {
      outputInfo: result.finalOutput,
      tripwireTriggered: result.finalOutput?.isMathHomework ?? false,
    };
  },
};

const agent = new Agent({
  name: 'Customer support agent',
  instructions: 'You are a customer support agent.',
  inputGuardrails: [mathGuardrail],
});

try {
  await run(agent, 'Hello, can you help me solve for x: 2x + 3 = 11?');
} catch (e) {
  if (e instanceof InputGuardrailTripwireTriggered) {
    console.log('Math homework guardrail tripped');
  }
}

Zadziałanie bramki rzuca wyjątek — InputGuardrailTripwireTriggered albo OutputGuardrailTripwireTriggered — więc przechwytuje się je normalnym try/catch. Flaga runInParallel jest tu decyzją o kompromisie: równoległe uruchomienie bramki i modelu jest szybsze, ale model zdąży zacząć pracę nad wejściem, które bramka odrzuci; false blokuje model do jej zakończenia i płaci za to opóźnieniem.

Ważne ograniczenie, przez które łatwo zbudować fałszywe poczucie bezpieczeństwa: bramki wejściowe uruchamiają się wyłącznie na pierwszym wejściu użytkownika w przebiegu. Nie są filtrem każdej tury rozmowy. Do walidacji na poziomie pojedynczych narzędzi są osobne bramki na narzędziach.

Człowiek w pętli

To jest funkcja, która najczęściej decyduje, czy agenta wolno wpuścić do procesu biznesowego. Narzędzie oznaczone needsApproval nie wykonuje się od razu — przebieg zatrzymuje się i zwraca listę przerwań, a cały stan da się zserializować, przenieść do innego procesu, zatwierdzić albo odrzucić i wznowić.

import { z } from 'zod';
import fs from 'node:fs/promises';
import { Agent, run, tool, RunState, RunResult } from '@openai/agents';

const getWeatherTool = tool({
  name: 'get_weather',
  description: 'Get the weather for a given city',
  parameters: z.object({ location: z.string() }),
  needsApproval: async (_context, { location }) => location === 'San Francisco',
  execute: async ({ location }) => `The weather in ${location} is sunny`,
});

const agent = new Agent({
  name: 'Basic test agent',
  instructions: 'You are a basic agent',
  tools: [getWeatherTool],
});

let result: RunResult<unknown, Agent<unknown, any>> = await run(
  agent,
  'What is the weather in Oakland and San Francisco?',
);

while (result.interruptions?.length > 0) {
  // stan przebiegu jako JSON — może poczekać w bazie na decyzję człowieka
  await fs.writeFile('result.json', JSON.stringify(result.state, null, 2), 'utf-8');

  const storedState = await fs.readFile('result.json', 'utf-8');
  const state = await RunState.fromString(agent, storedState);

  for (const interruption of result.interruptions) {
    const confirmed = await confirm(
      `Agent ${interruption.agent.name} would like to use the tool ${interruption.name} with "${interruption.arguments}". Do you approve?`,
    );
    confirmed ? state.approve(interruption) : state.reject(interruption);
  }

  result = await run(agent, state);
}

console.log(result.finalOutput);

Kluczowa własność jest w linii z JSON.stringify(result.state): stan przebiegu jest serializowalny. Nie trzeba trzymać żywego procesu w oczekiwaniu na decyzję. Stan zapisuje się do bazy, zwraca identyfikator, wysyła powiadomienie, człowiek zatwierdza godzinę później z panelu, a przebieg wznawia inny worker. Dla aplikacji w Laravelu to najważniejszy szczegół w całym SDK, bo pozwala poprowadzić zatwierdzanie przez kolejkę i model Eloquenta, zamiast przez trzymane połączenie HTTP.

Zauważ też, że needsApproval dostaje argumenty wywołania. Zatwierdzania nie trzeba więc włączać dla całego narzędzia — wystarczy dla przypadków, które przekraczają progi: zwrot powyżej kwoty, wysyłka poza kraj, operacja na koncie starszym niż rok.

Pamięć rozmowy

SDK daje cztery strategie i dokumentacja radzi wybrać jedną, a nie mieszać:

  • result.history — pełna ręczna kontrola, historia przekazywana do następnego przebiegu przez naszą aplikację. Działa z każdym dostawcą modelu i nadaje się do prostych pętli czatu,
  • session — dowolny obiekt implementujący interfejs Session. Runner sam pobiera historię przed przebiegiem i zapisuje nowe elementy po nim. W pakiecie są dwie implementacje: OpenAIConversationsSession (Conversations API po stronie OpenAI) i MemorySession (pamięć procesu, do testów i demonstracji). Własny magazyn to implementacja tego samego interfejsu — w examples/memory/ są przykłady oparte o Prismę i o pliki,
  • conversationId — stan trzymany po stronie OpenAI, współdzielony między workerami i usługami. Do kolejnej tury przekazuje się ten sam identyfikator i tylko nową wypowiedź użytkownika,
  • previousResponseId — najprostsza kontynuacja serwerowa, bez tworzenia konwersacji; wyłącznie Responses API.
import { Agent, run, MemorySession } from '@openai/agents';

const session = new MemorySession();
const agent = new Agent({ name: 'Assistant', instructions: 'Be brief.' });

await run(agent, 'Nazywam się Anna.', { session });
const second = await run(agent, 'Jak się nazywam?', { session });
console.log(second.finalOutput); // zna odpowiedź z historii sesji

Sesje mają też proste operacje CRUD na historii, więc funkcje „cofnij", „wyczyść rozmowę" albo audyt buduje się bez własnej warstwy. Przy wznawianiu przebiegu ze stanu (zatwierdzenia, przerwania) należy przekazywać tę samą sesję — wznowiona tura dopisuje się do pamięci bez ponownego przygotowywania wejścia.

Uwaga architektoniczna: conversationId i previousResponseId oznaczają, że treść rozmów zostaje po stronie OpenAI. Przy danych, które nie mogą tam zostać, wybór zawęża się do result.history albo własnej implementacji Session na naszej bazie.

Śledzenie przebiegów — i co z tego wynika dla RODO

SDK zbiera pełny ślad przebiegu: wywołania modelu, wywołania narzędzi, przekazania, bramki i zdarzenia własne. Efekt jest widoczny w panelu Traces na platformie OpenAI i przy diagnozowaniu agenta jest to najbardziej użyteczna rzecz w całym pakiecie — hierarchia domyślna to TaskSpanAgentSpanTurnSpan, z modelem i narzędziami zagnieżdżonymi pod turą, a każda tura raportuje numer, nazwę agenta oraz liczbę tokenów wejścia, wyjścia, wejścia z cache i zapisu do cache.

I teraz część, którą trzeba przeczytać dwa razy: śledzenie jest domyślnie włączone w środowiskach serwerowych (Node.js, Deno, Bun). Wyłączone jest domyślnie w przeglądarce i przy NODE_ENV=test. Oznacza to, że wdrożenie tego SDK bez zmiany konfiguracji wysyła treść rozmów i wyniki narzędzi do OpenAI w celu wyświetlenia ich w panelu.

Dla organizacji działających w reżimie Zero Data Retention śledzenie jest niedostępne. Dla wszystkich pozostałych, które przetwarzają dane osobowe, jest to decyzja do podjęcia świadomie i do udokumentowania — a nie ustawienie domyślne, które przechodzi bez rozmowy.

Trzy poziomy kontroli:

# globalnie, zmienną środowiskową
OPENAI_AGENTS_DISABLE_TRACING=1
// na poziomie runnera
const runner = new Runner({
  tracingDisabled: true,
  // albo: zachowaj spany, usuń z nich treść
  traceIncludeSensitiveData: false,
  workflowName: 'Obsługa zgłoszeń',
});

Wariant środkowy jest zwykle najlepszy: traceIncludeSensitiveData: false zostawia strukturę przebiegu, czasy i liczniki tokenów, a usuwa wejścia i wyjścia modelu oraz narzędzi. Diagnostyka „ile tur, które narzędzia, ile tokenów" zostaje, treść nie wychodzi. Alternatywa dla zupełnie innego celu to własny procesor śladów — ślady można kierować w inne miejsce, zamiast do OpenAI albo obok.

Praktyczny detal dla Cloudflare Workers: pętla eksportu śladów jest tam niedostępna, więc trzeba samodzielnie wywołać getGlobalTraceProvider().forceFlush() w cyklu życia żądania (typowo w bloku finally razem z waitUntil), inaczej ślady giną razem z workerem.

Agenci głosowi

Druga twarz tego SDK to agenci działający na żywo, w rozmowie głosowej. Zamiast pętli żądanie–odpowiedź jest tu RealtimeSession, która żyje przez cały czas rozmowy i obsługuje dźwięk, przerwania oraz historię.

import { RealtimeAgent, RealtimeSession, tool } from '@openai/agents/realtime';
import { z } from 'zod';

const getWeatherTool = tool({
  name: 'get_weather',
  description: 'Get the weather for a given city',
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => `The weather in ${city} is sunny`,
});

const agent = new RealtimeAgent({
  name: 'Data agent',
  instructions: 'You are a data agent',
  tools: [getWeatherTool],
});

// w przeglądarce: token efemeryczny z naszego backendu
const { apiKey } = await fetch('/api/realtime-token').then((r) => r.json());

const session = new RealtimeSession(agent, { model: 'gpt-realtime-2.1' });
await session.connect({ apiKey });

W przeglądarce connect() zestawia połączenie WebRTC i samo konfiguruje przechwytywanie mikrofonu oraz odtwarzanie dźwięku. W środowisku serwerowym, na przykład w Node.js, SDK automatycznie schodzi do WebSocketu. Narzędzia, przekazania i bramki działają tak samo jak w agencie tekstowym.

Najważniejsza reguła bezpieczeństwa: do przeglądarki nigdy nie trafia zwykły klucz API. Backend tworzy krótkotrwały token efemeryczny (zaczynający się od ek_) i tylko on idzie do klienta. Jeśli sesja potrzebuje hostowanych narzędzi MCP z nagłówkami autoryzacyjnymi, ta konfiguracja również musi wejść w żądanie serwerowe, a nie w kod w przeglądarce.

W stacku Laravela

SDK jest w TypeScripcie i nie ma odpowiednika w PHP, więc w naszym stacku wygląda to na przeszkodę. W praktyce nie jest, bo trzy naturalne architektury nie wymagają przepisywania niczego.

1. Laravel jako mintownica tokenów efemerycznych

Najprostszy i najczęstszy przypadek: agent głosowy albo czat działa w przeglądarce, a Laravel odpowiada wyłącznie za autoryzację użytkownika i wydanie krótkotrwałego tokenu. Cała logika uprawnień zostaje tam, gdzie już jest:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Http;

class RealtimeTokenController extends Controller
{
    public function store(): JsonResponse
    {
        $response = Http::withToken(config('services.openai.key'))
            ->post('https://api.openai.com/v1/realtime/client_secrets', [
                'session' => [
                    'type' => 'realtime',
                    'model' => 'gpt-realtime-2.1',
                ],
            ])
            ->throw();

        return response()->json([
            'apiKey' => $response->json('value'),
        ]);
    }
}

Trasa idzie za middleware auth i rozsądnym throttle — endpoint wydający płatne tokeny bez limitu to zaproszenie do nadużycia. Token jest krótkotrwały, więc backend musi umieć wydać nowy, gdy sesja się przedłuża.

2. Narzędzia agenta uderzające w API Laravela

Wiedza o domenie zostaje w aplikacji, a agent dostaje wąskie, opisane narzędzia wołające nasze endpointy. Nigdy odwrotnie — dawanie agentowi dostępu wprost do bazy odbiera nam walidację, polityki dostępu i obserwatory Eloquenta:

const findOrders = tool({
  name: 'find_orders',
  description: 'Wyszukuje zamówienia klienta po adresie e-mail.',
  parameters: z.object({
    email: z.string(),
    limit: z.number().int().min(1).max(20),
  }),
  timeoutMs: 5000,
  execute: async ({ email, limit }) => {
    const res = await fetch(`${process.env.APP_URL}/api/agent/orders?email=${encodeURIComponent(email)}&limit=${limit}`, {
      headers: { Authorization: `Bearer ${process.env.AGENT_API_TOKEN}` },
    });
    if (!res.ok) {
      return `Nie udało się pobrać zamówień (HTTP ${res.status}).`;
    }
    return await res.text();
  },
});

const refundOrder = tool({
  name: 'refund_order',
  description: 'Zwraca środki za zamówienie.',
  parameters: z.object({ orderId: z.string(), amount: z.number() }),
  // zatwierdzenie człowieka powyżej progu
  needsApproval: async (_ctx, { amount }) => amount > 200,
  execute: async ({ orderId, amount }) => {
    // ...wywołanie naszego endpointu zwrotów
  },
});

Endpoint /api/agent/* traktujemy jak każde inne API: własny token, Sanctum albo podpisane żądania, osobne uprawnienia i twarde limity. Agent jest klientem naszego API, nie jego współwłaścicielem — i dobrze, gdy w logach da się odróżnić jego ruch od ruchu ludzi.

3. Usługa agentowa obok aplikacji

Przy dłuższych przebiegach sensowna jest osobna usługa w Node.js, wołana z Laravela przez HTTP albo przez kolejkę. Laravel nadal jest właścicielem procesu biznesowego: zadanie kolejkowe wysyła żądanie, usługa zwraca wynik albo — gdy przebieg zatrzymał się na zatwierdzeniu — zserializowany stan, który zapisujemy w bazie razem z opisem oczekiwanej decyzji. Panel Filamenta pokazuje wtedy listę „do zatwierdzenia", a przycisk w panelu wywołuje endpoint wznawiający przebieg. Dokładnie to opisywaliśmy w rozdziale o człowieku w pętli — serializowalny RunState sprawia, że nie trzeba niczego trzymać w pamięci między decyzjami.

W tym wariancie zwykle warto od razu założyć trzy rzeczy: nazwę przebiegu w konfiguracji śledzenia (workflowName) pokrywającą się z nazwą procesu w naszej domenie, identyfikator korelacji przekazywany z Laravela do usługi i z powrotem, oraz maxTurns ustawiony niżej niż domyślna dziesiątka, jeśli agent ma jedno konkretne zadanie.

Inne modele niż OpenAI

SDK jest niezależne od dostawcy i najkrótsza droga do modelu spoza OpenAI prowadzi przez adapter Vercel AI SDK:

npm install @openai/agents-extensions @ai-sdk/openai
import { Agent, run } from '@openai/agents';
import { openai } from '@ai-sdk/openai';
import { aisdk } from '@openai/agents-extensions/ai-sdk';

const model = aisdk(openai('gpt-5.4'));

const agent = new Agent({
  name: 'My Agent',
  instructions: 'You are a helpful assistant.',
  model,
});

run(agent, 'What is the capital of Germany?');

Podmiana @ai-sdk/openai na inny pakiet dostawcy z ekosystemu AI SDK zmienia model bez zmiany reszty kodu. Trzeba tylko pamiętać, że funkcje platformy OpenAI — narzędzia hostowane, conversationId, previousResponseId, panel śladów — przy innym dostawcy albo nie działają, albo działają inaczej. Przenośność dotyczy pętli agenta, nie całego pakietu.

Błędy, na które trzeba się przygotować

SDK rzuca zwarty zestaw wyjątków i wszystkie dziedziczą po AgentsError, który może udostępniać właściwość state z bieżącym stanem przebiegu. Najważniejsze:

  • MaxTurnsExceededError — osiągnięty limit tur. W praktyce najczęstszy sygnał, że agent wpadł w pętlę wywołań narzędzia,
  • ModelBehaviorError — model zwrócił nieprawidłowe wyjście: zepsuty JSON, nieznane narzędzie, wyjście niezgodne ze schematem,
  • ModelRefusalError — model odmówił wykonania zadania,
  • ModelTimeoutError — żądanie do modelu przekroczyło czas i pozostało błędem po obsłudze ponowień,
  • ToolCallError i ToolTimeoutError — błąd oraz przekroczenie czasu w narzędziu funkcyjnym,
  • bramkiInputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered, ich odpowiedniki dla narzędzi oraz GuardrailExecutionError, gdy sama bramka się wywali,
  • UserError — błąd wynikający z naszej konfiguracji albo wejścia.

Zamiast łapać wszystko wyjątkami, część błędów można zamienić na wynik finalny przez errorHandlers, z kluczami maxTurns, modelRefusal, invalidFinalOutput i default. Handler dostaje { error, context, runData } i zwraca { finalOutput } zgodny z outputType agenta — SDK waliduje ten wynik zastępczy przed zakończeniem przebiegu. To jest właściwe miejsce na komunikat „nie udało mi się tego ustalić, przekazuję do konsultanta" zamiast błędu 500 w interfejsie.

Jeden niuans, który oszczędza godzinę zgadywania: bramki wejściowe nie odtworzą się ze zapisanego stanu, bo działają tylko na pierwszym wejściu przebiegu — ponowienie wymaga uruchomienia przebiegu od nowa z tym samym wejściem i kontekstem. Bramki wyjściowe działają po odpowiedzi modelu, więc te da się ponowić z zapisanego stanu, bez kolejnego wywołania modelu.

Na co uważać przed produkcją

  • To jest wersja 0.x. Pięć wydań w miesiąc i ponad sto commitów w tym samym okresie oznacza szybki rozwój i zmiany API między wersjami minor. Przypnij dokładną wersję w package.json, czytaj listę zmian przed aktualizacją i nie zakładaj, że kod z artykułu sprzed pół roku (także tego) skompiluje się bez poprawek,
  • Śledzenie jest domyślnie włączone na serwerze. Bez świadomej decyzji treść rozmów i wyników narzędzi trafia do OpenAI. Przy danych osobowych to ustawienie do przejrzenia w pierwszym dniu, nie w audycie,
  • Każdy obrót pętli to płatne wywołanie modelu. Agent z trzema narzędziami potrafi zużyć osiem tur na zadanie, które człowiek wykonałby jednym zapytaniem SQL. maxTurns jest zabezpieczeniem budżetowym, a nie tylko technicznym, i warto go ustawiać świadomie niżej niż domyślna dziesiątka,
  • Zod w wersji 4 jest wymagany. W projekcie z Zodem 3 w innych miejscach to osobna migracja do zaplanowania,
  • Nie ma SDK w PHP. Integracja z Laravelem oznacza usługę w Node.js albo kod w przeglądarce — nie ma trzeciej drogi. Warto to policzyć w kosztach utrzymania, zanim zapadnie decyzja,
  • Cloudflare Workers to wsparcie eksperymentalne, z ręcznym opróżnianiem kolejki śladów. Do produkcji na tej platformie trzeba tego pilnować samodzielnie,
  • clone() nie kopiuje list. Klon agenta współdzieli z oryginałem tablice tools, handoffs, mcpServers i bramek, o ile nie przekażesz nowej tablicy. Mutacja przez jednego agenta zmienia zachowanie drugiego — klasyczne źródło błędu, który wygląda na losowy.

Podsumowanie

OpenAI Agents SDK jest tym, czym powinna być biblioteka do agentów: cienką warstwą nad pętlą wywołań modelu, która rozwiązuje nudne i błędogenne części — parsowanie wywołań narzędzi, walidację argumentów, historię, limity, obserwowalność — i nie wymyśla własnego frameworka do reszty aplikacji. Co z tego wynika przy planowaniu wdrożenia:

  • Projekt jest bardzo aktywny — 3780 gwiazdek, 959 forków, wydanie v0.17.2 z 8 września 2026, ponad sto commitów w miesiącu, 16 otwartych zgłoszeń. Utrzymuje go OpenAI, a nie pojedyncza osoba po godzinach,
  • Licencja MIT — najbardziej przewidywalny scenariusz komercyjny, jaki jest,
  • Najprostsze użycie nie wymaga agenta. outputType z schematem Zoda to typowany ekstraktor danych z tekstu; do klasyfikacji zgłoszeń albo wyciągania pozycji z faktury wystarczy piętnaście linii,
  • Dwa wzorce orkiestracji, nie jeden. Menedżer z agentami jako narzędziami daje jeden punkt egzekwowania reguł, przekazania trzymają prompty krótkie. Przy różnych typach wyjścia używaj Agent.create(), żeby TypeScript widział unię możliwych wyników,
  • needsApproval i serializowalny RunState to najmocniejsza funkcja pakietu dla firm — pozwalają wpuścić agenta do procesu z pieniędzmi bez oddawania mu decyzji. Zatwierdzanie da się poprowadzić przez kolejkę i panel, nie przez trzymane połączenie,
  • Wybierz jedną strategię pamięciresult.history, session, conversationId albo previousResponseId — i nie mieszaj ich w jednej rozmowie. Dwie ostatnie zostawiają treść po stronie OpenAI,
  • Bramki wejściowe działają wyłącznie na pierwszym wejściu przebiegu. To nie jest filtr każdej tury; do tego są bramki na poziomie narzędzi,
  • Ustaw traceIncludeSensitiveData: false, jeśli chcesz zachować diagnostykę bez wysyłania treści, albo wyłącz śledzenie w całości przez OPENAI_AGENTS_DISABLE_TRACING=1,
  • W Laravelu: token efemeryczny z backendu, narzędzia uderzające w nasze API, agent jako klient tego API. Nigdy dostęp do bazy wprost — polityki, walidacja i obserwatory Eloquenta są po to, żeby ich nie omijać,
  • Traktuj wersję 0.x poważnie — przypnij dokładną wersję, czytaj listę zmian, planuj aktualizacje jako pracę, nie jako npm update.

Licencja: projekt jest rozpowszechniany na licencji MIT — permisywnej, bez copyleftu i bez warunków przy komercyjnym użyciu poza zachowaniem noty o prawach autorskich. Wolno go używać, modyfikować, wbudowywać w produkt zamknięty i sprzedawać rozwiązania na nim oparte, bez obowiązku publikowania czegokolwiek. Osobna rzecz, której licencja SDK nie dotyczy: koszt i warunki korzystania z samego modelu. Za wywołania płaci się dostawcy według jego cennika, a przy danych osobowych w promptach trzeba mieć uporządkowaną podstawę przetwarzania i umowę powierzenia — SDK na licencji MIT nie zmienia tu niczego w żadną stronę.