npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

proactive-gate

v0.7.2

Published

Decide whether a proactive AI agent may reach a user right now, and log why not. Ordered checks as code or JSON, a conformance spec, presets for platform and legal limits, adapters for AI SDK, Mastra, LangChain and OpenAI Agents, and a Python sibling.

Readme

proactive-gate

English | Türkçe

Proaktif bir yapay zekâ ajanının kullanıcıya şu an ulaşıp ulaşamayacağına karar verir ve neden ulaşamadığını kaydeder.

Proaktif bir asistanın iki yarısı vardır. Üreten yarı neyin söylenmeye değer olduğuna karar verir. Bastıran yarı ise onu şimdi mi, sonra mı, hiç mi söyleyeceğine karar verir. Proaktif yapay zekâ üzerine yazılan hemen her şey ilk yarı hakkındadır. Bu paket ikinci yarıdır: tek kapı, sıralı bir kontrol listesi ve her ret için bir gerekçe.

npm install proactive-gate
import { createGate, defaultChecks, RedisStore } from "proactive-gate";

const gate = createGate({
  store: new RedisStore(redis),                  // MemoryStore() for one instance
  checks: defaultChecks({ dailyLimit: 3, quietHoursFloor: "high" }),
  onDecision: (d) => log.info("gate", d),        // every decision, allowed or not
});

const decision = await gate.evaluate({ user, candidate });
if (decision.allowed && (await gate.commit(decision, { user, candidate }))) {
  await send(decision.surfaces, candidate.payload);
}

Kurmadan önce ne değiştirdiğini görün

npx proactive-gate simulate

Anahtar yok, hesap yok, ayar yok. Verisi hazır olduğunda tetiklenen, yani kullanıcının uyanık olup olmadığına bakmayan bir asistanın bir haftasını önce hiç geçit olmadan, sonra varsayılan sırayla tekrar oynatır ve her adaya ne olduğunu yazar.

| | geçit yok | proactive-gate | | ------------------------------------------------------ | --------: | -------------: | | iletildi | 171 | 74 | | tutuldu | 0 | 97 | | kişinin kendi sessiz saatleri içinde iletilen | 52 | 3 | | bunlardan kritik olanlar (eşiğin geçirdiği) | 5 | 3 | | bir kişinin bir günde aldığı en yüksek sayı | 6 | 5 |

Koyu satır iki kez okunmaya değer: herkesin kendi belirlediği pencereyi sayar, birinin başkası için seçtiği bir sokağa çıkma yasağını değil. Varsayılan sırada o pencereye giren üç mesajın üçü de kritikti, ki belgelenmiş öncelik eşiği tam bunun için var.

Kapsam ve yöntem, çünkü bunlar olmadan sayı süstür. Hafta bir üretici ve bir tohumdur, kimsenin gerçek trafiği değil: beş saat diliminde sekiz kullanıcı, adayların anları UTC günü boyunca düzgün dağılımla seçilmiş, bütün parametreler src/demo-week.ts içinde yazılı ve examples/week.jsonl dosyasına dökülmüş. Bir politikanın bir akışa ne yaptığını ölçer; bir mesajın istenip istendiğini ya da alıcının onunla ne yaptığını ölçemez.

npx proactive-gate simulate --disagreements --why      # her tutmanın arkasındaki cümle
npx proactive-gate simulate kendi-adaylarim.jsonl      # üretilmiş hafta değil, sizin trafiğiniz

Bu kütüphanenin yapmayacağı şeyler

  • On iki kontrol, politika yüzeyinin tamamıdır. On üçüncüsü için, on ikisinin ifade edemediği bir kuralı olan gerçek bir kurulum gerekir.
  • Preset kataloğu birleşmiş hâliyle donduruldu. Yeni bir preset için, o kanala ya da o mevzuata göre gerçekten ürün gönderen ve bunu issue'da söyleyen birine ihtiyaç var. "Bu ülkenin de bir yasası var" yeterli değil, çünkü her ülkenin var; kimsenin kullanmadığı preset çürür.
  • Adaptörler ve store'lar da aynı kuralla dondu. Bir sonraki, ona ihtiyacı olan kişiyle gelir.
  • Bunun yerine büyüyen şey kanıt: yukarıdaki simülatör, uygunluk paketi ve spesifikasyonun bizim dışımızda biri tarafından yazılmış bir uygulaması.
  • Asla olmayacaklar: sunucu, barındırılan hesap ya da mesaj içeriğini okuyan herhangi bir şey.

Sıfır bağımlılık. TypeScript. Node 20 ya da üstü. Framework'ten bağımsız: kapı, "model bir şey üretti" ile "kullanıcının telefonu titredi" arasında durur; hangi model ya da framework üretmiş olursa olsun. Örnekler: examples/vercel-ai-sdk.ts, examples/mastra.ts ve yeniden oynatılabilir bir politika olarak examples/policy.json. Dokümantasyon ve tarayıcıda oyun alanı: bubblegunn.github.io/proactive-gate. Python: python/.

Okumak yerine görmeyi tercih ederseniz: npm run bench:compare, kayıtlı bir günü hem bu kapıdan hem de elle yazılmış beş if ifadesinden geçirir ve ayrıştıkları altı yeri yazdırır: kotanın geçirmesi gereken kritik bir uyarı, iki günlük bir hesap, bir erteleme, arka arkaya gelen reddedişler, ve bir kullanıcıyı susturup bir diğerine iki kat mesaj veren yerel gün sınırı. Altısının da neden zevk meselesi olmadığı, bu dosyanın kısaltılmış olması nedeniyle yalnızca İngilizce README'de anlatılıyor: Compared with hand-rolled checks.

Bir karar neye benzer

{
  allowed: false,
  userId: "ayse",
  candidateId: "a1",
  rejectedBy: "quietHours",
  reason: "quiet hours 22:00 to 08:00 Europe/Istanbul; priority normal is below the floor (high)",
  surfaces: [],
  trace: [
    { id: "killSwitch", outcome: "pass", ms: 0.02 },
    { id: "consent",    outcome: "pass", ms: 0.01 },
    { id: "enabled",    outcome: "pass", ms: 0.01 },
    { id: "mode",       outcome: "pass", ms: 0.01 },
    { id: "snooze",     outcome: "pass", ms: 0.02 },
    { id: "mute",       outcome: "pass", ms: 0.01 },
    { id: "intensity",  outcome: "pass", ms: 0.02 },
    { id: "quietHours", outcome: "reject", reason: "quiet hours 22:00 to 08:00 …", ms: 0.09 }
  ],
  evaluatedAt: 2026-09-04T03:00:00.000Z
}

Şekil, replay komutunun --json çıktısından node scripts/trace-svg.mjs ile çizilir; her satır olduğu gibi alınmıştır.

Tek kapı ve kayıtlı bir gerekçe ile "kullanıcıya bu neden söylenmedi" sorusunun bir cevabı olur. Kontroller bir boru hattına dağılmışken dürüst cevap "bir yerde bir şey false döndü" olurdu.

Kontroller, varsayılanın çalıştırdığı sırayla

| # | kontrol | ne zaman reddeder | not | |---|---|---|---| | 1 | killSwitch(isOn) | bayrağınız açıksa | üretim acil durdurma; her üreticiyi aynı anda susturur | | 2 | consent() | user.consent false ise | her şeyden önce gelir, yoksa hiç rıza vermemiş biri için tercih değerlendirmiş olursunuz | | 3 | enabled() | user.proactiveEnabled === false ise | profil başına anahtar | | 4 | mode({ allow }) | user.mode listede değilse | örneğin yalnızca "normal", asla "focus" | | 5 | snooze() | user.snoozedUntil gelecekteyse | genel duraklatma | | 6 | mute() | candidate.type, user.mutedTypes içindeyse | tür bazlı susturma | | 7 | intensity() | öncelik, kullanıcının yoğunluk tabanının altındaysa | low yalnızca high duyar, normal normal ve üstünü, high her şeyi | | 8 | quietHours({ priorityFloor }) | kullanıcının yerel sessiz penceresi içindeyse | IANA saat dilimi, pencere gece yarısını geçebilir, taban ve üstünde atlanır; her gün tek pencere ya da güne göre bir çizelge | | 9 | trustRamp({ days, minPriority }) | kullanıcı days günden yeniyse ve öncelik tabanın altındaysa | sistem, kullanıcı en az bağışlayıcıyken en az kalibredir | | 10 | dismissalCooldown({ dismissals, withinDays, silenceDays }) | kullanıcı o türü pencere içinde dismissals kez reddettiyse | gate.record(user, candidate, "dismissed") ile beslenir; her yeni ret sessizliği yeniden başlatır | | 11 | adaptiveTiming({ nextGoodMoment, surfacesFor }) | asla | reddetmez: deliverAt değerini taşır ya da yüzeyleri daraltır; nonRejecting işaretli bir kontrol istese de reddedemez | | 12 | dailyBudget({ limit, bypassPriority }) | kullanıcının yerel gün sayacı sınırdaysa | evaluate okur, commit atomik artırır ve yine de reddedebilir |

Güne göre değişen sessiz saatler

Çalışma haftası her yerde pazartesiden cumaya değildir ve tatil günü zaten hafta içi değildir. quietHours tek bir pencerenin yanında bir çizelge de alır:

quietHours: {
  default: { start: "22:00", end: "08:00" },
  days: { fri: { start: "00:00", end: "23:59" }, sat: { start: "00:00", end: "23:59" }, sun: null },
  dates: { "2026-12-25": { start: "00:00", end: "23:59" } },
}

Tarih haftanın gününü, o da varsayılanı yener; null o günün sessiz saati yok demektir ve varsayılanın içinden bir iş günü böyle oyulur. Bir pencere açıldığı güne aittir, yani gece yarısını geçen bir pencere ertesi sabahı susturur ve gerekçe hangi günden geldiğini söyler.

Bunun bilerek yapmadığı iki şey var. Gömülü bir tatil takvimi yok: hangi tarihleri tuttuğunuz size aittir, gömülü olan ise kimse fark etmeden bayatlar. Ve tek bir satır 24 saatten fazlasını anlatamaz, yani cuma akşamından cumartesi akşamına uzanan bir sessizlik iki satırdır: fri: 18:00 to 00:00 ve sat: 00:00 to 20:00.

Tek pencere geçmek eskisi gibi çalışır ve yaygın durum olmayı sürdürür; her günü aynı pencereye çıkan bir çizelge, o pencereyle birebir aynı davranır.

Sıra bir tasarım kararıdır ve görünür olmalıdır. Rıza her şeyden önce gelmelidir. Sessiz saatler bütçeden önce gelmelidir, yoksa reddedilen bir aday hiç yapmadığı bir teslimi tüketir. İstediğiniz gibi yeniden sıralayın; iz ne yaptığınızı gösterecektir.

import { createGate, checks } from "proactive-gate";

const gate = createGate({
  checks: [
    checks.consent(),
    checks.quietHours({ priorityFloor: "high" }),
    checks.dailyBudget({ limit: 3, bypassPriority: "critical" }),
    myOwnCheck, // { id, run(ctx) => pass | reject | adjust | skip }
  ],
});

Kendi kontrolünüzü yazmak

Bir kontrol, id ve run fonksiyonu olan bir nesnedir. Kullanıcıyı, adayı, saati, çözülmüş önceliği, depoyu ve hâlâ masada olan yüzeyleri alır; pass, gerekçeli reject, adjust ya da skip döndürür. İzde yerleşik kontroller gibi görünür.

const weekendFloor = {
  id: "weekendFloor",
  run: ({ now, priority }) => {
    const day = now.getUTCDay();
    if ((day === 0 || day === 6) && priority !== "high" && priority !== "critical") {
      return { kind: "reject", reason: "weekend: only high priority" };
    }
    return { kind: "pass" };
  },
};
const gate = createGate({ checks: [checks.consent(), weekendFloor, checks.dailyBudget({ limit: 5 })] });

Bir kontrol yalnızca zamanlamayı taşıyabiliyor ya da yüzeyleri daraltabiliyorsa nonRejecting: true işaretleyin; kapı ondan gelen bir reddi yok sayar ve bunu izde söyler, böylece bir zamanlama modelindeki hata bir kullanıcıyı susturamaz.

Politika bir veridir

Aynı kontroller bir JSON belgesi olarak da yazılabilir; ürün ekibi kuralları dağıtım yapmadan değiştirir ve aynı dosya TypeScript'te, Python'da, CLI'da ve oyun alanında çalışır:

{
  "specVersion": "1.0.0",
  "checks": [
    { "id": "consent" },
    { "id": "snooze", "defer": true },
    { "id": "quietHours", "priorityFloor": "high" },
    { "preset": "usTcpa" },
    { "id": "utilityFloor", "costFalseAlarm": 1, "costMissedHelp": 2, "shadow": true },
    { "id": "dailyBudget", "limit": 3, "bypassPriority": "critical", "nearLimit": 0.67 }
  ]
}
const gate = createGate({ policy: JSON.parse(await readFile("policy.json", "utf8")), store });

Her girdi bir kontrol id'si ya da bir preset ve o kontrolün seçeneklerini taşır. Bilinmeyen bir id hata fırlatır ve bilinenleri sayar. Şema spec/schema/policy.schema.json dosyasındadır; examples/policy.js, fonksiyon gerektiren kontroller için kaçış yolu olarak durur.

Erteleme, gölge modu, sınıra yakınlık notları ve kancalar

Bir kontrol reddetmek yerine defer diyebilir: karar allowed: false, deferredBy ve retryAt taşır, çağıran ne zaman tekrar deneyeceğini bilir. snooze({ defer: true }) yerleşik örnektir.

shadow: true işaretli bir kontrol çalışır ve izde gerçek sonucuyla görünür, ama mesajı durduramaz; id'si decision.shadowed listesine düşer. Yeni bir kuralı bir hafta gölgede çalıştırın, kaç kez ateşleyeceğini sayın, sonra açın.

Bütçeler eşiğe (varsayılan yüzde 80) ulaşan geçişte nearLimit: { used, limit } bildirir; decision.nearLimit altında listelenir, böylece bir pano kimin susmak üzere olduğunu gösterir.

hooks: { before, after, error, finally } her kontrolü milisaniye maliyetiyle gözler; hata fırlatan bir kanca error kancasına yönlendirilir ve kararı asla değiştirmez. examples/otel.ts bunları kontrol başına bir span'e çevirir. Her kararın bir id'si vardır ve commit bu id üzerinde tekrarlanabilir: zaman aşımından sonraki bir yeniden deneme ikinci bir birim tüketmez.

Bir ürün yöneticisinin okuyabileceği ret gerekçesi

decision.reason izi elinde tutan mühendis için yazılmıştır. explain(decision) aynı kararı, asistanın fazla konuşkan olup olmadığına karar veren kişi için cümlelere çevirir; aynı izden, hiçbir şey eklemeden:

import { explain } from "proactive-gate";

explain(decision).summary;
// "Held until 08:00 because the user's quiet hours run 22:00 to 08:00 Europe/Istanbul and
//  normal priority is below the critical floor needed to override them."

"Bu mesaj 22:30'da neden gitti" sorusunun da bir yanıtı olur: izin verilen karar da kendini anlatır, explanation.checks ise çalışan her kontrol için sırayla bir cümle taşır. Renderer kararın saf bir fonksiyonudur: saat okumaz, adaya da kullanıcıya da bakmaz, dolayısıyla gate'in vermediği bir kararı anlatamaz. Hiçbir şablona uymayan bir gerekçe tahmin edilmez, olduğu gibi alıntılanır ve söyleyen kontrolün adıyla verilir. language bir parametredir: İngilizce en olarak gelir, başka bir dil catalogs içinde İngilizcenin üzerine birleşen bir Partial<Sentences>'tır, böylece yarım bir çeviri de görüntülenir. Python kardeşi aynı cümleleri yollar, CI her push'ta iki uygulamanın her fixture kararını karşılaştırır. Bu bölümü @LouisDeconinck #28 ile iki dilde birden yazdı.

İsteğe bağlı, kendi modelinizin beslediği kontroller

İkisi de kapalı gelir; adayın üzerine çağıranın koyduğu sayıları okurlar.

  • utilityFloor({ costFalseAlarm, costMissedHelp }) yalnızca candidate.pAccept değeri tau = cFA / (cFA + pNeed * cFN) eşiğini geçtiğinde konuşur (pNeed varsayılanı 1); pAccept yoksa atlar. Bu eşik klasik Bayes karar sınırıdır: konuşmanın maliyeti (1 - p) * cFA, susmanın maliyeti p * cFN, hangisi küçükse o seçilir. Uyarı alanındaki karşılığı Horvitz, Jacobs ve Hovel, "Attention-Sensitive Alerting", UAI 1999; o makaledeki sistemin adı Priorities.
  • boundedDeferral({ lambda, interruptCost, staleness, boundSeconds }) asla reddetmez. candidate.busy doğruysa deliverAt değerini now + t* yapar; t* = min(bound, lambda * interruptCost / (2 * staleness)), varsayılanlar 116 saniye verir. Türetim Achlioptas ve Horvitz, "Principles of Bounded Deferral" makalesinden.

Hangi varsayılan ölçüldü, hangisi bizim tercihimiz

Buradaki her varsayılan ya bir çalışmadan geliyor ve kaynağı yazılıyor, ya da bir kanaat ve bunu söylüyoruz. Birinci türden tek bir tane var.

| varsayılan | nereden geliyor | |---|---| | boundedDeferral içindeki lambda = 1/43 | Ölçüm. Yukarıdaki makale: 113 çalışan, üç ardışık iş günü, 10.00 ile 16.00 arası, 4.803 meşgul durum, ortalama meşguliyet süresi 43,12 saniye, standart sapma 51,79 saniye | | staleness = 0.0001, boundSeconds = 240 | Ölçek tercihi. t* yalnızca interruptCost / staleness oranına bağlı; bu çift "birkaç dakika" demenin bir yolu, iki sayıyı da sabitleyen bir bulgu yok | | trustRamp 7 gün | Bizim. Hiçbir çalışma bu sayıyı vermiyor | | dismissalCooldown 30 günde 3 kapatma, 7 gün sessizlik | Bizim. Kapatma, kullanıcının verdiği en net sinyal olduğu için biçim savunulabilir; üç sayı bize ait | | dailyBudget 5 | Bizim, ama yönü destekli. Pielot ve Rello'nun aktardığı yerinde günlük kayıt çalışmasında katılımcılar günde ortanca 63,5 bildirim alıyor; bir avuç mesaj bunun çok altında. O çalışma "beş" demiyor |

Ölçülen tek sayının içindeki dağılım, sayının kendisinden değerli: aynı makalenin iki kişilik çözümlemesinde uyarı sonrası düşük maliyetli duruma geçiş ortalaması birinde 11, diğerinde 101 saniye. İki kişi arasındaki fark varsayılanın kendisinden büyük.

Ertelemenin dayanağı var, susmanın bedeli de var

Ertelemenin işe yaradığına dair en güçlü kanıt Okoshi, Tsubouchi ve Tokuda, Pervasive and Mobile Computing 50:1-24 (2018): Yahoo! JAPAN Android uygulaması, 680.000'den fazla kullanıcı, üç hafta; bildirimi uygun ana kadar bekletmek yanıt süresini yüzde 49,7 kısaltmış. Bu, yönü destekler; bu paketteki hiçbir pencereyi, bütçeyi veya bekleme süresini desteklemez.

Karşı ağırlık da burada durmalı, çünkü susturan bir kapı bedelsiz değil. Pielot ve Rello, MobileHCI 2017 çalışmasında 30 gönüllü bir gün boyunca bildirimleri kapatmış. Daha az dağılmışlar, ama aynı zamanda bir şeyi kaçırmaktan endişelenmiş, telefonlarına daha sık bakmış ve çevrelerinden kopuk hissetmişler. Otuz kişiden on beşi acil bir şeyi kaçırmaktan korktuğunu söylemiş. Çalışma için görüşülen üç kişi, işyerinde sürekli ulaşılabilir olmaları beklendiği için katılmayı reddetmiş. Kullanıcının seçmediği bir sessizliğin bir bedeli var ve o bedel bu kütüphanenin yazdığı hiçbir izde görünmüyor.

Benimsemeden önce bilmeniz gereken bir sınır

Hata değil ve testle sabitlendi, böylece ileride değişecekse bilerek değişir.

Hafta, ISO haftasıdır; haftalık bütçe pazartesi yenilenir. Pazartesi, çoğu insan için haftanın başladığı gün değildir: en kalabalık yirmi ülkeden CLDR'ye göre yedisinde pazartesi, on birinde pazar, ikisinde cumartesi başlar; bunu kendiniz new Intl.Locale("und-EG").getWeekInfo().firstDay ile okuyabilirsiniz. Çalışma haftası pazardan perşembeye uzanan yerlerde ISO yenilenmesi haftanın birinci gününe denk gelir: pazar günü bütçesini harcayan bir kullanıcı pazartesi sabahı bütçesini geri alır ve önünde hâlâ dört iş günü vardır. Anahtar yine de ISO, iki nedenle. Deponuzdaki sayaç bu anahtarla tutuluyor ve anahtarı taşımak her kullanıcıyı haftanın ortasında sessizce sıfırlar. Ayrıca sayacın döndüğü gün, kullanıcının korunduğu gün değildir: sessiz saatler kullanıcının kendi haftalık gününü zaten okuyor, cuma ya da Şabat penceresi dahil, ve bildirimin ne zaman verilebileceğine onlar karar veriyor. Bütçe yalnız kaç tane olacağına karar verir. ISO haftası sizin kullanıcılarınız için yanlışsa kendi bütçe kontrolünüzü istediğiniz anahtarla yazın: id ve run taşıyan bir nesnedir, istediğiniz sırada dizilir ve izde yerleşik kontrollerin yanında görünür.

Hazır paketler: platform kotaları ve yasal sınırlar, kaynaklarıyla

import { presets } from "proactive-gate/presets";
const gate = createGate({ checks: [checks.consent(), ...presets.kakaoBrandMessage()] });

| paket | ne kodlar | |---|---| | lineMessagingApi({ plan }) | LINE planına göre aylık push bütçesi: 200, 5.000 ya da 30.000 | | wechatSubscriptionMessage | abonelik onayı başına bir mesaj | | wechatCustomerService | kullanıcının son mesajından sonraki 48 saat içinde en çok 5 | | wechatTemplateMessage | yalnızca kullanıcı eyleminden sonra, günde 3 şablon | | wecomAppMessage | üye başına dakikada 30 ve saatte 1.000 | | kakaoAlimtalk | yalnızca rıza; AlimTalk'ta saat kuralı yok | | kakaoBrandMessage | reklam rızası, 08:00 ile 20:50 Asia/Seoul | | krNetworkAct50 | reklam rızası, ayrıca 21:00 ile 08:00 yerel saat için gece rızası | | jpAntiSpamLaw | opt-in | | cnMinorMode | reşit olmayanlar için: 06:00 ile 22:00 Asia/Shanghai ve günde bir | | inTcccp | promosyon rızası; varsayılan olarak kapalı olan 00:00-10:00 ve 21:00-24:00 bantları ayrı ayrı opt-in ister (@LouisDeconinck, #29) | | brLgpd | pazarlama rızası; reşit olmayanlarda rıza ebeveynden ya da yasal vasiden gelir | | usTcpa | kullanıcının yerel saatiyle 08:00 ile 21:00 (47 CFR 64.1200) | | euEprivacy | pazarlama rızası, mevcut müşteriler için yumuşak opt-in | | telegramBot | sohbet başına saniyede 1 ve dakikada 20 | | slackApp | kanal başına saniyede 1 | | whatsappBusiness({ template }) | WhatsApp opt-in; serbest metin yalnızca 24 saatlik müşteri hizmetleri penceresinde, kullanıcı başına saatte 600 |

Her paket sources (sayıların geldiği sayfalar) ve neyi dışarıda bıraktığını söyleyen bir note taşır. Gözden geçirilebilir varsayılanlar, hukuki tavsiye değil: birkaç resmi kaynak birbiriyle çelişir ve not hangi değerin neden seçildiğini söyler.

Yasal bir pakete uzanmadan önce kapsamını okuyun. Yukarıdaki bütün düzenlemeler ticari iletişimi düzenler. usTcpa, euEprivacy, krNetworkAct50, jpAntiSpamLaw, inTcccp ve brLgpd birer pazarlama kuralıdır; yani mesajınızı ancak mesajın kendisi ticari olduğunda bağlar. Kullanıcının kendi istediği bir hatırlatma reklam değildir ve onun için pazarlama paketi kullanmak, yasanın size hiç koymadığı bir kısıtı kendi elinizle içeri almak olur. Aday promosyon niteliğindeyse kullanın; değilse dürüst sınırlar platform kotaları ve kendi sessiz saatlerinizdir. Alıcının kendi saat dilimini okuyan paketlerde (usTcpa, krNetworkAct50, inTcccp) user.timezone yoksa karşılaştırılacak yerel saat de yoktur: o kontroller atlanır ve mesaj çıkar. Her paketin notu bunu söyler; saat dilimi olmayan kullanıcı, bu paketlerden birine güvenmeden önce çözülmesi gereken durumdur.

Bazı ülkelerin neden burada olmadığı da aynı kapsam sınavıyla açıklanır. Kanada'nın CASL'i ve Avustralya'nın 2003 tarihli Spam Act'i rıza, gönderen kimliği ve abonelikten çıkma yükümlülükleri getirir; ikisinde de saat kısıtı yoktur. Brezilya tabloda penceresiz durur: internette dolaşan pencere PLS 48/2018 sayılı kanun teklifinden gelir, yürürlükteki bir kanundan değil, ve telefonla pazarlama aramalarını kapsar; bu yüzden brLgpd LGPD'nin rıza modelini kodlar, pencere kodlamaz. Hindistan ilginç olanı: sıkça tekrarlanan "09.00-21.00" birincil metinde yazmaz. TRAI düzenlemesi zaman bantlarını, içerik kategorisi ve gün tipiyle birlikte, abonenin operatörüne kaydettirdiği bir tercih yapar; sabit bir yasal sessizlik penceresi değildir. Üstelik pencereyi aktaran ikincil kaynaklar başlangıcın 09.00 mı 10.00 mı olduğunda birbiriyle çelişir. Düzenlemenin sabitlediği şey varsayılan durumdur: Schedule-II'deki dokuz bandın dördü, yani 00:00-10:00 ve 21:00-24:00 arasını kapsayanlar, abone o bandı açmadıkça her müşteri için kapalıdır. inTcccp bu bantları sabit bir pencere yerine bant başına birer opt-in rızası olarak kodlar.

Adaptörler

| alt yol | framework | kapı nerede durur | |---|---|---| | proactive-gate/ai-sdk | Vercel AI SDK | bir aracın needsApproval sorusunu yanıtlar (çevrimdışı çalışan örnek: examples/ai-sdk/) | | proactive-gate/mastra | Mastra | gönderimden önce bir çıktı işlemcisi (çevrimdışı çalışan örnek: examples/mastra/) | | proactive-gate/langchain | LangChain | gönderim aracının çevresinde middleware | | proactive-gate/openai-agents | OpenAI Agents | bir guardrail | | npx proactive-gate hook | Claude Code | bir PreToolUse kancası (examples/claude-code-hook.json) |

Adaptörler framework paketine değil, çağrının biçimine göre tiplenmiştir; başka bir şey kurmak gerekmez. Her biri kapının gerekçesiyle reddeder ve onayda bütçeyi tüketir.

Örneklerden ikisi framework kurulu olmadan ve ağ olmadan çalışır: node examples/mastra/run.mjs ve node examples/ai-sdk/run.mjs. İkisi de npm run examples ve test paketinin parçası. Diğer dört .ts dosyası ise fikstür değil, örnekleme: @langchain/langgraph, @mastra/core ve AI SDK'yı içe aktarıyorlar, hiçbiri buranın bağımlılığı değil, dolayısıyla ne derleniyor ne çalıştırılıyorlar ve bu README onların çalıştığını iddia etmiyor. Denetlenen şey, bizim denetleyebildiğimiz yarısı: test/example-imports.test.mjs, bu dosyaların proactive-gate'ten içe aktardığı her sembolün değer ya da tip olarak hâlâ var olduğunu doğrular, böylece bir export'un adı değiştiğinde yayımlanmış bir örnek okuyucuya artık var olmayan bir şeyi içe aktarmasını söylemeye sessizce devam edemez.

Python

pip install proactive-gate

Yayınlanmamış bir durumu denemek için depodan kurulur: pip install "proactive-gate @ git+https://github.com/Bubblegunn/proactive-gate#subdirectory=python". Python paketi npm paketiyle aynı workflow tarafından yayımlanıyor, dolayısıyla her dosyayı hangi deponun ve hangi workflow'un ürettiğini adlandıran PyPI yayın attestation'ları taşıyor. 0.2.2 öncesi sürümler yerel bir derlemeden token ile yüklendi ve hiçbir kanıt taşımıyor.

python/ sapan bir port değil, bir kardeştir: spec/fixtures altındaki her senaryoyu senkron Gate ve AsyncGate (Redis, redis.asyncio üzerinden) ile geçer; mypy strict, CI'da Python 3.11 ve 3.13. Bkz. python/README.md.

Sözleşme ve ikinci bir uygulama yazmak

spec/SPEC.md davranışı numaralı gereksinimler olarak yazar; spec/fixtures dile bağlı olmayan senaryoları tutar: America/New_York'taki yaz saati kenarı, Pacific/Apia, 2031'de bir duvar saati senaryosu, atomik commit, ISO haftası, erteleme, gölge modu, isteğe bağlı kontroller ve dokuz hazır paket. clock/ alanı adversarial saat setidir: silinen ve tekrarlanan yaz saati saatleri, 23 ve 25 saatlik yerel günler, hafta ortasında saat dilimi değişimi, Apia'da atlanan bir takvim günü, ISO yılı takvim yılından farklı haftalar, geriye saran bir saat ve set sorana kadar iki uygulamanın da yanlış yaptığı 1000'den küçük yıllar (#36, #37, #38). TypeScript ve Python testleri hepsini çalıştırır; npx proactive-gate replay --fixtures spec/fixtures komut satırından çalıştırır. Üçüncü bir uygulama bu kaynaktan değil, senaryolardan başlar.

Bütçe evaluate'te değil, commit'te uygulanır

İki örnek aynı kullanıcı için aynı adayı değerlendirebilir, ikisi de beşte dördün kullanıldığını görebilir ve ikisi de göndermeye karar verebilir. Bir sınırı yarış durumuna karşı güvenle uygulayabileceğiniz tek yer, göndermeden hemen önceki atomik artırmadır:

const decision = await gate.evaluate(input);        // reads the counter
if (decision.allowed && await gate.commit(decision, input)) {   // INCR, returns false on the sixth
  await send(...);
}

RedisStore, INCR kullanır ve günün TTL'sini ilk artırmada ekler. Sayaç kullanıcının yerel gününe göre anahtarlanır, bu yüzden bütçe UTC'de değil kullanıcının gece yarısında sıfırlanır.

Taşıma iki kez ilettiğinde tek mesaj

Mesaj taşımaları en az bir kez iletir. 200 yanıtını yeterince hızlı alamayan bir webhook yeniden gönderilir, bir kuyruk aynı olayı iki işçiye verir, zaman aşımından sonraki bir deneme ilki çoktan başarılı olmuşken gelir. Kullanıcı aynı mesajı iki kez görür ve bunu bozuk bir asistan diye okur.

dedupe bunun için, ve istemediğiniz sürece kapalıdır:

const gate = createGate({ checks: defaultChecks({ dedupe: true }), store });

await gate.evaluate({
  user,
  candidate: { id: crypto.randomUUID(), type: "shipping", dedupeKey: "order:42:shipped" },
});

Anahtar sizin, çünkü iki denemeyi aynı olay yapan şeyi yalnızca siz bilirsiniz. Onu olaydan türetin, order:42:shipped gibi; her deneme için yeni üretilen bir kimlikten ya da mesaj metninden değil, çünkü metin genellikle bir zaman damgası taşır ve her denemede farklı olur. dedupeKey yoksa kontrol bunu söyleyerek kenara çekilir, bir anahtar uydurup sessizce hiçbir şey yapmaz.

Elle yazılmış sürümlerin genellikle yanlış yaptığı üç nokta var.

Talep atomiktir ve commit anında olur. Aynı olayı tutan iki işçi, henüz hiçbir şey kaydedilmeden değerlendirir; yalnızca okuyan bir kontrol onları ayıramaz. dedupe evaluate'te okur, commit'te bütçelerin kullandığı artırmayla talep eder ve yalnızca ilk artırmayı alan gönderebilir. Önce oku sonra yaz biçiminde bir talep ikisini de geçirir ve şartname bunu uygunsuz sayar.

Bastırılan bir kopya mesaj hakkı harcamaz. dedupe bütçelerden önce tüketir, bu yüzden yarışı kaybettiğinde kapı orada durur ve sayaç hiç artmaz. Bu sıralamanın bedeli de gerçek: dedupe'u geçip ardından tükenmiş bir bütçeye takılan bir olay, pencerenin kalanı için anahtarını yakmış olur.

Pencere ilk talepten itibaren sabittir, kaymaz. Arka arkaya gelen kopyalar bitiş zamanını ileri itmez; buradaki her mağaza, bir anahtar yeniden artırıldığında ilk bitiş zamanını korur.

Varsayılan pencere 24 saat, ve bu bizim seçtiğimiz bir sayı değil, yaygın yeniden deneme ufku: Stripe bir idempotency anahtarını "en az 24 saatlik olduktan sonra" siliyor, Nylas da webhook tekilleştirmesi için aynı rakamı güvenli varsayılan olarak veriyor. windowSeconds değerini kendi taşımanızın yeniden deneme ufkundan seçin.

Bilerek açık başarısız olur

Depoya bağlı bir kontrol hata fırlattığında (Redis düştü), varsayılan adayı geçirir ve ize outcome: "skip", reason: "check threw (…); failing open" yazar. Bir önbellek kesintisi, bütün amacı konuşmak olan bir ürünün her kullanıcısını susturmamalıdır. Ürününüz sessiz kalmayı tercih ediyorsa onStoreError: "closed" geçin; aynı hata, kontrolü adıyla anan bir ret olur.

Bir politikayı yayınlamadan önce bir günü yeniden oynatın

CLI, { user, candidate, now } satırlarından oluşan bir JSONL dosyası alır ve bir politikanın ne yapacağını raporlar. --commit, üretimde olduğu gibi bütçeyi sırayla tüketir.

npx proactive-gate replay examples/day.jsonl --commit
17 candidates  ·  7 allowed (41.2%)  ·  10 rejected

check        rejected  example
---------------------------------------------------------------
intensity           3  priority low is below the "normal" intensity floor (normal)
consent             3  user has not consented to proactive behaviour
mode                2  operating mode "focus" does not allow proactive messages
quietHours          1  quiet hours 22:00 to 08:00 Europe/Istanbul; priority normal is below the floor (critical)
dailyBudget         1  daily budget of 5 used (5)

--policy examples/policy.js kendi kapınızı yükler; --json bir not defteri için satır başına tam bir karar basar. Bir haftalık gerçek adayı önerilen bir politikaya karşı oynatın; izin oranını ve sessizlik nedenlerini tek bir kullanıcı öğrenmeden önce bilirsiniz.

Olandan öğrenmek

await gate.record(user, candidate, "dismissed");   // feeds dismissalCooldown
await gate.record(user, candidate, "acted");       // recorded for you to extend
await gate.inspect(user);                          // { budgetUsed, dismissals }

Sessizlik ölçülebilir olmalıdır, yoksa bahaneye dönüşür. Her kararı onDecision ile kaydedin; izin oranı, en sık ret nedenleri ve izin verilenlerin reddedilme oranı, kapının ayarlı olup olmadığını söyleyen üç sayıdır.

Bunu yapmaz

  • Neyin söylenmeye değer olduğuna karar vermez. O üreten yarıdır ve modelinize ve ürününüze aittir.
  • Değeri dikkate karşı puanlamaz. adaptiveTiming, kullanıcının bir sonraki iyi anı için kendi modelinize bir kancadır; paket böyle bir model içermez.
  • Ürünler arasında koordinasyon yapmaz. Üç ajan her biri üçlük bir bütçeye uyarsa kullanıcı yine dokuz alır. Ajanlar arası katman ayrı bir problemdir.
  • Rıza hukukunun yerine geçmez. consent() sizin belirlediğiniz bir boolean'ı kontrol eder; onu nasıl aldığınız size aittir.

Nereden geliyor

Bu, Şubat 2026'dan beri tek başıma geliştirdiğim proaktif asistan LILA'nın teslim kapısıdır; çıkarılıp framework'ten bağımsız hâle getirildi. On iki kontrolün sırası, güven rampası, otuzda üç soğuması ve açık başarısız olan bütçe, üretimde verilmiş ve The hardest part of a proactive assistant is knowing when not to speak yazısında savunulmuş kararlardır. Tian Pan'ın bildirim bütçesi yazısı aynı davayı ürün tarafından savunur ve günde üç ile beş arası bir tavan önerir; defaultChecks({ dailyLimit }) varsayılanı beştir.

Biçimin daha eski akrabaları var. Matrix push kuralları, ilk eşleşen kuralın karar verdiği sıralı bir listedir. Android bildirim kanalları ve iOS kesinti seviyeleri kullanıcıya tür başına bir anahtar ve sessiz saati aşan bir öncelik tabanı verir. Horvitz'in karma girişim çalışmaları iki isteğe bağlı kontrolü sağladı. Bu paket o fikirleri izli tek bir listeye koyar ve onların dışarıda bıraktığı parçayı ekler: gönderim anında tüketilen bütçe.

Atıf

Her sürüm Zenodo'da bir DOI ile arşivleniyor, böylece bir makale ya da rapor tam olarak çalıştırdığı koda işaret edebiliyor.

DOI

Bu kavram DOI'si: her zaman en yeni sürüme çözümlenir. Çalıştırdığınız sürümün kendisini atıflamak için o sayfayı açıp yan çubuktan sürümü seçin ve orada yazan DOI'yi kullanın. Depodaki CITATION.cff aynı tanımlayıcıyı taşıyor, bu yüzden GitHub'ın "Cite this repository" düğmesi elle kopyalama olmadan doğru BibTeX ve APA üretiyor.

Geliştirme

npm ci
npm test               # tsc build, spec-lint, then node:test over dist/test
cd python && pytest    # the Python sibling against the same fixtures

MIT.