Jeśli dotknąłeś ostatnio jakiegokolwiek SDK agentowego — Claude Code, Codex, OpenCode albo Gemini — to wiesz, o co chodzi. Każdy z nich ma własny protokół zdarzeń, własne typy, własny format streamingu i własną logikę wywoływania narzędzi. Kod podłączający Claude Code nie zadziała z Codexem bez przepisania połowy modułu. To frustrujące, zwłaszcza gdy chcesz po prostu porównać, który agent lepiej radzi sobie z twoim zadaniem.
Dlatego napisałem @inharness-ai/agent-adapters — bibliotekę w TypeScripcie, która normalizuje cztery najpopularniejsze SDK agentowe do jednego strumienia zdarzeń. Zmieniasz silnik, kod aplikacji zostaje bez zmian.
Pierwsza wersja jest już na npm. W tym wpisie opiszę, o co chodzi, dlaczego to zrobiłem i jak tego użyć.
TL;DR
- Jedna biblioteka zamiast czterech niekompatybilnych SDK.
- Jednolity strumień 11 typów zdarzeń (
text_delta,thinking,tool_use,result,subagent_*i reszta) — ten sam kształt dla Claude Code, Codex, OpenCode i Gemini. - Pełne wsparcie Model Context Protocol (MCP) — stdio, SSE, HTTP oraz in-process (SDK).
- Providery: MiniMax, Ollama (lokalne modele), OpenRouter — odpalisz Claude Code lokalnie na Ollama albo przełączysz Codexa na tańszy backend bez zmiany kodu aplikacji.
- Tree-shakeable imports: importujesz tylko adapter, którego używasz.
- Autocomplete modeli w TypeScripcie (
'sonnet-4.6'zamiast'claude-sonnet-4-6-20250929'). - MIT, open source, repo: github.com/inharness/agent-adapters.
npm install @inharness-ai/agent-adaptersProblem: każdy agent AI to osobny świat
Ekosystem agentów AI w 2026 jest fragmentaryczny. Zobacz, co emitują cztery najpopularniejsze SDK:
| SDK | Typ zdarzenia | Streaming | Thinking |
|---|---|---|---|
@anthropic-ai/claude-agent-sdk | SDKMessage | Natywne delty | Natywny streaming |
@openai/codex-sdk | ThreadEvent | Syntetyzowany (pełny tekst) | Post-hoc summary |
@opencode-ai/sdk | SSE events | Natywne SSE | Natywne (reasoning) |
@google/gemini-cli-core | AgentEvent | Natywne | Natywne (thought) |
Każdy z tych SDK ma inny kształt payloadu, inną politykę obsługi podagentów (subagents), inne mechanizmy wznawiania sesji i inny sposób konfiguracji MCP. Kod, który ładnie streamuje odpowiedź z Claude Code, dla Codexa musi robić buforowanie, bo Codex delt nie emituje. Gemini rzuca thought tam, gdzie Claude daje thinking. OpenCode chodzi po SSE, ale jego model MCP akceptuje tylko stdio.
Jeśli budujesz orkiestrator agentów, playground do testowania promptów, narzędzie do benchmarkingu albo po prostu aplikację, w której użytkownik wybiera agenta — siedzisz po kolana w glue code. I ten glue code psuje się przy każdej aktualizacji którejkolwiek z zależności.
Rozwiązanie: jeden interfejs, cztery silniki (a nawet będzie więcej)
@inharness-ai/agent-adapters daje ci jeden kontrakt — RuntimeAdapter — który każdy z czterech zintegrowanych SDK spełnia. Tworzysz adapter, wywołujesz execute(), iterujesz po strumieniu. Reszta to detal implementacyjny.
import { createAdapter } from '@inharness-ai/agent-adapters';
const adapter = createAdapter('claude-code');
for await (const event of adapter.execute({
prompt: 'Read package.json and summarize it.',
systemPrompt: 'Be concise.',
model: 'sonnet-4.6',
})) {
if (event.type === 'text_delta') process.stdout.write(event.text);
if (event.type === 'result') console.log('\n\nDone. Tokens:', event.usage);
}Chcesz przełączyć się na Codexa? Zmieniasz jeden string:
const adapter = createAdapter('codex'); // reszta kodu bez zmianNa Gemini?
const adapter = createAdapter('gemini');To samo zadanie, inny silnik. Żadnego przepisywania logiki obsługi zdarzeń.
Zunifikowane zdarzenia: 11 typów, które pokrywają wszystko
Sercem biblioteki jest typ UnifiedEvent. Każdy adapter tłumaczy swój natywny protokół na ten sam zestaw 11 typów:
| Zdarzenie | Opis |
|---|---|
text_delta | Przyrostowy fragment tekstu (streaming) |
thinking | Rozumowanie modelu (extended thinking / reasoning) |
tool_use | Agent wywołał narzędzie |
tool_result | Narzędzie zwróciło wynik |
assistant_message | Pełna, znormalizowana wiadomość |
subagent_started / _progress / _completed | Zdarzenia podagentów (Agent tool w Claude Code, threadId w Gemini, task tool w OpenCode) |
result | Finał — pełny output, surowe wiadomości, usage (tokeny), sessionId |
error | Błąd — typowany (AdapterTimeoutError, AdapterAbortError, AdapterInitError) |
flush | Granica kompakcji kontekstu |
Co kluczowe: subagenty. Claude Code ma natywne narzędzie Agent, Gemini używa threadId, OpenCode ma task tool, a Codex podagentów w ogóle nie wspiera. Biblioteka mapuje wszystkie te warianty na subagent_started → subagent_progress → subagent_completed z taskId, po którym grupujesz zdarzenia nawet przy współbieżnych podagentach.
Wymień agenta, a nie kod: praktyczny przykład
Załóżmy, że chcesz zbenchmarkować to samo zadanie na różnych silnikach. Z agent-adapters wygląda to tak:
import { createAdapter, extractText } from '@inharness-ai/agent-adapters';
import type { Architecture } from '@inharness-ai/agent-adapters';
const prompt = 'Zrefaktoruj ten moduł, aby wydzielić czystą logikę domenową.';
const systemPrompt = 'Jesteś doświadczonym inżynierem TypeScript.';
const architectures: Architecture[] = ['claude-code', 'codex', 'opencode', 'gemini'];
for (const arch of architectures) {
const adapter = createAdapter(arch);
const stream = adapter.execute({
prompt,
systemPrompt,
model: defaultModelFor(arch),
timeoutMs: 120_000,
});
console.log(`\n=== ${arch} ===`);
const text = await extractText(stream);
console.log(text);
}To samo zadanie, cztery silniki, jedna pętla. Do tego helpery extractText, collectEvents, filterByType, takeUntilResult, splitBySubagent — wszystkie zbudowane nad UnifiedEvent.
Model Context Protocol (MCP) — wsparcie dla wszystkich czterech transportów
Jeśli budujesz coś realnego z agentami, prędzej czy później trafisz na MCP (Model Context Protocol). To otwarty standard zapoczątkowany przez Anthropic, który pozwala agentowi rozmawiać z zewnętrznymi narzędziami po zdefiniowanym protokole. Biblioteka wspiera wszystkie cztery transporty MCP zgodnie ze specyfikacją:
| Typ | Konfiguracja | Kto wspiera |
|---|---|---|
| Stdio | { command, args, env } — zewnętrzny proces | claude-code, opencode, gemini |
| SSE | { type: 'sse', url, headers } | claude-code, gemini |
| HTTP | { type: 'http', url, headers } | claude-code, gemini |
| In-process (SDK) | { type: 'sdk', name, instance } | claude-code |
Ten ostatni — in-process MCP — jest moim zdaniem najciekawszy. Pozwala zdefiniować narzędzia MCP wewnątrz twojej aplikacji, bez spawnowania subprocesu, z bezpośrednim dostępem do stanu aplikacji:
import { z } from 'zod';
import { createAdapter, createMcpServer, mcpTool } from '@inharness-ai/agent-adapters';
const tools = [
mcpTool('get_user', 'Wyszukaj użytkownika po ID', { userId: z.string() }, async (args) => {
const user = await db.users.find(args.userId);
return { content: [{ type: 'text', text: JSON.stringify(user) }] };
}),
mcpTool('list_orders', 'Wyświetl ostatnie zamówienia', { limit: z.number().default(10) }, async (args) => {
const orders = await db.orders.recent(args.limit);
return { content: [{ type: 'text', text: JSON.stringify(orders) }] };
}),
];
const { config } = createMcpServer({ name: 'my-app', tools });
const adapter = createAdapter('claude-code');
for await (const event of adapter.execute({
prompt: 'Wyszukaj użytkownika U123 i pokaż jego zamówienia.',
systemPrompt: 'Użyj dostępnych narzędzi.',
model: 'sonnet-4.6',
mcpServers: { 'my-app': config },
})) {
// handle events...
}Agent dostaje dostęp do twojej bazy danych, bez IPC, bez narzutu sieciowego. Dla aplikacji produkcyjnych z dziesiątkami wywołań agentowych na minutę to znacząca różnica.
Transporty można mieszać w jednym wywołaniu — in-process dla narzędzi aplikacyjnych, stdio dla lokalnego filesystem servera, SSE dla zdalnego serwisu:
mcpServers: {
app: appTools, // in-process
filesystem: { command: 'npx', args: ['...'] }, // stdio
remote: { type: 'sse', url: 'https://mcp.example.com' }, // SSE
}Providery: MiniMax, Ollama, OpenRouter — lokalnie i taniej
Nie każdy chce (albo może) płacić za API Anthropic czy OpenAI. Biblioteka obsługuje alternatywne backendy przez system providerów:
| Provider | Wspierane adaptery | Backend |
|---|---|---|
minimax | claude-code, opencode, codex | MiniMax API — kompatybilny z Anthropic/OpenAI |
ollama | claude-code | Lokalne modele przez Ollama |
openrouter | opencode | OpenRouter — bramka do dziesiątek modeli |
Claude Code lokalnie na Ollama
import { createAdapter } from '@inharness-ai/agent-adapters';
const adapter = createAdapter('claude-code-ollama');
for await (const event of adapter.execute({
prompt: 'Napisz test jednostkowy dla funkcji calculateTax.',
systemPrompt: 'Użyj Vitest.',
model: 'qwen-coder-32b',
})) {
if (event.type === 'text_delta') process.stdout.write(event.text);
}To nie żart — masz ten sam interfejs Claude Code, ale pod spodem lokalny Qwen Coder 32B przez Ollama. Zero kosztów, pełna prywatność danych, działa offline.
MiniMax jako tańsza alternatywa
const adapter = createAdapter('claude-code', {
provider: 'minimax',
apiKey: 'sk-...',
region: 'global',
});Ten sam kod aplikacji, inny silnik za kulisami.
Własny provider
Możesz zarejestrować własny provider pod dowolny backend API-kompatybilny:
import { registerProvider } from '@inharness-ai/agent-adapters';
registerProvider({
name: 'moj-provider',
architectures: ['claude-code', 'opencode'],
resolve(architecture, config) {
// logika mapowania config → parametry SDK
},
});Model aliases — autocomplete i koniec z literówkami
Nazwy modeli w SDK to horror — claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001. Nikt tego nie pamięta. Biblioteka ma mapę aliasów:
| Architektura | Alias | Pełne ID |
|---|---|---|
claude-code | sonnet-4.6 | claude-sonnet-4-6 |
opus-4.7 | claude-opus-4-7 | |
haiku-4.5 | claude-haiku-4-5-20251001 | |
codex | o3 | o3 |
o4-mini | o4-mini | |
gemini | gemini-2.5-pro | gemini-2.5-pro |
TypeScript daje ci compile-time autocomplete, jeśli doprecyzujesz generic architektury. Nieistniejący alias? Dostaniesz AdapterError z listą dostępnych opcji. Koniec z sytuacją, w której ktoś puszcza do produkcji claude-sonet-4-6 (literówka) i dowiaduje się o tym z błędu 404.
Obserwatorzy i utilsy streamingowe
Czasem nie chcesz konsumować strumienia — chcesz go tylko podsłuchać, np. żeby zalogować do telemetrii albo podbić metryki. Do tego jest observeStream:
import { createAdapter, observeStream } from '@inharness-ai/agent-adapters';
import type { StreamObserver } from '@inharness-ai/agent-adapters';
const logger: StreamObserver = {
onTextDelta(text) { process.stdout.write(text); },
onToolUse(name, id) { console.log(`\nTool: ${name}`); },
onResult(output, msgs, usage) { console.log(`\nTokens: ${usage.inputTokens}+${usage.outputTokens}`); },
};
const adapter = createAdapter('claude-code');
const stream = adapter.execute(params);
for await (const event of observeStream(stream, [logger])) {
// zdarzenia idą do obserwatora i nadal są dostępne w pętli
}Do tego zestaw helperów — collectEvents, filterByType, takeUntilResult, splitBySubagent, extractText — dzięki którym typowe zadania streamingowe to jedna linia.
Obsługa błędów — typowana hierarchia
Wszystkie adaptery emitują zdarzenia error z typowanym wyjątkiem:
import {
AdapterError, // bazowa klasa
AdapterInitError, // inicjalizacja SDK zawiodła (brak klucza, SDK niezainstalowany)
AdapterTimeoutError, // przekroczono timeoutMs
AdapterAbortError, // wywołano adapter.abort() ręcznie
} from '@inharness-ai/agent-adapters';
for await (const event of adapter.execute(params)) {
if (event.type === 'error') {
if (event.error instanceof AdapterTimeoutError) {
// retry z większym timeoutem
} else if (event.error instanceof AdapterAbortError) {
// user cancel
}
}
}Tree-shaking —tylko to, czego używasz
Jeśli używasz tylko Claude Code, nie musisz instalować @openai/codex-sdk ani @google/gemini-cli-core. Wszystkie SDK to opcjonalne peer dependencies. Importujesz bezpośrednio z subpath:
import { ClaudeCodeAdapter } from '@inharness-ai/agent-adapters/claude-code';
import { CodexAdapter } from '@inharness-ai/agent-adapters/codex';
import { OpencodeAdapter } from '@inharness-ai/agent-adapters/opencode';
import { GeminiAdapter } from '@inharness-ai/agent-adapters/gemini';Bundler bierze tylko to, co rzeczywiście wywołujesz.
Dla kogo to jest?
Trzy profile, dla których biblioteka ma sens:
- Zespoły budujące orkiestratory agentów — gdzie jedna aplikacja musi uruchomić zadanie na różnych silnikach AI, a koszt utrzymywania czterech równoległych integracji zaczyna boleć.
- Narzędzia developerskie z wyborem agenta — IDE, CLI, GUI do kodowania z AI. Użytkownik wybiera w ustawieniach „Claude” albo „Gemini”, a ty nie chcesz trzymać czterech kopii tego samego kodu obsługi streamu.
- Platformy benchmarkujące i ewaluacyjne — gdy musisz odpalić dokładnie to samo zadanie na czterech agentach i porównać wyniki. Bez biblioteki jesteś skazany na glue code per silnik.
Jeśli w twoim zespole powtarzają się rozmowy typu „a czy na Codexie to działa tak samo?” — biblioteka jest dla ciebie.
Czego jeszcze nie ma i dokąd zmierzam
Pierwsza wersja publiczna nie pokrywa wszystkiego. Świadomie wyciąłem rzeczy, które zrobiłyby API niestabilnym:
- Google GenAI i Google ADK — adaptery zaprojektowane, nieimplementowane. Zostają na wersję 0.3.
- Cursor, Windsurf, Aider — nie mają dojrzałych SDK TypeScript, więc jeszcze nie wchodzą.
- Pełny subagent support w Codexie — Codex SDK nie eksponuje podagentów, więc mapowanie jest „best effort”.
Roadmapa na najbliższe miesiące:
google-genaiadapter (Gemini Interactions API — stabilne, z wbudowanym MCP, Deep Research Agent).google-adkadapter (multi-agent orchestration w TypeScripcie).- Pełniejsze wsparcie wznawiania sesji w OpenCode.
- Więcej providerów (Azure OpenAI, AWS Bedrock).
Jeśli brakuje ci czegoś konkretnego — issue na GitHubie to najszybsza droga.
Jak zacząć
npm install @inharness-ai/agent-adapters
# Zainstaluj tylko te SDK, których używasz (peer deps):
npm install @anthropic-ai/claude-agent-sdk
npm install @openai/codex-sdk
npm install @opencode-ai/sdk
npm install @google/gemini-cli-core
# Dla in-process MCP servers:
npm install @modelcontextprotocol/sdk zodMinimalne wywołanie:
import { createAdapter } from '@inharness-ai/agent-adapters';
const adapter = createAdapter('claude-code');
for await (const event of adapter.execute({
prompt: 'Hello, world.',
systemPrompt: 'Be concise.',
model: 'sonnet-4.6',
})) {
if (event.type === 'text_delta') process.stdout.write(event.text);
}Pełna dokumentacja i przykłady (mcp-in-process.ts, swap-adapter.ts, observer-pattern.ts, session-resumption.ts) są w repozytorium na GitHubie.
Dlaczego to wydałem publicznie
Szczerze mówiąc — napisałem to, bo sam potrzebowałem. Pracuję nad kilkoma projektami, w których to samo zadanie musi iść przez różne silniki AI, i frustracja związana z ich różnicami API doszła do punktu krytycznego. W pewnym momencie miałem cztery praktycznie identyczne moduły obsługi streamingu, różniące się tylko typem zdarzeń.
Zanim napisałem własne rozwiązanie, sprawdziłem, co jest na rynku:
- coder/agentapi — owija procesy CLI, Go, bez typów TypeScript.
- AG-UI — to protokół wire-level, nie biblioteka.
- Vercel AI SDK — pokrywa API LLM-ów (Anthropic Messages API, OpenAI Chat), nie agentowe SDK.
Nic, co rozwiązywałoby problem w TypeScripcie. Stąd ta biblioteka.
Wydaję to na MIT z nadzieją, że przyda się innym zespołom, które mają ten sam ból. Feedback, issues, PR-y — wszystko mile widziane.
Linki:
FAQ
Czym różni się @inharness-ai/agent-adapters od Vercel AI SDK?
Vercel AI SDK pokrywa API LLM-ów (Anthropic Messages API, OpenAI Chat Completions, Google GenAI). agent-adapters pokrywa agentowe SDK (Claude Code, Codex, OpenCode, Gemini CLI) — czyli warstwę wyżej, gdzie agent sam wywołuje narzędzia, ma dostęp do systemu plików, terminala, MCP. To narzędzia komplementarne, nie konkurencyjne.
Czy mogę użyć własnego agenta (np. LangChain)?
Tak — biblioteka ma API rejestracji własnych adapterów. Implementujesz interfejs RuntimeAdapter (jeden generator emitujący UnifiedEvent), rejestrujesz przez registerAdapter('mojagent', () => new MojAdapter()) i używasz tak samo jak wbudowanych. Do walidacji kontraktu są gotowe testy (assertSimpleText, assertToolUse, assertThinking, assertMultiTurn) pod @inharness-ai/agent-adapters/testing.
Jak to się ma do Model Context Protocol (MCP)?
MCP to standard protokołu między agentem a narzędziami. agent-adapters wspiera pełen MCP dla wszystkich czterech transportów (stdio, SSE, HTTP, in-process SDK) — biblioteka mapuje twoją konfigurację mcpServers na natywne API każdego SDK. Ograniczenia per adapter: OpenCode tylko stdio, Codex wymaga prekonfiguracji przez CLI (codex mcp add).
Czy streaming działa tak samo na każdym adapterze?
Prawie. Claude Code, OpenCode i Gemini dają natywny streaming delt. Codex SDK delt nie emituje — dostajesz pełny tekst naraz. Biblioteka ujednolica to pod text_delta, ale przy Codexie dostaniesz jedno duże zdarzenie zamiast strumienia. Jeśli ci to przeszkadza, po prostu nie używaj Codexa w aplikacjach wymagających live-streamingu.
Ile to kosztuje?
Biblioteka jest darmowa i open source (MIT). Kosztują tylko tokeny u dostawców modeli (Anthropic, OpenAI, Google). Przez providera Ollama możesz uruchomić Claude Code lokalnie za darmo — własny sprzęt, lokalne modele, zero kosztów API.
Czy działa w przeglądarce / Cloudflare Workers / Deno?
Biblioteka celuje w Node.js ≥ 20. Niektóre SDK (Claude Code SDK, Gemini CLI) wymagają Node-owego runtime (filesystem, child_process). Do przeglądarki potrzebujesz proxy po stronie serwera — to zresztą typowy wzorzec dla aplikacji agentowych ze względów bezpieczeństwa (klucze API).
Czy wspiera wznawianie sesji?
Claude Code — tak (resumeSessionId → sessionId). Codex — tak (resumeThread). Gemini — przez threadId. OpenCode — jeszcze nie. Pełna tabela w README.
Jak dodać własny provider (np. Azure OpenAI, Bedrock)?
registerProvider({ name, architectures, resolve }). W resolve mapujesz konfigurację użytkownika na parametry SDK (zmienne środowiskowe, base URL, klucz). Przykład z README pokazuje integrację z OpenRouterem w 20 liniach.
Biblioteka dostępna na npm, kod na GitHubie. Pierwsza publiczna wersja to 0.2.0 — API może jeszcze ewoluować przed 1.0, ale staram się minimalizować breaking changes. Jeśli używasz biblioteki w produkcji i masz konkretny use case — napisz, będzie mi miło usłyszeć.









