proactive-gate 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # proactive-gate
2
2
 
3
+ English | [Türkçe](README.tr.md)
4
+
3
5
  Decide whether a proactive AI agent may reach a user right now, and log why not.
4
6
 
5
7
  A proactive assistant has two halves. The generating half decides what is worth
@@ -28,7 +30,9 @@ if (decision.allowed && (await gate.commit(decision, { user, candidate }))) {
28
30
 
29
31
  Zero dependencies. TypeScript. Node 20 or newer. Framework-agnostic: the gate sits
30
32
  between "the model produced something" and "the user's phone buzzed", whichever
31
- model or framework produced it.
33
+ model or framework produced it. Examples: [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts),
34
+ [`examples/mastra.ts`](examples/mastra.ts), and a replayable policy in
35
+ [`examples/policy.js`](examples/policy.js).
32
36
 
33
37
  ## What a decision looks like
34
38
 
@@ -54,6 +58,8 @@ model or framework produced it.
54
58
  }
55
59
  ```
56
60
 
61
+ <p align="center"><img src="assets/trace.png" width="900" alt="A real decision trace: eight checks ran, quiet hours rejected, each with its reason and cost"></p>
62
+
57
63
  With one gate and a logged reason, "why was the user not told about this" has an
58
64
  answer. With checks scattered through a pipeline, the honest answer is "somewhere,
59
65
  something returned false".
@@ -92,6 +98,31 @@ const gate = createGate({
92
98
  });
93
99
  ```
94
100
 
101
+ ### Writing your own check
102
+
103
+ A check is an object with an `id` and a `run` function. It receives the user, the
104
+ candidate, the clock, the resolved priority, the store and the surfaces still on the
105
+ table, and returns `pass`, `reject` with a reason, `adjust`, or `skip`. It appears in the
106
+ trace like every built-in one.
107
+
108
+ ```ts
109
+ const weekendFloor = {
110
+ id: "weekendFloor",
111
+ run: ({ now, priority }) => {
112
+ const day = now.getUTCDay();
113
+ if ((day === 0 || day === 6) && priority !== "high" && priority !== "critical") {
114
+ return { kind: "reject", reason: "weekend: only high priority" };
115
+ }
116
+ return { kind: "pass" };
117
+ },
118
+ };
119
+ const gate = createGate({ checks: [checks.consent(), weekendFloor, checks.dailyBudget({ limit: 5 })] });
120
+ ```
121
+
122
+ Mark a check `nonRejecting: true` when it may only move timing or narrow surfaces; the
123
+ gate then ignores a reject from it and says so in the trace, so a bug in a timing model
124
+ cannot silence a user.
125
+
95
126
  ## The budget is enforced at commit, not at evaluate
96
127
 
97
128
  Two instances can both evaluate a candidate for the same user, both see four of
package/README.tr.md ADDED
@@ -0,0 +1,216 @@
1
+ # proactive-gate
2
+
3
+ [English](README.md) | Türkçe
4
+
5
+ Proaktif bir yapay zekâ ajanının kullanıcıya şu an ulaşıp ulaşamayacağına karar verir ve
6
+ neden ulaşamadığını kaydeder.
7
+
8
+ Proaktif bir asistanın iki yarısı vardır. Üreten yarı neyin söylenmeye değer olduğuna karar
9
+ verir. Bastıran yarı ise onu şimdi mi, sonra mı, hiç mi söyleyeceğine karar verir. Proaktif
10
+ yapay zekâ üzerine yazılan hemen her şey ilk yarı hakkındadır. Bu paket ikinci yarıdır: tek
11
+ kapı, sıralı bir kontrol listesi ve her ret için bir gerekçe.
12
+
13
+ ```
14
+ npm install proactive-gate
15
+ ```
16
+
17
+ ```ts
18
+ import { createGate, defaultChecks, RedisStore } from "proactive-gate";
19
+
20
+ const gate = createGate({
21
+ store: new RedisStore(redis), // MemoryStore() for one instance
22
+ checks: defaultChecks({ dailyLimit: 3, quietHoursFloor: "high" }),
23
+ onDecision: (d) => log.info("gate", d), // every decision, allowed or not
24
+ });
25
+
26
+ const decision = await gate.evaluate({ user, candidate });
27
+ if (decision.allowed && (await gate.commit(decision, { user, candidate }))) {
28
+ await send(decision.surfaces, candidate.payload);
29
+ }
30
+ ```
31
+
32
+ Sıfır bağımlılık. TypeScript. Node 20 ya da üstü. Framework'ten bağımsız: kapı, "model bir
33
+ şey üretti" ile "kullanıcının telefonu titredi" arasında durur; hangi model ya da framework
34
+ üretmiş olursa olsun. Örnekler: [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts),
35
+ [`examples/mastra.ts`](examples/mastra.ts) ve yeniden oynatılabilir bir politika olarak
36
+ [`examples/policy.js`](examples/policy.js).
37
+
38
+ ## Bir karar neye benzer
39
+
40
+ ```ts
41
+ {
42
+ allowed: false,
43
+ userId: "ayse",
44
+ candidateId: "a1",
45
+ rejectedBy: "quietHours",
46
+ reason: "quiet hours 22:00 to 08:00 Europe/Istanbul; priority normal is below the floor (high)",
47
+ surfaces: [],
48
+ trace: [
49
+ { id: "killSwitch", outcome: "pass", ms: 0.02 },
50
+ { id: "consent", outcome: "pass", ms: 0.01 },
51
+ { id: "enabled", outcome: "pass", ms: 0.01 },
52
+ { id: "mode", outcome: "pass", ms: 0.01 },
53
+ { id: "snooze", outcome: "pass", ms: 0.02 },
54
+ { id: "mute", outcome: "pass", ms: 0.01 },
55
+ { id: "intensity", outcome: "pass", ms: 0.02 },
56
+ { id: "quietHours", outcome: "reject", reason: "quiet hours 22:00 to 08:00 …", ms: 0.09 }
57
+ ],
58
+ evaluatedAt: 2026-09-04T03:00:00.000Z
59
+ }
60
+ ```
61
+
62
+ <p align="center"><img src="assets/trace.png" width="900" alt="Gerçek bir karar izi: sekiz kontrol çalıştı, sessiz saatler reddetti, her biri gerekçesi ve maliyetiyle"></p>
63
+
64
+ Tek kapı ve kayıtlı bir gerekçe ile "kullanıcıya bu neden söylenmedi" sorusunun bir cevabı
65
+ olur. Kontroller bir boru hattına dağılmışken dürüst cevap "bir yerde bir şey false döndü"
66
+ olurdu.
67
+
68
+ ## Kontroller, varsayılanın çalıştırdığı sırayla
69
+
70
+ | # | kontrol | ne zaman reddeder | not |
71
+ |---|---|---|---|
72
+ | 1 | `killSwitch(isOn)` | bayrağınız açıksa | üretim acil durdurma; her üreticiyi aynı anda susturur |
73
+ | 2 | `consent()` | `user.consent` false ise | her şeyden önce gelir, yoksa hiç rıza vermemiş biri için tercih değerlendirmiş olursunuz |
74
+ | 3 | `enabled()` | `user.proactiveEnabled === false` ise | profil başına anahtar |
75
+ | 4 | `mode({ allow })` | `user.mode` listede değilse | örneğin yalnızca `"normal"`, asla `"focus"` |
76
+ | 5 | `snooze()` | `user.snoozedUntil` gelecekteyse | genel duraklatma |
77
+ | 6 | `mute()` | `candidate.type`, `user.mutedTypes` içindeyse | tür bazlı susturma |
78
+ | 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 |
79
+ | 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 |
80
+ | 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 |
81
+ | 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 |
82
+ | 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 |
83
+ | 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 |
84
+
85
+ Sıra bir tasarım kararıdır ve görünür olmalıdır. Rıza her şeyden önce gelmelidir. Sessiz
86
+ saatler bütçeden önce gelmelidir, yoksa reddedilen bir aday hiç yapmadığı bir teslimi
87
+ tüketir. İstediğiniz gibi yeniden sıralayın; iz ne yaptığınızı gösterecektir.
88
+
89
+ ```ts
90
+ import { createGate, checks } from "proactive-gate";
91
+
92
+ const gate = createGate({
93
+ checks: [
94
+ checks.consent(),
95
+ checks.quietHours({ priorityFloor: "high" }),
96
+ checks.dailyBudget({ limit: 3, bypassPriority: "critical" }),
97
+ myOwnCheck, // { id, run(ctx) => pass | reject | adjust | skip }
98
+ ],
99
+ });
100
+ ```
101
+
102
+ ### Kendi kontrolünüzü yazmak
103
+
104
+ Bir kontrol, `id` ve `run` fonksiyonu olan bir nesnedir. Kullanıcıyı, adayı, saati, çözülmüş
105
+ önceliği, depoyu ve hâlâ masada olan yüzeyleri alır; `pass`, gerekçeli `reject`, `adjust` ya
106
+ da `skip` döndürür. İzde yerleşik kontroller gibi görünür.
107
+
108
+ ```ts
109
+ const weekendFloor = {
110
+ id: "weekendFloor",
111
+ run: ({ now, priority }) => {
112
+ const day = now.getUTCDay();
113
+ if ((day === 0 || day === 6) && priority !== "high" && priority !== "critical") {
114
+ return { kind: "reject", reason: "weekend: only high priority" };
115
+ }
116
+ return { kind: "pass" };
117
+ },
118
+ };
119
+ const gate = createGate({ checks: [checks.consent(), weekendFloor, checks.dailyBudget({ limit: 5 })] });
120
+ ```
121
+
122
+ Bir kontrol yalnızca zamanlamayı taşıyabiliyor ya da yüzeyleri daraltabiliyorsa
123
+ `nonRejecting: true` işaretleyin; kapı ondan gelen bir reddi yok sayar ve bunu izde söyler,
124
+ böylece bir zamanlama modelindeki hata bir kullanıcıyı susturamaz.
125
+
126
+ ## Bütçe evaluate'te değil, commit'te uygulanır
127
+
128
+ İki örnek aynı kullanıcı için aynı adayı değerlendirebilir, ikisi de beşte dördün kullanıldığını
129
+ görebilir ve ikisi de göndermeye karar verebilir. Bir sınırı yarış durumuna karşı güvenle
130
+ uygulayabileceğiniz tek yer, göndermeden hemen önceki atomik artırmadır:
131
+
132
+ ```ts
133
+ const decision = await gate.evaluate(input); // reads the counter
134
+ if (decision.allowed && await gate.commit(decision, input)) { // INCR, returns false on the sixth
135
+ await send(...);
136
+ }
137
+ ```
138
+
139
+ `RedisStore`, `INCR` kullanır ve günün TTL'sini ilk artırmada ekler. Sayaç kullanıcının yerel
140
+ 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.
141
+
142
+ ## Bilerek açık başarısız olur
143
+
144
+ Depoya bağlı bir kontrol hata fırlattığında (Redis düştü), varsayılan adayı geçirir ve ize
145
+ `outcome: "skip", reason: "check threw (…); failing open"` yazar. Bir önbellek kesintisi, bütün
146
+ amacı konuşmak olan bir ürünün her kullanıcısını susturmamalıdır. Ürününüz sessiz kalmayı
147
+ tercih ediyorsa `onStoreError: "closed"` geçin; aynı hata, kontrolü adıyla anan bir ret olur.
148
+
149
+ ## Bir politikayı yayınlamadan önce bir günü yeniden oynatın
150
+
151
+ CLI, `{ user, candidate, now }` satırlarından oluşan bir JSONL dosyası alır ve bir politikanın
152
+ ne yapacağını raporlar. `--commit`, üretimde olduğu gibi bütçeyi sırayla tüketir.
153
+
154
+ ```
155
+ npx proactive-gate replay examples/day.jsonl --commit
156
+ ```
157
+
158
+ ```
159
+ 17 candidates · 7 allowed (41.2%) · 10 rejected
160
+
161
+ check rejected example
162
+ ---------------------------------------------------------------
163
+ intensity 3 priority low is below the "normal" intensity floor (normal)
164
+ consent 3 user has not consented to proactive behaviour
165
+ mode 2 operating mode "focus" does not allow proactive messages
166
+ quietHours 1 quiet hours 22:00 to 08:00 Europe/Istanbul; priority normal is below the floor (critical)
167
+ dailyBudget 1 daily budget of 5 used (5)
168
+ ```
169
+
170
+ `--policy examples/policy.js` kendi kapınızı yükler; `--json` bir not defteri için satır başına
171
+ tam bir karar basar. Bir haftalık gerçek adayı önerilen bir politikaya karşı oynatın; izin oranını
172
+ ve sessizlik nedenlerini tek bir kullanıcı öğrenmeden önce bilirsiniz.
173
+
174
+ ## Olandan öğrenmek
175
+
176
+ ```ts
177
+ await gate.record(user, candidate, "dismissed"); // feeds dismissalCooldown
178
+ await gate.record(user, candidate, "acted"); // recorded for you to extend
179
+ await gate.inspect(user); // { budgetUsed, dismissals }
180
+ ```
181
+
182
+ Sessizlik ölçülebilir olmalıdır, yoksa bahaneye dönüşür. Her kararı `onDecision` ile
183
+ kaydedin; izin oranı, en sık ret nedenleri ve izin verilenlerin reddedilme oranı, kapının
184
+ ayarlı olup olmadığını söyleyen üç sayıdır.
185
+
186
+ ## Bunu yapmaz
187
+
188
+ - Neyin söylenmeye değer olduğuna karar vermez. O üreten yarıdır ve modelinize ve ürününüze
189
+ aittir.
190
+ - Değeri dikkate karşı puanlamaz. `adaptiveTiming`, kullanıcının bir sonraki iyi anı için
191
+ kendi modelinize bir kancadır; paket böyle bir model içermez.
192
+ - Ürünler arasında koordinasyon yapmaz. Üç ajan her biri üçlük bir bütçeye uyarsa kullanıcı
193
+ yine dokuz alır. Ajanlar arası katman ayrı bir problemdir.
194
+ - Rıza hukukunun yerine geçmez. `consent()` sizin belirlediğiniz bir boolean'ı kontrol eder;
195
+ onu nasıl aldığınız size aittir.
196
+
197
+ ## Nereden geliyor
198
+
199
+ Bu, Şubat 2026'dan beri tek başıma geliştirdiğim proaktif asistan
200
+ [LILA](https://efe-genc-portfolio.vercel.app/projects/lila/)'nın teslim kapısıdır; çıkarılıp
201
+ framework'ten bağımsız hâle getirildi. On iki kontrolün sırası, güven rampası, otuzda üç
202
+ soğuması ve açık başarısız olan bütçe, üretimde verilmiş ve
203
+ [The hardest part of a proactive assistant is knowing when not to speak](https://efe-genc-portfolio.vercel.app/writing/knowing-when-not-to-speak/)
204
+ yazısında savunulmuş kararlardır. Tian Pan'ın
205
+ [bildirim bütçesi](https://tianpan.co/blog/2026-05-13-background-agents-notification-budget-attention-economy)
206
+ yazısı aynı davayı ürün tarafından savunur ve günde üç ile beş arası bir tavan önerir;
207
+ `defaultChecks({ dailyLimit })` varsayılanı beştir.
208
+
209
+ ## Geliştirme
210
+
211
+ ```
212
+ npm ci
213
+ npm test # tsc build, then node:test over dist/test
214
+ ```
215
+
216
+ MIT.
@@ -190,3 +190,22 @@ test("replay summarises a day of candidates and consumes the budget in order", a
190
190
  assert.match(text, /quietHours/);
191
191
  assert.match(text, /consent/);
192
192
  });
193
+ test("a custom check is an ordinary object: it runs in order, reads the context, and shows in the trace", async () => {
194
+ const weekendsOnlyHigh = {
195
+ id: "weekendFloor",
196
+ run: ({ now, priority }) => {
197
+ const day = now.getUTCDay();
198
+ if ((day === 0 || day === 6) && priority !== "high" && priority !== "critical")
199
+ return { kind: "reject", reason: "weekend: only high priority" };
200
+ return { kind: "pass" };
201
+ },
202
+ };
203
+ const gate = createGate({ checks: [checks.consent(), weekendsOnlyHigh, checks.dailyBudget({ limit: 5 })] });
204
+ const saturday = new Date("2026-09-05T10:00:00Z");
205
+ const d = await gate.evaluate({ user: user(), candidate: candidate(), now: saturday });
206
+ assert.equal(d.rejectedBy, "weekendFloor");
207
+ assert.deepEqual(d.trace.map((t) => `${t.id}:${t.outcome}`), ["consent:pass", "weekendFloor:reject"]);
208
+ const monday = await gate.evaluate({ user: user(), candidate: candidate(), now: new Date("2026-09-07T10:00:00Z") });
209
+ assert.equal(monday.allowed, true);
210
+ assert.equal(monday.trace.length, 3);
211
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proactive-gate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Decide whether a proactive AI agent may reach a user right now, and log why not. Ordered checks: kill switch, consent, quiet hours, trust ramp, dismissal cooldown, daily budget.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",