prefix-safe-json 0.0.1-alpha.2 → 0.0.1-alpha.4

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.
Files changed (58) hide show
  1. package/README.md +137 -4
  2. package/dist/coordinator/diagnostic-codes.d.ts +11 -0
  3. package/dist/coordinator/diagnostic-codes.d.ts.map +1 -0
  4. package/dist/coordinator/diagnostic-codes.js +22 -0
  5. package/dist/coordinator/diagnostic-codes.js.map +1 -0
  6. package/dist/coordinator/protocol.d.ts +1 -1
  7. package/dist/coordinator/protocol.d.ts.map +1 -1
  8. package/dist/gate/decide.d.ts +27 -0
  9. package/dist/gate/decide.d.ts.map +1 -0
  10. package/dist/gate/decide.js +138 -0
  11. package/dist/gate/decide.js.map +1 -0
  12. package/dist/gate/gate.d.ts +25 -0
  13. package/dist/gate/gate.d.ts.map +1 -0
  14. package/dist/gate/gate.js +96 -0
  15. package/dist/gate/gate.js.map +1 -0
  16. package/dist/gate/types.d.ts +184 -0
  17. package/dist/gate/types.d.ts.map +1 -0
  18. package/dist/gate/types.js +2 -0
  19. package/dist/gate/types.js.map +1 -0
  20. package/dist/guard/ai-sdk.d.ts +48 -0
  21. package/dist/guard/ai-sdk.d.ts.map +1 -0
  22. package/dist/guard/ai-sdk.js +51 -0
  23. package/dist/guard/ai-sdk.js.map +1 -0
  24. package/dist/guard/provider-guard.d.ts +4 -0
  25. package/dist/guard/provider-guard.d.ts.map +1 -0
  26. package/dist/guard/provider-guard.js +38 -0
  27. package/dist/guard/provider-guard.js.map +1 -0
  28. package/dist/guard/types.d.ts +48 -0
  29. package/dist/guard/types.d.ts.map +1 -0
  30. package/dist/guard/types.js +2 -0
  31. package/dist/guard/types.js.map +1 -0
  32. package/dist/index.d.ts +20 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +18 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/lexer/scanner.d.ts +0 -2
  37. package/dist/lexer/scanner.d.ts.map +1 -1
  38. package/dist/lexer/scanner.js +3 -21
  39. package/dist/lexer/scanner.js.map +1 -1
  40. package/dist/lexer/states.d.ts +0 -4
  41. package/dist/lexer/states.d.ts.map +1 -1
  42. package/dist/lexer/states.js +0 -4
  43. package/dist/lexer/states.js.map +1 -1
  44. package/dist/parser.js +26 -53
  45. package/dist/parser.js.map +1 -1
  46. package/dist/providers/ai-sdk.d.ts +58 -0
  47. package/dist/providers/ai-sdk.d.ts.map +1 -0
  48. package/dist/providers/ai-sdk.js +212 -0
  49. package/dist/providers/ai-sdk.js.map +1 -0
  50. package/dist/providers/openai.d.ts +7 -0
  51. package/dist/providers/openai.d.ts.map +1 -1
  52. package/dist/providers/openai.js +65 -7
  53. package/dist/providers/openai.js.map +1 -1
  54. package/dist/semantic/snapshot.d.ts +11 -4
  55. package/dist/semantic/snapshot.d.ts.map +1 -1
  56. package/dist/semantic/snapshot.js +11 -9
  57. package/dist/semantic/snapshot.js.map +1 -1
  58. package/package.json +8 -3
package/README.md CHANGED
@@ -2,13 +2,108 @@
2
2
 
3
3
  > ⚠️ **Alpha** — This project is under active development. APIs may change without notice. Do not use in production.
4
4
 
5
- Incremental LLM tool-call JSON parser and deterministic repair engine.
5
+ **Don't execute incomplete AI tool calls.**
6
+
7
+ `prefix-safe-json` is a fail-closed integrity layer for streamed LLM tool
8
+ calls. It distinguishes complete, unfabricated arguments from truncated
9
+ ones — including ones a syntax-level JSON repairer can make *look* complete
10
+ — before they reach a tool with real side effects (writing a file, running a
11
+ command, sending a request).
12
+
13
+ ```
14
+ model output truncated
15
+ ↓
16
+ json repair
17
+ ↓
18
+ looks valid
19
+ ↓
20
+ ❌ dangerous execution
21
+ ```
22
+
23
+ ```
24
+ prefix-safe-json
25
+ ↓
26
+ truncated
27
+ ↓
28
+ executable: false
29
+ ↓
30
+ retry
31
+ ```
6
32
 
7
33
  ![Demo: a tool call truncated mid-argument is correctly reported non-executable, while the same call delivered in full is reported executable](examples/demo.gif)
8
34
 
9
35
  *(Real terminal output from [`examples/anthropic-truncation-safety.mjs`](examples/anthropic-truncation-safety.mjs) — not staged. Same script CI runs on every push.)*
10
36
 
11
- ## Problem
37
+ ## From stream to safe execution, in about 10 lines
38
+
39
+ ```typescript
40
+ import { createAiSdkExecutionGuard } from "prefix-safe-json";
41
+
42
+ const guard = createAiSdkExecutionGuard({
43
+ schemas: {
44
+ write_file: {
45
+ type: "object",
46
+ properties: { path: { type: "string" }, content: { type: "string" } },
47
+ required: ["path", "content"],
48
+ },
49
+ },
50
+ });
51
+
52
+ for await (const part of result.fullStream) guard.push(part);
53
+
54
+ for (const decision of guard.finish().decisions) {
55
+ if (decision.action === "execute") await tools[decision.name](decision.value);
56
+ }
57
+ ```
58
+
59
+ `createAiSdkExecutionGuard()` is a drop-in guard for the [Vercel AI
60
+ SDK](https://ai-sdk.dev)'s `fullStream` — not a dependency of this library
61
+ (no `ai` import, no runtime version lock; see
62
+ [`examples/ai-sdk-guard.mjs`](examples/ai-sdk-guard.mjs)). It's a thin
63
+ composition over the same lower-level adapter + gate API this library has
64
+ always exposed — nothing about the decision logic changes based on which one
65
+ you call.
66
+
67
+ **What the decisions mean**
68
+
69
+ Every tool call gets exactly one verdict:
70
+
71
+ - **`execute`** — complete, unfabricated, and (if a schema is registered)
72
+ schema-valid. `decision.value` is safe to pass to your tool.
73
+ - **`retry`** — nothing is wrong with what arrived; there just isn't a
74
+ trustworthy complete value yet. Continue generation.
75
+ - **`reject`** — the data itself is the problem (malformed JSON, a schema
76
+ mismatch, a resource limit, a provider error, a content-policy
77
+ termination). Retrying the same input won't help.
78
+
79
+ **Why the model's finish reason matters as much as the JSON's shape**
80
+
81
+ A tool call cut short by a token limit, a provider error, or a content
82
+ filter can still contain syntactically complete, schema-valid JSON — the
83
+ model was simply stopped before finishing the *rest* of its response, not
84
+ necessarily mid-argument. Executing on JSON shape alone means trusting data
85
+ nobody, including the model, confirmed as final. This is exactly the failure
86
+ mode independently reported and reproduced in
87
+ [vercel/ai#19063](https://github.com/vercel/ai/issues/19063): tool calls
88
+ executing regardless of an unsafe finish reason, across AI SDK v5, v6, and
89
+ v7, with fixes landing across all three release lines (v7's in `ai@7.0.70`).
90
+ `prefix-safe-json` does not patch or depend on that fix — it's a
91
+ provider-independent guard that requires positive proof a stream ended
92
+ safely before treating any tool call as final, regardless of which SDK,
93
+ provider, or framework hands it the data. See
94
+ `test/guard/ai-sdk-compatibility.test.ts` for the cross-version test
95
+ evidence.
96
+
97
+ **Supported providers**: OpenAI (legacy `function_call` and Responses API),
98
+ Anthropic, Gemini, OpenRouter, generic OpenAI-compatible endpoints, and the
99
+ Vercel AI SDK. High-level guards currently ship for the AI SDK; every
100
+ provider has a public low-level adapter (see below) that composes with
101
+ `createToolCallExecutionGate()` the same way.
102
+
103
+ Full semantics, the decision table, provider finish-reason mapping, and the
104
+ high-level guard API: [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md).
105
+
106
+ ## The lower-level problem this is built on
12
107
 
13
108
  LLM providers (OpenAI, Anthropic, Gemini, OpenRouter) stream tool-call arguments as small JSON chunks. During streaming, the JSON is frequently incomplete: strings split mid-word, UTF-8 characters split mid-byte, containers left unclosed, numbers terminated at chunk boundaries.
14
109
 
@@ -98,6 +193,25 @@ draws exactly that distinction (`outcome: "truncated"`) and could gate
98
193
  execution on it without touching the syntax-repair behavior for the other
99
194
  case.
100
195
 
196
+ ## Scope: what this does and doesn't protect against
197
+
198
+ This library is honestly scoped as **execution integrity** — complete,
199
+ unfabricated, schema-valid arguments — not a general "AI security platform."
200
+ It does **not**:
201
+
202
+ - defend against prompt injection (it says nothing about whether the
203
+ arguments a model *chose* to send are the arguments it *should* have
204
+ sent — only whether they're complete and unfabricated)
205
+ - resolve authorization (it doesn't know which caller may invoke which tool)
206
+ - sandbox execution (`action: "execute"` means "this is genuinely the
207
+ complete value the model produced," not "safe to run without your own
208
+ validation and permissions")
209
+ - detect a malicious or wrong tool choice (a model calling the wrong tool
210
+ with complete, schema-valid arguments is still reported `execute`)
211
+
212
+ See [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#limitations---what-this-does-not-protect-against)
213
+ for the full list, including known internal limitations.
214
+
101
215
  ## Current Status (Alpha)
102
216
 
103
217
  ### Implemented
@@ -111,8 +225,10 @@ case.
111
225
  - Configurable resource limits (depth, input size, string length, queued events)
112
226
  - Machine-readable test corpus with 25+ canonical fixtures
113
227
  - Chunk invariance verification
114
- - Provider stream adapters for OpenAI (legacy `function_call` and Responses API), Anthropic, Gemini, OpenRouter, and generic OpenAI-compatible endpoints, plus a coordinator for tracking multiple concurrent tool-call streams
228
+ - Provider stream adapters for OpenAI (legacy `function_call` and Responses API), Anthropic, Gemini, OpenRouter, generic OpenAI-compatible endpoints, and the Vercel AI SDK, plus a coordinator for tracking multiple concurrent tool-call streams
115
229
  - Optional per-tool JSON Schema validation on the coordinator (see below) — a value can be structurally complete and still not match what a tool declared it needs
230
+ - `createToolCallExecutionGate()` — a fail-closed `execute` / `retry` / `reject` decision per tool call, built on top of the coordinator (see [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md))
231
+ - `createAiSdkExecutionGuard()` — a drop-in high-level guard for the Vercel AI SDK's `fullStream`, composing a provider adapter with the execution gate; every `ExecutionDecision` also carries an `evidence` object explaining why (see [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#high-level-guards))
116
232
 
117
233
  ### Not Yet Implemented
118
234
 
@@ -155,7 +271,14 @@ A schema mismatch also surfaces as a coordinator diagnostic (`E_SCHEMA_VALIDATIO
155
271
  npm install prefix-safe-json
156
272
  ```
157
273
 
158
- ## Quick Start
274
+ ## Low-level API: Quick Start
275
+
276
+ The AI SDK guard (above) covers the common case. For other providers, or
277
+ when you need the adapter/gate instance directly, `createToolCallExecutionGate()`
278
+ and the provider adapters (`AnthropicStreamAdapter`, `OpenAIStreamAdapter`,
279
+ etc.) are equally public — see [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#example).
280
+ The `createParser()` API both are built on is also fully public, for anyone who
281
+ needs direct access to parser state:
159
282
 
160
283
  ```typescript
161
284
  import { createParser } from "prefix-safe-json";
@@ -192,6 +315,16 @@ and asserts the truncated call is never reported executable while a
192
315
  genuinely complete one is. This example runs in CI on every push, so it
193
316
  can't silently rot into a stale claim.
194
317
 
318
+ For the same demonstration through the low-level execution gate and the
319
+ [Vercel AI SDK](https://ai-sdk.dev)'s `fullStream` shape instead, see
320
+ [`examples/ai-sdk-execution-gate.mjs`](examples/ai-sdk-execution-gate.mjs)
321
+ (`node examples/ai-sdk-execution-gate.mjs` after `pnpm run build`) — also
322
+ run in CI on every push. For the drop-in `createAiSdkExecutionGuard()` shown
323
+ at the top of this README, see
324
+ [`examples/ai-sdk-guard.mjs`](examples/ai-sdk-guard.mjs)
325
+ (`node examples/ai-sdk-guard.mjs`) — same scenarios, same guarantees, also
326
+ run in CI on every push.
327
+
195
328
  ## License
196
329
 
197
330
  Licensed under either of:
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Emitted (as a stream-wide `provider_diagnostic`, no `callRef`) by a
3
+ * provider adapter when the underlying provider reports that generation was
4
+ * stopped by a content-safety/policy filter, as opposed to a generic
5
+ * cancellation. The execution gate matches on this to return a distinct
6
+ * `reason: "content_filtered"` rather than lumping it in with ordinary
7
+ * incomplete-stream retries - a policy termination should not be retried
8
+ * blindly.
9
+ */
10
+ export declare const CONTENT_FILTERED_DIAGNOSTIC_CODE = "E_CONTENT_FILTERED";
11
+ //# sourceMappingURL=diagnostic-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostic-codes.d.ts","sourceRoot":"","sources":["../../src/coordinator/diagnostic-codes.ts"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,uBAAuB,CAAC"}
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Cross-module coordinator-level diagnostic codes.
3
+ //
4
+ // Most coordinator diagnostics (see coordinator.ts) are inline string
5
+ // literals local to wherever they're raised - there's no central registry,
6
+ // unlike the parser-level `DiagnosticCode` in diagnostics/codes.ts. This
7
+ // file exists only for the handful of codes that a provider adapter raises
8
+ // and something *outside* that adapter (e.g. the execution gate) needs to
9
+ // recognize by exact code, where a shared constant is safer than either
10
+ // module guessing the other's string literal.
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * Emitted (as a stream-wide `provider_diagnostic`, no `callRef`) by a
14
+ * provider adapter when the underlying provider reports that generation was
15
+ * stopped by a content-safety/policy filter, as opposed to a generic
16
+ * cancellation. The execution gate matches on this to return a distinct
17
+ * `reason: "content_filtered"` rather than lumping it in with ordinary
18
+ * incomplete-stream retries - a policy termination should not be retried
19
+ * blindly.
20
+ */
21
+ export const CONTENT_FILTERED_DIAGNOSTIC_CODE = "E_CONTENT_FILTERED";
22
+ //# sourceMappingURL=diagnostic-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostic-codes.js","sourceRoot":"","sources":["../../src/coordinator/diagnostic-codes.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mDAAmD;AACnD,EAAE;AACF,sEAAsE;AACtE,2EAA2E;AAC3E,yEAAyE;AACzE,2EAA2E;AAC3E,0EAA0E;AAC1E,wEAAwE;AACxE,8CAA8C;AAC9C,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,oBAAoB,CAAC"}
@@ -1,4 +1,4 @@
1
- export type ProviderName = "openai" | "anthropic" | "gemini" | "openrouter" | "openai-compatible" | "unknown";
1
+ export type ProviderName = "openai" | "anthropic" | "gemini" | "openrouter" | "openai-compatible" | "ai-sdk" | "unknown";
2
2
  export interface NormalizedEventBase {
3
3
  /**
4
4
  * Deterministic sequence number assigned by the stream coordinator.
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/coordinator/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,mBAAmB,GACnB,SAAS,CAAC;AAEd,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,MAAM,yBAAyB,GACjC,kBAAkB,GAClB,qBAAqB,GACrB,sBAAsB,GACtB,2BAA2B,GAC3B,gBAAgB,GAChB,uBAAuB,GACvB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA4B,SAAQ,mBAAmB;IACtE,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;CACrC;AAED,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,QAAQ,GACR,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;CACpC;AAED,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/coordinator/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,mBAAmB,GACnB,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,MAAM,yBAAyB,GACjC,kBAAkB,GAClB,qBAAqB,GACrB,sBAAsB,GACtB,2BAA2B,GAC3B,gBAAgB,GAChB,uBAAuB,GACvB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA4B,SAAQ,mBAAmB;IACtE,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;CACrC;AAED,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,QAAQ,GACR,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;CACpC;AAED,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B"}
@@ -0,0 +1,27 @@
1
+ import { CoordinatorDiagnostic, ToolCallState } from "../coordinator/types.js";
2
+ import { StreamEndReason } from "../coordinator/protocol.js";
3
+ import { ExecuteDecision, NonExecutableDecision } from "./types.js";
4
+ export interface DecisionContext {
5
+ /**
6
+ * The stream-level end reason - what actually happened to the underlying
7
+ * provider stream, not any single call's own (possibly still-"collecting")
8
+ * status. Stream-wide fail-closed conditions (provider_error) key off this
9
+ * rather than per-call state.
10
+ */
11
+ readonly streamEndReason: StreamEndReason;
12
+ /**
13
+ * The provider's own pre-normalization reason string for the stream-end
14
+ * event, when one was given (`ProviderStreamEndEvent.providerReason`).
15
+ * Observability only, mirrored onto `DecisionEvidence.providerReason` -
16
+ * never consulted by the decision logic itself.
17
+ */
18
+ readonly streamEndProviderReason?: string;
19
+ /**
20
+ * The coordinator's full, unfiltered diagnostics list (`snapshot().diagnostics`).
21
+ * `decideExecution` splits this into per-call (`internalId` matches) and
22
+ * global (`internalId` undefined) subsets itself.
23
+ */
24
+ readonly diagnostics: readonly CoordinatorDiagnostic[];
25
+ }
26
+ export declare function decideExecution(call: ToolCallState, ctx: DecisionContext): ExecuteDecision | NonExecutableDecision;
27
+ //# sourceMappingURL=decide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/gate/decide.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAoB,eAAe,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEvG,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAE1C;;;;;OAKG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAE1C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACxD;AAeD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,eAAe,GACnB,eAAe,GAAG,qBAAqB,CA+HzC"}
@@ -0,0 +1,138 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Pure execution-decision mapping.
3
+ //
4
+ // Deliberately a standalone function (no coordinator/adapter dependency) so
5
+ // the entire decision table can be unit-tested directly against hand-built
6
+ // ToolCallState fixtures, without spinning up a full push()/finish()
7
+ // pipeline for every row of the table.
8
+ //
9
+ // Priority order matters and is intentional: every fail-closed disqualifier
10
+ // (resource limits, provider errors, content-policy terminations, schema
11
+ // mismatches, malformed content, positively-observed truncation) is checked
12
+ // before the single positive "execute" branch. "execute" is reached only
13
+ // once nothing above has disqualified the call - it is the last positive
14
+ // case, never the first check made, so a future status value that happens
15
+ // to superficially resemble "complete" still can't skip the checks ahead of
16
+ // it.
17
+ // ---------------------------------------------------------------------------
18
+ import { DiagnosticCode } from "../diagnostics/codes.js";
19
+ import { CONTENT_FILTERED_DIAGNOSTIC_CODE } from "../coordinator/diagnostic-codes.js";
20
+ const PARSER_RESOURCE_LIMIT_CODES = new Set([
21
+ DiagnosticCode.E_LIMIT_DEPTH,
22
+ DiagnosticCode.E_LIMIT_INPUT_BYTES,
23
+ DiagnosticCode.E_LIMIT_STRING_BYTES,
24
+ DiagnosticCode.E_LIMIT_EVENT_QUEUE,
25
+ ]);
26
+ const COORDINATOR_RESOURCE_LIMIT_CODES = new Set([
27
+ "E_COORDINATOR_LIMIT_CALLS",
28
+ "E_COORDINATOR_LIMIT_EVENTS",
29
+ "E_TOOL_NAME_LIMIT",
30
+ ]);
31
+ export function decideExecution(call, ctx) {
32
+ const parserDiagnostics = call.parser.diagnostics;
33
+ const callCoordinatorDiagnostics = ctx.diagnostics.filter((d) => d.internalId === call.internalId);
34
+ const globalCoordinatorDiagnostics = ctx.diagnostics.filter((d) => d.internalId === undefined);
35
+ const coordinatorDiagnostics = [
36
+ ...globalCoordinatorDiagnostics,
37
+ ...callCoordinatorDiagnostics,
38
+ ];
39
+ const evidence = {
40
+ provider: call.provider,
41
+ providerReason: ctx.streamEndProviderReason,
42
+ streamEndReason: ctx.streamEndReason,
43
+ terminalConfirmed: ctx.streamEndReason !== "unknown",
44
+ structurallyComplete: call.parser.rootComplete,
45
+ parserExecutable: call.parser.executable,
46
+ schemaValid: call.schemaValid,
47
+ receivedBytes: call.parser.receivedBytes,
48
+ };
49
+ const common = {
50
+ internalId: call.internalId,
51
+ toolCallId: call.toolCallId,
52
+ toolIndex: call.toolIndex,
53
+ name: call.name,
54
+ stableValue: call.parser.stableValue,
55
+ parserDiagnostics,
56
+ coordinatorDiagnostics,
57
+ evidence,
58
+ };
59
+ function reject(reason) {
60
+ return { ...common, action: "reject", executable: false, reason };
61
+ }
62
+ function retry(reason) {
63
+ return { ...common, action: "retry", executable: false, reason };
64
+ }
65
+ // --- Universal fail-closed checks ---------------------------------------
66
+ // Evaluated first and unconditionally, regardless of call.status, so a
67
+ // resource limit, a provider-side failure, or a content-policy
68
+ // termination always wins over whatever the structural status looks like.
69
+ const hasResourceLimit = parserDiagnostics.some((d) => PARSER_RESOURCE_LIMIT_CODES.has(d.code)) ||
70
+ callCoordinatorDiagnostics.some((d) => COORDINATOR_RESOURCE_LIMIT_CODES.has(d.code)) ||
71
+ globalCoordinatorDiagnostics.some((d) => d.code === "E_COORDINATOR_LIMIT_EVENTS");
72
+ if (hasResourceLimit) {
73
+ return reject("resource_limit");
74
+ }
75
+ if (ctx.streamEndReason === "provider_error") {
76
+ return reject("provider_error");
77
+ }
78
+ const isContentFiltered = globalCoordinatorDiagnostics.some((d) => d.code === CONTENT_FILTERED_DIAGNOSTIC_CODE);
79
+ if (isContentFiltered) {
80
+ return reject("content_filtered");
81
+ }
82
+ // --- Status-specific handling --------------------------------------------
83
+ // Exhaustive over ToolCallState["status"] with no `default`: if a future
84
+ // coordinator version adds a new status literal without a matching case
85
+ // here, `noImplicitReturns` fails the build - not a silent runtime gap.
86
+ switch (call.status) {
87
+ case "complete": {
88
+ if (call.schemaValid === false) {
89
+ return reject("schema_invalid");
90
+ }
91
+ // The ONLY positive branch in this entire function, reached only
92
+ // after every disqualifying check above has already passed. The
93
+ // `call.name !== undefined` check is defensive/redundant with the
94
+ // coordinator's own invariant (a call only reaches status "complete"
95
+ // when its name is known) - narrowing on it here is what lets
96
+ // `ExecuteDecision.name` be typed as a required `string` instead of
97
+ // `string | undefined`, so it never needs re-checking at every call
98
+ // site. If the invariant were ever violated, this still fails closed
99
+ // to "stream_incomplete" rather than fabricating a name.
100
+ if (call.parser.executable === true &&
101
+ call.parser.stableValue !== undefined &&
102
+ call.name !== undefined) {
103
+ const decision = {
104
+ ...common,
105
+ name: call.name,
106
+ action: "execute",
107
+ executable: true,
108
+ reason: "complete",
109
+ value: call.parser.stableValue,
110
+ };
111
+ return decision;
112
+ }
113
+ // Root closed, syntactically valid, but not trusted: the parser's
114
+ // own executable contract failed it (stream-end-reason mismatch or
115
+ // trailing data), even though nothing here required a repair.
116
+ return retry("stream_incomplete");
117
+ }
118
+ case "truncated":
119
+ // The parser positively observed an open string/number/literal or an
120
+ // unclosed container - a real, raw mid-value cut, not a matter of
121
+ // interpretation.
122
+ return retry("truncated");
123
+ case "invalid":
124
+ // Duplicate key, unexpected token, or a missing/conflicting tool
125
+ // identity - a genuine content defect, independent of how the stream
126
+ // ended.
127
+ return reject("malformed");
128
+ case "salvaged":
129
+ // Root only closed via a safe-container-closing repair; the JSON
130
+ // *looks* complete, but provider metadata never confirmed genuine
131
+ // completion. This is the container-level truncation case.
132
+ return retry("stream_incomplete");
133
+ case "cancelled":
134
+ case "collecting":
135
+ return retry("stream_incomplete");
136
+ }
137
+ }
138
+ //# sourceMappingURL=decide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/gate/decide.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { CoordinatorLimits, JsonSchemaLike } from "../coordinator/types.js";
2
+ import { ParserOptions } from "../types.js";
3
+ import { ToolCallExecutionGate } from "./types.js";
4
+ /**
5
+ * Creates a fail-closed execution-decision gate for streamed LLM tool calls.
6
+ * Wraps a `ToolCallStreamCoordinator` (same constructor arguments) and adds
7
+ * exactly one operation: turn each call's settled state into an
8
+ * `ExecutionDecision` (`execute` / `retry` / `reject`) via a deterministic,
9
+ * fully-typed decision table. See `docs/EXECUTION_GATE.md`.
10
+ *
11
+ * ```ts
12
+ * const gate = createToolCallExecutionGate(undefined, undefined, {
13
+ * write_file: { type: "object", required: ["path", "content"], properties: {
14
+ * path: { type: "string" }, content: { type: "string" } } },
15
+ * });
16
+ * for (const raw of providerEvents) {
17
+ * for (const normalized of adapter.push(raw)) gate.push(normalized);
18
+ * }
19
+ * for (const decision of gate.finish().decisions) {
20
+ * if (decision.action === "execute") await tools[decision.name](decision.value);
21
+ * }
22
+ * ```
23
+ */
24
+ export declare function createToolCallExecutionGate(limits?: Partial<CoordinatorLimits>, parserOptions?: ParserOptions, toolSchemas?: Record<string, JsonSchemaLike>): ToolCallExecutionGate;
25
+ //# sourceMappingURL=gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate/gate.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,iBAAiB,EAEjB,cAAc,EAGf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAEL,qBAAqB,EAEtB,MAAM,YAAY,CAAC;AA+EpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,EACnC,aAAa,CAAC,EAAE,aAAa,EAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAC3C,qBAAqB,CAEvB"}
@@ -0,0 +1,96 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ToolCallExecutionGate — high-level execution safety layer.
3
+ //
4
+ // Built by composition over ToolCallStreamCoordinator, not by extending or
5
+ // reimplementing it: the gate holds a coordinator instance and adds exactly
6
+ // one thing coordinator state doesn't already capture - which stream-end
7
+ // reason actually got applied - then maps every ToolCallState through the
8
+ // pure decideExecution() table. No re-parsing, no duplicate event pipeline.
9
+ // ---------------------------------------------------------------------------
10
+ import { createToolCallStreamCoordinator } from "../coordinator/coordinator.js";
11
+ import { decideExecution } from "./decide.js";
12
+ class DefaultToolCallExecutionGate {
13
+ coordinator;
14
+ // Mirrors the coordinator's own "first provider_stream_end (from push(),
15
+ // or from finish()'s meta if none arrived) wins" rule - see push()/finish()
16
+ // below and DefaultToolCallStreamCoordinator.finish()'s matching
17
+ // `if (!this.isFinished)` guard. streamEndProviderReason is captured
18
+ // alongside streamEndReason purely for DecisionEvidence.providerReason -
19
+ // it is never read by decideExecution()'s own logic.
20
+ streamEndReason;
21
+ streamEndProviderReason;
22
+ streamEndCaptured = false;
23
+ constructor(limits, parserOptions, toolSchemas) {
24
+ this.coordinator = createToolCallStreamCoordinator(limits, parserOptions, toolSchemas);
25
+ }
26
+ push(event) {
27
+ if (event.type === "provider_stream_end" && !this.streamEndCaptured) {
28
+ this.streamEndCaptured = true;
29
+ this.streamEndReason = event.reason;
30
+ this.streamEndProviderReason = event.providerReason;
31
+ }
32
+ return this.coordinator.push(event);
33
+ }
34
+ snapshot() {
35
+ const snap = this.coordinator.snapshot();
36
+ const ctx = {
37
+ // Pre-finish, no stream-end reason has been observed yet ("unknown"
38
+ // is a neutral placeholder here, not a claim anything went wrong) -
39
+ // every in-flight call is still "collecting" regardless, so this
40
+ // never influences whether something looks executable.
41
+ streamEndReason: this.streamEndReason ?? "unknown",
42
+ streamEndProviderReason: this.streamEndProviderReason,
43
+ diagnostics: snap.diagnostics,
44
+ };
45
+ return snap.calls.map((call) => decideExecution(call, ctx));
46
+ }
47
+ drainEvents() {
48
+ return this.coordinator.drainEvents();
49
+ }
50
+ finish(meta) {
51
+ if (!this.streamEndCaptured) {
52
+ this.streamEndCaptured = true;
53
+ this.streamEndReason = meta?.reason ?? "unknown";
54
+ this.streamEndProviderReason = meta?.providerReason;
55
+ }
56
+ const result = this.coordinator.finish(meta);
57
+ const diagnostics = this.coordinator.snapshot().diagnostics;
58
+ // Unlike snapshot() above, this point is only ever reached after
59
+ // streamEndCaptured is guaranteed true (set on entry to this method, or
60
+ // already true from an earlier push()/finish() call) - and every path
61
+ // that sets streamEndCaptured sets streamEndReason to a defined value in
62
+ // the same branch, so the pre-finish "unknown" placeholder never applies
63
+ // here.
64
+ const ctx = {
65
+ streamEndReason: this.streamEndReason,
66
+ streamEndProviderReason: this.streamEndProviderReason,
67
+ diagnostics,
68
+ };
69
+ const decisions = result.calls.map((call) => decideExecution(call, ctx));
70
+ return { decisions, diagnostics };
71
+ }
72
+ }
73
+ /**
74
+ * Creates a fail-closed execution-decision gate for streamed LLM tool calls.
75
+ * Wraps a `ToolCallStreamCoordinator` (same constructor arguments) and adds
76
+ * exactly one operation: turn each call's settled state into an
77
+ * `ExecutionDecision` (`execute` / `retry` / `reject`) via a deterministic,
78
+ * fully-typed decision table. See `docs/EXECUTION_GATE.md`.
79
+ *
80
+ * ```ts
81
+ * const gate = createToolCallExecutionGate(undefined, undefined, {
82
+ * write_file: { type: "object", required: ["path", "content"], properties: {
83
+ * path: { type: "string" }, content: { type: "string" } } },
84
+ * });
85
+ * for (const raw of providerEvents) {
86
+ * for (const normalized of adapter.push(raw)) gate.push(normalized);
87
+ * }
88
+ * for (const decision of gate.finish().decisions) {
89
+ * if (decision.action === "execute") await tools[decision.name](decision.value);
90
+ * }
91
+ * ```
92
+ */
93
+ export function createToolCallExecutionGate(limits, parserOptions, toolSchemas) {
94
+ return new DefaultToolCallExecutionGate(limits, parserOptions, toolSchemas);
95
+ }
96
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/gate/gate.ts"],"names":[],"mappings":"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"}