proactive-gate 0.1.1 → 0.2.0
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 +282 -4
- package/README.tr.md +134 -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 +74 -9
- package/dist/src/checks.js +164 -12
- 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 -21
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +4 -2
- 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.d.ts +11 -0
- package/dist/src/stores.js +48 -0
- package/dist/src/types.d.ts +74 -2
- package/package.json +62 -10
- package/dist/test/gate.test.d.ts +0 -1
- package/dist/test/gate.test.js +0 -211
package/README.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
English | [Türkçe](README.tr.md)
|
|
4
4
|
|
|
5
|
+
<p>
|
|
6
|
+
<img src="https://img.shields.io/npm/v/proactive-gate?style=flat-square&color=111111&label=npm" alt="npm">
|
|
7
|
+
<img src="https://img.shields.io/npm/dm/proactive-gate?style=flat-square&color=111111" alt="npm downloads">
|
|
8
|
+
<img src="https://img.shields.io/github/actions/workflow/status/Bubblegunn/proactive-gate/ci.yml?style=flat-square&color=111111&label=ci" alt="ci">
|
|
9
|
+
<img src="https://img.shields.io/bundlephobia/minzip/proactive-gate?style=flat-square&color=111111" alt="minzipped size">
|
|
10
|
+
<img src="https://img.shields.io/github/stars/Bubblegunn/proactive-gate?style=flat-square&color=111111" alt="stars">
|
|
11
|
+
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
5
14
|
Decide whether a proactive AI agent may reach a user right now, and log why not.
|
|
6
15
|
|
|
7
16
|
A proactive assistant has two halves. The generating half decides what is worth
|
|
@@ -28,11 +37,24 @@ if (decision.allowed && (await gate.commit(decision, { user, candidate }))) {
|
|
|
28
37
|
}
|
|
29
38
|
```
|
|
30
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
|
+
|
|
31
51
|
Zero dependencies. TypeScript. Node 20 or newer. Framework-agnostic: the gate sits
|
|
32
52
|
between "the model produced something" and "the user's phone buzzed", whichever
|
|
33
53
|
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
|
|
35
|
-
[`examples/policy.
|
|
54
|
+
[`examples/mastra.ts`](examples/mastra.ts), [`examples/langgraph.ts`](examples/langgraph.ts), and a
|
|
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).
|
|
36
58
|
|
|
37
59
|
## What a decision looks like
|
|
38
60
|
|
|
@@ -58,7 +80,10 @@ model or framework produced it. Examples: [`examples/vercel-ai-sdk.ts`](examples
|
|
|
58
80
|
}
|
|
59
81
|
```
|
|
60
82
|
|
|
61
|
-
<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.
|
|
62
87
|
|
|
63
88
|
With one gate and a logged reason, "why was the user not told about this" has an
|
|
64
89
|
answer. With checks scattered through a pipeline, the honest answer is "somewhere,
|
|
@@ -81,6 +106,12 @@ something returned false".
|
|
|
81
106
|
| 11 | `adaptiveTiming({ nextGoodMoment, surfacesFor })` | never | non-rejecting: moves `deliverAt` or narrows surfaces; a check marked `nonRejecting` cannot reject even if it tries |
|
|
82
107
|
| 12 | `dailyBudget({ limit, bypassPriority })` | the user's local-day counter is at the limit | `evaluate` reads, `commit` increments atomically and can still refuse |
|
|
83
108
|
|
|
109
|
+
`weeklyBudget({ limit, bypassPriority })` is the same shape keyed on the user's local ISO
|
|
110
|
+
week; `defaultChecks({ weeklyLimit })` places it just before the daily one. Budgets are
|
|
111
|
+
consumed in check order at commit, so when a weekly check passes and the daily one then
|
|
112
|
+
refuses, that weekly unit is spent without a delivery. It only happens when two commits
|
|
113
|
+
race after a shared evaluate.
|
|
114
|
+
|
|
84
115
|
Order is a design decision and it should be visible. Consent has to come before
|
|
85
116
|
everything. Quiet hours have to come before the budget, or a rejected candidate
|
|
86
117
|
consumes a delivery it never made. Reorder freely; the trace will show what you did.
|
|
@@ -123,6 +154,102 @@ Mark a check `nonRejecting: true` when it may only move timing or narrow surface
|
|
|
123
154
|
gate then ignores a reject from it and says so in the trace, so a bug in a timing model
|
|
124
155
|
cannot silence a user.
|
|
125
156
|
|
|
157
|
+
## A policy is data
|
|
158
|
+
|
|
159
|
+
The same checks as a JSON document, so a product team can change the rules without a
|
|
160
|
+
deploy and the same file runs in TypeScript, in Python, in the CLI and in the
|
|
161
|
+
[playground](https://bubblegunn.github.io/proactive-gate/playground/):
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"specVersion": "1.0.0",
|
|
166
|
+
"checks": [
|
|
167
|
+
{ "id": "consent" },
|
|
168
|
+
{ "id": "snooze", "defer": true },
|
|
169
|
+
{ "id": "quietHours", "priorityFloor": "high" },
|
|
170
|
+
{ "preset": "usTcpa" },
|
|
171
|
+
{ "id": "utilityFloor", "costFalseAlarm": 1, "costMissedHelp": 2, "shadow": true },
|
|
172
|
+
{ "id": "dailyBudget", "limit": 3, "bypassPriority": "critical", "nearLimit": 0.67 }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const gate = createGate({ policy: JSON.parse(await readFile("policy.json", "utf8")), store });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Each entry names a check `id` or a `preset` plus that check's options. An unknown id throws
|
|
182
|
+
and names the known ones. `compilePolicy` is exported for callers that want the check list,
|
|
183
|
+
and the schema is at [`spec/schema/policy.schema.json`](spec/schema/policy.schema.json).
|
|
184
|
+
`examples/policy.js` stays as the escape hatch for checks that need functions.
|
|
185
|
+
|
|
186
|
+
## Defer, shadow mode, near-limit notes and hooks
|
|
187
|
+
|
|
188
|
+
A check can `defer` instead of rejecting: the decision has `allowed: false`, `deferredBy` and
|
|
189
|
+
`retryAt`, and the caller knows when to try again. `snooze({ defer: true })` is the built-in
|
|
190
|
+
example.
|
|
191
|
+
|
|
192
|
+
A check with `shadow: true` runs and is traced with its real outcome, but cannot stop the
|
|
193
|
+
message; its id lands in `decision.shadowed`. Ship a new rule in shadow for a week, count how
|
|
194
|
+
often it would have fired, then turn it on.
|
|
195
|
+
|
|
196
|
+
Budgets report `nearLimit: { used, limit }` on the pass that reaches the threshold (80 percent
|
|
197
|
+
by default), listed under `decision.nearLimit`, so a dashboard can show who is about to go
|
|
198
|
+
quiet.
|
|
199
|
+
|
|
200
|
+
`hooks: { before, after, error, finally }` observe every check with its cost in milliseconds;
|
|
201
|
+
a hook that throws is routed to `error` and never changes the decision. `examples/otel.ts`
|
|
202
|
+
turns them into one span per check. Every decision has an `id`, and `commit` is idempotent on
|
|
203
|
+
it: a retry after a timeout does not consume a second unit.
|
|
204
|
+
|
|
205
|
+
## Optional checks, fed by your own model
|
|
206
|
+
|
|
207
|
+
Both ship off. They read numbers the caller puts on the candidate.
|
|
208
|
+
|
|
209
|
+
- `utilityFloor({ costFalseAlarm, costMissedHelp })` acts only when `candidate.pAccept` clears
|
|
210
|
+
`tau = cFA / (cFA + pNeed * cFN)` (`pNeed` defaults to 1) and skips when there is no
|
|
211
|
+
`pAccept`. This is Horvitz's expected-utility rule with the PRISM threshold.
|
|
212
|
+
- `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` never rejects. When
|
|
213
|
+
`candidate.busy` is true it moves `deliverAt` to `now + t*`, with
|
|
214
|
+
`t* = min(bound, lambda * interruptCost / (2 * staleness))`; the defaults give 116 seconds.
|
|
215
|
+
|
|
216
|
+
Neither check ships a model, a cost or a probability. `costFalseAlarm`, `costMissedHelp`,
|
|
217
|
+
`interruptCost` and `staleness` are yours to measure, and the package has no opinion about
|
|
218
|
+
what an interruption costs your users. The rules come from Eric Horvitz's work on
|
|
219
|
+
attention-sensitive alerting and bounded deferral; the field measurement people usually
|
|
220
|
+
reach for is [Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
|
|
221
|
+
2007](https://erichorvitz.com/CHI_2007_Iqbal_Horvitz.pdf), which logged real users and put
|
|
222
|
+
the return to a suspended task in the region of 11 to 16 minutes. The widely repeated "23
|
|
223
|
+
minutes 15 seconds" figure is not from a peer-reviewed paper and is not used here.
|
|
224
|
+
|
|
225
|
+
## Presets: platform quotas and legal limits, with sources
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { presets } from "proactive-gate/presets";
|
|
229
|
+
const gate = createGate({ checks: [checks.consent(), ...presets.kakaoBrandMessage()] });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| preset | encodes |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `lineMessagingApi({ plan })` | monthly push budget by LINE plan: 200, 5,000 or 30,000 |
|
|
235
|
+
| `wechatSubscriptionMessage` | one message per subscription opt-in |
|
|
236
|
+
| `wechatCustomerService` | within 48 h of the user's last message, at most 5 |
|
|
237
|
+
| `wechatTemplateMessage` | only after a user action, 3 templates a day |
|
|
238
|
+
| `wecomAppMessage` | 30 a minute and 1,000 an hour per member |
|
|
239
|
+
| `kakaoAlimtalk` | consent only; AlimTalk has no time-of-day rule |
|
|
240
|
+
| `kakaoBrandMessage` | advertising consent, 08:00 to 20:50 Asia/Seoul |
|
|
241
|
+
| `krNetworkAct50` | advertising consent, plus night consent for 21:00 to 08:00 local |
|
|
242
|
+
| `jpAntiSpamLaw` | opt-in |
|
|
243
|
+
| `cnMinorMode` | for minors: 06:00 to 22:00 Asia/Shanghai and one a day |
|
|
244
|
+
| `usTcpa` | 08:00 to 21:00 at the user's local time (47 CFR 64.1200) |
|
|
245
|
+
| `euEprivacy` | marketing consent with the soft opt-in for existing customers |
|
|
246
|
+
| `telegramBot` | 1 a second and 20 a minute per chat |
|
|
247
|
+
| `slackApp` | 1 a second per channel |
|
|
248
|
+
|
|
249
|
+
Each preset carries `sources` (the pages the numbers come from) and a `note` on what it leaves
|
|
250
|
+
out. Reviewable defaults, not legal advice: several official sources disagree with each other,
|
|
251
|
+
and the note says which value was chosen and why.
|
|
252
|
+
|
|
126
253
|
## The budget is enforced at commit, not at evaluate
|
|
127
254
|
|
|
128
255
|
Two instances can both evaluate a candidate for the same user, both see four of
|
|
@@ -140,6 +267,13 @@ if (decision.allowed && await gate.commit(decision, input)) { // INCR, returns
|
|
|
140
267
|
counter is keyed on the user's local day, so a budget resets at the user's midnight,
|
|
141
268
|
not at UTC.
|
|
142
269
|
|
|
270
|
+
## Stores
|
|
271
|
+
|
|
272
|
+
`MemoryStore` keeps values in process memory and is useful for a single instance. `RedisStore`
|
|
273
|
+
shares values across instances. `SqliteStore` persists values in a SQLite database without
|
|
274
|
+
adding a package dependency. `SqliteStore` requires Node.js 22.5 or newer; the SQLite module
|
|
275
|
+
is loaded only when the store is constructed so the package can still be used on Node.js 20. On Node 22 the module prints an ExperimentalWarning on first use; it is stable from Node 24.
|
|
276
|
+
|
|
143
277
|
## Fail open, on purpose
|
|
144
278
|
|
|
145
279
|
When a store-backed check throws (Redis is down), the default lets the candidate
|
|
@@ -173,6 +307,143 @@ dailyBudget 1 daily budget of 5 used (5)
|
|
|
173
307
|
per line for a notebook. Replay a week of real candidates against a proposed policy
|
|
174
308
|
and you know its allow rate and its silence reasons before a single user does.
|
|
175
309
|
|
|
310
|
+
## Compared with hand-rolled checks and feature flags
|
|
311
|
+
|
|
312
|
+
Most products start with a few `if` statements next to the send call and grow from there.
|
|
313
|
+
The difference is not the checks, which anyone can write, but four properties that are hard
|
|
314
|
+
to keep once the checks are scattered:
|
|
315
|
+
|
|
316
|
+
- The order is one list in one place, so "consent before everything" is a fact you can read
|
|
317
|
+
rather than a convention you hope each caller followed.
|
|
318
|
+
- Every rejection names the check and the reason, so "why was the user not told" has an
|
|
319
|
+
answer in the log instead of "something returned false somewhere".
|
|
320
|
+
- The budget is consumed by an atomic increment at send time, so two instances cannot both
|
|
321
|
+
send the sixth message; scattered checks read a counter and race.
|
|
322
|
+
- A policy can be replayed over a day of real candidates before it ships, and a non-rejecting
|
|
323
|
+
check cannot reject even if a bug makes it try.
|
|
324
|
+
|
|
325
|
+
Those are claims, so the repository runs them. `npm run bench:compare` replays a
|
|
326
|
+
committed day of 21 candidates for 7 users through `bench/naive.mjs`, an honest
|
|
327
|
+
hand-rolled policy of five `if` statements, and through a gate built from
|
|
328
|
+
`bench/fixtures/policy.json`:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
gate: 11 sent, 10 stopped
|
|
332
|
+
hand-rolled: 13 sent, 8 stopped
|
|
333
|
+
|
|
334
|
+
6 disagreements, and none of them is a matter of taste:
|
|
335
|
+
a5 a critical alert: the gate lets priority bypass the cap, the cap in the if statements does not
|
|
336
|
+
b1 a two-day-old account: the gate holds normal messages back for a week, the if statements never knew
|
|
337
|
+
c1 the user pressed snooze: the gate defers to when it ends, the if statements have no snooze
|
|
338
|
+
e1 three dismissals of this type: the gate is silent for a week, the if statements do not track outcomes
|
|
339
|
+
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
|
|
340
|
+
g4 18:00 in Los Angeles, still the same local day: the UTC-day key already rolled, so the cap pays out twice
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The hand-rolled policy is not a straw man. It checks consent, enabled, mute, quiet
|
|
344
|
+
hours and a daily cap, which is what actually gets written, and it takes the three
|
|
345
|
+
shortcuts that actually get taken: a fixed UTC offset per zone, the cap keyed by the
|
|
346
|
+
UTC calendar day, and the cap read then written. `test/naive.test.mjs` pins each one
|
|
347
|
+
against a real instant:
|
|
348
|
+
|
|
349
|
+
- The clocks change. At `2026-11-01T12:30:00Z` New York has left daylight time, so it is
|
|
350
|
+
07:30 there and inside quiet hours; an offset captured in the summer computes 08:00 and
|
|
351
|
+
sends. That is twice a year, for every zone that observes it.
|
|
352
|
+
- The day boundary is local. The same UTC-day key silences the Tokyo user for the nine
|
|
353
|
+
hours between local midnight and 09:00, and hands the Los Angeles user a second full
|
|
354
|
+
budget at 17:00 while it is still their afternoon.
|
|
355
|
+
- Two deliveries are in flight. Read, compare, write lets both take the last slot, and the
|
|
356
|
+
counter still reads 2 afterwards, so nothing looks wrong. `commit()` takes the unit with
|
|
357
|
+
an atomic increment and returns `false` to the loser.
|
|
358
|
+
|
|
359
|
+
A feature-flag system does a different job better: rolling a behaviour out to a percentage
|
|
360
|
+
of users, per-tenant overrides, and an audit trail of who flipped what. Use flags to decide
|
|
361
|
+
whether the gate runs at all, and the gate to decide whether this message reaches this
|
|
362
|
+
person now.
|
|
363
|
+
|
|
364
|
+
## Adapters
|
|
365
|
+
|
|
366
|
+
| subpath | framework | where the gate sits |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| `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/)) |
|
|
369
|
+
| `proactive-gate/mastra` | Mastra | an output processor before the send ([`examples/mastra.ts`](examples/mastra.ts); runnable offline: [`examples/mastra/`](examples/mastra/)) |
|
|
370
|
+
| `proactive-gate/langchain` | LangChain | middleware around the send tool |
|
|
371
|
+
| `proactive-gate/openai-agents` | OpenAI Agents | a guardrail |
|
|
372
|
+
| `npx proactive-gate hook` | Claude Code | a `PreToolUse` hook ([`examples/claude-code-hook.json`](examples/claude-code-hook.json)) |
|
|
373
|
+
|
|
374
|
+
The adapters are typed against the shape of a call, not against the framework package, so
|
|
375
|
+
nothing else has to be installed. Each denies with the gate's reason and commits the budget on
|
|
376
|
+
approval. The pattern is the same everywhere: the model decides whether there is something to
|
|
377
|
+
say, `gate.evaluate` decides whether it may be said now, and `gate.commit` runs right before
|
|
378
|
+
the message leaves. [`examples/langgraph.ts`](examples/langgraph.ts) shows the same thing
|
|
379
|
+
inside a LangGraph node.
|
|
380
|
+
|
|
381
|
+
Two of the examples run without the framework installed and without a network: `node
|
|
382
|
+
examples/mastra/run.mjs` makes the same `processOutputResult` call Mastra makes, over a day of
|
|
383
|
+
candidates with the clock taken from each line, and `node examples/ai-sdk/run.mjs` answers a
|
|
384
|
+
day of tool-approval requests, one of which is a critical alert that a legal window (the TCPA
|
|
385
|
+
preset) still refuses. Both are part of `npm run examples` and of the test suite.
|
|
386
|
+
|
|
387
|
+
## Python
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
pip install proactive-gate
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
from proactive_gate import Gate
|
|
395
|
+
gate = Gate.from_policy(policy) # the same policy.json
|
|
396
|
+
decision = gate.evaluate(inp)
|
|
397
|
+
if decision.allowed and gate.commit(decision, inp): send(...)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`python/` is a sibling, not a port that drifts: it passes every fixture under `spec/fixtures`
|
|
401
|
+
through a sync `Gate` and an `AsyncGate` (Redis over `redis.asyncio`), with mypy strict, on
|
|
402
|
+
Python 3.11 and 3.13 in CI. See [`python/README.md`](python/README.md).
|
|
403
|
+
|
|
404
|
+
## Properties, not just examples
|
|
405
|
+
|
|
406
|
+
`test/properties.test.ts` generates gates, users and candidates from a seeded
|
|
407
|
+
32-bit PRNG and asserts what has to hold for all of them, rather than for the
|
|
408
|
+
cases someone thought of:
|
|
409
|
+
|
|
410
|
+
- The trace is always a prefix of the declared check order. Nothing is skipped,
|
|
411
|
+
nothing is reordered, every check reports exactly once, and a stopped decision
|
|
412
|
+
ends on the check that stopped it with a reason attached.
|
|
413
|
+
- A check marked `nonRejecting` cannot stop a decision even when it returns a
|
|
414
|
+
rejection on purpose.
|
|
415
|
+
- However many deliveries race, `commit()` hands out exactly `min(racers, limit)`
|
|
416
|
+
units, and replaying one decision any number of times spends one.
|
|
417
|
+
- `MemoryStore` and `SqliteStore` answer the same random sequence of `get`, `set`,
|
|
418
|
+
`incr`, `del` and clock movement identically, TTLs included.
|
|
419
|
+
|
|
420
|
+
The generator is forty lines because the package has no dependencies; a property
|
|
421
|
+
library would shrink failures better. Each assertion prints its seed, so a failure
|
|
422
|
+
reproduces exactly. The race property was checked against a mutant: rewriting
|
|
423
|
+
`consume` as read-then-write, the shortcut in `bench/naive.mjs`, makes it fail.
|
|
424
|
+
|
|
425
|
+
## The spec, and writing a second implementation
|
|
426
|
+
|
|
427
|
+
[`spec/SPEC.md`](spec/SPEC.md) states the behaviour as numbered requirements, and
|
|
428
|
+
[`spec/fixtures`](spec/fixtures) holds language-neutral cases: the DST edge in
|
|
429
|
+
America/New_York, Pacific/Apia, a wall-clock case in 2031, atomic commit, the ISO week,
|
|
430
|
+
deferral, shadow mode, the optional checks and four presets. The TypeScript tests and the
|
|
431
|
+
Python tests both run all of them; `npx proactive-gate replay --fixtures spec/fixtures` runs
|
|
432
|
+
them from the command line. A third implementation starts from the fixtures, not from this
|
|
433
|
+
source.
|
|
434
|
+
|
|
435
|
+
## Performance
|
|
436
|
+
|
|
437
|
+
`npm run bench` runs `gate.evaluate()` ten thousand times with the default twelve checks and
|
|
438
|
+
`MemoryStore`. On 5 September 2026:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
evaluate() x 10,000, twelve checks, MemoryStore: median 48.7 µs, p95 91.1 µs (v24.13.0, Apple M4 Max)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
With `RedisStore` the two store-backed checks add one round trip each; the gate itself is
|
|
445
|
+
not where the time goes.
|
|
446
|
+
|
|
176
447
|
## Learning from what happened
|
|
177
448
|
|
|
178
449
|
```ts
|
|
@@ -209,11 +480,18 @@ Tian Pan's
|
|
|
209
480
|
essay argues the same case from the product side and suggests a daily cap of three
|
|
210
481
|
to five; `defaultChecks({ dailyLimit })` defaults to five.
|
|
211
482
|
|
|
483
|
+
The shape has older relatives. Matrix push rules are an ordered list where the first matching
|
|
484
|
+
rule decides. Android notification channels and iOS interruption levels give the user a
|
|
485
|
+
per-type switch and a priority floor that bypasses quiet time. Horvitz's work on mixed
|
|
486
|
+
initiative supplied the two optional checks. This package puts those ideas in one list with a
|
|
487
|
+
trace, and adds the part they leave out: the budget consumed at send time.
|
|
488
|
+
|
|
212
489
|
## Development
|
|
213
490
|
|
|
214
491
|
```
|
|
215
492
|
npm ci
|
|
216
|
-
npm test
|
|
493
|
+
npm test # tsc build, spec-lint, then node:test over dist/test
|
|
494
|
+
cd python && pytest # the Python sibling against the same fixtures
|
|
217
495
|
```
|
|
218
496
|
|
|
219
497
|
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,125 @@ 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, Horvitz'in beklenen fayda kuralı ve PRISM eşiğidir.
|
|
186
|
+
- `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` asla reddetmez.
|
|
187
|
+
`candidate.busy` doğruysa `deliverAt` değerini `now + t*` yapar;
|
|
188
|
+
`t* = min(bound, lambda * interruptCost / (2 * staleness))`, varsayılanlar 116 saniye verir.
|
|
189
|
+
|
|
190
|
+
## Hazır paketler: platform kotaları ve yasal sınırlar, kaynaklarıyla
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { presets } from "proactive-gate/presets";
|
|
194
|
+
const gate = createGate({ checks: [checks.consent(), ...presets.kakaoBrandMessage()] });
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| paket | ne kodlar |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `lineMessagingApi({ plan })` | LINE planına göre aylık push bütçesi: 200, 5.000 ya da 30.000 |
|
|
200
|
+
| `wechatSubscriptionMessage` | abonelik onayı başına bir mesaj |
|
|
201
|
+
| `wechatCustomerService` | kullanıcının son mesajından sonraki 48 saat içinde en çok 5 |
|
|
202
|
+
| `wechatTemplateMessage` | yalnızca kullanıcı eyleminden sonra, günde 3 şablon |
|
|
203
|
+
| `wecomAppMessage` | üye başına dakikada 30 ve saatte 1.000 |
|
|
204
|
+
| `kakaoAlimtalk` | yalnızca rıza; AlimTalk'ta saat kuralı yok |
|
|
205
|
+
| `kakaoBrandMessage` | reklam rızası, 08:00 ile 20:50 Asia/Seoul |
|
|
206
|
+
| `krNetworkAct50` | reklam rızası, ayrıca 21:00 ile 08:00 yerel saat için gece rızası |
|
|
207
|
+
| `jpAntiSpamLaw` | opt-in |
|
|
208
|
+
| `cnMinorMode` | reşit olmayanlar için: 06:00 ile 22:00 Asia/Shanghai ve günde bir |
|
|
209
|
+
| `usTcpa` | kullanıcının yerel saatiyle 08:00 ile 21:00 (47 CFR 64.1200) |
|
|
210
|
+
| `euEprivacy` | pazarlama rızası, mevcut müşteriler için yumuşak opt-in |
|
|
211
|
+
| `telegramBot` | sohbet başına saniyede 1 ve dakikada 20 |
|
|
212
|
+
| `slackApp` | kanal başına saniyede 1 |
|
|
213
|
+
|
|
214
|
+
Her paket `sources` (sayıların geldiği sayfalar) ve neyi dışarıda bıraktığını söyleyen bir
|
|
215
|
+
`note` taşır. Gözden geçirilebilir varsayılanlar, hukuki tavsiye değil: birkaç resmi kaynak
|
|
216
|
+
birbiriyle çelişir ve not hangi değerin neden seçildiğini söyler.
|
|
217
|
+
|
|
218
|
+
## Adaptörler
|
|
219
|
+
|
|
220
|
+
| alt yol | framework | kapı nerede durur |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `proactive-gate/ai-sdk` | Vercel AI SDK | bir aracın `needsApproval` sorusunu yanıtlar (çevrimdışı çalışan örnek: [`examples/ai-sdk/`](examples/ai-sdk/)) |
|
|
223
|
+
| `proactive-gate/mastra` | Mastra | gönderimden önce bir çıktı işlemcisi (çevrimdışı çalışan örnek: [`examples/mastra/`](examples/mastra/)) |
|
|
224
|
+
| `proactive-gate/langchain` | LangChain | gönderim aracının çevresinde middleware |
|
|
225
|
+
| `proactive-gate/openai-agents` | OpenAI Agents | bir guardrail |
|
|
226
|
+
| `npx proactive-gate hook` | Claude Code | bir `PreToolUse` kancası ([`examples/claude-code-hook.json`](examples/claude-code-hook.json)) |
|
|
227
|
+
|
|
228
|
+
Adaptörler framework paketine değil, çağrının biçimine göre tiplenmiştir; başka bir şey
|
|
229
|
+
kurmak gerekmez. Her biri kapının gerekçesiyle reddeder ve onayda bütçeyi tüketir.
|
|
230
|
+
|
|
231
|
+
## Python
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
pip install proactive-gate
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`python/` sapan bir port değil, bir kardeştir: `spec/fixtures` altındaki her senaryoyu senkron
|
|
238
|
+
`Gate` ve `AsyncGate` (Redis, `redis.asyncio` üzerinden) ile geçer; mypy strict, CI'da Python
|
|
239
|
+
3.11 ve 3.13. Bkz. [`python/README.md`](python/README.md).
|
|
240
|
+
|
|
241
|
+
## Sözleşme ve ikinci bir uygulama yazmak
|
|
242
|
+
|
|
243
|
+
[`spec/SPEC.md`](spec/SPEC.md) davranışı numaralı gereksinimler olarak yazar;
|
|
244
|
+
[`spec/fixtures`](spec/fixtures) dile bağlı olmayan senaryoları tutar: America/New_York'taki
|
|
245
|
+
yaz saati kenarı, Pacific/Apia, 2031'de bir duvar saati senaryosu, atomik commit, ISO haftası,
|
|
246
|
+
erteleme, gölge modu, isteğe bağlı kontroller ve dört hazır paket. TypeScript ve Python
|
|
247
|
+
testleri hepsini çalıştırır; `npx proactive-gate replay --fixtures spec/fixtures` komut
|
|
248
|
+
satırından çalıştırır. Üçüncü bir uygulama bu kaynaktan değil, senaryolardan başlar.
|
|
249
|
+
|
|
126
250
|
## Bütçe evaluate'te değil, commit'te uygulanır
|
|
127
251
|
|
|
128
252
|
İki örnek aynı kullanıcı için aynı adayı değerlendirebilir, ikisi de beşte dördün kullanıldığını
|
|
@@ -206,11 +330,18 @@ yazısında savunulmuş kararlardır. Tian Pan'ın
|
|
|
206
330
|
yazısı aynı davayı ürün tarafından savunur ve günde üç ile beş arası bir tavan önerir;
|
|
207
331
|
`defaultChecks({ dailyLimit })` varsayılanı beştir.
|
|
208
332
|
|
|
333
|
+
Biçimin daha eski akrabaları var. Matrix push kuralları, ilk eşleşen kuralın karar verdiği
|
|
334
|
+
sıralı bir listedir. Android bildirim kanalları ve iOS kesinti seviyeleri kullanıcıya tür
|
|
335
|
+
başına bir anahtar ve sessiz saati aşan bir öncelik tabanı verir. Horvitz'in karma girişim
|
|
336
|
+
çalışmaları iki isteğe bağlı kontrolü sağladı. Bu paket o fikirleri izli tek bir listeye
|
|
337
|
+
koyar ve onların dışarıda bıraktığı parçayı ekler: gönderim anında tüketilen bütçe.
|
|
338
|
+
|
|
209
339
|
## Geliştirme
|
|
210
340
|
|
|
211
341
|
```
|
|
212
342
|
npm ci
|
|
213
|
-
npm test
|
|
343
|
+
npm test # tsc build, spec-lint, then node:test over dist/test
|
|
344
|
+
cd python && pytest # the Python sibling against the same fixtures
|
|
214
345
|
```
|
|
215
346
|
|
|
216
347
|
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;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function gateToolApproval(options) {
|
|
2
|
+
const commit = options.commit ?? true;
|
|
3
|
+
return async (call) => {
|
|
4
|
+
const input = options.toInput(call);
|
|
5
|
+
const decision = await options.gate.evaluate(input);
|
|
6
|
+
if (!decision.allowed)
|
|
7
|
+
return { approved: false, reason: describe(decision) };
|
|
8
|
+
if (commit && !(await options.gate.commit(decision, input)))
|
|
9
|
+
return { approved: false, reason: "a budget was exhausted at commit" };
|
|
10
|
+
return { approved: true, ...(decision.deliverAt ? { reason: `deliver at ${decision.deliverAt.toISOString()}` } : {}) };
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function describe(decision) {
|
|
14
|
+
if (decision.deferredBy)
|
|
15
|
+
return `deferred by ${decision.deferredBy} until ${decision.retryAt?.toISOString()}: ${decision.reason}`;
|
|
16
|
+
return `rejected by ${decision.rejectedBy}: ${decision.reason}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain middleware that wraps tool calls. For the tools listed in `tools`
|
|
3
|
+
* the gate decides first; a rejection returns a tool message carrying the
|
|
4
|
+
* reason instead of running the tool.
|
|
5
|
+
*
|
|
6
|
+
* createAgent({ tools: [sendMessage], middleware: [gateMiddleware({ gate, tools: ["send_message"], toInput: (req) => req.toolCall.args.gate })] })
|
|
7
|
+
*/
|
|
8
|
+
import type { Gate } from "../gate.js";
|
|
9
|
+
import type { EvaluateInput } from "../types.js";
|
|
10
|
+
export interface ToolCallRequest {
|
|
11
|
+
toolCall: {
|
|
12
|
+
name: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
args: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface ToolMessageLike {
|
|
18
|
+
type: "tool";
|
|
19
|
+
content: string;
|
|
20
|
+
tool_call_id: string;
|
|
21
|
+
status: "success" | "error";
|
|
22
|
+
}
|
|
23
|
+
export interface ToolCallMiddleware<R extends ToolCallRequest = ToolCallRequest, T = unknown> {
|
|
24
|
+
name: string;
|
|
25
|
+
wrapToolCall(request: R, handler: (request: R) => Promise<T>): Promise<T | ToolMessageLike>;
|
|
26
|
+
}
|
|
27
|
+
export declare function gateMiddleware<R extends ToolCallRequest = ToolCallRequest, T = unknown>(options: {
|
|
28
|
+
gate: Gate;
|
|
29
|
+
tools: string[];
|
|
30
|
+
toInput: (request: R) => EvaluateInput;
|
|
31
|
+
commit?: boolean;
|
|
32
|
+
}): ToolCallMiddleware<R, T>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe } from "./ai-sdk.js";
|
|
2
|
+
export function gateMiddleware(options) {
|
|
3
|
+
const commit = options.commit ?? true;
|
|
4
|
+
const watched = new Set(options.tools);
|
|
5
|
+
return {
|
|
6
|
+
name: "proactive-gate",
|
|
7
|
+
async wrapToolCall(request, handler) {
|
|
8
|
+
if (!watched.has(request.toolCall.name))
|
|
9
|
+
return handler(request);
|
|
10
|
+
const input = options.toInput(request);
|
|
11
|
+
const decision = await options.gate.evaluate(input);
|
|
12
|
+
const refuse = (reason) => ({ type: "tool", content: `proactive-gate: ${reason}`, tool_call_id: request.toolCall.id ?? "", status: "error" });
|
|
13
|
+
if (!decision.allowed)
|
|
14
|
+
return refuse(describe(decision));
|
|
15
|
+
if (commit && !(await options.gate.commit(decision, input)))
|
|
16
|
+
return refuse("a budget was exhausted at commit");
|
|
17
|
+
return handler(request);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Mastra output processor. Put it in the agent's `outputProcessors`; when
|
|
3
|
+
* the gate rejects, the processor calls abort(reason) and the agent's result
|
|
4
|
+
* is stopped before it reaches the user.
|
|
5
|
+
*
|
|
6
|
+
* outputProcessors: [gateProcessor({ gate, toInput: ({ messages }) => ({ user, candidate: { id, type: "reply" } }) })]
|
|
7
|
+
*/
|
|
8
|
+
import type { Gate } from "../gate.js";
|
|
9
|
+
import type { EvaluateInput } from "../types.js";
|
|
10
|
+
export interface ProcessorArgs<M = unknown> {
|
|
11
|
+
messages: M[];
|
|
12
|
+
abort: (reason?: string) => never;
|
|
13
|
+
}
|
|
14
|
+
export interface OutputProcessor<M = unknown> {
|
|
15
|
+
id: string;
|
|
16
|
+
processOutputResult(args: ProcessorArgs<M>): Promise<M[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare function gateProcessor<M = unknown>(options: {
|
|
19
|
+
gate: Gate;
|
|
20
|
+
toInput: (args: {
|
|
21
|
+
messages: M[];
|
|
22
|
+
}) => EvaluateInput;
|
|
23
|
+
id?: string;
|
|
24
|
+
commit?: boolean;
|
|
25
|
+
}): OutputProcessor<M>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe } from "./ai-sdk.js";
|
|
2
|
+
export function gateProcessor(options) {
|
|
3
|
+
const commit = options.commit ?? true;
|
|
4
|
+
return {
|
|
5
|
+
id: options.id ?? "proactive-gate",
|
|
6
|
+
async processOutputResult({ messages, abort }) {
|
|
7
|
+
const input = options.toInput({ messages });
|
|
8
|
+
const decision = await options.gate.evaluate(input);
|
|
9
|
+
if (!decision.allowed)
|
|
10
|
+
return abort(`proactive-gate: ${describe(decision)}`);
|
|
11
|
+
if (commit && !(await options.gate.commit(decision, input)))
|
|
12
|
+
return abort("proactive-gate: a budget was exhausted at commit");
|
|
13
|
+
return messages;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|