prefix-safe-json 0.4.4 → 0.5.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/LICENSE +27 -4
- package/README.md +273 -398
- package/dist/ajv.d.ts +2 -0
- package/dist/ajv.d.ts.map +1 -0
- package/dist/ajv.js +19 -0
- package/dist/ajv.js.map +1 -0
- package/dist/conformance/runner.d.ts +19 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +78 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +115 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +2 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance.d.ts +3 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +11 -0
- package/dist/conformance.js.map +1 -0
- package/dist/coordinator/coordinator.d.ts +25 -11
- package/dist/coordinator/coordinator.d.ts.map +1 -1
- package/dist/coordinator/coordinator.js +140 -25
- package/dist/coordinator/coordinator.js.map +1 -1
- package/dist/coordinator/types.d.ts +3 -2
- package/dist/coordinator/types.d.ts.map +1 -1
- package/dist/gate/gate.d.ts +2 -2
- package/dist/gate/gate.d.ts.map +1 -1
- package/dist/gate/gate.js +4 -4
- package/dist/gate/gate.js.map +1 -1
- package/dist/guard/provider-guard.js +1 -1
- package/dist/guard/provider-guard.js.map +1 -1
- package/dist/guard/types.d.ts +11 -4
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +89 -8
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts +122 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +442 -25
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +47 -1
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +381 -84
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +459 -67
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +14 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/standard-schema.d.ts +2 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +11 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/validation/ajv-validator.d.ts +43 -0
- package/dist/validation/ajv-validator.d.ts.map +1 -0
- package/dist/validation/ajv-validator.js +63 -0
- package/dist/validation/ajv-validator.js.map +1 -0
- package/dist/validation/standard-schema-validator.d.ts +41 -0
- package/dist/validation/standard-schema-validator.d.ts.map +1 -0
- package/dist/validation/standard-schema-validator.js +131 -0
- package/dist/validation/standard-schema-validator.js.map +1 -0
- package/dist/validation/types.d.ts +60 -0
- package/dist/validation/types.d.ts.map +1 -0
- package/dist/validation/types.js +77 -0
- package/dist/validation/types.js.map +1 -0
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -1,316 +1,122 @@
|
|
|
1
1
|
# prefix-safe-json
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/prefix-safe-json)
|
|
4
|
+
[](https://github.com/canblmz1/prefix-safe-json/actions/workflows/ci.yml)
|
|
5
|
+
[](#license)
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
terminal state before caller-owned side effects.**
|
|
7
|
+
**Fail-closed execution integrity for streamed LLM tool calls.**
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Install
|
|
9
|
+
Prevent truncated, conflicting, or unconfirmed streamed tool arguments
|
|
10
|
+
from reaching side effects. `prefix-safe-json` distinguishes complete,
|
|
11
|
+
unfabricated raw arguments from truncated or unconfirmed input — including
|
|
12
|
+
SDK-projected or repaired values that merely *look* complete. **JSON
|
|
13
|
+
validity is not execution authority.**
|
|
17
14
|
|
|
18
15
|
```bash
|
|
19
16
|
pnpm add prefix-safe-json ai
|
|
20
17
|
# or: npm install prefix-safe-json ai
|
|
21
18
|
```
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
development and release jobs remain on Node 22/24 because the development
|
|
25
|
-
toolchain and the exact `ai@7.0.77` lifecycle proof require newer Node; those
|
|
26
|
-
requirements do not apply to the package's runtime dependency graph.
|
|
20
|
+
## 30-line safe execution example
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
```javascript
|
|
23
|
+
import { streamText } from "ai";
|
|
24
|
+
import { createAiSdkExecutionGuard, createAiSdkExecutionLock } from "prefix-safe-json";
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[`RELEASE_INTEGRITY.md`](docs/RELEASE_INTEGRITY.md) for why); the verifier
|
|
36
|
-
establishes its release commit through verified provenance instead, failing
|
|
37
|
-
closed if that provenance is missing or disagrees. See the command-driven
|
|
38
|
-
[maintainer audit](docs/MAINTAINER_AUDIT.md),
|
|
39
|
-
the exact [release/hash mapping](docs/RELEASE_INTEGRITY.md), the
|
|
40
|
-
[runtime dependency graph](docs/RUNTIME_DEPENDENCIES.md), and the
|
|
41
|
-
[execution-critical source map](docs/EXECUTION_AUDIT_SURFACE.md).
|
|
26
|
+
// 1. Lock tool definitions before the AI SDK sees them - removes every
|
|
27
|
+
// SDK-invoked callback that could run your code before this guard does.
|
|
28
|
+
const lockedTools = createAiSdkExecutionLock({
|
|
29
|
+
write_file: { description: "Write a UTF-8 text file", inputSchema: writeFileSchema },
|
|
30
|
+
});
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
// 2. Stream, feeding the real fullStream into the guard.
|
|
33
|
+
const result = streamText({ model, prompt, tools: lockedTools });
|
|
34
|
+
const guard = createAiSdkExecutionGuard({ schemas: { write_file: writeFileSchema } });
|
|
35
|
+
for await (const part of result.fullStream) {
|
|
36
|
+
guard.push(part);
|
|
37
|
+
}
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// 3. finish() signals the stream ended. Every decision is replayable
|
|
40
|
+
// diagnostic state - it does not perform anything by itself.
|
|
41
|
+
const final = guard.finish();
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} from "prefix-safe-json";
|
|
55
|
-
|
|
56
|
-
const writeFileSchema = {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
path: { type: "string" },
|
|
60
|
-
content: { type: "string" },
|
|
61
|
-
},
|
|
62
|
-
required: ["path", "content"],
|
|
63
|
-
additionalProperties: false,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export async function runToolCall(model, prompt, callerOwnedSideEffect) {
|
|
67
|
-
const lockedTools = createAiSdkExecutionLock({
|
|
68
|
-
write_file: {
|
|
69
|
-
description: "Write a UTF-8 text file",
|
|
70
|
-
inputSchema: jsonSchema(writeFileSchema),
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const result = streamText({ model, prompt, tools: lockedTools });
|
|
75
|
-
const guard = createAiSdkExecutionGuard({
|
|
76
|
-
schemas: { write_file: writeFileSchema },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
for await (const part of result.fullStream) {
|
|
80
|
-
guard.push(part);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const final = guard.finish(); // replayable diagnostic state
|
|
84
|
-
for (const observed of final.decisions) {
|
|
85
|
-
const authority = guard.takeDecision(observed.internalId);
|
|
86
|
-
if (authority) await callerOwnedSideEffect(authority.value);
|
|
87
|
-
}
|
|
43
|
+
// 4. The caller owns dispatch: take each call's authority exactly once,
|
|
44
|
+
// then run the real, irreversible side effect yourself.
|
|
45
|
+
for (const observed of final.decisions) {
|
|
46
|
+
const authority = guard.takeDecision(observed.internalId);
|
|
47
|
+
if (authority) await writeFile(authority.value.path, authority.value.content);
|
|
88
48
|
}
|
|
89
49
|
```
|
|
90
50
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
51
|
+
`prefix-safe-json` never calls `writeFile` (or anything like it) itself.
|
|
52
|
+
The last line above is the only place a side effect happens, and it's
|
|
53
|
+
entirely yours. See [`examples/`](examples/) for full, runnable
|
|
54
|
+
demonstrations against real provider/SDK shapes, no mocked internals.
|
|
55
|
+
|
|
56
|
+
## Why this exists
|
|
57
|
+
|
|
58
|
+
LLM providers stream tool-call arguments as small JSON chunks. During
|
|
59
|
+
streaming, the JSON is frequently incomplete: strings split mid-word,
|
|
60
|
+
UTF-8 characters split mid-byte, containers left unclosed, numbers
|
|
61
|
+
terminated at chunk boundaries — and even once the JSON *looks* complete,
|
|
62
|
+
nothing about its shape says whether the provider actually confirmed the
|
|
63
|
+
call is done, or just stopped for an unrelated reason (a token limit, an
|
|
64
|
+
error, a content filter) partway through.
|
|
65
|
+
|
|
66
|
+
> **The core invariant: parse completion is not execution authority.**
|
|
67
|
+
|
|
68
|
+
Structural validity, confirmed completeness, and execution authority are
|
|
69
|
+
three separate questions. Most streaming JSON helpers — including some
|
|
70
|
+
already used for LLM tool-call arguments in the wild — are built to show
|
|
71
|
+
*something* as early as possible, even if that means guessing. Given a
|
|
72
|
+
stream that has so far delivered `{"city":"Tok` (the model is still
|
|
73
|
+
typing "Tokyo"):
|
|
74
|
+
|
|
75
|
+
| | `city` field after this chunk |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [vercel/ai's `fixJson`](https://github.com/vercel/ai/blob/main/packages/ai/src/util/fix-json.ts) | `"Tok"` — closes the open string as-is, reports a successful parse |
|
|
78
|
+
| [langchain's `parsePartialJson`](https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-core/src/utils/json.ts) | `"Tok"` — returns whatever string content it collected |
|
|
79
|
+
| **`prefix-safe-json`** (`snapshot().stableValue`) | *(absent — `city` is not committed yet)* |
|
|
80
|
+
|
|
81
|
+
Verified by cloning both projects and running their actual code
|
|
82
|
+
side-by-side with this library. Neither is a bug — they're built for
|
|
83
|
+
progressively rendering a value in a UI, where showing `"Tok"` then
|
|
84
|
+
`"Tokyo"` a moment later is good UX. `prefix-safe-json` answers a
|
|
85
|
+
different question: *is it safe to act on this value yet* — where
|
|
86
|
+
treating `"Tok"` as the city would be wrong. If you need live
|
|
87
|
+
"filling in..." UI text, a partial-JSON renderer is the right tool. If
|
|
88
|
+
you need to know precisely when it's safe to execute a tool call with the
|
|
89
|
+
parsed arguments, that's what this library is for.
|
|
90
|
+
|
|
91
|
+
## What `execute` / `retry` / `reject` mean
|
|
113
92
|
|
|
114
93
|
Every tool call gets exactly one verdict:
|
|
115
94
|
|
|
116
|
-
- **`execute`** — complete, unfabricated, and (if a schema is
|
|
117
|
-
|
|
95
|
+
- **`execute`** — complete, unfabricated, and (if a schema/validator is
|
|
96
|
+
registered) valid. `decision.value` is safe to pass to your tool.
|
|
118
97
|
- **`retry`** — nothing is wrong with what arrived; there just isn't a
|
|
119
98
|
trustworthy complete value yet. Continue generation.
|
|
120
99
|
- **`reject`** — the data itself is the problem (malformed JSON, a schema
|
|
121
100
|
mismatch, a resource limit, a provider error, a content-policy
|
|
122
101
|
termination). Retrying the same input won't help.
|
|
123
102
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
A tool call cut short by a token limit, a provider error, or a content
|
|
127
|
-
filter can still contain syntactically complete, schema-valid JSON — the
|
|
128
|
-
model was simply stopped before finishing the *rest* of its response, not
|
|
129
|
-
necessarily mid-argument. Executing on JSON shape alone means trusting data
|
|
130
|
-
nobody, including the model, confirmed as final. This is exactly the failure
|
|
131
|
-
mode independently reported and reproduced in
|
|
132
|
-
[vercel/ai#19063](https://github.com/vercel/ai/issues/19063): tool calls
|
|
133
|
-
executing regardless of an unsafe finish reason, across AI SDK v5, v6, and
|
|
134
|
-
v7, with fixes landing across all three release lines.
|
|
135
|
-
`prefix-safe-json` does not patch or depend on that fix — it's a
|
|
136
|
-
provider-independent guard that requires positive proof a stream ended
|
|
137
|
-
safely before treating any tool call as final, regardless of which SDK,
|
|
138
|
-
provider, or framework hands it the data. See
|
|
139
|
-
`test/guard/ai-sdk-compatibility.test.ts` for the cross-version test
|
|
140
|
-
evidence.
|
|
141
|
-
|
|
142
|
-
**Execution ownership: what must never also happen**
|
|
143
|
-
|
|
144
|
-
The guard's decisions only mean something if they're the sole thing deciding
|
|
145
|
-
whether your tool function runs. The recommended pattern above prevents the
|
|
146
|
-
AI SDK's own tool-calling loop from running caller callbacks first —
|
|
147
|
-
in order from strongest guarantee to weakest:
|
|
148
|
-
|
|
149
|
-
- **Strongest — `createAiSdkExecutionLock()`** — wrap your tool definitions
|
|
150
|
-
with it before passing them to `streamText()`/`generateText()`. It removes
|
|
151
|
-
every AI SDK-invoked callback capable of running your code before this
|
|
152
|
-
library's decision — not just `execute`, but also `onInputStart`,
|
|
153
|
-
`onInputDelta`, and `onInputAvailable` (verified directly against
|
|
154
|
-
`ai@5`/`ai@6`/`ai@7`'s own real source: all three fire unconditionally,
|
|
155
|
-
independent of `needsApproval`/approval status — `needsApproval: true`
|
|
156
|
-
alone does **not** stop them, which is exactly the gap this function
|
|
157
|
-
closes). It also forces `needsApproval: true`, which still closes the
|
|
158
|
-
`execute` gap specifically on `ai@6`+ via the SDK's own approval
|
|
159
|
-
mechanism. It also rejects a **function-valued `description`**
|
|
160
|
-
(`ai@7`+ invokes it during tool preparation, before the model call even
|
|
161
|
-
begins — arbitrary caller code on the same pre-decision timeline as the
|
|
162
|
-
callback trio; a string `description` is unaffected on every major), and
|
|
163
|
-
any **provider tool shape whose real execution location it cannot
|
|
164
|
-
verify**: `isProviderExecuted: true` (real operation runs entirely on the
|
|
165
|
-
model provider's remote infrastructure), `ai@6`'s discriminator-less
|
|
166
|
-
`{ type: "provider" }`, and `ai@5`'s discriminator-less
|
|
167
|
-
`{ type: "provider-defined" }` are all rejected rather than silently
|
|
168
|
-
accepted; `ai@7`'s `{ type: "provider", isProviderExecuted: false }` is
|
|
169
|
-
accepted (verified to have no `execute` field at all on that shape, so the
|
|
170
|
-
SDK can never auto-run it). The guarantee is precisely about **this
|
|
171
|
-
function's own output, unchanged, on a supported local tool definition**:
|
|
172
|
-
it says nothing about a tool that bypasses this function, one
|
|
173
|
-
mutated/reconstructed after this function returns it, or a rejected
|
|
174
|
-
shape. Still dispatch manually from
|
|
175
|
-
`guard.takeDecision(observed.internalId)`, exactly as shown above; this only closes the
|
|
176
|
-
door on the SDK (or your own callbacks) doing it *for* you. See
|
|
177
|
-
[`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#execution-ownership-tool-resulttool-error-as-evidence).
|
|
178
|
-
- **Safe, supported pattern (all majors, including `ai@5`)** — define your
|
|
179
|
-
tools *without* an AI SDK-native `execute` callback at all. Not even a
|
|
180
|
-
no-op one: the guard treats any `tool-result`/`tool-error` it observes as
|
|
181
|
-
proof the SDK's tool loop already invoked that call, and a no-op callback
|
|
182
|
-
still produces one — omitting `execute` entirely is what keeps the SDK
|
|
183
|
-
from running the tool itself in the first place. Consume `fullStream`
|
|
184
|
-
yourself, call `guard.finish()` for diagnostic state, and dispatch manually
|
|
185
|
-
only from `guard.takeDecision(observed.internalId)`, exactly as shown above. `createAiSdkExecutionLock()`
|
|
186
|
-
above gets you this same shape automatically (it drops `execute` too) plus
|
|
187
|
-
the SDK-enforced backstop where that backstop exists.
|
|
188
|
-
- **Unprotected / misuse pattern** — attach the real, irreversible operation
|
|
189
|
-
(or any side-effecting code at all) directly as `execute`, `onInputStart`,
|
|
190
|
-
`onInputDelta`, or `onInputAvailable` on a tool definition that bypasses
|
|
191
|
-
`createAiSdkExecutionLock()` entirely, *and* separately run the guard on
|
|
192
|
-
the same stream. All four are callbacks the SDK invokes on its own
|
|
193
|
-
schedule, independent of and often before this guard ever reaches a
|
|
194
|
-
decision — `onInputStart`/`onInputDelta`/`onInputAvailable` in particular
|
|
195
|
-
run regardless of finish reason, schema validity, or approval status, on
|
|
196
|
-
every major including `ai@5`. If that happens, the side effect has
|
|
197
|
-
already run — `prefix-safe-json` cannot retroactively undo it, and on
|
|
198
|
-
`ai@5` specifically nothing in this library can prevent any of these four
|
|
199
|
-
callbacks from firing at all (verified directly, not assumed — see
|
|
200
|
-
`test/integration/ai-sdk-lifecycle/ai-v5.real.test.ts`).
|
|
201
|
-
|
|
202
|
-
The guard does defend against the second pattern in one specific way, and
|
|
203
|
-
only for `execute` specifically: a `tool-result` or `tool-error` part on
|
|
204
|
-
`fullStream` is direct evidence the SDK's own loop already invoked *some*
|
|
205
|
-
call's `execute`. When it names a specific call (a real `toolCallId`), only
|
|
206
|
-
that call is disqualified; when it doesn't, the guard cannot tell which
|
|
207
|
-
in-flight call was affected and fails closed for **every** call in that
|
|
208
|
-
stream rather than guess. Either way the guard never again reports
|
|
209
|
-
`action: "execute"` for the affected call(s)
|
|
210
|
-
(`reason: "sdk_execution_observed"`, which takes priority over every other
|
|
211
|
-
rejection reason), so a caller who also runs the documented manual-dispatch
|
|
212
|
-
loop will not invoke the tool function a *second* time. There is no
|
|
213
|
-
equivalent detection for a bypassed `onInputStart`/`onInputDelta`/
|
|
214
|
-
`onInputAvailable` — those callbacks produce no observable `fullStream`
|
|
215
|
-
evidence of having run at all, so a misuse of any of the three is invisible
|
|
216
|
-
to the guard entirely; the only defense is calling
|
|
217
|
-
`createAiSdkExecutionLock()` in the first place. It does not undo
|
|
218
|
-
whatever the SDK's own callback already did. See
|
|
219
|
-
[`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#execution-ownership-tool-resulttool-error-as-evidence)
|
|
220
|
-
for the full behavior and test matrix.
|
|
221
|
-
|
|
222
|
-
**Supported providers**: OpenAI (legacy `function_call` and Responses API),
|
|
223
|
-
Anthropic, Gemini, OpenRouter, generic OpenAI-compatible endpoints, and the
|
|
224
|
-
Vercel AI SDK. High-level guards currently ship for the AI SDK; every
|
|
225
|
-
provider has a public low-level adapter (see below) that composes with
|
|
226
|
-
`createToolCallExecutionGate()` the same way.
|
|
227
|
-
|
|
228
|
-
Gemini's adapter exposes its structured argument projection for inspection
|
|
229
|
-
and validation, but it never grants strict execute authority because Gemini
|
|
230
|
-
does not provide raw argument text at this seam. OpenAI-compatible and
|
|
231
|
-
OpenRouter events require a non-negative integer `choice.index`; missing,
|
|
232
|
-
invalid, or duplicate choice identity fails closed instead of guessing zero.
|
|
103
|
+
See [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md) for the full
|
|
104
|
+
decision table and every `reason` value.
|
|
233
105
|
|
|
234
|
-
|
|
235
|
-
`streamText()`/`generateText()` `fullStream` surface, whose `finishReason`
|
|
236
|
-
is a plain unified string (`"stop"`, `"length"`, ...). They are not
|
|
237
|
-
currently an adapter for the lower-level `@ai-sdk/provider` `doStream()`
|
|
238
|
-
boundary (`LanguageModelV3`/`V4`), where finish reason is
|
|
239
|
-
`{ unified, raw }`-object-shaped — building your own provider against that
|
|
240
|
-
interface directly needs different handling of that field.
|
|
241
|
-
|
|
242
|
-
Full semantics, the decision table, provider finish-reason mapping, and the
|
|
243
|
-
high-level guard API: [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md).
|
|
244
|
-
|
|
245
|
-
## The lower-level problem this is built on
|
|
246
|
-
|
|
247
|
-
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.
|
|
248
|
-
|
|
249
|
-
This library parses each chunk incrementally and provides:
|
|
250
|
-
|
|
251
|
-
- **Committed values**: Fields whose values are definitively complete
|
|
252
|
-
- **Pending tokens**: Partially received values awaiting more data
|
|
253
|
-
- **Monotonic semantic events**: JSON Pointer-based events that are never retracted
|
|
254
|
-
- **Parse diagnostics**: Structured error reporting with severity and recoverability
|
|
255
|
-
- **Deterministic repair**: When the stream ends, a clear accounting of what was repaired and why
|
|
256
|
-
- **Execution safety**: Whether the parsed result is safe to use for tool invocation
|
|
257
|
-
|
|
258
|
-
## Core Principle
|
|
259
|
-
|
|
260
|
-
> The parser never fabricates missing data. It clearly distinguishes what is definite, what is pending, what was deterministically repaired, and what is lost.
|
|
261
|
-
|
|
262
|
-
> **Valid JSON is not safe generation completion. Safe generation completion is not permission to execute a side effect.** Structural validity, confirmed completeness, and execution authority are three separate questions — the gate layer (below) answers all three before ever returning `execute`, not just the first. See [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) for the full guarantee/non-guarantee list.
|
|
263
|
-
|
|
264
|
-
## Why not a general "partial JSON" parser?
|
|
265
|
-
|
|
266
|
-
Most streaming JSON helpers (including ones already used in the wild for
|
|
267
|
-
LLM tool-call arguments) are designed to show *something* as early as
|
|
268
|
-
possible, even if that means guessing. Concretely, given a stream that has
|
|
269
|
-
so far delivered `{"city":"Tok` (the model is still typing "Tokyo"):
|
|
270
|
-
|
|
271
|
-
| | `city` field after this chunk |
|
|
272
|
-
|---|---|
|
|
273
|
-
| [vercel/ai's `fixJson`](https://github.com/vercel/ai/blob/main/packages/ai/src/util/fix-json.ts) | `"Tok"` — closes the open string as-is and reports a successful parse |
|
|
274
|
-
| [langchain's `parsePartialJson`](https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-core/src/utils/json.ts) | `"Tok"` — its recursive-descent parser returns whatever string content it collected before running out of input |
|
|
275
|
-
| **this library** (`snapshot().stableValue`) | *(absent — `city` is not committed yet)* |
|
|
276
|
-
|
|
277
|
-
Both of those are real, current, well-engineered implementations — verified
|
|
278
|
-
by cloning `vercel/ai` (commit `fbb154a0`) and `langchain-ai/langchainjs`
|
|
279
|
-
(commit `555d6f14`) and running their actual code side-by-side with this
|
|
280
|
-
library. Neither is a bug: they're built for progressively rendering a
|
|
281
|
-
value in a UI, where showing `"Tok"` and then `"Tokyo"` a moment later is
|
|
282
|
-
good UX. This library is built for the different question — *is it safe to
|
|
283
|
-
act on this value yet* — where treating `"Tok"` as the city would be
|
|
284
|
-
wrong. It's why `city` only appears in `stableValue` once its closing
|
|
285
|
-
quote genuinely arrives, and why `executable` only becomes `true` once the
|
|
286
|
-
whole document is unambiguously, definitively complete.
|
|
287
|
-
|
|
288
|
-
If you need live "filling in..." UI text, a partial-JSON renderer is the
|
|
289
|
-
right tool. If you need to know precisely when it's safe to execute a tool
|
|
290
|
-
call with the parsed arguments, that's what this library is for.
|
|
291
|
-
|
|
292
|
-
### A concrete case where this matters: silent execution on truncated data
|
|
106
|
+
## Failure cases
|
|
293
107
|
|
|
294
108
|
The risk above isn't hypothetical. [Cline](https://github.com/cline/cline)
|
|
295
109
|
(a coding agent that writes files and runs terminal commands from LLM tool
|
|
296
110
|
calls — verified by cloning it, commit `81cce3d70e1`) wires a JSON-repair
|
|
297
|
-
step directly into tool execution:
|
|
298
|
-
[`
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
that runs. That function's own docstring names its target: "truncated
|
|
302
|
-
payloads ... common with weaker models." Its repair path
|
|
303
|
-
([`sdk/packages/shared/src/parse/json.ts:42`](https://github.com/cline/cline/blob/81cce3d70e10244cdde40dbd0eb0bb711c93006d/sdk/packages/shared/src/parse/json.ts#L42))
|
|
304
|
-
runs the raw argument text through
|
|
305
|
-
[`jsonrepair`](https://github.com/josdejong/jsonrepair), which — like the
|
|
306
|
-
two implementations above — closes an unterminated string as-is rather
|
|
307
|
-
than rejecting it.
|
|
308
|
-
|
|
309
|
-
Feeding `parseJsonStream` (copied verbatim from that file) and this
|
|
310
|
-
library the same truncated `write_file` tool call — a `content` argument
|
|
311
|
-
cut off mid-value, the exact scenario the docstring describes:
|
|
111
|
+
step directly into tool execution: whatever
|
|
112
|
+
[`repairMalformedToolCall`](https://github.com/cline/cline/blob/81cce3d70e10244cdde40dbd0eb0bb711c93006d/sdk/packages/llms/src/providers/ai-sdk.ts#L1332)
|
|
113
|
+
returns *is* the tool call that runs, and its repair path closes an
|
|
114
|
+
unterminated string as-is, same as the table above.
|
|
312
115
|
|
|
313
|
-
|
|
116
|
+
Feeding both implementations the same truncated `write_file` call — a
|
|
117
|
+
`content` argument cut off mid-value:
|
|
118
|
+
|
|
119
|
+
```text
|
|
314
120
|
input: {"path":"config/database.yml","content":"production:\n host: db.prod.internal\n password: correct-horse-battery-sta
|
|
315
121
|
|
|
316
122
|
Cline's parseJsonStream() -> jsonrepair:
|
|
@@ -318,166 +124,235 @@ Cline's parseJsonStream() -> jsonrepair:
|
|
|
318
124
|
"content": "production:\n host: db.prod.internal\n password: correct-horse-battery-sta" }
|
|
319
125
|
No error, no warning. This is what gets written to disk.
|
|
320
126
|
|
|
321
|
-
|
|
127
|
+
prefix-safe-json:
|
|
322
128
|
outcome: "truncated", executable: false
|
|
323
129
|
stableValue: { "path": "config/database.yml" } // content is absent, not guessed
|
|
324
130
|
```
|
|
325
131
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
132
|
+
This is one instance of a class of defect independently found across
|
|
133
|
+
several unrelated agent runtimes — see
|
|
134
|
+
[`docs/REAL_WORLD_FAILURES.md`](docs/REAL_WORLD_FAILURES.md) for the full,
|
|
135
|
+
strictly-classified matrix (which reports are upstream-fixed, which are
|
|
136
|
+
still open, which are internal reproductions only) and
|
|
137
|
+
[`docs/CASE_STUDY_SANDBASE.md`](docs/CASE_STUDY_SANDBASE.md) for how one
|
|
138
|
+
independently maintained runtime closed this exact gap in production.
|
|
139
|
+
|
|
140
|
+
## Supported boundaries
|
|
141
|
+
|
|
142
|
+
`prefix-safe-json` owns **tool-call execution integrity** — raw argument
|
|
143
|
+
evidence, identity correlation, lifecycle completeness, truncation
|
|
144
|
+
detection, conflicting evidence, and validator-verdict composition, up
|
|
145
|
+
through a one-shot execution-authority decision. It does **not** own tool
|
|
146
|
+
permission, application authorization, human approval, sandboxing,
|
|
147
|
+
application idempotency, distributed exactly-once semantics, or prompt
|
|
148
|
+
injection defense — see
|
|
149
|
+
[`docs/PRODUCT_POSITIONING.md`](docs/PRODUCT_POSITIONING.md) for the full
|
|
150
|
+
owns/does-not-own boundary and
|
|
151
|
+
[`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) for the complete guarantee/
|
|
152
|
+
non-guarantee list, including known internal limitations.
|
|
153
|
+
|
|
154
|
+
Concretely, it does **not**:
|
|
342
155
|
|
|
343
156
|
- defend against prompt injection (it says nothing about whether the
|
|
344
157
|
arguments a model *chose* to send are the arguments it *should* have
|
|
345
158
|
sent — only whether they're complete and unfabricated)
|
|
346
|
-
- resolve authorization (it doesn't know which caller may invoke which
|
|
159
|
+
- resolve authorization (it doesn't know which caller may invoke which
|
|
160
|
+
tool)
|
|
347
161
|
- sandbox execution (`action: "execute"` means "this is genuinely the
|
|
348
162
|
complete value the model produced," not "safe to run without your own
|
|
349
163
|
validation and permissions")
|
|
350
164
|
- detect a malicious or wrong tool choice (a model calling the wrong tool
|
|
351
|
-
with complete,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
165
|
+
with complete, valid arguments is still reported `execute`)
|
|
166
|
+
|
|
167
|
+
## AI SDK integration
|
|
168
|
+
|
|
169
|
+
The 30-line example above covers the common case. Three integration
|
|
170
|
+
patterns exist, strongest first:
|
|
171
|
+
|
|
172
|
+
- **Strongest — `createAiSdkExecutionLock()`.** Wrap your tool
|
|
173
|
+
definitions with it before passing them to `streamText()`/
|
|
174
|
+
`generateText()`. It removes every AI SDK-invoked callback capable of
|
|
175
|
+
running your code before this library's decision — not just `execute`,
|
|
176
|
+
but also `onInputStart`, `onInputDelta`, and `onInputAvailable`
|
|
177
|
+
(verified directly against `ai@5`/`ai@6`/`ai@7`'s own source: all three
|
|
178
|
+
fire unconditionally, independent of `needsApproval`). It also forces
|
|
179
|
+
`needsApproval: true` and rejects any provider-tool shape whose real
|
|
180
|
+
execution location it cannot verify. Still dispatch manually from
|
|
181
|
+
`guard.takeDecision(observed.internalId)` — this only closes the door
|
|
182
|
+
on the SDK (or your own callbacks) doing it *for* you.
|
|
183
|
+
- **Safe, supported pattern (all majors, including `ai@5`)** — define
|
|
184
|
+
your tools *without* an AI SDK-native `execute` callback at all. Not
|
|
185
|
+
even a no-op one. Consume `fullStream` yourself and dispatch manually.
|
|
186
|
+
`createAiSdkExecutionLock()` gets you this automatically.
|
|
187
|
+
- **Unprotected / misuse pattern** — attach the real side effect directly
|
|
188
|
+
as `execute`/`onInputStart`/`onInputDelta`/`onInputAvailable` on a tool
|
|
189
|
+
definition that bypasses the lock. If that happens, the side effect has
|
|
190
|
+
already run before this library ever reaches a decision; nothing here
|
|
191
|
+
can retroactively undo it.
|
|
192
|
+
|
|
193
|
+
The guard does defend against the misuse pattern in one specific way: a
|
|
194
|
+
`tool-result`/`tool-error` part on `fullStream` is direct evidence the
|
|
195
|
+
SDK's own loop already invoked *some* call's `execute`, and once observed
|
|
196
|
+
this library never again reports `action: "execute"` for the affected
|
|
197
|
+
call(s) (`reason: "sdk_execution_observed"` — the highest-priority
|
|
198
|
+
rejection reason of any). There is no equivalent detection for a bypassed
|
|
199
|
+
`onInputStart`/`onInputDelta`/`onInputAvailable` — those callbacks
|
|
200
|
+
produce no observable evidence of having run at all. Full behavior, the
|
|
201
|
+
exact test matrix, and every verified `ai@5`/`ai@6`/`ai@7` behavioral
|
|
202
|
+
difference:
|
|
203
|
+
[`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#execution-ownership-tool-resulttool-error-as-evidence).
|
|
357
204
|
|
|
358
|
-
|
|
205
|
+
`AiSdkStreamAdapter`/`createAiSdkExecutionGuard()` target the public
|
|
206
|
+
`streamText()`/`generateText()` `fullStream` surface (`finishReason` as a
|
|
207
|
+
plain unified string), not the lower-level `@ai-sdk/provider`
|
|
208
|
+
`doStream()` boundary.
|
|
359
209
|
|
|
360
|
-
|
|
210
|
+
## Provider adapters
|
|
361
211
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
- Semantic event emission for committed scalar values
|
|
366
|
-
- Duplicate key detection and rejection
|
|
367
|
-
- `push()` / `snapshot()` / `drainEvents()` / `finish()` API
|
|
368
|
-
- Configurable resource limits (depth, input size, string length, queued events)
|
|
369
|
-
- Machine-readable test corpus with 25+ canonical fixtures
|
|
370
|
-
- Chunk invariance verification
|
|
371
|
-
- 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
|
|
372
|
-
- 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
|
|
373
|
-
- `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))
|
|
374
|
-
- `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))
|
|
375
|
-
- Call-scoped one-shot authority via `takeDecision(internalId)`; replayable `finish()` results remain available for diagnostics, while the recommended dispatch path cannot take the same execute authority twice
|
|
376
|
-
- SDK execution-ownership detection — direct evidence that the AI SDK's own tool loop already invoked a call (attributed or, when it can't be, stream-wide) permanently blocks this library from also authorizing it, with the highest rejection-reason priority of any disqualifier (see [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md#execution-ownership-tool-resulttool-error-as-evidence))
|
|
212
|
+
Every provider below has a public low-level adapter that composes with
|
|
213
|
+
`createToolCallExecutionGate()` the same way the high-level AI SDK guard
|
|
214
|
+
does internally:
|
|
377
215
|
|
|
378
|
-
|
|
216
|
+
**OpenAI** (legacy `function_call` and Responses API), **Anthropic**,
|
|
217
|
+
**Gemini**, **OpenRouter**, generic **OpenAI-compatible** endpoints, and
|
|
218
|
+
the **Vercel AI SDK**.
|
|
379
219
|
|
|
380
|
-
|
|
381
|
-
|
|
220
|
+
```typescript
|
|
221
|
+
import { createToolCallExecutionGate, OpenAIStreamAdapter } from "prefix-safe-json";
|
|
382
222
|
|
|
383
|
-
|
|
223
|
+
const adapter = new OpenAIStreamAdapter();
|
|
224
|
+
const gate = createToolCallExecutionGate();
|
|
225
|
+
for (const rawChunk of stream) {
|
|
226
|
+
for (const event of adapter.push(rawChunk)) gate.push(event);
|
|
227
|
+
}
|
|
228
|
+
for (const event of adapter.finish({ reason: "complete" })) gate.push(event);
|
|
229
|
+
const { decisions } = gate.finish();
|
|
230
|
+
```
|
|
384
231
|
|
|
385
|
-
|
|
232
|
+
Notes: Gemini's adapter exposes its structured argument projection for
|
|
233
|
+
inspection and validation, but never grants strict execute authority —
|
|
234
|
+
Gemini does not provide raw argument text at this seam. OpenAI-compatible
|
|
235
|
+
and OpenRouter events require a non-negative integer `choice.index`;
|
|
236
|
+
missing, invalid, or duplicate choice identity fails closed instead of
|
|
237
|
+
guessing zero. Full semantics, the decision table, and provider
|
|
238
|
+
finish-reason mapping: [`docs/EXECUTION_GATE.md`](docs/EXECUTION_GATE.md).
|
|
386
239
|
|
|
387
|
-
|
|
240
|
+
### Bring your own validator
|
|
388
241
|
|
|
389
|
-
|
|
242
|
+
Schema/validator checking is optional and validator-agnostic — plug in
|
|
243
|
+
Zod, TypeBox, Valibot, a Standard Schema-compliant validator, or a
|
|
244
|
+
hand-written check via the separate `validators` option, alongside or
|
|
245
|
+
instead of a raw JSON Schema object via the pre-existing `toolSchemas`/
|
|
246
|
+
`schemas` option (statically compiled through Ajv):
|
|
390
247
|
|
|
391
248
|
```typescript
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const coordinator = createToolCallStreamCoordinator(undefined, undefined, {
|
|
395
|
-
write_file: {
|
|
396
|
-
type: "object",
|
|
397
|
-
properties: {
|
|
398
|
-
path: { type: "string" },
|
|
399
|
-
content: { type: "string" },
|
|
400
|
-
},
|
|
401
|
-
required: ["path", "content"],
|
|
402
|
-
},
|
|
249
|
+
const gate = createToolCallExecutionGate(undefined, undefined, undefined, {
|
|
250
|
+
write_file: { validate: (v) => WriteFileSchema.safeParse(v).success ? { valid: true } : { valid: false } },
|
|
403
251
|
});
|
|
252
|
+
```
|
|
404
253
|
|
|
405
|
-
|
|
254
|
+
A tool name may be registered in `toolSchemas`/`schemas` or in `validators`,
|
|
255
|
+
never both — registering both throws at construction time. No forced
|
|
256
|
+
dependency on any specific validation ecosystem. See
|
|
257
|
+
[`docs/VALIDATION.md`](docs/VALIDATION.md).
|
|
406
258
|
|
|
407
|
-
|
|
408
|
-
call.schemaValid; // true | false | undefined (undefined = no schema registered for this tool)
|
|
409
|
-
```
|
|
259
|
+
## Conformance
|
|
410
260
|
|
|
411
|
-
|
|
261
|
+
[`conformance/`](conformance/) packages this project's failure corpus as
|
|
262
|
+
a portable, provider-neutral fixture format and a small deterministic
|
|
263
|
+
runner (`prefix-safe-json/conformance`) — useful even to a project that
|
|
264
|
+
never installs this package at runtime, since the fixture format and
|
|
265
|
+
expected outcomes are meaningful against any implementation of this
|
|
266
|
+
problem class. `prefix-safe-json` is the reference implementation and
|
|
267
|
+
dogfoods this exact runner in its own test suite. See
|
|
268
|
+
[`docs/CONFORMANCE.md`](docs/CONFORMANCE.md).
|
|
412
269
|
|
|
413
|
-
##
|
|
270
|
+
## Threat model / non-goals
|
|
414
271
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
272
|
+
`prefix-safe-json` is honestly scoped as **execution integrity**, not a
|
|
273
|
+
general "AI security platform." The complete guarantee/non-guarantee
|
|
274
|
+
list, trust boundaries, and known internal limitations live in
|
|
275
|
+
[`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md); the product-level
|
|
276
|
+
owns/does-not-own boundary this is drawn from is
|
|
277
|
+
[`docs/PRODUCT_POSITIONING.md`](docs/PRODUCT_POSITIONING.md).
|
|
278
|
+
|
|
279
|
+
## Supply-chain verification
|
|
280
|
+
|
|
281
|
+
Verify releases independently rather than relying on publisher claims.
|
|
282
|
+
Published artifacts have npm provenance from this repository's GitHub
|
|
283
|
+
Actions publish workflow, and a tarball can be rebuilt and compared
|
|
284
|
+
against its release tag with `npm run verify:published-release -- <version>`
|
|
285
|
+
from a clone with tags fetched. See the command-driven
|
|
286
|
+
[maintainer audit](docs/MAINTAINER_AUDIT.md), the exact
|
|
287
|
+
[release/hash mapping](docs/RELEASE_INTEGRITY.md), the
|
|
288
|
+
[runtime dependency graph](docs/RUNTIME_DEPENDENCIES.md), and the
|
|
289
|
+
[execution-critical source map](docs/EXECUTION_AUDIT_SURFACE.md).
|
|
422
290
|
|
|
423
|
-
##
|
|
291
|
+
## Compatibility
|
|
424
292
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
293
|
+
ESM only (`import`, not `require`), Node `>=18.0.0`. Node 18/20 are
|
|
294
|
+
end-of-life and no longer receive security patches; this describes the
|
|
295
|
+
package's runtime requirement, not a recommendation to operate an
|
|
296
|
+
unpatched Node release. Repository development and release jobs run on
|
|
297
|
+
newer Node because the toolchain needs it — that requirement does not
|
|
298
|
+
apply to the published package's own runtime dependency graph.
|
|
299
|
+
|
|
300
|
+
**Vercel AI SDK**, verified directly against each exact pinned major via
|
|
301
|
+
a real `streamText()` lifecycle proof (no API key, no network request, no
|
|
302
|
+
paid model call — `pnpm run example:ai-sdk-lifecycle-proof`, run in CI on
|
|
303
|
+
every push):
|
|
304
|
+
|
|
305
|
+
| Major | Status |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| v5 | verified |
|
|
308
|
+
| v6 | verified |
|
|
309
|
+
| v7 | verified |
|
|
310
|
+
|
|
311
|
+
Not a claim that every version within a major is tested. Full
|
|
312
|
+
per-provider compatibility matrix, exact AI SDK pin requirements, and the
|
|
313
|
+
runtime-vs-toolchain distinction:
|
|
314
|
+
[`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md).
|
|
315
|
+
|
|
316
|
+
## Development / audit
|
|
317
|
+
|
|
318
|
+
### Current status
|
|
319
|
+
|
|
320
|
+
**Implemented:** incremental UTF-8 decoder, 17-state lexical JSON state
|
|
321
|
+
machine, grammar stack, semantic event emission, duplicate-key rejection,
|
|
322
|
+
`push()`/`snapshot()`/`drainEvents()`/`finish()`, configurable resource
|
|
323
|
+
limits, a machine-readable test corpus plus the public
|
|
324
|
+
[Tool Call Integrity conformance corpus](conformance/), provider stream
|
|
325
|
+
adapters (OpenAI legacy + Responses, Anthropic, Gemini, OpenRouter,
|
|
326
|
+
OpenAI-compatible, Vercel AI SDK) with a coordinator for concurrent
|
|
327
|
+
tool-call streams, validator-agnostic per-tool validation (see
|
|
328
|
+
[`docs/VALIDATION.md`](docs/VALIDATION.md)), `createToolCallExecutionGate()`,
|
|
329
|
+
`createAiSdkExecutionGuard()`, call-scoped one-shot authority via
|
|
330
|
+
`takeDecision(internalId)`, and SDK execution-ownership detection.
|
|
331
|
+
|
|
332
|
+
**Not yet implemented:** a CLI tool, network/SSE client integration.
|
|
333
|
+
|
|
334
|
+
### Low-level parser API
|
|
431
335
|
|
|
432
336
|
```typescript
|
|
433
337
|
import { createParser } from "prefix-safe-json";
|
|
434
338
|
|
|
435
339
|
const parser = createParser();
|
|
436
|
-
|
|
437
|
-
// Feed chunks as they arrive from the LLM stream
|
|
438
340
|
parser.push('{"tool":"calc",');
|
|
439
341
|
parser.push('"args":{"x":42}}');
|
|
440
|
-
|
|
441
|
-
// Get committed values at any point
|
|
442
|
-
const snap = parser.snapshot();
|
|
443
|
-
console.log(snap.stableValue);
|
|
444
|
-
|
|
445
|
-
// Drain semantic events
|
|
446
|
-
const events = parser.drainEvents();
|
|
447
|
-
|
|
448
|
-
// Signal stream completion
|
|
342
|
+
console.log(parser.snapshot().stableValue);
|
|
449
343
|
const result = parser.finish({ reason: "complete" });
|
|
450
344
|
console.log(result.executable); // true if safe to execute
|
|
451
345
|
```
|
|
452
346
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
Anthropic's real streaming shape — through `createToolCallStreamCoordinator()`,
|
|
463
|
-
and asserts the truncated call is never reported executable while a
|
|
464
|
-
genuinely complete one is. This example runs in CI on every push, so it
|
|
465
|
-
can't silently rot into a stale claim.
|
|
466
|
-
|
|
467
|
-
For the exact-major AI SDK ownership proof, run
|
|
468
|
-
`pnpm run example:ai-sdk-lifecycle-proof` after `pnpm run build`. It asserts
|
|
469
|
-
for each exact pin that an unlocked control invokes caller code before guard
|
|
470
|
-
authority, locked unsafe calls perform no operation, and locked safe calls are
|
|
471
|
-
manually dispatched exactly once. CI and the release workflow run this same
|
|
472
|
-
command.
|
|
473
|
-
|
|
474
|
-
For the same raw-evidence demonstration through the low-level execution gate and the
|
|
475
|
-
[Vercel AI SDK](https://ai-sdk.dev)'s `fullStream` shape instead, see
|
|
476
|
-
[`examples/ai-sdk-execution-gate.mjs`](examples/ai-sdk-execution-gate.mjs)
|
|
477
|
-
(`node examples/ai-sdk-execution-gate.mjs` after `pnpm run build`) — also
|
|
478
|
-
run in CI on every push. [`examples/ai-sdk-guard.mjs`](examples/ai-sdk-guard.mjs)
|
|
479
|
-
is a lower-level wire-shape guard demonstration; it is not the canonical
|
|
480
|
-
execution-ownership example. Both remain CI-checked.
|
|
347
|
+
Runnable, CI-checked, real-API examples (no mocked internals):
|
|
348
|
+
[`examples/anthropic-truncation-safety.mjs`](examples/anthropic-truncation-safety.mjs),
|
|
349
|
+
[`examples/ai-sdk-execution-gate.mjs`](examples/ai-sdk-execution-gate.mjs),
|
|
350
|
+
[`examples/ai-sdk-lifecycle-proof.mjs`](examples/ai-sdk-lifecycle-proof.mjs).
|
|
351
|
+
|
|
352
|
+
### Contributing
|
|
353
|
+
|
|
354
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for how to run tests, add a
|
|
355
|
+
provider fixture or conformance case, and propose adapter support.
|
|
481
356
|
|
|
482
357
|
## License
|
|
483
358
|
|