Tool Çağrıları, Onay Akışı ve Generative UI: Asistanın Component Seçmesine İzin Vermek
Asistanın tool çağrılarını kullanıcıya göstermek, hassas işlemi süreli ve tek kullanımlık bir onay kartına bağlamak ve modelin metin yerine izinli bir component seçmesine izin vermek, Angular'da tiplenmiş SSE event'leri ve doğrulayan bir component registry ile yapılır. Bu yazıda küçük modelin kart göstermeden 'gösterdim' dediğini, bir hata metninin props olarak arayüze geldiğini ve istemci doğrulamasının bunu durdurduğunu gösteriyor, Hashbrown'ı ve postinstall telemetrisini inceliyoruz.
Sürüm notu: Bu yazıdaki kodlar Angular 22.2 (TypeScript 6.0, Node.js 24) ve .NET 10 ile derlenip denenmiştir; backend'de Microsoft.Extensions.AI 11.0.0 ve OllamaSharp 5.5.2, tarayıcı denemelerinde Playwright 1.65 ile headless Chromium 158, yerel modelde Ollama 0.34.4 (qwen3:4b) kullanılmıştır. Hashbrown 0.8.3 yalnız incelendi, kurulmadı.
Önce Tahmin Et
Bookstore asistanına (sitedeki serilerin ortak örnek uygulaması Bookstore, bir online kitapçı) iki istek geliyor. Birincisi "Recommend crime novels under 20 TL." Asistanın elinde kitapları "sepete ekle" butonlu kartlar olarak gösteren bir tool var ve system prompt "kitapları metinde listeleme, kartları göster" diyor. Küçük bir model bu talimata uyar mı? İkinci istek "Please cancel my order 1042." Sipariş iptali onay isteyen bir tool. Kullanıcı onay kartını iki dakika boyunca görmezden gelirse ne olur, ve kullanıcı reddederse model ne der?
Önceki yazılarda asistan yalnız konuşuyordu. Bu yazıda tool çağrılarını arayüze yansıtıyor, hassas işlemler için bir onay kartı ekliyor ve modelin metin yazmak yerine "hangi component, hangi veriyle" seçtiği generative UI'ı kuruyoruz. Sorulara gerçek modelle cevap vereceğiz; cevapların bir kısmı tasarımı değiştirmemi gerektirdi.
Tool Event'lerini Arayüze Yansıtmak
1. yazıdaki tool event'i her tool çağrısının başlangıcını ve bitişini callId ve adıyla bildiriyor. Arayüz için yapılacak iş, teknik adı kullanıcının anlayacağı bir cümleye çevirmek:
// Tool çalışırken kullanıcıya gösterilen durum satırları
const TOOL_LABELS: Record<string, string> = {
show_books: 'Looking through the catalog…',
cancel_order: 'Cancelling the order…',
};
@for (tool of message.tools; track tool.callId) {
@if (tool.phase === 'started') {
<p class="status">{{ toolLabel(tool.name) }}</p>
}
}
@for (block of message.blocks; track $index) {
<app-generative-block [block]="block" />
}
@if (message.approval; as approval) {
<app-approval-card [request]="approval" [disabled]="store.busy()" (decide)="store.respond(message.id, $event)" />
}
<app-message-body [text]="message.text" [final]="message.ending !== null" [sources]="message.sources" />
Bilinmeyen bir tool adı için "Working on it…" gösteriliyor; sunucuya yeni bir tool eklendiğinde arayüz bozulmuyor, yalnız daha genel bir cümle çıkıyor. Tool adını, argümanlarını ya da sonucunu ham hâliyle göstermek cazip gelebilir ama hem kullanıcıya bir şey söylemez hem de iç ayrıntıyı (tablo adı, iç ID'ler) ekrana taşır. Store tarafında 3. yazıdaki state machine'e bir değişiklik geldi: ilk token zamanlayıcısını artık yalnız thinking ya da delta değil, modelden gelen her hayat belirtisi (tool, approval, ui) durduruyor.
Onay Akışı: Sunucu Tarafı
Sipariş iptali geri alınamayan bir işlem; model bunu kendi başına yapmamalı. .NET ile Yapay Zeka serisinin tool calling yazısında ApprovalRequiredAIFunction ile kurduğumuz akışı, AI Agent'ları ve MCP (Model Context Protocol; modellerin dış araçlara ve verilere bağlanması için açık protokol) serisinin human-in-the-loop (bir adımı insanın onayına bağlamak) yazısındaki tasarım kararlarıyla birlikte burada streaming'e taşıyoruz. FunctionInvokingChatClient, onay isteyen bir tool çağrısını çalıştırmak yerine akışa bir ToolApprovalRequestContent koyup turu bitiriyor; ToSseAsync bunu bir approval event'ine çeviriyor:
case ToolApprovalRequestContent approval when approval.ToolCall is FunctionCallContent call:
yield return new SseItem<object>(new ApprovalEvent(approval.RequestId, call.Name, call.Arguments,
(int)PendingApprovals.Lifetime.TotalSeconds), "approval");
break;
Sunucu o ana kadarki mesajları (kullanıcının mesajı, modelin tool çağrısı) iki dakikalık bir süreyle saklıyor. Onay tek kullanımlık ve süreli:
// Onay bekleyen turlar; süresi dolan onay kabul edilmez. Demo için bellekte.
public sealed class PendingApprovals(TimeProvider time)
{
public static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(2);
private readonly ConcurrentDictionary<string, PendingApproval> _items = new();
public void Save(ToolApprovalRequestContent request, Guid conversationId, List<ChatMessage> history,
ChatMessage userMessage, List<ChatMessage> messages) =>
_items[request.RequestId] = new(request, conversationId, history, userMessage, messages, time.GetUtcNow() + Lifetime);
// Bir onay yalnız bir kez ve süresi içinde kullanılabilir
public PendingApproval? Take(string requestId) =>
_items.TryRemove(requestId, out PendingApproval? item) && item.ExpiresAt > time.GetUtcNow() ? item : null;
}
// Onay kartındaki karar: bekleyen turu onay cevabıyla birlikte modele geri verir ve akışı sürdürür
app.MapPost("/chat/approvals/{approvalId}", IResult (string approvalId, ApprovalDecision decision, IChatClient chat,
AssistantTools tools, PendingApprovals pending, ILogger<Program> logger, CancellationToken ct) =>
{
PendingApproval? item = pending.Take(approvalId);
if (item is null)
{
return TypedResults.Problem(title: "Approval not found", statusCode: 404,
detail: "This approval request has expired or was already answered.");
}
item.Messages.Add(new ChatMessage(ChatRole.User, [item.Request.CreateResponse(decision.Approved, decision.Reason)]));
IAsyncEnumerable<SseItem<object>> events =
StreamTurn(item.ConversationId, item.History, item.UserMessage, item.Messages, chat, tools, pending, logger, ct);
return TypedResults.ServerSentEvents(events.WithHeartbeatAsync(TimeSpan.FromSeconds(5), ct));
}).RequireRateLimiting("chat");
curl ile iki adım ve aynı onayın ikinci kez kullanılması:
$ POST /chat/sse {"message":"Please cancel my order 1042."}
243 ms event: thinking
4830 ms event: approval
4830 ms data: {"approvalId":"ficc_955a064b","tool":"cancel_order","arguments":{"orderId":1042},"expiresInSeconds":120}
4874 ms event: usage
4874 ms event: done
$ POST /chat/approvals/ficc_955a064b {"approved":true}
6 ms event: tool
6 ms data: {"callId":"955a064b","name":"cancel_order","phase":"started"}
8 ms event: tool
8 ms data: {"callId":"955a064b","name":"cancel_order","phase":"completed"}
4085 ms event: done
TEXT: 'Your order 1042 has been successfully cancelled.'
$ POST /chat/approvals/ficc_955a064b {"approved":true}
HTTP/1.1 404 Not Found
{"title":"Approval not found","status":404,"detail":"This approval request has expired or was already answered.",...}
# sunucu log'u
cancel_order(1042): cancelled
İlk akışta cancel_order için bir tool started event'i yok: FunctionInvokingChatClient onay isteyen çağrıyı akışta bir onay isteğiyle değiştiriyor. Tool, onay geldikten sonra ikinci akışın başında çalıştı ve 2 milisaniye sürdü. Onay kimliğini tahmin edip başkasının adına onay vermek ya da eski bir onayı tekrar kullanmak 404 ile bitiyor. Production'da Take ayrıca onayın, isteği başlatan kullanıcıdan geldiğini kontrol etmeli; .NET ile Yapay Zeka serisindeki PendingApprovals bunu yapıyordu.
Onay Kartı
Kart, isteği insan diliyle gösteriyor, iki buton ve bir geri sayım içeriyor:
// Hassas tool'lar için insan diliyle bir özet; bilinmeyen tool'un adı ve argümanları olduğu gibi gösterilir
function describe(tool: string, args: Record<string, unknown> | null): string {
if (tool === 'cancel_order') {
return `Cancel order ${args?.['orderId']}?`;
}
return `Run ${tool} with ${JSON.stringify(args)}?`;
}
@Component({
selector: 'app-approval-card',
template: `
<div class="card">
<p>{{ question() }}</p>
@if (request().state !== 'pending') {
<p class="muted">{{ request().state === 'approved' ? 'Approved.' : 'Rejected.' }}</p>
} @else if (secondsLeft() === 0) {
<p class="muted">This request expired. Ask the assistant again.</p>
} @else {
<button type="button" (click)="decide.emit(true)" [disabled]="disabled()">Approve</button>
<button type="button" (click)="decide.emit(false)" [disabled]="disabled()">Reject</button>
<span class="muted">{{ secondsLeft() }} s left</span>
}
</div>
`,
styles: `.card { padding: 8px 12px; border: 1px solid #f59e0b; border-radius: 8px; background: #fffbeb; }
.muted { color: #6b7280; font-size: 0.875rem; }`,
})
export class ApprovalCard {
readonly request = input.required<ApprovalRequest>();
readonly disabled = input(false);
readonly decide = output<boolean>();
protected readonly question = computed(() => describe(this.request().tool, this.request().arguments));
private readonly now = signal(Date.now());
protected readonly secondsLeft = computed(() =>
Math.max(0, Math.ceil((this.request().expiresAt - this.now()) / 1000)),
);
constructor() {
// Geri sayım için saniyede bir "şimdi"yi güncelle
const timer = setInterval(() => this.now.set(Date.now()), 1000);
inject(DestroyRef).onDestroy(() => clearInterval(timer));
}
}
Butonlar akış sürerken kapalı ([disabled]="store.busy()"): sunucu bekleyen turu done event'inden hemen önce kaydediyor, dolayısıyla kullanıcı done gelmeden onay veremiyor ve 404'e düşmüyor. Karar store'da aynı asistan mesajına devam ediyor; yeni bir kullanıcı mesajı oluşmuyor:
// Onay kartındaki karar: sunucu aynı turu sürdürür, cevap aynı asistan mesajına eklenir
async respond(messageId: number, approved: boolean): Promise<void> {
const approval = this.messages().find((m) => m.id === messageId)?.approval;
if (!approval || approval.state !== 'pending' || !this.canSend() || !this.to('submitted')) {
return;
}
this.patch(messageId, { approval: { ...approval, state: approved ? 'approved' : 'rejected' }, ending: null });
await this.consume(messageId, (signal) => this.api.approve(approval.approvalId, approved, signal));
}
Üç senaryoyu Playwright'ta gerçek modelle çalıştırdım:
$ node run05.mjs approve
card : Cancel order 1042? | Approve | Reject | 120 s left
7019 ms Sending… | Cancel order 1042? | Approved.
12836 ms Cancel order 1042? | Approved.
bubble: Cancel order 1042? | Approved. | Your order 1042 has been successfully cancelled.
$ node run05.mjs reject
6535 ms Sending… | Cancel order 1042? | Rejected.
22380 ms Cancel order 1042? | Rejected.
bubble: Cancel order 1042? | Rejected. | Order cancellation requires your approval. Please confirm to proceed.
$ node run05.mjs expire
card : Cancel order 1043? | Approve | Reject | 120 s left
later : Cancel order 1043? | This request expired. Ask the assistant again.
Onay ve süre dolumu beklendiği gibi. Reddetmenin sonucu ise not edilmeye değer: sipariş iptal edilmedi ama model "onayınız gerekiyor, lütfen onaylayın" diye cevap verdi, sanki reddi bir eksik onay sanmış gibi. İstemci ret sebebi göndermiyordu; CreateResponse(false, "The customer decided to keep the order.") gibi bir sebep modele ne olduğunu açıkça söyler. Bu da arayüzün işi: "Reject" yanına kısa bir sebep alanı ya da en azından sabit, anlamlı bir sebep.
Generative UI: Model Component Seçer
Kitap önerisi metin olarak geldiğinde kullanıcı kitap adını kopyalayıp aramak zorunda. Generative UI'da model metin yerine bir component seçer ve onu hangi veriyle göstereceğini söyler; arayüz bu seçimi kendi, güvenilir component'leriyle render eder. Model component'in kodunu ya da davranışını üretmez, yalnız izin verilen listeden seçer.
Seçimi modele bir "UI tool" olarak açtım. Tool'un sonucu modele normal bir tool sonucu olarak gidiyor, arayüze de bir ui event'i olarak:
// UI tool'ları: sonucu metin olarak değil, bir component olarak gösterilir
public static readonly IReadOnlyDictionary<string, string> UiComponents =
new Dictionary<string, string> { ["show_books"] = "book-list" };
[Description("Shows books from the catalog to the customer as cards with an 'Add to cart' button. " +
"Use this to recommend books instead of listing them in text.")]
public object ShowBooks(
[Description("Optional genre: science-fiction, fantasy, classic or crime.")] string? genre = null,
[Description("Optional maximum price in TRY.")] decimal? maxPriceTry = null,
[Description("Optional short heading for the cards.")] string heading = "Recommended books")
{
// Model yalnız filtre seçer; kitap, yazar ve fiyat her zaman katalogdan gelir
if (genre is not null && !Catalog.Books.Any(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase)))
{
return new { error = $"Unknown genre '{genre}'. Use science-fiction, fantasy, classic or crime." };
}
List<BookCard> books = [.. Catalog.Books
.Where(b => genre is null || b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.Where(b => maxPriceTry is null || b.PriceTry <= maxPriceTry)
.Take(6)
.Select(b => new BookCard(b.Isbn, b.Title, b.Author, b.PriceTry))];
logger.LogInformation("show_books(genre={Genre}, maxPriceTry={Max}) -> {Count} cards", genre, maxPriceTry, books.Count);
return new BookListProps(heading.Length > 80 ? heading[..80] : heading, books);
}
Bu tasarıma üçüncü denemede vardım. İlk tasarımda iki tool vardı: search_books kataloğu arıyor, show_books(heading, isbns) modelin seçtiği ISBN'leri (kitapları tanımlayan uluslararası standart numara) kart olarak gösteriyordu. qwen3:4b search_books'u çağırdı, show_books'u çağırmadı ve kullanıcıya şunu yazdı:
TEXT: 'Here are 3 crime novels under 20 TL available for viewing.'
# sunucu log'u
search_books(genre=crime, maxPriceTry=20) -> 3
Ekranda hiç kart yoktu ama model kartları göstermiş gibi konuştu. İki adımlı bir tool zinciri küçük bir model için fazla; tek bir UI tool'una indirdim. İkinci denemede model show_books'u çağırdı ama zorunlu heading parametresini vermedi; FunctionInvokingChatClient exception'ı yakaladı ve tool sonucunu bir hata metniyle değiştirdi. Benim ToSseAsync'im de bu sonucu kontrol etmeden arayüze yolladı:
11363 ms event: ui
11363 ms data: {"callId":"c3e342ae","component":"book-list","props":"Error: Function failed."}
# sunucu log'u
show_books invocation failed.
System.ArgumentException: The arguments dictionary is missing a value for the required parameter 'heading'.
İki düzeltme yaptım: heading'i varsayılan değerli bir parametre yaptım ve sunucu artık yalnız hatasız biten bir tool sonucunu ui event'ine çeviriyor (result.Exception is null). Bu, Sık Düşülen Tuzaklar'daki ilk maddenin canlı örneği: component'e giden veri, kimden gelirse gelsin doğrulanmalı. Üçüncü denemede model show_books(genre: "crime", maxPriceTry: 20)'yi çağırdı ve kartlar geldi. Tasarımın kendisi de bir güvenlik kararı: model kitap adını, yazarı ya da fiyatı yazmıyor, yalnız filtre seçiyor. Model "Dune'u 1 TL'ye göster" dese bile kartta katalogdaki fiyat görünür.
Component Registry ve Doğrulama
Sunucu güvenilir olsa da istemci ui event'ini olduğu gibi component'e vermiyor. Her component'in bir props şeması var ve listede olmayan component render edilmiyor:
// Sunucudan gelen ui event'ini doğrular; şemaya uymayan blok atılır
export function parseUiBlock(component: string, props: unknown): UiBlock | null {
if (component !== 'book-list' || typeof props !== 'object' || props === null) {
return null;
}
const p = props as Record<string, unknown>;
if (!isText(p['heading'], 120) || !Array.isArray(p['books']) || p['books'].length > 12) {
return null;
}
const books = p['books'].map(parseBook);
return books.every((b) => b !== null) ? { component: 'book-list', heading: p['heading'], books } : null;
}
// Component registry: modelin seçebileceği her component burada açıkça listelenir
@Component({
selector: 'app-generative-block',
imports: [BookList],
template: `
@switch (block().component) {
@case ('book-list') {
<app-book-list [heading]="block().heading" [books]="block().books" />
}
}
`,
})
export class GenerativeBlock {
readonly block = input.required<UiBlock>();
}
parseBook her kitap için 13 haneli bir ISBN, boş olmayan ve 200 karakteri geçmeyen bir başlık ve yazar, sıfır ya da pozitif bir fiyat istiyor. Registry'yi denemek için sahte bir akışla üç kötü blok gönderdim: ikinci denemedeki hata metni, listede olmayan bir admin-panel ve negatif fiyatlı bir kitap:
$ node run05.mjs bad
[console.warning] Rejected UI block "book-list" Error: Function failed.
[console.warning] Rejected UI block "admin-panel" {userId: 1}
[console.warning] Rejected UI block "book-list" {heading: Deals, books: Array(1)}
bubble: Done. | book lists: 0
Üçü de reddedildi, ekrana hiçbir kart çıkmadı ve metin normal gösterildi. @switch'in @default'u bilerek yok: tanınmayan bir component için "bilinmeyen içerik" kutusu göstermek bile modelin ekrana bir şey koymasına izin vermek demek.
Sepete Ekle Her Zaman Kullanıcıdan
Kartlardaki "Add to cart" butonu, Bookstore'un normal sepet service'ini çağırıyor. Modelin elinde sepete ekleyen bir tool yok; ekleme yalnız kullanıcının tıklamasıyla oluyor:
// Sepete yalnız kullanıcının tıklaması ekler; model hiçbir zaman doğrudan çağırmaz
add(book: BookCardData): void {
this.items.update((list) => [...list, book]);
}
Gerçek modelle:
$ node run05.mjs books
166 ms Sending…
389 ms Thinking…
12713 ms Thinking… | Looking through the catalog…
12722 ms Thinking… | Recommended books
45441 ms Recommended books
bubble: Recommended books | And Then There Were None | Agatha Christie | ₺16.90 | Add to cart | Murder on the Orient Express | ...
| The Hound of the Baskervilles | Arthur Conan Doyle | ₺12.90 | Add to cart
| Here are crime novels under 20 TRY: Agatha Christie’s And Then There Were None (16.90 TRY), Murder on the Orient Express ...
header before: Cart: 0
header after : Cart: 1
# sunucu log'u
show_books(genre=crime, maxPriceTry=20) -> 3 cards
Kartlar 12,7. saniyede geldi; model ikinci turda 33 saniye daha düşünüp bir cümle yazdı. Önce Tahmin Et'in ilk sorusunun cevabı "yarım": model kartları gösterdi ama "kitapları metinde listeleme" talimatına uymayıp aynı üç kitabı metinde de saydı. Arayüz açısından zararsız ama tekrarlı; daha büyük bir model ya da tool sonucunda "kartlar gösterildi, listeyi tekrar etme" diyen bir mesaj bunu azaltır. Fiyatlar currency: 'TRY' : 'symbol-narrow' ile ₺ işaretiyle gösteriliyor; ilk denemede symbol ile TRY16.90 çıkıyordu.
Hazır Kütüphane: Hashbrown ve Ne Zaman Hiç Kullanmamalı
Kendi registry'mizi yazmak öğretici ama tek alternatif değil. Hashbrown, Angular ve React için generative UI library'si; Angular paketi @hashbrownai/angular yazım günü 0.8.3 sürümünde, yani 1.0 öncesi, ve peer dependency olarak @angular/core ^22.0.0 istiyor. Paketin type tanımlarına baktım: exposeComponent ile bir component'i açıklaması ve props şemasıyla modele açıyorsun, uiChatResource ise mesajları, akışı ve modelin seçtiği component'leri signal'larla veren bir resource. Sağlayıcı çağrısı yine bir backend üzerinden yapılıyor; dokümanda ASP.NET Core için de bir başlangıç rehberi var. Bizim elle yazdığımız registry'nin karşılığı bu: izinli component listesi, şemalı props ve streaming sırasında kısmi JSON'un çözülmesi.
Paketi kurmadım ve bunun bir sebebi var. npm pack ile indirip içine baktığımda package.json'da bir postinstall script'i buldum. Script kurulumda ~/.hashbrown/installation-id dosyasını oluşturuyor, global ve yerel git config'inden user.name ve user.email değerlerini ve origin remote'undan repo sahibini okuyup bunları kurulum telemetrisi olarak hashbrown.dev'e göndermeye çalışıyor. DO_NOT_TRACK=1 ya da HASHBROWN_TELEMETRY_DISABLED=1 ortam değişkeni bunu kapatıyor. Bu kötü niyet anlamına gelmiyor, ama kurumsal bir projede bir bağımlılığın geliştiricinin e-posta adresini dışarı göndermesi onay gerektiren bir karar. Lifecycle script'lerini yönetmek Frontend Güvenliği serisinin npm supply chain yazısının konusu.
Generative UI'ı hiç kullanmamak da çoğu zaman doğru cevap. Kullanıcı ne istediğini biliyorsa ve istek bir forma sığıyorsa ("polisiye, 20 TL altı") bir filtre paneli daha hızlı, daha ucuz ve her seferinde aynı sonucu veriyor: bu yazıdaki kartlar 12 saniye ve iki model turu sürdü, bir filtre sorgusu ise milisaniyeler içinde döner. Generative UI'ın değeri, isteğin serbest metinde geldiği ve hangi component'in gerektiğinin önceden bilinmediği yerde.
Üretimde Dikkat
Onayı kullanıcıya bağla. Onay kimliği tahmin edilemez olsa da tek savunma bu olmamalı; onay endpoint'i oturumdaki kullanıcıyla isteği başlatan kullanıcıyı karşılaştırmalı.
Onay ekranında işlemin geri dönüşünü yaz. "Cancel order 1042?" kısa ama yeterli değil; para iadesi, kargo ve geri alınamazlık gibi bilgiler kartta olmalı. Bu metni model değil, tool'un sahibi olan kod yazmalı.
Onay süresini işlemin riskine göre seç. İki dakika bir sipariş iptali için makul; bir ödeme için daha kısa, bir rapor için daha uzun olabilir. Süre dolunca sunucu da isteği reddetmeli; yalnız arayüzde gizlemek yetmez.
UI tool sonuçlarını ölç. Model kartları ne sıklıkla gösteriyor, ne sıklıkla metinde listeliyor, kullanıcılar kartlardan sepete ne kadar ekliyor? Bu sayılar generative UI'ın gerçekten işe yarayıp yaramadığını söyler.
Sık Düşülen Tuzaklar
Birincisi, modelin ya da tool'un ürettiği veriyi doğrulamadan component input'una vermek. Ölçümde bir hata metni kitap listesinin props'u olarak arayüze kadar geldi; istemci tarafındaki şema kontrolü olmasa ekranda bozuk bir component olurdu.
İkincisi, fiyatı, stoğu ya da ürün adını modelin yazdığı props'tan almak. Model yalnız seçim yapmalı; veri her zaman sistemin kendi kaynağından gelmeli.
Üçüncüsü, sepete ekleme, ödeme ya da sipariş gibi aksiyonları modele doğrudan tool olarak vermek. Kullanıcının tıklaması hem niyetin kanıtı hem de prompt injection'a karşı en basit savunma.
Dördüncüsü, onay kartını akış sürerken etkin bırakmak. Sunucu bekleyen turu henüz kaydetmeden gelen bir onay 404'e düşer ve kullanıcı ne olduğunu anlamaz.
Beşincisi, küçük bir modele çok adımlı bir tool zinciri bırakmak. Model zinciri yarıda bırakıp sonucu varmış gibi anlatabiliyor; ölçümde kart yokken "available for viewing" dedi.
Küçük Bir Deneme
★☆☆ "Reject" butonunun yanına bir sebep seçeneği ekle ("Keep the order", "Wrong order") ve sebebi CreateResponse'a geçir. Modelin ret sonrası cevabı değişiyor mu?
★★☆ Registry'ye ikinci bir component ekle: siparişin durumunu gösteren bir order-status kartı ve onu üreten bir show_order UI tool'u. parseUiBlock'u iki component'i ayırt edecek şekilde bir discriminated union ile genişlet.
★★★ Hashbrown'ı DO_NOT_TRACK=1 ile ya da --ignore-scripts ile ayrı bir projeye kur ve aynı book-list component'ini exposeComponent ile modele aç. Kendi registry'mizle karşılaştır: hangi doğrulamayı library yapıyor, hangisini hâlâ senin yapman gerekiyor?
Asistan artık tool çağırıyor, onay istiyor ve component seçiyor. Bunların hepsini kendi küçük SSE (Server-Sent Events; sunucudan istemciye tek yönlü event akışı) sözleşmemizle yaptık; her yeni yetenek için sunucuya ve istemciye yeni bir event ekledik. Bir sonraki yazıda aynı işi standart protokollerle yapmayı deniyoruz: AG-UI'ın (Agent-User Interaction Protocol; agent ile arayüz arasındaki açık event protokolü) ve AI SDK UI'ın event aileleri, Angular tarafındaki paketleri ve kendi sözleşmemize karşı ne kazandırıp ne kaybettirdikleri.