Agen yang selalu aktif perlu terus mengawasi dan menanggapi event, dari jauh lebih banyak sumber daripada yang bisa dilacaknya di context window. Harness terdistribusi milik Comma, Salix, memberi agen kemampuan untuk memuat program eBPF mungil ke dalam cluster dan menjalankannya terus-menerus, 24/7, untuk memantau event, memanggil Jev untuk keputusan cerdas yang cepat, dan membangunkan loop agen utama saat terjadi sesuatu yang menarik.
Loop dalam saja tidak cukup
Loop agen yang kita kenal itu sederhana: kirim konteks ke model, jalankan tool call yang dikembalikannya, tambahkan hasilnya, ulangi. Loop itu bagus untuk mengerjakan pekerjaan. Tapi buruk untuk menunggu pekerjaan.
Minta agen untuk “kabari saya saat kontraknya kembali dalam keadaan sudah ditandatangani” atau “awasi repo ini dan tandai apa pun yang menyentuh billing”, dan loop dalam hanya punya dua pilihan buruk. Ia bisa melakukan polling: bangun setiap beberapa menit, membaca ulang inbox, dan menghabiskan satu panggilan model penuh hanya untuk memutuskan bahwa tidak terjadi apa-apa. Atau ia bisa menyerahkan tugas itu ke jadwal heartbeat atau cron, yang sebenarnya polling yang sama dengan interval lebih panjang dan latensi lebih buruk. Apa pun pilihannya, bagian sistem yang paling mahal, yaitu model besar yang membaca konteks besar, berjalan di setiap tick, padahal hampir setiap tick itu sepi.
Yang sebenarnya dibutuhkan agen yang selalu aktif adalah loop luar: sesuatu yang berada di aliran event, melakukan penyaringan murah, dan baru meneruskan ke model saat ada hal yang layak dipikirkan. Dan karena setiap agen mengawasi hal yang berbeda dengan cara yang berbeda, loop luar itu tidak bisa berupa fitur produk yang tetap. Agen harus menulisnya sendiri.
Di Salix, loop luar itu disebut Loop.
Loop adalah program C kecil
Loop adalah satu file C, ditulis oleh agen, dikompilasi ke eBPF, dan dijalankan di cluster di samping agen pemiliknya. Setiap Loop punya bentuk yang sama:
Loop menunggu timer atau event, memeriksa sesuatu, dan, sesekali saja, membangunkan agen. Agen mendapat header SDK yang persis dan panduan pemrograman dari tool loop.sdk, menulis programnya, mengompilasinya dengan loop.build, lalu menjalankannya dengan loop.create. Tidak ada template dan tidak ada DSL. Jika agen bisa menjabarkan pengawasannya dalam C, agen bisa menjalankannya.
Kenapa C dan eBPF, dan bukan, misalnya, skrip Python di dalam container? Karena angka-angka yang menjadi dasar desain Salix. Salix menjalankan jutaan agen, dengan lebih dari 100 agen per core CPU. Agen tidak bisa punya sandbox yang terus menyala, jadi pengawasannya pun tidak bisa. Loop harus cukup murah untuk terus berjalan selamanya, untuk setiap agen, di node multi-tenant yang dipakai bersama, sambil menjalankan kode yang tidak pernah ditinjau manusia.
eBPF sangat cocok untuk kebutuhan itu:
Kecil. Loop yang sedang menunggu hanya memakai puluhan kilobyte memori residen. Tidak ada proses, tidak ada interpreter, tidak ada heap.
Aman untuk menjalankan kode yang tidak tepercaya. Target-nya tidak punya syscall, tidak punya function pointer, dan tidak punya stack tanpa batas. Yang bisa dilakukan program hanyalah menghitung di memorinya sendiri dan memanggil fungsi host yang kami berikan.
Murah saat menunggu. Loop yang sedang tertahan di
sf_event_nextatausf_sleep_mstidak memakan apa pun selain memorinya.
Harganya adalah dialek yang terbatas, dan panduan SDK menyatakannya terus terang: hanya C integer (tanpa floating point, tanpa pembagian signed), stack frame 4 KiB, kedalaman maksimal 8 frame, tanpa rekursi, tanpa sprintf, dan paling banyak 128 handle aktif. Ternyata model tidak masalah dengan ini. Model sudah paham C, dan batasan-batasan ini adalah jenis yang dijelaskan dengan baik oleh error compiler.
Spinfoam: runtime di balik setiap Loop
Loop berjalan di spinfoam, runtime loop eBPF kami. Spinfoam dibangun di atas async-ebpf, runtime eBPF userspace yang ramah async, sepenuhnya preemptive, dan memiliki keamanan memori yang terverifikasi secara formal di intinya.
eBPF userspace, dibuat async
Spinfoam bukan eBPF kernel. Spinfoam adalah proses Rust biasa, satu per node Salix, yang tidak butuh dukungan eBPF kernel maupun hak istimewa, dan berjalan sama di Linux dan macOS. Salix berkomunikasi dengannya lewat JSON-RPC di stdin dan stdout. Pembagian tanggung jawabnya tegas: spinfoam memegang eksekusi lokal, isolasi antarprogram, pembatalan, dan pengiriman pesan yang terbatas. Salix memegang semua yang harus bertahan saat crash: penempatan, state yang durable, kebijakan restart, kredensial, dan jaringan.
Bagian “async” di async-ebpf-lah yang membuat Loop terlihat seperti C sekuensial biasa. Setiap program yang dimuat mendapat satu pemanggilan berumur panjang, yang berjalan di coroutine-nya sendiri. Saat program memanggil sf_sleep_ms, sf_event_next, atau sf_host_call, helper menangguhkan coroutine itu dan menyerahkan penantiannya ke Tokio. Stack C dan variabel global program tetap persis di tempatnya. Saat timer berbunyi, event tiba, atau Salix menjawab host call, coroutine berlanjut di baris berikutnya. Agen menulis for (;;) { wait; check; wake; } dan tidak pernah melihat callback.
“Sepenuhnya preemptive” adalah yang membuatnya aman untuk dipakai bersama. Semua kode guest berjalan di satu thread Tokio, dan sebuah thread pengawas menyela program apa pun yang berjalan terlalu lama tanpa melepas giliran. Loop yang berputar di for (;;) {} yang ketat kehilangan jatah waktunya seperti yang lain, dan menghentikannya tidak butuh kerja samanya. Satu Loop yang bermasalah tidak bisa membuat Loop tetangganya macet.
Sebagian besar Loop menghabiskan hampir seluruh waktunya untuk menunggu, jadi desain ini bisa dikemas dengan padat. Dalam uji kualifikasi kami, 10.000 Loop pemantau kecil, semuanya dikompilasi oleh compiler bawaan, berjalan di satu thread eksekusi dengan sekitar 64 KB memori residen per Loop. Memuat dan menjalankan seluruh 10.000 Loop butuh 3,07 detik. Mengirim satu event ke setiap Loop, dan menjawab host call yang dibuat tiap Loop sebagai tanggapannya, butuh 1,56 detik. Request kontrol tetap di bawah seperempat milidetik pada p99. Seluruh proses memakai empat thread OS saat memuat dan dua saat idle.
Keamanan memori yang bisa Anda periksa
Menjalankan ribuan program tulisan agen dalam satu proses hanya masuk akal jika tidak satu pun dari program itu bisa menyentuh memori yang bukan miliknya. Di async-ebpf, jaminan itu dimulai dari tata letak memori dan berakhir dengan bukti yang diperiksa mesin.
Tata letaknya dulu. Data setiap program berada di dalam pointer cage: wilayah yang dicadangkan dengan guard page acak di sekelilingnya. Stack guest disusun sebagai pulau-pulau berisi satu frame masing-masing, dipisahkan oleh celah yang tidak bisa diakses dan lebih lebar dari jangkauan instruksi memori eBPF mana pun, jadi fungsi yang keluar dari frame-nya sendiri akan memicu fault, bukan membaca frame pemanggilnya. Halaman kode JIT tidak pernah bisa ditulis dan dieksekusi pada saat yang sama.
Lalu buktinya. async-ebpf mengompilasi setiap fungsi eBPF ke kode native saat pertama kali fungsi itu berjalan. Backend x86_64-nya disusun sedemikian rupa sehingga bagian yang mengambil keputusan bisa dibuktikan:
Tahap-tahap ini berada dalam satu modul Rust yang dikompilasi dan dijalankan oleh runtime. Charon dan Aeneas menerjemahkan modul yang sama itu ke Lean, jadi buktinya menyangkut kode yang benar-benar berjalan, bukan model dari kode itu. Sekitar 40.000 baris Lean yang ditulis tangan membuktikan, antara lain:
Validator-nya sound. Di sepanjang setiap eksekusi program yang diterimanya, program tidak pernah mencapai instruksi yang tidak terdefinisi, tidak pernah melompat ke tengah instruksi atau keluar dari program, dan frame pointer selalu menjadi basis frame untuk kedalaman pemanggilan saat itu.
Fungsi bersifat tertutup. Kendali tidak pernah meninggalkan fungsi kecuali lewat pemanggilan lokal atau return, dan inilah yang memungkinkan JIT mengompilasi satu fungsi dalam satu waktu.
Kode yang dihasilkan aman memori. Jika checker menerima sebuah fungsi, setiap eksekusi kode native-nya hanya menyentuh sekumpulan alamat yang diizinkan (frame-nya, wilayah guest miliknya, stack dan literal pool miliknya) dan kembali dengan state pemanggil yang utuh. Callee yang dikompilasi secara lazy tersusun dengan teorema yang sama.
Byte-nya sesuai dengan yang dinyatakan model. Setiap instruksi yang dikeluarkan assembler bisa di-decode kembali menjadi instruksi yang dinalar oleh model.
Bukti-bukti ini terhubung ke perangkat keras di dua tempat. Sebuah simulator yang bisa dieksekusi dibuktikan sebagai satu jalannya model mesin Lean, dan sebuah uji diferensial menjalankan dua puluh ribu urutan instruksi acak di simulator dan di prosesor sungguhan. Sebelum setiap pemanggilan, runtime memeriksa tata letak memori konkret terhadap hipotesis teorema, dan menolak berjalan jika hipotesis itu tidak terpenuhi.
Kami terbuka soal apa yang tidak dibuktikan. Basis tepercayanya mencakup semantik instruksi x86 (diuji terhadap perangkat keras, tidak dibuktikan), entry trampoline, fault handler, pemetaan memori, sisi host dari setiap pemanggilan helper, serta Charon dan Aeneas sendiri. Teoremanya menyangkut keamanan memori, bukan kebenaran fungsional atau kebocoran informasi. Dan teorema itu hanya mencakup backend x86_64; backend arm64 sudah diuji tetapi belum dibuktikan.
Bukti-bukti ini menjaga guest tetap di memorinya sendiri. Semua yang dilakukan Loop di dunia luar melewati host call, dan batas itu milik Salix: allowlist kemampuan dan aturan wewenang yang dijelaskan di bawah.
Compiler-nya juga berjalan di dalam sandbox
Agen mengompilasi Loop-nya sendiri, jadi compiler juga termasuk wilayah input yang tidak tepercaya. Spinfoam menyertakan TinyCC, yang dikompilasi ke eBPF, dan menjalankannya di async-ebpf seperti guest lainnya. Setiap build mendapat instance compiler baru dengan arena 8 MiB, file system khusus memori yang hanya berisi source yang dikirim dan header SDK, batas waktu 15 detik, serta tanpa akses jaringan, proses, atau file host. Output-nya juga diperlakukan sebagai tidak tepercaya. Setiap object melewati validasi yang sama saat dimuat, entah berasal dari compiler atau bukan.
Itulah sebabnya loop.build tidak butuh toolchain, container, maupun hak istimewa di node. Source C milik agen tidak pernah menyentuh compiler native.
Membangunkan agen adalah bagian yang mahal
Loop berjalan sendiri, tapi tidak ada gunanya jika tidak bisa memberi tahu agen apa pun. Satu-satunya cara Loop melakukannya adalah agent.notify:
Notifikasi itu dikirim ke Session target milik Loop sebagai pesan biasa. Baru saat itulah loop agen utama berjalan, dan baru saat itulah ia menghabiskan token model. Satu jam yang sepi sama sekali tidak memakai token.
dedup_key lebih penting daripada kelihatannya. Loop bisa di-restart, event bisa dikirim ulang, dan guest bisa mengulang panggilan yang tidak pasti berhasil. Salix mengirim wake sebagai loop:<id>:<dedup_key>, jadi email, issue, atau alert yang sama hanya membangunkan agen sekali.
Karena wake adalah bagian yang mahal, wake juga diberi anggaran. Sebuah Loop boleh membangunkan agennya enam kali per sepuluh menit. Loop yang terus berada di batas itu selama satu jam akan dijeda dengan alasan budget, dan agen diberi tahu. Loop yang bermasalah atau terlalu bersemangat berakhir sebagai Loop yang terlihat dan dijeda, bukan sebagai tagihan.
Keputusan cepat tanpa model besar
Bagian sulit dari pengawasan biasanya bukan “apakah ada yang berubah”, melainkan “apakah perubahan ini penting”. Apakah email ini sesuatu yang harus ditindaklanjuti pemiliknya hari ini? Apakah PR ini menyentuh bagian sistem yang kita pedulikan? Loop yang ditulis dalam C integer tidak bisa menjawabnya sendiri, dan memanggil model frontier di setiap event akan membawa kita kembali ke titik awal.
Jadi Loop mendapat satu kemampuan lagi: decide. Kemampuan ini mengirim pertanyaan kecil yang bertipe ke model keputusan yang cepat (Jev) dan mendapat jawaban terstruktur. Berikut pertanyaan dari prototipe pengawas email kami:
decide mendukung choice untuk memilih satu kandidat, pertanyaan ya/tidak terpisah untuk beberapa kecocokan, dan score untuk relevansi berurutan. Kemampuan ini hanya pernah mengembalikan keputusan. Ia tidak membaca sumber, tidak menjalankan tool, dan tidak memberi wewenang. Loop yang menyediakan datanya, mengajukan pertanyaannya, dan menerapkan ambang batas di kodenya sendiri.
Dua pilihan desain membuat ini bekerja dengan baik:
Ketidakpastian adalah jawaban yang sah. Pilihan
noneataudeferyang eksplisit berarti “saya tidak bisa memastikan”, dan itu hasil yang berhasil, bukan error transport. Loop menyimpan event-nya alih-alih mengarang keputusan “quiet”.Cukup murah untuk dijalankan di setiap event. Katalog harga kami mencantumkan
typesafe/jev-1.13.0seharga $0.042 per juta token input, dengan token output gratis. Itu cukup murah untuk menyaring setiap email, bukan hanya email yang lolos filter kata kunci.
Hasilnya adalah sistem dua tingkat: model kecil menyaring setiap event, dan model besar hanya melihat event yang lolos.
Event masuk, secara durable
Timer sudah cukup untuk polling, tapi banyak sumber bisa melakukan push. Loop punya mailbox, dan sistem eksternal mengisinya dengan tiga cara:
Salix API:
POST /v1/agent-groups/:group_id/loops/:loop_id/eventsdengan API key grup;URL webhook rahasia, yang diaktifkan, dirotasi, atau dicabut agen dengan
loop.webhook;trigger Composio dari aplikasi yang dihubungkan pengguna, yang datang dengan ID event dari provider dan slug trigger.
Respons 202 dari salah satunya berarti PostgreSQL sudah menyimpan event itu di Loop-nya. Itu tidak berarti Loop sudah memprosesnya. Guest memanggil loop.ack saat sudah mencapai titik aman: keputusan “quiet”, atau serah terima yang durable seperti agent.notify yang berhasil. Sampai saat itu, event tetap tertunda, dan Reconciler memutar ulang event yang tertunda setelah Loop berpindah atau di-restart, dan bagaimanapun juga sekali setiap menit. Event yang tidak di-acknowledge dalam 15 menit membuat Loop gagal secara terlihat, dengan event tertunda tetap disimpan agar agen bisa mencoba ulang atau membuangnya.
Satu pelajaran dari membangun ini: mailbox spinfoam menghapus duplikat ID event pada saat admission, bukan saat penyelesaian bisnis. Di prototipe awal, kami mengira panggilan model yang gagal akan dicoba ulang karena provider mengirim ulang event-nya. Ternyata tidak; pengiriman ulang itu dengan benar dibuang sebagai duplikat. Jadi percobaan ulang adalah urusan guest, yang menyimpan event dan mencoba ulang dalam jumlah terbatas, sedangkan pemulihan setelah crash adalah urusan inbox yang durable. Sekarang kami menuliskan aturan itu di panduan SDK.
Acknowledgement, admission Session yang durable, pesan yang terlihat, dan efek eksternal adalah empat fakta yang terpisah. Kami memberi masing-masing pemiliknya sendiri, mewajibkan kunci deduplikasi yang stabil di hilir, dan tidak menjanjikan efek eksternal yang tepat sekali.
Berjalan selamanya di cluster yang terus bergerak
“24/7” mudah diucapkan dan sulit dilakukan di cluster tempat node datang dan pergi. Loop berjalan di node yang memegang lease agennya, dan mengikuti lease itu.
Penempatan. Saat proses server agen mengklaim lease-nya, proses itu mengambil alih Loop-Loop aktifnya. Saat agen dipasifkan atau di-fence, proses itu melepaskannya. Loop yang aktif membuat agennya tetap residen, jadi agen yang idle dengan sebuah Loop memperbarui lease-nya alih-alih parkir.
Inkarnasi. Setiap kali Loop dimuat, inkarnasinya naik; ini adalah nilai fence di baris Loop. Host call dari object yang bukan inkarnasi saat ini ditolak, begitu pula
ackdarinya. Salinan Loop yang usang di node yang sedang pergi tidak bisa bertindak.Checkpoint.
loop.state.putmenyimpan hingga 16 KiB, dan pemuatan berikutnya mendapatkannya kembali sebagaiconfig.state. Agen menyimpan checkpoint berupa cursor dan ID terakhir yang dilihat, bukan semuanya.Fault. Fault memuat ulang Loop dari checkpoint-nya, paling banyak tiga kali per jam. Setelah itu statusnya
failed, dan agen diberi tahu sekali.Loop terlantar. Sapuan berkala mencari Loop aktif yang tidak punya object terpasang, atau terpasang di node yang sudah pergi, lalu me-restart pemiliknya.
Loop juga punya sumber kebenaran di luar runtime. loop.build menulis ELF hasil kompilasi ke file system agen, dan loop.create mencatat path dan SHA-256-nya. Setiap pemuatan membaca ulang file itu dan memeriksa hash-nya, jadi Loop menjalankan persis program yang dipakai saat dibuat, atau tidak berjalan sama sekali.
Yang boleh dilakukan Loop
Loop adalah kode yang ditulis oleh model, berjalan tanpa pengawasan, sepanjang waktu. Wewenangnya harus lebih kecil daripada wewenang agen, bukan sama.
Loop memanggil allowlist tertutup berisi kemampuan host: agent.notify, panggilan loop.state.*, loop.ack, dan loop.log, tool Salix yang diklasifikasikan sebagai hanya-baca, tool lingkungan dan perangkat, tool SSH, membaca percakapan, web.http_request, composio.execute untuk aplikasi yang terhubung, dan decide. Spinfoam menolak nama lain apa pun dengan SF_DENIED. Tidak ada langkah pemberian izin yang bisa salah.
Setiap panggilan melewati dispatch tool dan pemeriksaan aliran informasi yang sama dengan giliran agen biasa. Loop bertindak sebagai pembuatnya, lewat principal yang didelegasikan schedule|loop:<id>|<creator>, dengan aturan pengungkapan milik pembuatnya dan origin Loop yang tersegel. Payload event adalah data. Payload tidak membawa wewenang dan tidak bisa memperluas apa yang boleh dilakukan Loop, dan itulah sebabnya prompt keputusan di atas secara eksplisit memperlakukan email sebagai input yang tidak tepercaya.
Terakhir, kuota menjaga sistem tetap terbatas: 20 Loop aktif per agen, 100 per grup, 32 event tertunda berukuran masing-masing 16 KiB per Loop, dan rate limit untuk ingress webhook.
Skrip: runtime yang sama, untuk satu giliran
Begitu kami punya runtime yang aman dan murah untuk C tulisan agen, muncul kegunaan kedua. script.run mengompilasi dan menjalankan program C integer sekali, di dalam satu tool call, dengan akses ke tool milik giliran pemanggil lewat salix.call. Skrip tidak punya baris, inkarnasi, checkpoint, maupun agent.notify. Skrip adalah cara bagi agen untuk menggabungkan banyak tool call ke dalam satu program deterministik, alih-alih banyak bolak-balik ke model. Dua skill yang kami rilis adalah program C yang dijalankan dengan cara ini.
Agen yang memprogram sistem event-nya sendiri
Jika digabungkan, agen Comma yang diminta untuk berjaga melakukan tiga hal yang tidak bisa dilakukan agen tradisional: agen menulis pengawasannya sebagai program, cluster menjalankan program itu selama dibutuhkan dengan biaya beberapa kilobyte saja, dan model kecil memutuskan, event demi event, apakah model besar perlu dibangunkan sama sekali.
Loop dalam tetap menjadi tempat agen berpikir. Loop luar adalah tempat agen memperhatikan. Salix memungkinkan agen membangun keduanya.