İçeriğe geç

Sohbet köprüleri

Bir sohbet köprüsü, harici bir sohbet ürününü — bugün Slack’i — bir Control Center çalışma alanına birinci sınıf bir giriş yolu haline getirir. Botu etiketleyin, bir ajan uyanır; Slack dizisinde yanıtlayın, konuşma kaldığı yerden sürer; bir slash komutu yazın, bir bilet açılır. Altında, masaüstü uygulamasının kullandığı alanların, transkriptlerin, biletlerin ve atıfların aynısı vardır. Sohbet, çalışma alanı üzerinde bir yüzeydir, onun bir kopyası değil.

Ürünle birlikte gelen tek sağlayıcı Slack’tir. Ürünün hiçbir yerinde bir Discord, Telegram, Teams veya Matrix uygulaması yoktur. Özellik bilinçli olarak sağlayıcıdan bağımsız yazılmıştır — ikinci bir ürün eklemek bir enum değeri, bir adaptör ve bir eklentidir — ama bugün “sohbet” Slack demektir.

Bu işi iki yüzey kapsar ve bunlar farklı kişilere aittir. Bir çalışma alanını Slack’e bağlamak ve botu özelleştirmek, Ayarlar → Çalışma alanı → Genel altındaki yönetici eylemleridir. Kendi sohbet hesabınızı Control Center kimliğinize bağlamak ise Ayarlar → Çalışma alanı → Profil ve kimlik altındaki üye eylemidir.

Tüm özelliği üç karar şekillendirir: bağlantının hangi yönde kurulduğu, dışarıdan gelen bir iletiyi neyin yetkilendirdiği ve ürüne özgü bilginin nerede yaşamasına izin verildiği.

Köprü, sağlayıcıyı asla dinlemez. Slack’e Socket Mode üzerinden ulaşılır: sunucu Slack’ten bir soket URL’si ister ve dışarı bağlanır, ardından etiketlemeleri, DM’leri ve slash komutlarını bu bağlantı üzerinden alır.

Asıl mesele bu sonuçtur. NAT arkasındaki bir dizüstü, alan adı olmayan bir ev sunucusu, ağlar arasında gezen bir makine — hepsi köprüyü gelen port olmadan, tünel olmadan, TLS sertifikası olmadan ve değiştiğinde döndürülecek bir herkese açık URL olmadan çalıştırabilir. Güvence altına alınacak bir webhook uç noktası yoktur, çünkü ortada bir webhook uç noktası yoktur. Control Center uygulama tanımını oluştururken Socket Mode’u dayatır ve varsa istek URL’sini kaldırır; çünkü ikisi için yapılandırılmış bir uygulama, yarı yarıya çalışan bir uygulama olurdu.

Bunun bedeli, canlı tutulması gereken bir bağlantıdır ve bu sunucunun işidir: yeniden bağlanma, 30 saniyeyle sınırlanmış, jitter eklenmiş üstel geri çekilmedir; sağlayıcının başlattığı bir soket rotasyonu, bu geri çekilmeyi artırmadan yeniden bağlanır ve reddedilen bir kimlik bilgisi nihaidir — iptal edilmiş bir token’ı sonsuza dek yeniden denemek yalnızca bir hız sınırı kazandırır; bu yüzden köprü durur ve ayarlarda sağlayıcının kendi hatasını birebir bildirirken yeni token’ları bekler.

Sohbet uygulaması olmayan bir çalışma alanının maliyeti hiç yoktur: soket yok, zamanlayıcı yok, trafik yok. Üstelik her şey sunucunun hazır bildiriminden sonra başlar; çünkü yanıt vermekte yavaş kalan bir sohbet uygulaması açılışı asla geciktirmemelidir.

Uygulamayı kuran kişi, iletiyi gönderen kişi değildir. Bu yüzden her gelen olay, herhangi bir şey olmadan önce bir Principal’a çözümlenir ve bir açıklamayla birlikte reddeder:

  • Bağlı olmayan bir sohbet hesabına, yazdığı Slack dizisinde nasıl bağlanacağı söylenir.
  • Bağlı bir hesabın kullanıcısı bu çalışma alanının üyesi değilse, bir yöneticiden davet istemesi söylenir.
  • Rolü salt okunur olan bir üyeye (viewer, guest), rolünün iş başlatamayacağı söylenir.

Bunların hiçbiri devam etmez. Bir sohbet iletisi, onu göndermeyen birine asla atfedilmez — denetim kaydını, principal başına hız sınırlarını ve bilet bildirenleri dürüst tutan şey budur. Bu sayfanın dayandığı üyelik modeli için bkz. Çok oyunculu — kimlik, üyelik ve varlık.

Bağlama, bir kimlik çözümlemesidir, bir erişim izni değildir. Üyelik her olayda yeniden denetlenir; böylece sonradan çıkarılmış birine ait bir bağlantı, eski ayrıcalıklarına değil bir redde çözümlenir. Bağlantı kurmanın iki yolu vardır: üyenin sohbette yazdığı, kısa ömürlü ve tek kullanımlık bir kod ya da sağlayıcının doğrulanmış e-postası üzerinden otomatik eşleşme — ve e-posta yolu, eşleşen kullanıcının zaten üye olmasını gerektirir; dolayısıyla adresi sunucuda bulunan bir yabancı hiçbir şeye bağlanmaz.

Kodlar bilinçli olarak bellekte, 15 dakikalık bir yaşam süresiyle tutulur. Birini kalıcı hale getirmek, yok olmaya yazgılı bir şey için kalıcı, kimlik bilgisi biçiminde bir satır yaratırdı ve bir sunucu yeniden başlatmasının bekleyen her kodu geçersiz kılması bir gerileme değil, doğru davranıştır — üye yeniden “hesabımı bağla” seçeneğine basar.

Bir ret, iletinin açtığı Slack dizisinde, gerçek bir yanıt olarak verilir. Diziye verilen bir yanıt, yanıtladığı iletiye rozet ekler, ihtiyaç duyan tek kişiye ulaşır ve yine de “bağlı değilsiniz” uyarısını kanalın kendisinden uzak tutar. İleti göndermek ayrıca her sağlayıcının sahip olduğu tek yetenektir; böylece açıklama, uygulamanın taşımayabileceği bir özelliğe asla bağlı kalmaz.

Her iki yöne de aynalayan bir köprü, iki yönü birbirinden ayıran bir şey olmadıkça döngüye girer. Köprünün içeri getirdiği her ileti metadata['chat'] ile damgalanır — sağlayıcı, ekip, konuşma, dizi, ileti ve gönderen. Dışa dönük aynalama, bu damgayı taşıyan her iletiyi atlar.

Çift yönlü aynamayı güvenli kılan tek kural budur: Control Center’da yazılan bir insan iletisi, yazarın adı öne eklenerek bağlı diziye aktarılır; sohbetten gelen bir ileti ise girdiği yerde durur.

Dışarı geçen şey bilinçli olarak dardır. Ajanın yanıtı, sıradan sohbet metni olarak aktarılır. Kısa bir düşünce, kartın Thinking… satırında görünebilir. Araç çıktısı bir diff duvarıdır ve Control Center’da kalır; kısaltılmış bir yanıtın işaret ettiği yer de orasıdır.

Sağlayıcının bunları görüntülediği yerlerde, bir tur tek bir görev kartı taşır; kart, tur sürdüğü sürece yerinde düzenlenir ve açıkladığı yanıtın üstünde durur.

Kart, ezilmiş bir paragraf değil, gruplanmış tek bir plandır. Slack aynı görevdeki details ile output değerlerini birleştirir; bu yüzden bir satırın ayrıntıları bir kez gönderilir (Thinking… satırındaki düşünce) ve output asla akış halinde gönderilmez. Kurulum, başlığı değiştirilen tek bir satırdır (Working on it…, Cloning …). Akıl yürütme bir Thinking… satırı ekler. Her araç kendi satırıdır (Read lib/main.dart). Yanıt, kartın altındaki sıradan metindir — Slack’in plan görünümü onu görevlerin içine karışmış bir parçadan değil, sonradan eklenen bir markdown bloğundan okur. Başka hiçbir şey yoktur: araç çıktısı yoktur ve yalnızca kısa bir izin listesinden gelen bir araç argümanı vardır; çünkü bilinmeyen bir aracın argümanları koca bir dosya gövdesi olabilir ve bir satır tek satırdır.

Kart, turdan önceki bekleyişi de kapsar. İlk etiketleme konuşmayı oluşturur ve bunu yapmak, ajanın çalışacağı her depoyu klonlar — gerçek bir depoda bu, ajana henüz hiçbir şey sorulmamışken geçen dakikalar demektir ve diziyi izleyene bot isteği yok saymış gibi görünür. Bu yüzden kart, etiketleme kabul edildiği anda açılır (Working on it…) ve ardından bu kurulum satırı, masaüstünün gösterdiği aynı sözcüklerle güncellenir — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… — sonra ajan akıl yürütmeye başlar başlamaz bir Thinking… satırı, ardından her araç için bir satır gelir. Tek kart, tek ileti: “sizi duydum”dan, altındaki sıradan metin olarak yanıta kadar. Kurulum yalnızca sohbette birinin beklediği yerde bildirilir: uygulamada hazırlanan bir konuşma ya da sunucu açılışında kaldığı yerden sürdürülen başıboş bir konuşma, hiç kart olmadan hazırlanır.

Kart, sohbetin okuyucuya veremeyeceği tek şeyi taşır: yanıtın arkasındaki konuşmaya giden bir View in Control Center bağlantısı. Sohbet ürünleri bir bağlantıda http(s) dışında hiçbir şey kabul etmez; bu yüzden düğme, bu sunucunun sunduğu küçük bir sayfayı işaret eder ve o sayfa tarayıcıyı masaüstü uygulamasının control-center:// bağlantısına yönlendirir. O sayfa hiçbir şey okumaz ve hiçbir şeyi çözümlemez — URL’sindeki kimlikler yalnızca bir derin bağlantıya kopyalanır, başka hiçbir yere — ve erişilebilir herkese açık URL’si olmayan bir sunucu, bozuk bir kart yerine düğmesiz bir kart üretir.

Kartlar da her şey gibi bir yetenektir. Kartı olmayan bir sağlayıcı tıpkı eskisi gibi davranır, yalnızca metin; çağrı anında kartı reddeden bir sağlayıcı ise akışını korur: yanıt her zaman onu anlatan karttan üstündür. Akış tamamen kapalıysa, tamamlanan kart bunun yerine gönderilen tek iletiye biner. Açılan bir bilet de aynı muameleyi görür — /cc ticket onayı, var oluş sebebi yeni oluşturulan bilete giden bağlantı olan tamamlanmış bir karttır.

Sohbet ürünleri, neleri görüntüleyebildikleri konusunda farklılaşır; bu yüzden köprü en zengin yüzeye göre yazılmıştır — bir dizi içinde canlı olarak büyüyen bir yanıt, geçici bir “düşünüyor…” durumu, başlıklı bir dizi, bir slash komutuna geçici bir yanıt — ve yetenekleri tek tek azaltır. Bunların hiçbirine sahip olmayan bir sağlayıcı yine de çalışır; yanıtlar tek bir ileti olarak gelir.

Bir yeteneği bildirmek, onu vaat etmek değildir. Slack’in akış halindeki yanıtları ücretli bir plan özelliği gerektirir ve Slack yalnızca çağrı anında reddeder. Kalıcı olarak işaretlenen bir ret, bağlantının tüm ömrü boyunca hatırlanır: köprü sormayı bırakır ve o andan sonra yanıtları bütün halinde gönderir ve ayarlar bunu açıkça yazar; çünkü “yanıtlarım artık akış halinde gelmiyor” sorusu bir günlük okumadan yanıtlanabilmelidir. Slack bir görev kartını reddederse kartlar da aynı şekilde bırakılır.

Her sınır röleye değil sağlayıcıya aittir: ileti üst sınırı, akış parçası üst sınırı ve bir akışın dizi çapası gerektirip gerektirmediği adaptör tarafından bildirilir ve uzun bir yanıt, sözcüğün ortasından değil, paragraf, satır ya da sözcük sınırından bölünür.

Ürüne özgü her şey bir portun arkasında yaşar. Genel çekirdek markdown, sohbet konuşmaları, sohbet dizileri ve üyeleri bilir; Slack zarf yapıları, mrkdwn ya da gateway intent’leri hakkında hiçbir şey bilmez. Bunu işler kılan üç parça vardır:

  • Adaptör tek bir ürünü normalleştirir: taşımasını, API çağrılarını, metin kodekini ve zarf tuhaflıklarını. Slack’inki öğreticidir — bir kanal etiketlemesi iki kez gelir (app_mention olarak ve message olarak) ve çoğu ileti alt türü yeni istekler değil, süslemedir. İkisi de adaptörde çözümlenir; böylece çekirdek, Slack’in çift teslim ettiğini asla öğrenmez.
  • Tanımlayıcı, sağlayıcının genel görünümüdür: hangi kimlik bilgilerine ihtiyaç duyduğu, biçimlerinin ne olduğu, hangi URL’lerin kullanıcının bunları bulmasına yardımcı olduğu, neler yapabildiği ve uygulamasının buradan oluşturulup düzenlenebilip düzenlenemeyeceği. Ayarlar kartı, bağlantı iletişim kutusu ve argüman doğrulaması bunun üzerinden üretilir — istemci geriye bir sağlayıcı ve alan kimliğine göre anahtarlanmış bir eşleme gönderir.
  • Eklenti, sunucu tarafındaki bileşimdir: kimlik bilgilerinin nasıl doğrulanmış bir bağlantıya dönüştüğü, bir adaptörün nasıl kurulduğu ve sağlayıcı tarafındaki uygulamanın nasıl yönetildiği.

Yani bir ürün eklemek, bir enum değeri, bir adaptör ve bir eklentidir. Yeni bir RPC işlemi yok, şema değişikliği yok, istemci değişikliği yok — ve istemci dürüst kalır; çünkü hiç duymadığı bir alan bile sunucunun sağladığı etiketle görüntülenir.

İki ayrıntı, soyutlamanın yukarı doğru sızmasını engeller. Bir sağlayıcının API’si olmayan uygulama yönetimi adımları (bir token üret, uygulamayı yükle, izin değişikliğinden sonra yeniden yükle) kendi bağlantılarına sahip adlandırılmış adımlar olarak döndürülür; böylece kurulumu tamamlama ekranı, hangi sağlayıcı olduğunu bilmeden bunları gösterir. Ve köprünün yazdığı her talimat, sağlayıcıyı ve uygulamanın gerçekte kullandığı komutu adlandırır; böylece adı değişmiş bir komut ya da ikinci bir sağlayıcı, yeni bir metin gerekmeden doğru okunur.

Bir çalışma alanının sohbet token’ları, o çalışma alanının kendi dizininde, veritabanının yanında durur — <dataDir>/<workspaceId>/chat_credentials/<provider>.json yolunda düz JSON olarak; veritabanında değil ve bilinçli olarak işletim sistemi anahtar zincirinde de değil. İki sonucun sonradan gözden kaçması kolaydır: bir çalışma alanını silmek dizinin bağlantısını keser ve token’ları da onunla gider; bir çalışma alanını dışa aktarmak veya yedeklemek workspace.db dosyasını kopyalar, böylece başkasına verilen bir çalışma alanı içinde canlı bot token’ları taşımaz. Dosya, sunucu makinesinin dosya sistemi güven sınırını devralır — veri dizinine okuma erişimi olan her şey bir bot token’ını okuyabilir; bu da veritabanlarının arkasında durduğu sınırın aynısıdır.

Sağlayıcı başına tek dosya, atomik olarak ve yalnızca sahibinin erişimiyle yazılır: iptal edilen bir uygulama tek bir dosyanın bağlantısı kesilerek silinir ve bozuk bir dosya, tüm sağlayıcılar yerine yalnızca birine mal olur. Kimlik bilgileri bağlantı işlemiyle içeri girer ve asla geri çıkmaz — durum okumaları yalnızca bağlantı meta verileriyle yanıt verir.

  • Yapısal uygulama ayarları. Taşıma, kapsamlar ve olay abonelikleri dayatılır, açığa çıkarılmaz. Bir ayarlar ekranından sessizliğe ayarlanabilen bir uygulama, bir özellik değil bir destek talebidir.
  • Konuşma artı slash komutları. Slack yüzeyi iletiler, dizi yanıtları ve slash komutlarıdır.
  • İkinci bir doğruluk kaynağı. Sohbet, işin saklandığı bir yer değildir. Slack dizisi bir alandır, yanıt bir transkripttir, bilet bir bilettir — hepsi çalışma alanında ve her istemciden erişilebilir.