proactive-gate 0.1.2 → 0.2.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 +305 -11
- package/README.tr.md +209 -3
- package/dist/src/adapters/ai-sdk.d.ts +35 -0
- package/dist/src/adapters/ai-sdk.js +17 -0
- package/dist/src/adapters/langchain.d.ts +32 -0
- package/dist/src/adapters/langchain.js +20 -0
- package/dist/src/adapters/mastra.d.ts +25 -0
- package/dist/src/adapters/mastra.js +16 -0
- package/dist/src/adapters/openai-agents.d.ts +31 -0
- package/dist/src/adapters/openai-agents.js +16 -0
- package/dist/src/checks.d.ts +118 -12
- package/dist/src/checks.js +196 -24
- package/dist/src/cli.d.ts +14 -1
- package/dist/src/cli.js +154 -24
- package/dist/src/conformance.d.ts +42 -0
- package/dist/src/conformance.js +83 -0
- package/dist/src/gate.d.ts +13 -5
- package/dist/src/gate.js +73 -28
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +3 -1
- package/dist/src/init.d.ts +16 -0
- package/dist/src/init.js +115 -0
- package/dist/src/policy.d.ts +8 -0
- package/dist/src/policy.js +74 -0
- package/dist/src/presets.d.ts +9 -0
- package/dist/src/presets.js +45 -0
- package/dist/src/stores.js +5 -8
- package/dist/src/types.d.ts +74 -2
- package/package.json +57 -9
- package/dist/test/gate.test.d.ts +0 -1
- package/dist/test/gate.test.js +0 -261
package/README.md
CHANGED
|
@@ -37,11 +37,24 @@ if (decision.allowed && (await gate.commit(decision, { user, candidate }))) {
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Or start from a policy file and the wiring for your framework, in one command:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npx proactive-gate init --preset usTcpa --framework mastra
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That writes `proactive-gate.policy.json` with the ten checks in order, appends the
|
|
47
|
+
preset you named, and prints the preset's own source next to the few lines that plug
|
|
48
|
+
the gate into that framework. `npx proactive-gate init --list` shows the fourteen
|
|
49
|
+
platform and legal presets and the four frameworks.
|
|
50
|
+
|
|
40
51
|
Zero dependencies. TypeScript. Node 20 or newer. Framework-agnostic: the gate sits
|
|
41
52
|
between "the model produced something" and "the user's phone buzzed", whichever
|
|
42
53
|
model or framework produced it. Examples: [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts),
|
|
43
54
|
[`examples/mastra.ts`](examples/mastra.ts), [`examples/langgraph.ts`](examples/langgraph.ts), and a
|
|
44
|
-
replayable policy in [`examples/policy.
|
|
55
|
+
replayable policy in [`examples/policy.json`](examples/policy.json). Docs and a browser playground:
|
|
56
|
+
[bubblegunn.github.io/proactive-gate](https://bubblegunn.github.io/proactive-gate/). API reference:
|
|
57
|
+
[`docs/api`](docs/api/README.md). Python: [`python/`](python/README.md).
|
|
45
58
|
|
|
46
59
|
## What a decision looks like
|
|
47
60
|
|
|
@@ -67,7 +80,10 @@ replayable policy in [`examples/policy.js`](examples/policy.js). API reference:
|
|
|
67
80
|
}
|
|
68
81
|
```
|
|
69
82
|
|
|
70
|
-
<p align="center"><img src="assets/trace.
|
|
83
|
+
<p align="center"><img src="docs/assets/trace.svg" width="900" alt="Two real decision traces side by side: candidate a1 rejected by quietHours after eight checks, candidate a5 allowed after thirteen, each check with its outcome, reason and cost"></p>
|
|
84
|
+
|
|
85
|
+
The figure is drawn from the replay's `--json` output by `node scripts/trace-svg.mjs`, every
|
|
86
|
+
line verbatim; the left decision is the one printed above.
|
|
71
87
|
|
|
72
88
|
With one gate and a logged reason, "why was the user not told about this" has an
|
|
73
89
|
answer. With checks scattered through a pipeline, the honest answer is "somewhere,
|
|
@@ -96,6 +112,22 @@ consumed in check order at commit, so when a weekly check passes and the daily o
|
|
|
96
112
|
refuses, that weekly unit is spent without a delivery. It only happens when two commits
|
|
97
113
|
race after a shared evaluate.
|
|
98
114
|
|
|
115
|
+
### Two limits you should know before you adopt this
|
|
116
|
+
|
|
117
|
+
Neither is a bug, and both are pinned by tests so a future change has to be deliberate.
|
|
118
|
+
|
|
119
|
+
**The week is the ISO week, so the weekly budget refills on Monday.** Where the working
|
|
120
|
+
week runs Sunday to Thursday, that refill lands one day in: a user who spends the budget on
|
|
121
|
+
Sunday has it back on Monday, with four working days still to run. Changing the key would
|
|
122
|
+
move every counter already in your store, so it is documented rather than quietly altered.
|
|
123
|
+
Pass your own budget check keyed how you like if the ISO week is wrong for your users.
|
|
124
|
+
|
|
125
|
+
**Quiet hours are a single window, the same on every day of the week.** A user carries one
|
|
126
|
+
`start` and one `end`, so a Friday window, a Shabbat window or a public holiday cannot be
|
|
127
|
+
expressed. The day of the week is never read. If you need one, write a check: it is an
|
|
128
|
+
object with an `id` and a `run`, it composes in the order you choose, and the trace will
|
|
129
|
+
show it firing beside the built-in ones.
|
|
130
|
+
|
|
99
131
|
Order is a design decision and it should be visible. Consent has to come before
|
|
100
132
|
everything. Quiet hours have to come before the budget, or a rejected candidate
|
|
101
133
|
consumes a delivery it never made. Reorder freely; the trace will show what you did.
|
|
@@ -138,6 +170,168 @@ Mark a check `nonRejecting: true` when it may only move timing or narrow surface
|
|
|
138
170
|
gate then ignores a reject from it and says so in the trace, so a bug in a timing model
|
|
139
171
|
cannot silence a user.
|
|
140
172
|
|
|
173
|
+
## A policy is data
|
|
174
|
+
|
|
175
|
+
The same checks as a JSON document, so a product team can change the rules without a
|
|
176
|
+
deploy and the same file runs in TypeScript, in Python, in the CLI and in the
|
|
177
|
+
[playground](https://bubblegunn.github.io/proactive-gate/playground/):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"specVersion": "1.0.0",
|
|
182
|
+
"checks": [
|
|
183
|
+
{ "id": "consent" },
|
|
184
|
+
{ "id": "snooze", "defer": true },
|
|
185
|
+
{ "id": "quietHours", "priorityFloor": "high" },
|
|
186
|
+
{ "preset": "usTcpa" },
|
|
187
|
+
{ "id": "utilityFloor", "costFalseAlarm": 1, "costMissedHelp": 2, "shadow": true },
|
|
188
|
+
{ "id": "dailyBudget", "limit": 3, "bypassPriority": "critical", "nearLimit": 0.67 }
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const gate = createGate({ policy: JSON.parse(await readFile("policy.json", "utf8")), store });
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Each entry names a check `id` or a `preset` plus that check's options. An unknown id throws
|
|
198
|
+
and names the known ones. `compilePolicy` is exported for callers that want the check list,
|
|
199
|
+
and the schema is at [`spec/schema/policy.schema.json`](spec/schema/policy.schema.json).
|
|
200
|
+
`examples/policy.js` stays as the escape hatch for checks that need functions.
|
|
201
|
+
|
|
202
|
+
## Defer, shadow mode, near-limit notes and hooks
|
|
203
|
+
|
|
204
|
+
A check can `defer` instead of rejecting: the decision has `allowed: false`, `deferredBy` and
|
|
205
|
+
`retryAt`, and the caller knows when to try again. `snooze({ defer: true })` is the built-in
|
|
206
|
+
example.
|
|
207
|
+
|
|
208
|
+
A check with `shadow: true` runs and is traced with its real outcome, but cannot stop the
|
|
209
|
+
message; its id lands in `decision.shadowed`. Ship a new rule in shadow for a week, count how
|
|
210
|
+
often it would have fired, then turn it on.
|
|
211
|
+
|
|
212
|
+
Budgets report `nearLimit: { used, limit }` on the pass that reaches the threshold (80 percent
|
|
213
|
+
by default), listed under `decision.nearLimit`, so a dashboard can show who is about to go
|
|
214
|
+
quiet.
|
|
215
|
+
|
|
216
|
+
`hooks: { before, after, error, finally }` observe every check with its cost in milliseconds;
|
|
217
|
+
a hook that throws is routed to `error` and never changes the decision. `examples/otel.ts`
|
|
218
|
+
turns them into one span per check. Every decision has an `id`, and `commit` is idempotent on
|
|
219
|
+
it: a retry after a timeout does not consume a second unit.
|
|
220
|
+
|
|
221
|
+
## Optional checks, fed by your own model
|
|
222
|
+
|
|
223
|
+
Both ship off. They read numbers the caller puts on the candidate.
|
|
224
|
+
|
|
225
|
+
- `utilityFloor({ costFalseAlarm, costMissedHelp })` acts only when `candidate.pAccept` clears
|
|
226
|
+
`tau = cFA / (cFA + pNeed * cFN)` (`pNeed` defaults to 1) and skips when there is no
|
|
227
|
+
`pAccept`. That threshold is the classical Bayes decision boundary: alerting costs
|
|
228
|
+
`(1 - p) * cFA`, silence costs `p * cFN`, so you alert when the first is the smaller.
|
|
229
|
+
The alerting application is [Horvitz, Jacobs and Hovel, "Attention-Sensitive Alerting",
|
|
230
|
+
UAI 1999](https://arxiv.org/abs/1301.6707), whose system is named Priorities.
|
|
231
|
+
- `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` never rejects. When
|
|
232
|
+
`candidate.busy` is true it moves `deliverAt` to `now + t*`, with
|
|
233
|
+
`t* = min(bound, lambda * interruptCost / (2 * staleness))`; the defaults give 116 seconds.
|
|
234
|
+
|
|
235
|
+
Neither check ships a model, a cost or a probability. `costFalseAlarm`, `costMissedHelp`,
|
|
236
|
+
`interruptCost` and `staleness` are yours to measure, and the package has no opinion about
|
|
237
|
+
what an interruption costs your users. The field measurement people usually reach for is
|
|
238
|
+
[Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
|
|
239
|
+
2007](https://erichorvitz.com/CHI_2007_Iqbal_Horvitz.pdf), which logged real users and put
|
|
240
|
+
the return to a suspended task in the region of 11 to 16 minutes. The widely repeated "23
|
|
241
|
+
minutes 15 seconds" figure is not from a peer-reviewed paper and is not used here.
|
|
242
|
+
|
|
243
|
+
`boundedDeferral` implements the derivation in [Achlioptas and Horvitz, "Principles of
|
|
244
|
+
Bounded Deferral for Balancing Information Awareness with
|
|
245
|
+
Interruption"](http://erichorvitz.com/Bounded_Deferral.pdf): expected cost is stationary
|
|
246
|
+
where `f'(t0) = lambda * c`, so a quadratic staleness `f(t) = s * t²` gives
|
|
247
|
+
`t* = lambda * c / (2 * s)`.
|
|
248
|
+
|
|
249
|
+
## Which defaults are measured and which are ours
|
|
250
|
+
|
|
251
|
+
Every default here is either taken from a study, which is then named, or chosen by
|
|
252
|
+
judgement, which is then admitted. There is one of the first kind.
|
|
253
|
+
|
|
254
|
+
| default | where it comes from |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `lambda = 1/43` in `boundedDeferral` | Measured. Achlioptas and Horvitz above: 113 Microsoft employees (42 program managers, 25 developers, 19 testers, 10 administrators, 9 managers, 4 in sales and marketing, 4 research scientists), three sequential business days between 10am and 4pm, 4,803 busy situations, mean busy session 43.12 s, standard deviation 51.79 s |
|
|
257
|
+
| `staleness = 0.0001`, `boundSeconds = 240` | Scale choices. Only the ratio `interruptCost / staleness` changes `t*`, so this pair is one way to write "a few minutes". Nothing fixes either number |
|
|
258
|
+
| `trustRamp` 7 days | Ours. No study sets it |
|
|
259
|
+
| `dismissalCooldown` 3 in 30 days buying 7 days | Ours. A dismissal is the clearest signal a user gives, so the shape is defensible; the three numbers are not from anywhere |
|
|
260
|
+
| `dailyBudget` 5 | Ours, in a supported direction. Pielot and Rello (below) cite an in-situ log study where participants received a median of 63.5 notifications a day, so a handful sits far below the ambient load. Nothing in that work says five |
|
|
261
|
+
|
|
262
|
+
The spread inside the one measured number is worth more than the number. The same paper's
|
|
263
|
+
two-subject analysis puts the mean time to a lower-cost state after an alert at 11 seconds
|
|
264
|
+
for one person and 101 seconds for the other, so the variation between two people is larger
|
|
265
|
+
than the default itself. Measure your own users before you trust it.
|
|
266
|
+
|
|
267
|
+
### Deferring is supported; silence is not free
|
|
268
|
+
|
|
269
|
+
The strongest evidence that deferral works at all is [Okoshi, Tsubouchi and Tokuda,
|
|
270
|
+
"Real-world large-scale study on adaptive notification scheduling on smartphones",
|
|
271
|
+
*Pervasive and Mobile Computing* 50:1-24
|
|
272
|
+
(2018)](https://keio.elsevierpure.com/en/publications/real-world-large-scale-study-on-adaptive-notification-scheduling-/):
|
|
273
|
+
the Yahoo! JAPAN Android app, more than 680,000 users over three weeks, where holding a
|
|
274
|
+
notification until an interruptible moment was detected cut response time by 49.7 percent
|
|
275
|
+
against immediate delivery. That supports the direction. It says nothing about any window,
|
|
276
|
+
budget or cooldown in this package.
|
|
277
|
+
|
|
278
|
+
The counterweight belongs here too, because a gate that suppresses is not free. In [Pielot
|
|
279
|
+
and Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications", MobileHCI
|
|
280
|
+
2017](https://arxiv.org/abs/1612.02314), 30 volunteers switched notifications off for a day.
|
|
281
|
+
They were less distracted, and they also worried about missing information, checked their
|
|
282
|
+
phones more often, and felt less connected to the people around them. Fifteen of the thirty
|
|
283
|
+
agreed they were afraid of missing something urgent. Three people approached for the study
|
|
284
|
+
refused outright, because their workplace expected them to be reachable. A silence your user
|
|
285
|
+
did not choose costs them something, and that cost does not appear in any trace this library
|
|
286
|
+
prints.
|
|
287
|
+
|
|
288
|
+
## Presets: platform quotas and legal limits, with sources
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { presets } from "proactive-gate/presets";
|
|
292
|
+
const gate = createGate({ checks: [checks.consent(), ...presets.kakaoBrandMessage()] });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| preset | encodes |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `lineMessagingApi({ plan })` | monthly push budget by LINE plan: 200, 5,000 or 30,000 |
|
|
298
|
+
| `wechatSubscriptionMessage` | one message per subscription opt-in |
|
|
299
|
+
| `wechatCustomerService` | within 48 h of the user's last message, at most 5 |
|
|
300
|
+
| `wechatTemplateMessage` | only after a user action, 3 templates a day |
|
|
301
|
+
| `wecomAppMessage` | 30 a minute and 1,000 an hour per member |
|
|
302
|
+
| `kakaoAlimtalk` | consent only; AlimTalk has no time-of-day rule |
|
|
303
|
+
| `kakaoBrandMessage` | advertising consent, 08:00 to 20:50 Asia/Seoul |
|
|
304
|
+
| `krNetworkAct50` | advertising consent, plus night consent for 21:00 to 08:00 local |
|
|
305
|
+
| `jpAntiSpamLaw` | opt-in |
|
|
306
|
+
| `cnMinorMode` | for minors: 06:00 to 22:00 Asia/Shanghai and one a day |
|
|
307
|
+
| `usTcpa` | 08:00 to 21:00 at the user's local time (47 CFR 64.1200) |
|
|
308
|
+
| `euEprivacy` | marketing consent with the soft opt-in for existing customers |
|
|
309
|
+
| `telegramBot` | 1 a second and 20 a minute per chat |
|
|
310
|
+
| `slackApp` | 1 a second per channel |
|
|
311
|
+
|
|
312
|
+
Each preset carries `sources` (the pages the numbers come from) and a `note` on what it leaves
|
|
313
|
+
out. Reviewable defaults, not legal advice: several official sources disagree with each other,
|
|
314
|
+
and the note says which value was chosen and why.
|
|
315
|
+
|
|
316
|
+
**Read the scope before you reach for a legal preset.** Every instrument above regulates
|
|
317
|
+
*commercial* communication. `usTcpa`, `euEprivacy`, `krNetworkAct50` and `jpAntiSpamLaw` are
|
|
318
|
+
marketing rules, so they bind your message only when the message itself is commercial. A
|
|
319
|
+
reminder your user asked for is not advertising, and pulling in a marketing preset for it
|
|
320
|
+
imports a restriction the law never placed on you, which is its own kind of wrong answer.
|
|
321
|
+
Use them when the candidate is promotional; when it is not, the platform quotas and your own
|
|
322
|
+
quiet hours are the honest constraints.
|
|
323
|
+
|
|
324
|
+
That scope test is also why some jurisdictions people ask for are missing. Canada's CASL and
|
|
325
|
+
Australia's Spam Act 2003 set consent, identification and unsubscribe duties, and neither
|
|
326
|
+
carries a time-of-day rule at all. The Brazilian window quoted around the web comes from bill
|
|
327
|
+
PLS 48/2018, a proposal rather than enacted law, and it covers telemarketing calls. India is
|
|
328
|
+
the interesting one: the widely repeated "9am to 9pm" is not what the primary text says. The
|
|
329
|
+
Telecom Commercial Communications Customer Preference Regulations make time bands a
|
|
330
|
+
*preference the subscriber registers* with their access provider, alongside content category
|
|
331
|
+
and day type, not a fixed statutory quiet window, and the secondary sources that quote a
|
|
332
|
+
window disagree with each other about whether it starts at 09:00 or 10:00. A preset built on
|
|
333
|
+
that would encode a number no primary source states, so there is none.
|
|
334
|
+
|
|
141
335
|
## The budget is enforced at commit, not at evaluate
|
|
142
336
|
|
|
143
337
|
Two instances can both evaluate a candidate for the same user, both see four of
|
|
@@ -210,22 +404,115 @@ to keep once the checks are scattered:
|
|
|
210
404
|
- A policy can be replayed over a day of real candidates before it ships, and a non-rejecting
|
|
211
405
|
check cannot reject even if a bug makes it try.
|
|
212
406
|
|
|
407
|
+
Those are claims, so the repository runs them. `npm run bench:compare` replays a
|
|
408
|
+
committed day of 21 candidates for 7 users through `bench/naive.mjs`, an honest
|
|
409
|
+
hand-rolled policy of five `if` statements, and through a gate built from
|
|
410
|
+
`bench/fixtures/policy.json`:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
gate: 11 sent, 10 stopped
|
|
414
|
+
hand-rolled: 13 sent, 8 stopped
|
|
415
|
+
|
|
416
|
+
6 disagreements, and none of them is a matter of taste:
|
|
417
|
+
a5 a critical alert: the gate lets priority bypass the cap, the cap in the if statements does not
|
|
418
|
+
b1 a two-day-old account: the gate holds normal messages back for a week, the if statements never knew
|
|
419
|
+
c1 the user pressed snooze: the gate defers to when it ends, the if statements have no snooze
|
|
420
|
+
e1 three dismissals of this type: the gate is silent for a week, the if statements do not track outcomes
|
|
421
|
+
f4 01:00 in Tokyo, a new local day: the gate resets the cap, the UTC-day key stays on yesterday for nine more hours
|
|
422
|
+
g4 18:00 in Los Angeles, still the same local day: the UTC-day key already rolled, so the cap pays out twice
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The hand-rolled policy is not a straw man. It checks consent, enabled, mute, quiet
|
|
426
|
+
hours and a daily cap, which is what actually gets written, and it takes the three
|
|
427
|
+
shortcuts that actually get taken: a fixed UTC offset per zone, the cap keyed by the
|
|
428
|
+
UTC calendar day, and the cap read then written. `test/naive.test.mjs` pins each one
|
|
429
|
+
against a real instant:
|
|
430
|
+
|
|
431
|
+
- The clocks change. At `2026-11-01T12:30:00Z` New York has left daylight time, so it is
|
|
432
|
+
07:30 there and inside quiet hours; an offset captured in the summer computes 08:00 and
|
|
433
|
+
sends. That is twice a year, for every zone that observes it.
|
|
434
|
+
- The day boundary is local. The same UTC-day key silences the Tokyo user for the nine
|
|
435
|
+
hours between local midnight and 09:00, and hands the Los Angeles user a second full
|
|
436
|
+
budget at 17:00 while it is still their afternoon.
|
|
437
|
+
- Two deliveries are in flight. Read, compare, write lets both take the last slot, and the
|
|
438
|
+
counter still reads 2 afterwards, so nothing looks wrong. `commit()` takes the unit with
|
|
439
|
+
an atomic increment and returns `false` to the loser.
|
|
440
|
+
|
|
213
441
|
A feature-flag system does a different job better: rolling a behaviour out to a percentage
|
|
214
442
|
of users, per-tenant overrides, and an audit trail of who flipped what. Use flags to decide
|
|
215
443
|
whether the gate runs at all, and the gate to decide whether this message reaches this
|
|
216
444
|
person now.
|
|
217
445
|
|
|
218
|
-
##
|
|
446
|
+
## Adapters
|
|
219
447
|
|
|
220
|
-
|
|
|
448
|
+
| subpath | framework | where the gate sits |
|
|
221
449
|
|---|---|---|
|
|
222
|
-
| Vercel AI SDK | [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts)
|
|
223
|
-
| Mastra | [`examples/mastra.ts`](examples/mastra.ts)
|
|
224
|
-
|
|
|
450
|
+
| `proactive-gate/ai-sdk` | Vercel AI SDK | answers a tool's `needsApproval` ([`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts); runnable offline: [`examples/ai-sdk/`](examples/ai-sdk/)) |
|
|
451
|
+
| `proactive-gate/mastra` | Mastra | an output processor before the send ([`examples/mastra.ts`](examples/mastra.ts); runnable offline: [`examples/mastra/`](examples/mastra/)) |
|
|
452
|
+
| `proactive-gate/langchain` | LangChain | middleware around the send tool |
|
|
453
|
+
| `proactive-gate/openai-agents` | OpenAI Agents | a guardrail |
|
|
454
|
+
| `npx proactive-gate hook` | Claude Code | a `PreToolUse` hook ([`examples/claude-code-hook.json`](examples/claude-code-hook.json)) |
|
|
455
|
+
|
|
456
|
+
The adapters are typed against the shape of a call, not against the framework package, so
|
|
457
|
+
nothing else has to be installed. Each denies with the gate's reason and commits the budget on
|
|
458
|
+
approval. The pattern is the same everywhere: the model decides whether there is something to
|
|
459
|
+
say, `gate.evaluate` decides whether it may be said now, and `gate.commit` runs right before
|
|
460
|
+
the message leaves. [`examples/langgraph.ts`](examples/langgraph.ts) shows the same thing
|
|
461
|
+
inside a LangGraph node.
|
|
462
|
+
|
|
463
|
+
Two of the examples run without the framework installed and without a network: `node
|
|
464
|
+
examples/mastra/run.mjs` makes the same `processOutputResult` call Mastra makes, over a day of
|
|
465
|
+
candidates with the clock taken from each line, and `node examples/ai-sdk/run.mjs` answers a
|
|
466
|
+
day of tool-approval requests, one of which is a critical alert that a legal window (the TCPA
|
|
467
|
+
preset) still refuses. Both are part of `npm run examples` and of the test suite.
|
|
468
|
+
|
|
469
|
+
## Python
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
pip install proactive-gate
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from proactive_gate import Gate
|
|
477
|
+
gate = Gate.from_policy(policy) # the same policy.json
|
|
478
|
+
decision = gate.evaluate(inp)
|
|
479
|
+
if decision.allowed and gate.commit(decision, inp): send(...)
|
|
480
|
+
```
|
|
225
481
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
482
|
+
`python/` is a sibling, not a port that drifts: it passes every fixture under `spec/fixtures`
|
|
483
|
+
through a sync `Gate` and an `AsyncGate` (Redis over `redis.asyncio`), with mypy strict, on
|
|
484
|
+
Python 3.11 and 3.13 in CI. See [`python/README.md`](python/README.md).
|
|
485
|
+
|
|
486
|
+
## Properties, not just examples
|
|
487
|
+
|
|
488
|
+
`test/properties.test.ts` generates gates, users and candidates from a seeded
|
|
489
|
+
32-bit PRNG and asserts what has to hold for all of them, rather than for the
|
|
490
|
+
cases someone thought of:
|
|
491
|
+
|
|
492
|
+
- The trace is always a prefix of the declared check order. Nothing is skipped,
|
|
493
|
+
nothing is reordered, every check reports exactly once, and a stopped decision
|
|
494
|
+
ends on the check that stopped it with a reason attached.
|
|
495
|
+
- A check marked `nonRejecting` cannot stop a decision even when it returns a
|
|
496
|
+
rejection on purpose.
|
|
497
|
+
- However many deliveries race, `commit()` hands out exactly `min(racers, limit)`
|
|
498
|
+
units, and replaying one decision any number of times spends one.
|
|
499
|
+
- `MemoryStore` and `SqliteStore` answer the same random sequence of `get`, `set`,
|
|
500
|
+
`incr`, `del` and clock movement identically, TTLs included.
|
|
501
|
+
|
|
502
|
+
The generator is forty lines because the package has no dependencies; a property
|
|
503
|
+
library would shrink failures better. Each assertion prints its seed, so a failure
|
|
504
|
+
reproduces exactly. The race property was checked against a mutant: rewriting
|
|
505
|
+
`consume` as read-then-write, the shortcut in `bench/naive.mjs`, makes it fail.
|
|
506
|
+
|
|
507
|
+
## The spec, and writing a second implementation
|
|
508
|
+
|
|
509
|
+
[`spec/SPEC.md`](spec/SPEC.md) states the behaviour as numbered requirements, and
|
|
510
|
+
[`spec/fixtures`](spec/fixtures) holds language-neutral cases: the DST edge in
|
|
511
|
+
America/New_York, Pacific/Apia, a wall-clock case in 2031, atomic commit, the ISO week,
|
|
512
|
+
deferral, shadow mode, the optional checks and four presets. The TypeScript tests and the
|
|
513
|
+
Python tests both run all of them; `npx proactive-gate replay --fixtures spec/fixtures` runs
|
|
514
|
+
them from the command line. A third implementation starts from the fixtures, not from this
|
|
515
|
+
source.
|
|
229
516
|
|
|
230
517
|
## Performance
|
|
231
518
|
|
|
@@ -275,11 +562,18 @@ Tian Pan's
|
|
|
275
562
|
essay argues the same case from the product side and suggests a daily cap of three
|
|
276
563
|
to five; `defaultChecks({ dailyLimit })` defaults to five.
|
|
277
564
|
|
|
565
|
+
The shape has older relatives. Matrix push rules are an ordered list where the first matching
|
|
566
|
+
rule decides. Android notification channels and iOS interruption levels give the user a
|
|
567
|
+
per-type switch and a priority floor that bypasses quiet time. Horvitz's work on mixed
|
|
568
|
+
initiative supplied the two optional checks. This package puts those ideas in one list with a
|
|
569
|
+
trace, and adds the part they leave out: the budget consumed at send time.
|
|
570
|
+
|
|
278
571
|
## Development
|
|
279
572
|
|
|
280
573
|
```
|
|
281
574
|
npm ci
|
|
282
|
-
npm test
|
|
575
|
+
npm test # tsc build, spec-lint, then node:test over dist/test
|
|
576
|
+
cd python && pytest # the Python sibling against the same fixtures
|
|
283
577
|
```
|
|
284
578
|
|
|
285
579
|
MIT.
|
package/README.tr.md
CHANGED
|
@@ -33,7 +33,9 @@ Sıfır bağımlılık. TypeScript. Node 20 ya da üstü. Framework'ten bağıms
|
|
|
33
33
|
şey üretti" ile "kullanıcının telefonu titredi" arasında durur; hangi model ya da framework
|
|
34
34
|
üretmiş olursa olsun. Örnekler: [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts),
|
|
35
35
|
[`examples/mastra.ts`](examples/mastra.ts) ve yeniden oynatılabilir bir politika olarak
|
|
36
|
-
[`examples/policy.
|
|
36
|
+
[`examples/policy.json`](examples/policy.json). Dokümantasyon ve tarayıcıda oyun alanı:
|
|
37
|
+
[bubblegunn.github.io/proactive-gate](https://bubblegunn.github.io/proactive-gate/). Python:
|
|
38
|
+
[`python/`](python/README.md).
|
|
37
39
|
|
|
38
40
|
## Bir karar neye benzer
|
|
39
41
|
|
|
@@ -59,7 +61,10 @@ Sıfır bağımlılık. TypeScript. Node 20 ya da üstü. Framework'ten bağıms
|
|
|
59
61
|
}
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
<p align="center"><img src="assets/trace.
|
|
64
|
+
<p align="center"><img src="docs/assets/trace.svg" width="900" alt="Yan yana iki gerçek karar izi: a1 adayı sekiz kontrolden sonra sessiz saatlerde reddedildi, a5 adayı on üç kontrolden sonra geçti; her kontrol sonucu, gerekçesi ve maliyetiyle"></p>
|
|
65
|
+
|
|
66
|
+
Şekil, replay komutunun `--json` çıktısından `node scripts/trace-svg.mjs` ile çizilir; her satır
|
|
67
|
+
olduğu gibi alınmıştır.
|
|
63
68
|
|
|
64
69
|
Tek kapı ve kayıtlı bir gerekçe ile "kullanıcıya bu neden söylenmedi" sorusunun bir cevabı
|
|
65
70
|
olur. Kontroller bir boru hattına dağılmışken dürüst cevap "bir yerde bir şey false döndü"
|
|
@@ -123,6 +128,200 @@ Bir kontrol yalnızca zamanlamayı taşıyabiliyor ya da yüzeyleri daraltabiliy
|
|
|
123
128
|
`nonRejecting: true` işaretleyin; kapı ondan gelen bir reddi yok sayar ve bunu izde söyler,
|
|
124
129
|
böylece bir zamanlama modelindeki hata bir kullanıcıyı susturamaz.
|
|
125
130
|
|
|
131
|
+
## Politika bir veridir
|
|
132
|
+
|
|
133
|
+
Aynı kontroller bir JSON belgesi olarak da yazılabilir; ürün ekibi kuralları dağıtım yapmadan
|
|
134
|
+
değiştirir ve aynı dosya TypeScript'te, Python'da, CLI'da ve
|
|
135
|
+
[oyun alanında](https://bubblegunn.github.io/proactive-gate/playground/) çalışır:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"specVersion": "1.0.0",
|
|
140
|
+
"checks": [
|
|
141
|
+
{ "id": "consent" },
|
|
142
|
+
{ "id": "snooze", "defer": true },
|
|
143
|
+
{ "id": "quietHours", "priorityFloor": "high" },
|
|
144
|
+
{ "preset": "usTcpa" },
|
|
145
|
+
{ "id": "utilityFloor", "costFalseAlarm": 1, "costMissedHelp": 2, "shadow": true },
|
|
146
|
+
{ "id": "dailyBudget", "limit": 3, "bypassPriority": "critical", "nearLimit": 0.67 }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const gate = createGate({ policy: JSON.parse(await readFile("policy.json", "utf8")), store });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Her girdi bir kontrol `id`'si ya da bir `preset` ve o kontrolün seçeneklerini taşır. Bilinmeyen
|
|
156
|
+
bir id hata fırlatır ve bilinenleri sayar. Şema
|
|
157
|
+
[`spec/schema/policy.schema.json`](spec/schema/policy.schema.json) dosyasındadır;
|
|
158
|
+
`examples/policy.js`, fonksiyon gerektiren kontroller için kaçış yolu olarak durur.
|
|
159
|
+
|
|
160
|
+
## Erteleme, gölge modu, sınıra yakınlık notları ve kancalar
|
|
161
|
+
|
|
162
|
+
Bir kontrol reddetmek yerine `defer` diyebilir: karar `allowed: false`, `deferredBy` ve
|
|
163
|
+
`retryAt` taşır, çağıran ne zaman tekrar deneyeceğini bilir. `snooze({ defer: true })` yerleşik
|
|
164
|
+
örnektir.
|
|
165
|
+
|
|
166
|
+
`shadow: true` işaretli bir kontrol çalışır ve izde gerçek sonucuyla görünür, ama mesajı
|
|
167
|
+
durduramaz; id'si `decision.shadowed` listesine düşer. Yeni bir kuralı bir hafta gölgede
|
|
168
|
+
çalıştırın, kaç kez ateşleyeceğini sayın, sonra açın.
|
|
169
|
+
|
|
170
|
+
Bütçeler eşiğe (varsayılan yüzde 80) ulaşan geçişte `nearLimit: { used, limit }` bildirir;
|
|
171
|
+
`decision.nearLimit` altında listelenir, böylece bir pano kimin susmak üzere olduğunu gösterir.
|
|
172
|
+
|
|
173
|
+
`hooks: { before, after, error, finally }` her kontrolü milisaniye maliyetiyle gözler; hata
|
|
174
|
+
fırlatan bir kanca `error` kancasına yönlendirilir ve kararı asla değiştirmez.
|
|
175
|
+
`examples/otel.ts` bunları kontrol başına bir span'e çevirir. Her kararın bir `id`'si vardır ve
|
|
176
|
+
`commit` bu id üzerinde tekrarlanabilir: zaman aşımından sonraki bir yeniden deneme ikinci
|
|
177
|
+
bir birim tüketmez.
|
|
178
|
+
|
|
179
|
+
## İsteğe bağlı, kendi modelinizin beslediği kontroller
|
|
180
|
+
|
|
181
|
+
İkisi de kapalı gelir; adayın üzerine çağıranın koyduğu sayıları okurlar.
|
|
182
|
+
|
|
183
|
+
- `utilityFloor({ costFalseAlarm, costMissedHelp })` yalnızca `candidate.pAccept` değeri
|
|
184
|
+
`tau = cFA / (cFA + pNeed * cFN)` eşiğini geçtiğinde konuşur (`pNeed` varsayılanı 1);
|
|
185
|
+
`pAccept` yoksa atlar. Bu eşik klasik Bayes karar sınırıdır: konuşmanın maliyeti
|
|
186
|
+
`(1 - p) * cFA`, susmanın maliyeti `p * cFN`, hangisi küçükse o seçilir. Uyarı alanındaki
|
|
187
|
+
karşılığı [Horvitz, Jacobs ve Hovel, "Attention-Sensitive Alerting", UAI
|
|
188
|
+
1999](https://arxiv.org/abs/1301.6707); o makaledeki sistemin adı Priorities.
|
|
189
|
+
- `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` asla reddetmez.
|
|
190
|
+
`candidate.busy` doğruysa `deliverAt` değerini `now + t*` yapar;
|
|
191
|
+
`t* = min(bound, lambda * interruptCost / (2 * staleness))`, varsayılanlar 116 saniye verir.
|
|
192
|
+
Türetim [Achlioptas ve Horvitz, "Principles of Bounded
|
|
193
|
+
Deferral"](http://erichorvitz.com/Bounded_Deferral.pdf) makalesinden.
|
|
194
|
+
|
|
195
|
+
## Hangi varsayılan ölçüldü, hangisi bizim tercihimiz
|
|
196
|
+
|
|
197
|
+
Buradaki her varsayılan ya bir çalışmadan geliyor ve kaynağı yazılıyor, ya da bir kanaat ve
|
|
198
|
+
bunu söylüyoruz. Birinci türden tek bir tane var.
|
|
199
|
+
|
|
200
|
+
| varsayılan | nereden geliyor |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `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 |
|
|
203
|
+
| `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 |
|
|
204
|
+
| `trustRamp` 7 gün | Bizim. Hiçbir çalışma bu sayıyı vermiyor |
|
|
205
|
+
| `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 |
|
|
206
|
+
| `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 |
|
|
207
|
+
|
|
208
|
+
Ölçülen tek sayının içindeki dağılım, sayının kendisinden değerli: aynı makalenin iki kişilik
|
|
209
|
+
çözümlemesinde uyarı sonrası düşük maliyetli duruma geçiş ortalaması birinde 11, diğerinde
|
|
210
|
+
101 saniye. İki kişi arasındaki fark varsayılanın kendisinden büyük.
|
|
211
|
+
|
|
212
|
+
### Ertelemenin dayanağı var, susmanın bedeli de var
|
|
213
|
+
|
|
214
|
+
Ertelemenin işe yaradığına dair en güçlü kanıt [Okoshi, Tsubouchi ve Tokuda, *Pervasive and
|
|
215
|
+
Mobile Computing* 50:1-24
|
|
216
|
+
(2018)](https://keio.elsevierpure.com/en/publications/real-world-large-scale-study-on-adaptive-notification-scheduling-/):
|
|
217
|
+
Yahoo! JAPAN Android uygulaması, 680.000'den fazla kullanıcı, üç hafta; bildirimi uygun ana
|
|
218
|
+
kadar bekletmek yanıt süresini yüzde 49,7 kısaltmış. Bu, yönü destekler; bu paketteki
|
|
219
|
+
hiçbir pencereyi, bütçeyi veya bekleme süresini desteklemez.
|
|
220
|
+
|
|
221
|
+
Karşı ağırlık da burada durmalı, çünkü susturan bir kapı bedelsiz değil. [Pielot ve Rello,
|
|
222
|
+
MobileHCI 2017](https://arxiv.org/abs/1612.02314) çalışmasında 30 gönüllü bir gün boyunca
|
|
223
|
+
bildirimleri kapatmış. Daha az dağılmışlar, ama aynı zamanda bir şeyi kaçırmaktan
|
|
224
|
+
endişelenmiş, telefonlarına daha sık bakmış ve çevrelerinden kopuk hissetmişler. Otuz kişiden
|
|
225
|
+
on beşi acil bir şeyi kaçırmaktan korktuğunu söylemiş. Çalışma için görüşülen üç kişi,
|
|
226
|
+
işyerinde sürekli ulaşılabilir olmaları beklendiği için katılmayı reddetmiş. Kullanıcının
|
|
227
|
+
seçmediği bir sessizliğin bir bedeli var ve o bedel bu kütüphanenin yazdığı hiçbir izde
|
|
228
|
+
görünmüyor.
|
|
229
|
+
|
|
230
|
+
## Benimsemeden önce bilmeniz gereken iki sınır
|
|
231
|
+
|
|
232
|
+
İkisi de hata değil ve ikisi de testle sabitlendi, böylece ileride değişecekse bilerek değişir.
|
|
233
|
+
|
|
234
|
+
**Hafta, ISO haftasıdır; haftalık bütçe pazartesi yenilenir.** Çalışma haftası pazardan
|
|
235
|
+
perşembeye uzanan yerlerde bu yenilenme haftanın birinci gününe denk gelir: pazar günü
|
|
236
|
+
bütçesini harcayan bir kullanıcı pazartesi sabahı bütçesini geri alır ve önünde hâlâ dört
|
|
237
|
+
iş günü vardır. Anahtarı değiştirmek deponuzdaki bütün sayaçları kaydıracağı için bunu
|
|
238
|
+
sessizce değiştirmek yerine yazıyoruz. ISO haftası sizin kullanıcılarınız için yanlışsa
|
|
239
|
+
kendi bütçe kontrolünüzü istediğiniz anahtarla yazabilirsiniz.
|
|
240
|
+
|
|
241
|
+
**Sessiz saatler tek bir penceredir ve haftanın her günü aynıdır.** Kullanıcıda tek bir
|
|
242
|
+
`start` ve tek bir `end` vardır; cuma penceresi, Şabat penceresi veya resmî tatil
|
|
243
|
+
tanımlanamaz. Haftanın günü hiç okunmaz. Böyle bir kurala ihtiyacınız varsa kendi
|
|
244
|
+
kontrolünüzü yazın: `id` ve `run` taşıyan bir nesnedir, istediğiniz sırada dizilir ve izde
|
|
245
|
+
yerleşik kontrollerin yanında görünür.
|
|
246
|
+
|
|
247
|
+
## Hazır paketler: platform kotaları ve yasal sınırlar, kaynaklarıyla
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { presets } from "proactive-gate/presets";
|
|
251
|
+
const gate = createGate({ checks: [checks.consent(), ...presets.kakaoBrandMessage()] });
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| paket | ne kodlar |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `lineMessagingApi({ plan })` | LINE planına göre aylık push bütçesi: 200, 5.000 ya da 30.000 |
|
|
257
|
+
| `wechatSubscriptionMessage` | abonelik onayı başına bir mesaj |
|
|
258
|
+
| `wechatCustomerService` | kullanıcının son mesajından sonraki 48 saat içinde en çok 5 |
|
|
259
|
+
| `wechatTemplateMessage` | yalnızca kullanıcı eyleminden sonra, günde 3 şablon |
|
|
260
|
+
| `wecomAppMessage` | üye başına dakikada 30 ve saatte 1.000 |
|
|
261
|
+
| `kakaoAlimtalk` | yalnızca rıza; AlimTalk'ta saat kuralı yok |
|
|
262
|
+
| `kakaoBrandMessage` | reklam rızası, 08:00 ile 20:50 Asia/Seoul |
|
|
263
|
+
| `krNetworkAct50` | reklam rızası, ayrıca 21:00 ile 08:00 yerel saat için gece rızası |
|
|
264
|
+
| `jpAntiSpamLaw` | opt-in |
|
|
265
|
+
| `cnMinorMode` | reşit olmayanlar için: 06:00 ile 22:00 Asia/Shanghai ve günde bir |
|
|
266
|
+
| `usTcpa` | kullanıcının yerel saatiyle 08:00 ile 21:00 (47 CFR 64.1200) |
|
|
267
|
+
| `euEprivacy` | pazarlama rızası, mevcut müşteriler için yumuşak opt-in |
|
|
268
|
+
| `telegramBot` | sohbet başına saniyede 1 ve dakikada 20 |
|
|
269
|
+
| `slackApp` | kanal başına saniyede 1 |
|
|
270
|
+
|
|
271
|
+
Her paket `sources` (sayıların geldiği sayfalar) ve neyi dışarıda bıraktığını söyleyen bir
|
|
272
|
+
`note` taşır. Gözden geçirilebilir varsayılanlar, hukuki tavsiye değil: birkaç resmi kaynak
|
|
273
|
+
birbiriyle çelişir ve not hangi değerin neden seçildiğini söyler.
|
|
274
|
+
|
|
275
|
+
**Yasal bir pakete uzanmadan önce kapsamını okuyun.** Yukarıdaki bütün düzenlemeler *ticari*
|
|
276
|
+
iletişimi düzenler. `usTcpa`, `euEprivacy`, `krNetworkAct50` ve `jpAntiSpamLaw` birer pazarlama
|
|
277
|
+
kuralıdır; yani mesajınızı ancak mesajın kendisi ticari olduğunda bağlar. Kullanıcının kendi
|
|
278
|
+
istediği bir hatırlatma reklam değildir ve onun için pazarlama paketi kullanmak, yasanın size
|
|
279
|
+
hiç koymadığı bir kısıtı kendi elinizle içeri almak olur. Aday promosyon niteliğindeyse
|
|
280
|
+
kullanın; değilse dürüst sınırlar platform kotaları ve kendi sessiz saatlerinizdir.
|
|
281
|
+
|
|
282
|
+
Bazı ülkelerin neden burada olmadığı da aynı kapsam sınavıyla açıklanır. Kanada'nın CASL'i ve
|
|
283
|
+
Avustralya'nın 2003 tarihli Spam Act'i rıza, gönderen kimliği ve abonelikten çıkma
|
|
284
|
+
yükümlülükleri getirir; ikisinde de saat kısıtı yoktur. İnternette dolaşan Brezilya penceresi
|
|
285
|
+
PLS 48/2018 sayılı kanun *teklifinden* gelir, yürürlükteki bir kanundan değil, ve
|
|
286
|
+
telefonla pazarlama aramalarını kapsar. Hindistan ilginç olanı: sıkça tekrarlanan "09.00-21.00"
|
|
287
|
+
birincil metinde yazmaz. TRAI düzenlemesi zaman bantlarını, içerik kategorisi ve gün tipiyle
|
|
288
|
+
birlikte, abonenin operatörüne *kaydettirdiği bir tercih* yapar; sabit bir yasal sessizlik
|
|
289
|
+
penceresi değildir. Üstelik pencereyi aktaran ikincil kaynaklar başlangıcın 09.00 mı 10.00 mı
|
|
290
|
+
olduğunda birbiriyle çelişir. Bunun üzerine kurulacak bir paket, hiçbir birincil kaynağın
|
|
291
|
+
yazmadığı bir sayıyı kodlardı; o yüzden yok.
|
|
292
|
+
|
|
293
|
+
## Adaptörler
|
|
294
|
+
|
|
295
|
+
| alt yol | framework | kapı nerede durur |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `proactive-gate/ai-sdk` | Vercel AI SDK | bir aracın `needsApproval` sorusunu yanıtlar (çevrimdışı çalışan örnek: [`examples/ai-sdk/`](examples/ai-sdk/)) |
|
|
298
|
+
| `proactive-gate/mastra` | Mastra | gönderimden önce bir çıktı işlemcisi (çevrimdışı çalışan örnek: [`examples/mastra/`](examples/mastra/)) |
|
|
299
|
+
| `proactive-gate/langchain` | LangChain | gönderim aracının çevresinde middleware |
|
|
300
|
+
| `proactive-gate/openai-agents` | OpenAI Agents | bir guardrail |
|
|
301
|
+
| `npx proactive-gate hook` | Claude Code | bir `PreToolUse` kancası ([`examples/claude-code-hook.json`](examples/claude-code-hook.json)) |
|
|
302
|
+
|
|
303
|
+
Adaptörler framework paketine değil, çağrının biçimine göre tiplenmiştir; başka bir şey
|
|
304
|
+
kurmak gerekmez. Her biri kapının gerekçesiyle reddeder ve onayda bütçeyi tüketir.
|
|
305
|
+
|
|
306
|
+
## Python
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
pip install proactive-gate
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`python/` sapan bir port değil, bir kardeştir: `spec/fixtures` altındaki her senaryoyu senkron
|
|
313
|
+
`Gate` ve `AsyncGate` (Redis, `redis.asyncio` üzerinden) ile geçer; mypy strict, CI'da Python
|
|
314
|
+
3.11 ve 3.13. Bkz. [`python/README.md`](python/README.md).
|
|
315
|
+
|
|
316
|
+
## Sözleşme ve ikinci bir uygulama yazmak
|
|
317
|
+
|
|
318
|
+
[`spec/SPEC.md`](spec/SPEC.md) davranışı numaralı gereksinimler olarak yazar;
|
|
319
|
+
[`spec/fixtures`](spec/fixtures) dile bağlı olmayan senaryoları tutar: America/New_York'taki
|
|
320
|
+
yaz saati kenarı, Pacific/Apia, 2031'de bir duvar saati senaryosu, atomik commit, ISO haftası,
|
|
321
|
+
erteleme, gölge modu, isteğe bağlı kontroller ve dört hazır paket. TypeScript ve Python
|
|
322
|
+
testleri hepsini çalıştırır; `npx proactive-gate replay --fixtures spec/fixtures` komut
|
|
323
|
+
satırından çalıştırır. Üçüncü bir uygulama bu kaynaktan değil, senaryolardan başlar.
|
|
324
|
+
|
|
126
325
|
## Bütçe evaluate'te değil, commit'te uygulanır
|
|
127
326
|
|
|
128
327
|
İki örnek aynı kullanıcı için aynı adayı değerlendirebilir, ikisi de beşte dördün kullanıldığını
|
|
@@ -206,11 +405,18 @@ yazısında savunulmuş kararlardır. Tian Pan'ın
|
|
|
206
405
|
yazısı aynı davayı ürün tarafından savunur ve günde üç ile beş arası bir tavan önerir;
|
|
207
406
|
`defaultChecks({ dailyLimit })` varsayılanı beştir.
|
|
208
407
|
|
|
408
|
+
Biçimin daha eski akrabaları var. Matrix push kuralları, ilk eşleşen kuralın karar verdiği
|
|
409
|
+
sıralı bir listedir. Android bildirim kanalları ve iOS kesinti seviyeleri kullanıcıya tür
|
|
410
|
+
başına bir anahtar ve sessiz saati aşan bir öncelik tabanı verir. Horvitz'in karma girişim
|
|
411
|
+
çalışmaları iki isteğe bağlı kontrolü sağladı. Bu paket o fikirleri izli tek bir listeye
|
|
412
|
+
koyar ve onların dışarıda bıraktığı parçayı ekler: gönderim anında tüketilen bütçe.
|
|
413
|
+
|
|
209
414
|
## Geliştirme
|
|
210
415
|
|
|
211
416
|
```
|
|
212
417
|
npm ci
|
|
213
|
-
npm test
|
|
418
|
+
npm test # tsc build, spec-lint, then node:test over dist/test
|
|
419
|
+
cd python && pytest # the Python sibling against the same fixtures
|
|
214
420
|
```
|
|
215
421
|
|
|
216
422
|
MIT.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel AI SDK tool approvals. Give the send tool `needsApproval: true` and
|
|
3
|
+
* answer each approval request through the gate:
|
|
4
|
+
*
|
|
5
|
+
* const approve = gateToolApproval({ gate, toInput: (call) => call.input.gate });
|
|
6
|
+
* const { approved, reason } = await approve(call);
|
|
7
|
+
* // then addToolApprovalResponse({ id: call.approvalId, approved, reason })
|
|
8
|
+
*
|
|
9
|
+
* Typed against the shape of a tool call, not against the `ai` package, so
|
|
10
|
+
* nothing needs to be installed to build proactive-gate.
|
|
11
|
+
*/
|
|
12
|
+
import type { Gate } from "../gate.js";
|
|
13
|
+
import type { EvaluateInput } from "../types.js";
|
|
14
|
+
export interface ToolApprovalRequest {
|
|
15
|
+
toolName?: string;
|
|
16
|
+
toolCallId?: string;
|
|
17
|
+
approvalId?: string;
|
|
18
|
+
input?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolApprovalResult {
|
|
21
|
+
approved: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function gateToolApproval<T extends ToolApprovalRequest>(options: {
|
|
25
|
+
gate: Gate;
|
|
26
|
+
toInput: (call: T) => EvaluateInput;
|
|
27
|
+
/** Also consume the budget on approval. Default true. */
|
|
28
|
+
commit?: boolean;
|
|
29
|
+
}): (call: T) => Promise<ToolApprovalResult>;
|
|
30
|
+
export declare function describe(decision: {
|
|
31
|
+
rejectedBy?: string;
|
|
32
|
+
deferredBy?: string;
|
|
33
|
+
retryAt?: Date;
|
|
34
|
+
reason?: string;
|
|
35
|
+
}): string;
|