1. Po co agentowi „myślenie produkcyjne”
Gdy piszesz zwykły backend, sama idea „wejścia na produkcję” automatycznie włącza tryb paranoi: autoryzacja, logowanie, obsługa błędów, limity, sekrety w .env, a nie w kodzie.
Z agentem trzeba włączyć ten sam tryb, tylko jeszcze bardziej rygorystyczny. Powód jest prosty: zwykły backend wykonuje dokładnie to, co napisałeś, a agent — to, co model sam postanowi zrobić w ramach nadanych mu narzędzi i instrukcji. Iluzja kontroli jest tu silniejsza niż w klasycznym kodzie: wydaje się, że prompt wszystko opisuje, ale w rzeczywistości kontrolujesz tylko środowisko i dostępne działania, a nie wszystkie „myśli” modelu.
Dlatego w tym wykładzie będziemy stopniowo otaczać naszego agenta „warstwami ochrony”:
- najpierw ograniczymy, co dokładnie może robić (uprawnienia narzędzi i rozdzielenie agentów),
- potem odizolujemy środowisko wykonania (sandbox i limity),
- uporządkujemy kwestię sekretów i PII,
- i wreszcie włączymy obserwowalność: logi, metryki i podstawowy tracing.
Dla konkretu będziemy kontynuować historię z naszym GiftGenius: agentem, który pomaga dobrać prezent i lekko zahacza o świat commerce (przez zamówienie i checkout, ale bez szczegółów ACP — to będzie później).
2. Uprawnienia: agent nie potrzebuje „wszystkich przycisków świata”
Zasada najmniejszych uprawnień (Least Privilege)
Pierwsza zasada: agent nie musi umieć wszystkiego. Im więcej ma narzędzi, tym większa szansa, że wywoła „nie tę” funkcję w „nie tym” momencie. Zamiast jednego monstrualnego manageEverything(), który czyta i pisze cokolwiek, projektujemy małe, precyzyjne funkcje, rozdzielone co najmniej na odczyt i zapis.
Dla GiftGenius jest to szczególnie jasne: co innego — czytać listę prezentów i preferencje użytkownika, a co innego — tworzyć lub potwierdzać zamówienie (tam już są pieniądze). Dlatego zazwyczaj robimy:
- zestaw bezpiecznych narzędzi „read‑only” (wyszukiwanie prezentów, podgląd szczegółów),
- oddzielne narzędzia „write” (tworzenie szkicu zamówienia, anulowanie zamówienia),
- oraz, jeśli trzeba, jeszcze jeden poziom dla szczególnie niebezpiecznych operacji (potwierdzenie płatności, zmiany masowe).
Różni agenci do różnych zadań
Jeszcze jeden mocny zabieg — rozdzielać agentów według stref odpowiedzialności. Jeden agent — „dobór prezentów”, drugi — „zarządzanie zamówieniami”. Wtedy nawet jeśli model w agencie gift trochę się „wymknie spod kontroli”, fizycznie nie będzie mógł wywołać narzędzia płatniczego, bo po prostu nie ma go w jego konfiguracji.
Wyobraźmy sobie minimalistyczny typ konfiguracji agenta i narzędzi:
// Uproszczone typy do wyjaśnienia idei
type ToolName = 'suggest_gifts' | 'get_gift_details' |
'create_order_draft' | 'confirm_order';
type AgentConfig = {
id: string;
allowedTools: ToolName[];
maxSteps: number;
};
Teraz opiszmy dwóch agentów GiftGenius:
export const giftPlannerAgent: AgentConfig = {
id: 'gift-planner',
allowedTools: ['suggest_gifts', 'get_gift_details'],
maxSteps: 6,
};
export const orderAgent: AgentConfig = {
id: 'order-manager',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 4,
};
Tak, to na razie abstrakcja, ale sedno jest proste: nawet jeśli w kodzie są wszystkie cztery narzędzia, konkretny agent dostaje tylko potrzebny podzbiór.
Powiązanie uprawnień z użytkownikiem i rolami
Ważne pamiętać, że mamy dwie różne jednostki:
- użytkownik i jego uprawnienia (czy ten user_id może w ogóle coś kupować, anulować, widzieć historię),
- agent i jego dozwolone narzędzia.
W idealnym przypadku każde wywołanie narzędzia powinno przechodzić dwa sprawdzenia: „czy agent ma do tego prawo?” oraz „czy użytkownik też ma do tego prawo?”.
Umownie:
type UserRole = 'guest' | 'customer' | 'admin';
function canUserCallTool(role: UserRole, tool: ToolName): boolean {
if (tool === 'confirm_order') {
return role === 'customer' || role === 'admin';
}
if (tool === 'create_order_draft') {
return role !== 'guest';
}
return true; // odczyt dozwolony dla wszystkich
}
Po stronie MCP/backendu przy obsłudze wywołania narzędzia możemy robić podwójne sprawdzenie:
function assertToolAllowed(
agent: AgentConfig,
userRole: UserRole,
tool: ToolName,
) {
if (!agent.allowedTools.includes(tool)) {
throw new Error(`Narzędzie ${tool} jest zabronione dla agenta ${agent.id}`);
}
if (!canUserCallTool(userRole, tool)) {
throw new Error(`Użytkownik z rolą ${userRole} nie może wywołać ${tool}`);
}
}
W efekcie nawet jeśli model nagle zdecyduje się wywołać confirm_order z niewłaściwego agenta albo w imieniu gościa — wywołanie rozbije się o to sprawdzenie i zamieni się w kontrolowany błąd, a nie w nieplanowaną płatność.
Różne konfiguracje w zależności od środowiska
W środowiskach dev i staging często chcesz dać agentowi więcej swobody: testowe narzędzia, fałszywe serwisy płatnicze, funkcje eksperymentalne. W produkcji odwrotnie — konfiguracja jest maksymalnie rygorystyczna: część narzędzi wyłączona, endpoints tylko produkcyjne, tokeny wyłącznie prawdziwe.
Najprostszy schemat:
type Env = 'dev' | 'staging' | 'production';
const env = (process.env.APP_ENV as Env) ?? 'dev';
const orderAgentByEnv: Record<Env, AgentConfig> = {
dev: {
id: 'order-manager-dev',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 8,
},
staging: {
id: 'order-manager-staging',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 6,
},
production: {
id: 'order-manager-prod',
allowedTools: ['create_order_draft'], // confirm tylko przez osobną ścieżkę
maxSteps: 4,
},
};
export const currentOrderAgent = orderAgentByEnv[env];
Na produkcji confirm_order może być w ogóle wyniesiony do osobnego „niebezpiecznego” agenta, którego wywołujesz dopiero po jawnym kliknięciu „Potwierdź zamówienie” w widżecie i dodatkowych weryfikacjach.
3. Sandbox: agent nie potrzebuje dostępu root do waszego świata
Poziomy izolacji
Po tym, jak nadaliśmy uprawnienia agentom i użytkownikom, przechodzimy do następnego poziomu ochrony — sandbox i izolacji środowiska wykonania.
Sandbox dla agenta i jego narzędzi można umownie podzielić na kilka poziomów:
- Poziom kodu narzędzi. Ograniczamy dostęp do systemu plików, sieci i zasobów procesu: nie pozwalamy pisać gdzie popadnie, chodzić do dowolnych domen, bez końca mielić CPU ani zjadać gigabajtów pamięci.
- Poziom Agents SDK. Ustalamy limity na kroki cyklu run, liczbę wywołań narzędzi i rozmiar kontekstu (limit tokenów). Model nie może w nieskończoność „myśleć” i produkować tool‑calls — w pewnym momencie run zakończy się błędem „limit kroków” lub „limit czasu”.
To wszystko składa się na klasyczną „architekturę obronną”, którą wygodnie przedstawić schematem.
graph TD
A[Prompt / instrukcje systemowe] --> B[JSON Schema narzędzi]
B --> C[Uprawnienia agenta i użytkownika]
C --> D[Sandbox infrastruktury]
D --> E[Zewnętrzne serwisy / baza danych]
subgraph Agent
A
B
C
end
subgraph Infrastruktura
D
end
Prompt to najsłabsza ochrona; prawdziwa siła zaczyna się tam, gdzie fizycznie ograniczasz, co może zrobić twój kod i jakie API są dostępne.
Limity cyklu run: kroki, czas, wywołania narzędzi
Część sandboxa można wyrazić bezpośrednio w konfiguracji agenta: maksymalna liczba kroków, łączny czas wykonania, limit wywołań narzędzi. To nie tylko ochrona przed runaway‑pętlami, ale też kontrola kosztów.
Przykład abstrakcyjnej konfiguracji opcji run:
type RunLimits = {
maxSteps: number;
maxToolCalls: number;
timeoutMs: number;
};
const defaultLimits: RunLimits = {
maxSteps: 8,
maxToolCalls: 10,
timeoutMs: 30_000,
};
Takie limity przekazujesz potem do „obudowy”, która uruchamia agenta. Jeśli model postanowi wywołać narzędzie po raz 11 — przerywasz uruchomienie agenta i szczerze mówisz użytkownikowi, że zadanie jest zbyt złożone, zamiast pozwalać agentowi bez kontroli przepalać budżet.
Izolacja kodu i sieci
Na poziomie kontenera/procesu typowe praktyki są następujące:
Kod serwera MCP i/lub serwisu agenta uruchamiany jest w kontenerze z systemem plików tylko do odczytu (poza specjalnie wydzielonym katalogiem roboczym) i ograniczonymi zasobami (CPU, RAM). Sieć jest skonfigurowana w trybie allow‑list: można łączyć się tylko z potrzebnymi zewnętrznymi serwisami (twój backend commerce, płatności, kilka zewnętrznych API), a nie z dowolnym internetem.
Dla scenariuszy agentowych to szczególnie krytyczne: model może próbować wyjść do jakiegoś „lewego” API albo odczytać nieoczekiwane pliki, i dobrze, jeśli nawet przy takich próbach fizycznie nie będzie mieć praw, by dostać się do zbędnych zasobów.
W kodzie zwykle nie wygląda to jak „magiczna linijka TypeScriptu”, tylko jak ustawienia twojego orkiestratora (Docker Compose, Kubernetes, Vercel, Fly.io itd.). Ale docelowo warto myśleć o tym już na etapie projektowania:
- narzędzie, które uruchamia obcy kod (np. generowanie raportu z komendami shell), powinno działać w osobnym, mocno izolowanym środowisku;
- narzędzia nie powinny mieć możliwości czytania cudzych plików, sekretów, konfiguracji;
- dostęp sieciowy lepiej jawnie ograniczać po domenach lub IP.
4. Sekrety i dane poufne: czego agent nie musi wiedzieć
Gdzie powinny żyć sekrety, a gdzie nie
Podstawowa zasada: żadne sekrety — klucze API, hasła, tokeny dostępu — nie powinny trafiać do promptu modelu, do widżetu, do logów ani do repozytorium. One żyją:
- w zmiennych środowiskowych (process.env.SOMETHING),
- w menedżerze sekretów (AWS Secrets Manager, GCP Secret Manager, Vault itp.),
- w osobnych zaszyfrowanych magazynach, do których dostęp jest ściśle kontrolowany.
W naszym GiftGenius, na przykład, jest klucz do commerce‑API sklepu. Potrzebujemy, aby agent mógł tworzyć szkic zamówienia przez narzędzie MCP, ale sam klucz nie powinien być widoczny dla modelu.
// mcp/tools/createOrderDraft.ts
const COMMERCE_API_KEY = process.env.COMMERCE_API_KEY!;
export async function createOrderDraft(args: {
userId: string;
giftId: string;
quantity: number;
}) {
// Model nigdy nie zobaczy COMMERCE_API_KEY — jest tylko tutaj, po stronie serwera
const res = await fetch(`${process.env.COMMERCE_API_URL}/orders/draft`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${COMMERCE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
if (!res.ok) {
throw new Error(`Commerce API returned ${res.status}`);
}
return res.json(); // W odpowiedzi agentowi zwrócimy już bezpieczny obiekt
}
Ważne: w odpowiedzi narzędzia nie powinieneś „przemycać” kluczy ani innych wrażliwych szczegółów. Agentowi wystarczy znać draftOrderId, listę pozycji i, ewentualnie, status.
PII i minimalizacja danych w kontekście
Oprócz sekretów jest jeszcze kategoria PII (dane osobowe użytkowników): imiona, telefony, adresy dostawy, e‑maile itp. Agentowi często nie jest potrzebny cały ten „surowy” tekst. Wystarczy ustrukturyzowany profil: „lubi gry planszowe”, „wiek 30–35”, „orientacyjny budżet 50–70$”.
Zamiast wrzucać do promptu pełną historię zamówień użytkownika, można zrobić narzędzie get_user_profile_summary, które zwróci już zredukowany i zanonimizowany profil.
type ProfileSummary = {
ageRange: '18-25' | '26-35' | '36-50' | '50+';
interests: string[];
preferredBudget: { min: number; max: number };
};
export async function getUserProfileSummary(userId: string): Promise<ProfileSummary> {
// Tutaj sięgasz do bazy danych, ale na zewnątrz oddajesz tylko zagregowane informacje
return {
ageRange: '26-35',
interests: ['gry planszowe', 'gadżety'],
preferredBudget: { min: 30, max: 80 },
};
}
Model widzi dokładnie tyle, ile potrzeba do doboru prezentu — i ani trochę więcej.
Scrubbing logów
Logi to naturalne miejsce, gdzie przypadkiem wypływają sekrety i PII. Zwłaszcza gdy napisać „wygodny” logger w stylu console.log(...) i drukować „wszystko jak leci”.
Dobrym podejściem jest posiadanie centralnego loggera, który przed wydrukiem przechodzi po ładunku i maskuje pola wrażliwe.
type LogPayload = Record<string, unknown>;
const SENSITIVE_KEYS = ['email', 'phone', 'cardNumber', 'token'];
function scrub(payload: LogPayload): LogPayload {
const result: LogPayload = {};
for (const [key, value] of Object.entries(payload)) {
if (SENSITIVE_KEYS.includes(key)) {
result[key] = '***redacted***';
} else {
result[key] = value;
}
}
return result;
}
export function logEvent(event: string, payload: LogPayload) {
const safe = scrub(payload);
console.log(JSON.stringify({ event, ...safe }));
}
Zamiast kiedyś złapać incydent produkcyjny „logujemy telefony i tokeny klientów od pół roku”, budujesz system tak, że to po prostu niemożliwe. To nie tylko kwestia staranności, ale i przyszłych wymogów compliance (GDPR i lokalne przepisy): im mniej PII w logach, tym łatwiej żyje się produktowi.
5. Monitoring i obserwowalność agenta
Co dokładnie trzeba widzieć
Ograniczyliśmy, co agent może robić, jakie dane widzi i co trafia do logów. Kolejne pytanie — jak zrozumieć, czy w całym tym „zoo” agent na produkcji zachowuje się tak, jak zamierzaliśmy?
Zwykły monitoring „serwis żyje / nie żyje” dla agenta jest prawie bezużyteczny. Ważne jest nie tylko wiedzieć, że proces działa, ale rozumieć jego zachowanie: jakie kroki wykonuje, jakie narzędzia wywołuje, gdzie popełnia błędy, gdzie się zapętla.
Minimalny zestaw danych dla każdego run:
- agent_run_id — unikalny identyfikator uruchomienia;
- anonimowy user_id lub ID sesji;
- nazwa agenta i środowisko;
- lista wywołanych narzędzi: nazwa, liczba, łączny czas;
- kroki workflow i na którym kroku się zatrzymaliśmy;
- status końcowy: success, partial_success, failed, canceled, timeout, limits_exceeded.
Można to ująć jako strukturę:
type RunStatus =
| 'success'
| 'partial_success'
| 'failed'
| 'canceled'
| 'timeout'
| 'limits_exceeded';
type ToolCallLog = {
name: ToolName;
durationMs: number;
success: boolean;
};
type AgentRunLog = {
runId: string;
agentId: string;
userId: string;
env: Env;
startedAt: string;
finishedAt: string;
status: RunStatus;
toolCalls: ToolCallLog[];
errorMessage?: string;
};
Przykład „wrappera” wokół uruchomienia agenta
Załóżmy, że masz funkcję runAgent, która enkapsuluje realne wywołanie Agents SDK. Owińmy ją monitoringiem:
async function runAgentWithLogging(
agent: AgentConfig,
input: string,
userId: string,
): Promise<string> {
const runId = crypto.randomUUID();
const startedAt = new Date();
const toolCalls: ToolCallLog[] = [];
try {
const result = await runAgent(agent, input, {
userId,
limits: defaultLimits,
onToolCall: (name, durationMs, success) => {
toolCalls.push({ name, durationMs, success });
},
});
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'success',
toolCalls,
};
logEvent('agent_run', log);
return result;
} catch (err) {
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'failed',
toolCalls,
errorMessage: (err as Error).message,
};
logEvent('agent_run', log);
throw err;
}
}
Tutaj runAgent to czarna skrzynka, która może być zaimplementowana przez prawdziwe Agents SDK; my pokazujemy, jak dodać obserwowalność, nie wiążąc się z konkretnym API.
Logi vs metryki vs tracing
Wygodnie rozróżniać trzy poziomy obserwowalności:
| Poziom | Co to jest | Przykład dla agenta GiftGenius |
|---|---|---|
| Logi | „Historie” o konkretnych uruchomieniach (runs) | Szczegółowy AgentRunLog z krokami i narzędziami |
| Metryki | Zagregowane wskaźniki liczbowe | p95 czas trwania run, średnia liczba tool‑calls, error‑rate |
| Tracing | Drzewo/graf żądań i podżądań | Run → kroki → tool‑calls → wywołania zewnętrznych API (commerce, baza danych itp.) |
Metryki są potrzebne, by odpowiedzieć na pytanie „czy ogólnie jest dobrze?” (np. error‑rate z ostatniej godziny). Logi i tracing — by zrozumieć „dlaczego źle akurat tutaj?” i odtworzyć konkretne problematyczne uruchomienie.
Przykład zalążka metryk można zrealizować ponad logami: zadanie cykliczne agreguje zdarzenia agent_run i liczy p95 czasu trwania, liczbę błędów itp.
6. Jak to wygląda w GiftGenius w całości
Żeby to nie brzmiało jak zestaw abstrakcji, złóżmy obrazek dla naszej aplikacji szkoleniowej.
Agent gift-planner w środowisku produkcyjnym ma tylko bezpieczne narzędzia: dobór prezentów i pobieranie szczegółów. Nie widzi ani płatności, ani zarządzania zamówieniami. Jego instrukcje systemowe mówią, że nie powinien obiecywać użytkownikowi „zapłacę za ciebie”, a co najwyżej — przygotować rekomendacje i, ewentualnie, szkic listy prezentów.
Agent order-manager istnieje osobno i umie tylko pracować z zamówieniami. Na produkcji może tworzyć jedynie szkic zamówienia (create_order_draft), a potwierdzenie zamówienia (confirm_order) albo wykonuje człowiek poprzez jawny wyzwalacz UI w widżecie, albo jest dostępne tylko w dev/staging. Jego narzędzia używają sekretów (klucze API sklepu) wyłącznie po stronie backendu, a w odpowiedzi przekazują tylko potrzebne pola.
Obaj agenci są uruchamiani przez obudowę runAgentWithLogging, która nakłada limity i zapisuje logi z agent_run_id, userId, środowiskiem i listą narzędzi. W logach nie ma e‑maili ani telefonów; te pola są wcześniej czyszczone scrubberem. Profil użytkownika jest używany w formie zanonimizowanej: zakres wiekowy, zainteresowania, budżet, ale nie pełny tekst historii zakupów.
Infrastruktura, w której żyje serwer MCP i serwis agenta, jest odizolowana: kontenery z systemem plików tylko do odczytu (poza /tmp lub specjalnie wydzielonym katalogiem), ograniczeniem CPU/RAM, siecią po allow‑list domen. Jeśli agent spróbuje wywołać „coś lewego”, po prostu fizycznie nie będzie mógł się do tego dostać.
Jeśli w pewnym momencie widzisz skok metryki „odsetek runów ze statusem limits_exceeded” albo „średnia liczba tool‑calls > 10”, rozumiesz, że albo prompt stał się zbyt gadatliwy, albo któreś narzędzie się psuje i zmusza agenta do powtarzania kroków.
To już zachowanie dorosłego serwisu, a nie eksperymentalnego agenta „byle działało”.
7. Typowe błędy przy wyprowadzaniu agentów na produkcję
Wszystko, o czym mówiliśmy wyżej, to „prawidłowy” obraz agenta produkcyjnego. W praktyce jednak najczęściej spotyka się typowe miny. Zbierzmy je w jedną listę: jeśli unikniesz choćby tych błędów, wejście na produkcję przebiegnie znacznie spokojniej.
Błąd nr 1: agentowi „pozwolono na wszystko”.
Częsty scenariusz: opisaliście mnóstwo narzędzi MCP (wyszukiwanie, modyfikacja, usuwanie, płatności), a przy tworzeniu agenta po prostu podaliście mu całą listę. W efekcie model może przypadkowo wywołać usunięcie albo płatność tam, gdzie chcieliście tylko odczyt. Leczy się to rozdzieleniem narzędzi według ról i stworzeniem kilku węższych agentów, z których każdy ma własne allowedTools.
Błąd nr 2: weryfikacja uprawnień tylko w promcie.
Czasem deweloperzy piszą w instrukcjach systemowych: „nigdy nic nie kupuj bez potwierdzenia użytkownika” i na tym poprzestają. Ale prompt to słaba ochrona, a jail‑breaki i zwykłe błędy nikt nie odwołał. Potrzebne są realne sprawdzenia na poziomie backendu: „agent ma dozwolone to narzędzie” i „użytkownik ma dozwolone to narzędzie”, inaczej jedna nieostrożna generacja może doprowadzić do działań, których nikt nie planował.
Błąd nr 3: sekrety w promptach i logach.
Czasem kusi, żeby „przyspieszyć integrację” i po prostu włożyć klucz API do system‑promptu lub przekazać go w argumentach narzędzia, by agent sam chodził do zewnętrznego API. W rezultacie klucz trafia i do logów modelu, i potencjalnie do systemów zewnętrznych. To prosta droga do wycieków i bana w Store. Sekrety muszą żyć tylko po stronie serwera, w zmiennych środowiskowych lub menedżerze sekretów, i nigdy nie trafiać do kontekstu modelu.
Błąd nr 4: „surowe” logi bez scrubowania.
Podczas debugowania wygodnie pisać console.log(...) i o tym zapomnieć. Po paru miesiącach okazuje się, że w logach leżą adresy użytkowników, telefony, numery zamówień z PII. Szczególnie nieprzyjemne w świecie GDPR i innych regulacji. Lepiej od razu wprowadzić centralny logger i automatyczne maskowanie pól wrażliwych, nawet jeśli wydaje się, że „logujemy tylko na dev”.
Błąd nr 5: brak limitów na zachowanie agenta.
Bez ograniczeń na kroki, czas i liczbę wywołań narzędzi agent może się zapętlić: wielokrotnie wołać to samo narzędzie, próbować bez końca poprawiać ten sam błąd, zużywać mnóstwo tokenów i obciążać zewnętrzne API. W najlepszym razie dostaniesz gigantyczne rachunki za modele, w najgorszym — położysz backend i zezłościsz wszystkich użytkowników. Limity cyklu run i rozsądne domyślne wartości timeoutów są obowiązkową częścią konfiguracji.
Błąd nr 6: mieszanie operacji read i write w jednym narzędziu.
Czasem powstają „wygodne” metody w stylu getOrCreateOrder, które przy braku zamówienia tworzą nowe. Dla klasycznego backendu to dopuszczalny wzorzec, ale w świecie agentów może prowadzić do nieoczekiwanych skutków ubocznych: model chciał tylko poznać stan, a narzędzie coś utworzyło. Dużo bezpieczniej oddzielać get_order_details i create_order_draft, wtedy nawet przy powtórnych wywołaniach konsekwencje są bardziej kontrolowalne.
Błąd nr 7: ignorowanie obserwowalności.
Wielu zaczyna od „później dołożymy logi i metryki, teraz najważniejsze, żeby działało”. Agenci bez monitoringu to czarna skrzynka: nie wiesz, jakie narzędzia wywołują, ile kroków robią, gdzie popełniają błędy. Każda skarga użytkownika zamienia się w dochodzenie w ciemnym pokoju. Dużo łatwiej od razu założyć strukturę logów (agent_run_id, narzędzia, status) i podstawowe metryki, niż później próbować dobudować to ponad chaotycznym kodem.
GO TO FULL VERSION