@cobound/prova-sdk
v0.5.0
Published
Agent-side SDK for the Prova AI control plane (ingest, gateway-check, register).
Maintainers
Readme
@cobound/prova-sdk
Agent-side SDK for the Prova AI control plane. Thin wrappers around the three HTTP endpoints every customer hits at runtime:
POST /api/v1/audit/ingest: persist signed AI decision eventsPOST /api/v1/gateway/check: pre-execution policy + detector evaluationPOST /api/v1/inventory: actively register an integration
Plus an Ed25519 receipt verifier and a one-shot migration tool that bulk-imports existing LangSmith / Langfuse / OpenAI logs into the Audit Vault.
This package is separate from the legacy prova-sdk (the reasoning-chain
verifier). See /docs/sdk for guidance on which one to install.
Install
npm install @cobound/prova-sdkRequires Node 18+ (uses the built-in fetch and crypto modules).
Quick start
import { ProvaClient } from '@cobound/prova-sdk';
const prova = new ProvaClient({ apiKey: process.env.PROVA_API_KEY! });
// Ingest a decision after the model call.
await prova.ingest({
kind: 'model_call',
source: { org_id: 'YOUR_ORG', framework: 'langgraph', app_id: 'claims-orchestrator' },
model: { provider: 'openai', name: 'gpt-4o' },
payload: { messages, response },
});
// Or check before the model call:
const { action, findings } = await prova.gatewayCheck({
kind: 'model_call',
payload: { messages },
});
if (action === 'block') throw new Error(`blocked: ${findings.map(f => f.detector).join(', ')}`);Pass verifyReceipts: true to make the client verify every returned receipt's
Ed25519 signature against the published public key before resolving.
LangChain.js / LangGraph.js auto-instrumentation
Drop the handler into any chain or graph invoke. Every LLM call, node, and tool call is ingested as a signed receipt. Fail-silent: a Prova outage never breaks the agent.
import { ProvaClient } from '@cobound/prova-sdk';
import { ProvaCallbackHandler } from '@cobound/prova-sdk/callbacks';
const prova = new ProvaClient({ apiKey: process.env.PROVA_API_KEY! });
const handler = new ProvaCallbackHandler(prova, {
appId: 'claims-orchestrator',
environment: 'production',
framework: 'langgraph',
});
await graph.invoke(inputs, { callbacks: [handler] });No hard dependency on @langchain/core; the handler is a plain handler object
LangChain.js accepts in the callbacks array.
Gate deploys on regression (prova-eval)
Tag runs with the deploy that produced them (the client reads PROVA_RELEASE
automatically and stamps source.release on every event), then fail CI when a
new release regresses against the last good one. Deterministic and label-free:
it compares health, flag rate, loop rate, and cost between two releases with a
confidence interval behind every verdict.
# exits 1 only when the candidate release actually regressed
PROVA_API_KEY=prv_... npx @cobound/prova-sdk prova-eval \
--app-id claims-agent --baseline "$LAST_GOOD_SHA" --candidate "$GIT_SHA"Without --baseline/--candidate it lists the releases seen in the window. The
same comparison is available as GET /api/v1/eval/compare for non-Node CI.
Vercel AI SDK
Wrap your model with provaMiddleware and every generateText / streamText
call emits a signed model_call receipt with token usage, so cost is computed
and signed server-side. Streaming calls are tapped, not buffered. Works with the
ai package v5 and v6.
import { wrapLanguageModel } from 'ai';
import { openai } from '@ai-sdk/openai';
import { ProvaClient } from '@cobound/prova-sdk';
import { provaMiddleware } from '@cobound/prova-sdk/vercel';
const prova = new ProvaClient({ apiKey: process.env.PROVA_API_KEY! });
const model = wrapLanguageModel({
model: openai('gpt-4o'),
middleware: provaMiddleware(prova, { appId: 'my-agent' }),
});
// use `model` with generateText / streamText exactly as beforeTelemetry is fire-and-forget: a failed ingest never breaks your model call. The
ai package is an optional peer dependency, so the SDK installs cleanly without
it.
Catch the loop as it forms
The handler accumulates the { node, reads, writes } trace and emits one
agent_run receipt per run, so the server-side coordination_loop detector
fires from auto-instrumentation (it only triggers on agent_run, never on
per-step events). By default it also runs the same detection in-process and
warns (console.warn) the moment a persistent loop forms, so you see it in
real time rather than only later in the dashboard.
The default warns, it does not stop the run. A structural loop is also what a
healthy planner/executor iteration looks like, and stopping every cycle would
break agents that are working correctly. Pass breakOnLoop: true to upgrade
the warning to a stop: the handler throws CoordinationLoopError and sets
raiseError so LangChain.js propagates it and the run halts.
import { ProvaCallbackHandler, CoordinationLoopError } from '@cobound/prova-sdk';
const handler = new ProvaCallbackHandler(prova, {
appId: 'claims-orchestrator',
breakOnLoop: true,
});
try {
await graph.invoke(inputs, { callbacks: [handler] });
} catch (e) {
if (e instanceof CoordinationLoopError) {
// e.match: { agents, born_at_step, persistence_steps, total_steps, total_agents }
console.error('stopped a coordination loop across', e.match.agents);
}
}The detection is a faithful port of the canonical server-side detector, and is
verified against it in CI, so a loop seen locally is the same loop an auditor
sees in the receipt. For a runtime without LangChain callbacks, drive
LoopGuard directly:
import { LoopGuard, CoordinationLoopError } from '@cobound/prova-sdk';
const guard = new LoopGuard(); // raiseOnDetect: true by default
for (const { node, reads, writes } of runAgent()) {
guard.observe(node, reads, writes); // throws CoordinationLoopError on a persistent loop
}Circuit breaker: stop runaway spend
budgetUsd and maxSteps are hard caps you set, so they stop the run by
default (unlike loop detection, which warns). Combine them with breakOnLoop
for one circuit breaker against runaway agents:
import { ProvaCallbackHandler, BoundaryViolationError } from '@cobound/prova-sdk';
const handler = new ProvaCallbackHandler(prova, {
appId: 'claims-orchestrator',
budgetUsd: 0.50, // stop if the run's estimated spend exceeds $0.50
maxSteps: 40, // stop after 40 agent steps
breakOnLoop: true, // stop on a coordination loop
});
try {
await graph.invoke(inputs, { callbacks: [handler] });
} catch (e) {
if (e instanceof BoundaryViolationError) {
console.error('circuit breaker tripped:', e.match.dimension);
}
}The handler attaches normalized token_usage to every model_call receipt so
the server can compute and sign the canonical cost_usd. The in-process cost
estimate (built-in catalog, override with setModelPrice) is used only to trip
the budget locally.
Run it locally, no account
analyzeLocal(events) runs loop detection and cost estimation entirely
in-process. No API key, no network. Pass the events the SDK emits and read the
report. The loop algorithm matches the server, so a loop seen locally is the
loop a signed receipt would report.
import { analyzeLocal } from '@cobound/prova-sdk';
const report = analyzeLocal(events);
// { events, steps, model_calls, models, estimated_cost_usd, loop }
if (report.loop) console.error('coordination loop:', report.loop.agents);Migrate existing logs
CLI:
PROVA_API_KEY=prv_... npx prova-migrate --source langsmith --file runs.ndjsonOr programmatically:
import { migrate, readNdjson } from '@cobound/prova-sdk/migrate';
import { createReadStream } from 'fs';
await migrate({
client: prova,
source: 'langfuse',
rows: readNdjson(createReadStream('observations.ndjson', 'utf-8')),
onProgress: (p) => console.error(p),
});Supported sources: langsmith, langfuse, openai. Each row becomes a
signed AIDecisionEvent with payload._migrated_from set to the source.
Idempotency keys are derived from the source row id, so re-running the
migration is safe.
Verify a receipt offline
import { verifyReceipt } from '@cobound/prova-sdk';
await verifyReceipt(receipt, { publicKeyPem: PUBLIC_KEY_PEM });Or fetch the public key from the deployment automatically:
await verifyReceipt(receipt, { baseUrl: 'https://api.prova.cobound.dev' });