npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prefix-safe-json

v0.6.3

Published

Fail-closed execution integrity for streamed LLM tool calls — reject truncated or unconfirmed arguments before side effects

Readme

prefix-safe-json

npm version CI License: MIT OR Apache-2.0

Keep an incomplete or ambiguously identified LLM tool call from reaching your application's tools. prefix-safe-json checks the original argument stream, completion signals and schema before your application runs a tool, even when the SDK's parsed arguments look usable.

The core package returns one-shot decisions; your application performs the side effect. The separate integration below keeps tool execution in the SDK. Provider signals must accurately describe completion; see the provider-honesty limit.

For AI SDK 7 with OpenAI-compatible Chat Completions, the experimental guarded chat integration keeps your ordinary execute callbacks and the SDK's automatic tool-result continuation:

For a new application, install the separately versioned companion and a tested SDK/provider pair together. In an existing application, keep your supported locked peer versions; see the integration's installation instructions.

npm install --save-exact [email protected] [email protected] [email protected] @ai-sdk/[email protected]
import { createGuardedChat } from "psj-ai-sdk";
import { stepCountIs } from "ai";

const chat = createGuardedChat({ name: "provider", baseURL, apiKey });
const result = chat({ model: modelId, tools, prompt, stopWhen: stepCountIs(5) });

Text/reasoning can stream while tool calls wait for the entire response and batch validation. Optional local recording saves refused responses for offline wire-policy replay and explicit CI expectations. See the integration's supported policies and provisional-text limits. Run the offline example without a key. The core runtime gains no AI SDK dependency.

For prepublication review, use the two packed artifacts described in the evaluation kit. The whole-batch contract distinguishes the fixed 0.3.2 policy from older previews. The kit also compares identical responses through the current native SDK, a small native check and the companion.

For caller-owned dispatch, install prefix-safe-json from npm and use the complete TypeScript agent-loop example: download its ZIP, extract it, then run npm install --engine-strict --no-audit --no-fund and npm start with Node 22+. It uses published PSJ 0.6.1, runs tools, sends their matched results back to the model, and returns a final answer. Five authored scenarios use memory counters; no API key or repository build is needed. Choose another SDK.

Safe execution example

import { writeFile } from "node:fs/promises";
import { streamText } from "ai";
import { z } from "zod";
import {
  createAiSdkExecutionGuard,
  createAiSdkExecutionLock,
} from "prefix-safe-json";
import { fromStandardSchema } from "prefix-safe-json/standard-schema";

const writeFileSchema = z.object({ path: z.string(), content: z.string() }).strict();

const tools = createAiSdkExecutionLock({
  write_file: {
    description: "Write a UTF-8 text file",
    inputSchema: writeFileSchema,
  },
});

const guard = createAiSdkExecutionGuard({
  validators: { write_file: fromStandardSchema(writeFileSchema) },
});

const result = streamText({ model, prompt, tools });
for await (const part of result.fullStream) {
  guard.push(part);
}

const { decisions, diagnostics } = guard.finish();
for (const decision of decisions) {
  const authority = guard.takeDecision(decision.internalId);
  if (authority) {
    const input = writeFileSchema.parse(authority.value);
    await writeFile(input.path, input.content);
  } else {
    console.warn(`skipped ${decision.name}: ${decision.action} (${decision.reason})`);
  }
}
if (decisions.length === 0 && diagnostics.length > 0) {
  console.warn("no tool call was executed:", diagnostics);
}

model and prompt are yours. The lock stops the SDK from running write_file itself; the guard checks each streamed call against the same schema and hands out a one-shot decision. The caller owns dispatch, and a call the guard doesn't hand out never runs.

Using jsonSchema() from ai instead of Zod? Give the guard the raw schema inside it: schemas: { write_file: writeFileSchema.jsonSchema }. The wrapper itself is refused at construction, because it would otherwise validate nothing.

Typed inputs and a complete agent loop

Experimental createTypedAiSdkExecutionGuard in prefix-safe-json/standard-schema requires PSJ 0.6.0+. The strict TypeScript agent-loop example checks inferred raw inputs, matching result IDs and a final model answer, plus four refusal/error scenarios. It runs against the actual tarball without an API key. The caller owns dispatch and continuation. Schema transformations, defaults and removed keys are refused; validation details.

To try the published version without cloning this repository, download the 0.6.1 typed-loop example ZIP, extract it, then run npm install --engine-strict --no-audit --no-fund and npm start inside the extracted directory. Node 22+ is required. The example uses authored model responses and memory counters; no API key is needed.

When this adds value

PSJ fits applications that can retain original stream evidence and own tool dispatch, especially when they need the same completion and identity policy across providers. Check your integration before adding it:

| Your current boundary | First step | | --- | --- | | You retain argument deltas and control dispatch | Compare a harmless tool with and without the guard | | Your SDK already refuses the failure you care about | Test that native behavior first; an extra dependency may add no value | | Your framework runs tools before exposing raw events | Find a pre-execution hook or keep the native path; observing later cannot prevent an earlier effect | | You want partial JSON for a UI or recovery of truncated XML | Use a preview/recovery tool; PSJ's execution policy does not provide that behavior |

The integration guide explains the migration steps and external fit audit records useful integrations and cases where no additional benefit was shown. The usability audit separates reproduced integration blockers, fixes and the limits that still need evaluation.

For a concrete difference beyond unsafe termination, the identity comparison feeds complete, schema-valid calls through real streamText(): native execution records both calls when they reuse an ID; the locked PSJ path refuses that ambiguous identity. With distinct IDs, both paths record both calls. These are authored fixtures, not live-provider or WorkflowAgent certification. Select ai-sdk in the launcher for this comparison, or run npm run identity in that installed consumer.

Replay an incident and keep it as a regression test

Have a recorded Anthropic or AI SDK tool stream? The experimental local replay workflow explains each call's execution decision and compares it with reviewed expectations in CI. It runs from a repository checkout without model requests or tool dispatch.

To test an SDK upgrade with original HTTP/SSE bytes, use the separate experimental regression development package. It checks actual installed SDK inputs, harmless effects and paired results in CI. Install its experimental preview tarball as a development dependency; it is distributed separately from the PSJ runtime package.

Run a standalone npm demo

From this checkout, with Node 22+ and npm:

node scripts/try-npm-demo.mjs

The default guarded trial installs companion 0.3.2, core PSJ 0.6.2, AI SDK 7.0.143 and OpenAI-compatible provider 3.0.71. It uses ordinary SDK callbacks and automatic result continuation. The explicit typed trial installs published PSJ 0.6.1, AI SDK 7.0.143 and Zod 4.4.3 in a separate temporary project. It checks a complete tool → result → model loop, token limits, duplicate IDs, cancellation and a tool error. The complete control returns a final answer; refused batches do not continue. The directory stays available for editing and rerunning.

Choose another boundary by passing one of these names:

| Choice | PSJ pin | What to inspect | | --- | --- | --- | | guarded (default) | core 0.6.2 + companion 0.3.2 | Native callbacks and automatic continuation | | typed | 0.6.1 | Typed inputs and caller-owned continuation | | ai-sdk | 0.5.4 | Termination, interleaved calls and native/PSJ identity comparison | | openai | 0.5.4 | Original Chat Completions evidence and provider-honesty limit | | anthropic | 0.5.4 | Final message termination and interrupted streams | | langchain | 0.5.4 | Parsed candidates versus original provider evidence | | openrouter | 0.5.4 | Raw events, manual dispatch and paired results | | tanstack | 0.5.4 | Native finish reasons, usage trailers and server-tool results |

All trials use authored fixtures and harmless counters; they need no API key, model server or repository build. Installation requires npm registry access. A passing fixture is not a live-provider or own-project integration result. The older six-SDK ZIP retains its original six choices and 0.5.4 pins; typed is available in the current checkout and the separate 0.6.1 ZIP linked above.

To connect a real application, follow the integration guide. Your application must retain original evidence, stop automatic execution and send accepted tool results into its own continuation loop.

Why parse success is not enough

Tool-call arguments often arrive as partial JSON. A renderer may reasonably repair or close an unfinished prefix so it can show progressive UI. That is useful for display, but it answers a different question from whether a side effect should run.

Given the prefix:

{"city":"Tok

some partial-JSON helpers can expose "Tok" immediately. prefix-safe-json keeps that field uncommitted until the stream provides enough evidence that the value is complete.

The core distinction is simple:

Parse completion is not execution authority.

See docs/REAL_WORLD_FAILURES.md for independently reproduced examples and docs/EXECUTION_GATE.md for the decision model.

Decisions

Every observed tool call ends in one of three actions:

  • execute — arguments are complete and, when configured, validator/schema checks pass.
  • retry — no trustworthy complete value exists yet.
  • reject — the stream contains malformed, conflicting, invalid, or terminally unsafe evidence.

The full reason matrix lives in docs/EXECUTION_GATE.md.

Resource handling

The coordinator retains the first diagnostic for each rejected-push category: normalized-event budget exhaustion, input after stream end, and conflicting terminal reason. Repeating an already-rejected push does not keep appending that category to its diagnostic history or observation queue. Draining the queue does not erase the retained evidence or restore execution authority. This bounds those three categories, not total application memory or network traffic; existing parser/input limits and caller-owned cancellation still apply.

Scope and non-goals

The library covers streamed argument evidence, call identity, lifecycle completion, truncation detection, conflicting evidence, validation composition, and one-shot execution decisions.

It does not provide:

  • application authorization,
  • human approval workflows,
  • sandboxing,
  • prompt-injection defense,
  • distributed exactly-once semantics,
  • a guarantee that the model selected the correct tool.

An execute decision means the tool arguments were observed as complete under the configured boundary. Your application still owns permissions, policy, validation, and the side effect itself.

See docs/PRODUCT_POSITIONING.md and docs/THREAT_MODEL.md for the detailed boundary.

Vercel AI SDK integration

For streamText(), the recommended pattern is:

  1. wrap tool definitions with createAiSdkExecutionLock(),
  2. consume fullStream,
  3. feed stream parts into createAiSdkExecutionGuard(), and
  4. dispatch manually only from takeDecision().

generateText() returns a non-streaming result, not fullStream, so this raw-stream guard recipe does not apply to it. The lock can remove callbacks from tools supplied to either function, but that alone does not provide the raw argument evidence this guard requires. Do not synthesize stream deltas from generateText()'s parsed tool-call results to claim raw-stream integrity.

createAiSdkExecutionLock() removes SDK-invoked callbacks that could execute application code before the guard reaches a final decision. A tool definition that bypasses that lock and performs side effects directly is outside the protection boundary.

The adapter targets the public AI SDK fullStream surface, not the lower-level @ai-sdk/provider doStream() boundary.

Detailed lifecycle behavior and version-specific tests are documented in docs/EXECUTION_GATE.md.

Provider adapters

Low-level adapters are included for:

  • OpenAI legacy function_call,
  • OpenAI Responses API,
  • Anthropic,
  • Gemini,
  • OpenRouter,
  • generic OpenAI-compatible endpoints,
  • Vercel AI SDK.
import {
  createToolCallExecutionGate,
  OpenAIStreamAdapter,
} from "prefix-safe-json";

const adapter = new OpenAIStreamAdapter();
const gate = createToolCallExecutionGate();

for (const rawChunk of stream) {
  for (const event of adapter.push(rawChunk)) {
    gate.push(event);
  }
}

for (const event of adapter.finish({ reason: "complete" })) {
  gate.push(event);
}

const { decisions } = gate.finish();

Gemini exposes structured argument projections at this seam rather than raw argument text, so its adapter does not grant the same strict raw-text execution authority. Provider-specific semantics are documented in docs/EXECUTION_GATE.md.

Validation

Validation is optional and validator-agnostic. You can use JSON Schema/Ajv, Standard Schema-compatible validators, Zod, TypeBox, Valibot, or a custom validator.

schemas takes plain JSON Schema documents (TypeBox schemas count). Zod and other Standard Schema libraries go through validators with fromStandardSchema() from prefix-safe-json/standard-schema. Handing schemas a Zod schema or an AI SDK jsonSchema() wrapper throws at construction instead of silently validating nothing.

const gate = createToolCallExecutionGate(undefined, undefined, undefined, {
  write_file: {
    validate: (value) =>
      WriteFileSchema.safeParse(value).success
        ? { valid: true }
        : { valid: false },
  },
});

See docs/VALIDATION.md.

Conformance corpus

conformance/ contains a provider-neutral fixture format and deterministic runner for this problem class. Projects can use the fixtures without adopting this package as their runtime implementation.

See docs/CONFORMANCE.md.

Compatibility

  • ESM only. CommonJS code can require() it on Node 20.19+ / 22.12+ (Node's require(esm)); older runtimes need import().
  • Runtime: Node >=18.0.0.
  • Repository development/release tooling uses newer Node versions.
  • CI exercises pinned Vercel AI SDK v5, v6, and v7 integration paths.

| AI SDK major | CI integration check | | --- | --- | | v5 | yes | | v6 | yes | | v7 | yes |

This is not a claim that every patch release inside each major is tested. Exact pins and provider notes live in docs/COMPATIBILITY.md.

Low-level parser API

import { createParser } from "prefix-safe-json";

const parser = createParser();
parser.push('{"tool":"calc",');
parser.push('"args":{"x":42}}');

console.log(parser.snapshot().stableValue);

const result = parser.finish({ reason: "complete" });
console.log(result.executable);

Development

Implemented components include the incremental UTF-8 decoder, lexical/grammar state tracking, semantic events, duplicate-key rejection, configurable resource limits, provider adapters, concurrent call coordination, validation hooks, and execution-gate APIs.

Not currently provided: a standalone CLI or network/SSE client.

Runnable examples:

See CONTRIBUTING.md for the development workflow.

Release verification

Published packages use npm provenance from this repository's GitHub Actions release workflow. A release tarball can also be rebuilt and compared with its tag using:

npm run verify:published-release -- <version>

Additional release and dependency documentation is available in:

License

Licensed under either of:

at your option.