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 +32 -1
- package/README.tr.md +216 -0
- package/dist/test/gate.test.js +19 -0
- package/package.json +1 -1
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.
|
package/dist/test/gate.test.js
CHANGED
|
@@ -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.
|
|
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",
|