Jev Reactor
An open-source runtime for real-time typed decisions. It uses Jev for parallel semantic judgments and ordinary code for policy, control flow, and side effects.
pip install jev-reactor # add [mcp] for the MCP gateway (below)
jev-reactor init # copies examples, fixtures and packs here
jev-reactor run examples/tool_loop.py --mock # offline: recorded answers, no key, no network
export TYPESAFE_API_KEY=... # then, against live Jev:
jev-reactor run examples/tool_loop.py
Decision: skip
Reason: redundant_tool_call
Redundancy probability: 0.91
Choice confidence: 0.86
That is the output of examples/basic_decision.py --mock. In mock mode the answers are
hand-written fixtures, not Jev output; run it live to see what Jev says. Jev returns typed
values but can still make semantic mistakes. Nothing here is a guarantee that a decision is
correct.
What it is
A software event arrives (an agent proposes a tool call, a context item ages, a turn ends). Jev Reactor turns it into a compact, redacted state, asks Jev several small typed questions in one request, and hands the answers to a deterministic Python policy. The policy returns an immutable action. The Reactor never executes anything; your application does, and only for the actions you choose.
event -> bounded state -> parallel Jev questions -> typed answers -> policy -> action -> host
(redacted) (one request) (probabilities) (plain Python)
Jev judges, code decides, adapters execute. It is not a chatbot, an agent framework, or a safety guarantee, and it does not replace Jev.
Use it from Python
# runnable
import asyncio
from jev_reactor import MockProvider, Reactor, ToolLoop, ToolSpec
tools = [
ToolSpec(
name="search_invoices", risk="read", idempotent=True, requires_permission="invoices:read"
),
ToolSpec(name="send_email", risk="irreversible", requires_permission="email:send"),
]
loop = ToolLoop(tools) # a question pack (what Jev is asked) + a policy (what happens)
# Recorded answers stand in for Jev. Swap in TypeSafeProvider() (needs TYPESAFE_API_KEY).
provider = MockProvider(
{
"relevant": 0.96,
"should_call": 0.94,
"redundant": 0.04,
"task_complete": 0.03,
"needs_user_input": 0.02,
"injection_suspected": 0.01,
"next_action": ("call_tool", 0.91),
}
)
async def main() -> None:
async with Reactor(provider, pack=loop.pack, policy=loop.policy) as reactor:
event = reactor.new_event(
"tool_call_proposed",
{"recent_calls": []},
goal="Find the status of invoice INV-2041",
proposed_action={"tool": "search_invoices", "arguments": {"invoice_id": "INV-2041"}},
metadata={"permissions": ["invoices:read"]},
)
decision = await reactor.decide(event)
print(decision.action, decision.reason_codes) # -> allow ['relevant_and_useful']
# A hard rule decides this before Jev is asked; no answer can override it.
mail = reactor.new_event(
"tool_call_proposed",
{},
goal="Tell the customer",
proposed_action={"tool": "send_email", "arguments": {"to": "a@b.c"}},
metadata={"permissions": ["email:send"]},
)
held = await reactor.decide(mail)
print(held.action, held.reason_codes, held.provider_status)
# -> review ['irreversible_needs_approval'] not_called
asyncio.run(main())
MCP gateway: decide every tool call before it runs
jev-reactor-mcp is a real MCP server that fronts your
existing MCP servers. Every tools/call goes through the allowlist, your hard rules, Jev and
your policy first; a held-back call never reaches the downstream server, and the agent gets
a plain reason it can act on.
pip install "jev-reactor[mcp]"
jev-reactor-mcp init --demo -o gateway.yaml # fronts a safe fake invoice server, no API key
jev-reactor-mcp serve gateway.yaml # stdio: use it as a server in Claude, Cursor, ...
- Only tools you list are exposed; permissions, amount limits and the goal come from your config, never from tool arguments.
- Irreversible actions need the human's approval, asked through MCP elicitation and bound to the exact call. A client that cannot be asked gets a held-back result, not a silent pass.
- Tool descriptions that read like instructions to the model are withheld.
- Serves stdio, or Streamable HTTP on loopback (a remote bind needs a bearer token).
The demo uses a scripted stand-in for Jev, so it shows the mechanics and says nothing about what Jev would decide. Setup for Claude Desktop, Claude Code and Cursor, the config reference and the limits are in docs/mcp.md.
The three question types
| Type | Asks | Returns | Notes |
|---|---|---|---|
| Noul | Is this true? | noul in 0..1 |
No separate confidence: the number is the answer and the certainty. |
| Choice | Which of these options? | choice, probabilities, confidence |
Up to 255 options. Always include an abstain option (other, unclear). |
| Score | Which level? | score, probabilities, confidence |
2 to 10 ordered levels, numbered from 0. Describe situations, not degrees. |
Ask many narrow questions about one state in one request; they run in parallel. Combine the answers in code. See docs/question-packs.md.
What is different about how this uses Jev
Jev's own documentation and the community's measurements shaped several choices that differ from the obvious approach. The full list, with sources, is in docs/design-notes.md. The short version:
- Your hard rules run first and never depend on Jev: allowlist, permissions, user denials, amount limits, approval for irreversible actions, exact-duplicate calls. Jev's docs say adversarial text in state "can move the answer".
- Two-sided thresholds. Between "confident no" and "confident yes" is uncertain, and uncertain goes to review instead of being forced to a coin flip.
- Answers are not assumed to agree. A Noul and a Choice answer different questions, so disagreement is escalated, not resolved silently.
- Fail safe, by risk. When Jev is unavailable, reads fall back to your default handling, writes go to review, and irreversible actions are blocked. Nothing defaults to allow.
- When in doubt, keep it (context retention): pins are decided in code, discarding needs several signals to agree, and Jev never rewrites text.
- Replay without Jev. Every decision is recorded (a digest of the redacted state, not the
state);
jev-reactor replayrecomputes policy over the recorded answers and shows what a new threshold set would change.
Command line
jev-reactor init scaffold examples, fixtures and packs here
jev-reactor run <file.py> [--mock] run an example
jev-reactor replay <runs.jsonl> [--policy strict] [--dry-run]
jev-reactor report <runs.jsonl> [--json]
jev-reactor validate-pack <pack.yaml> lint a question pack
jev-reactor inspect <file> pretty-print a fixture, event, pack or run
jev-reactor label <runs.jsonl> <event-id> --expected allow
jev-reactor export-pack tool-loop a built-in pack as YAML, to start your own
jev-reactor bench [--live] measure latency on your machine
jev-reactor mcp serve|check|init the MCP gateway (same as jev-reactor-mcp; needs [mcp])
There is no telemetry and nothing is uploaded. A test checks that no core module can open a network connection.
Security and data: read this before using it on real data
- Redaction (secret-shaped keys and credential patterns) runs before every provider call and every write. It is best effort: keep secrets out of state, and allowlist the fields you send.
- By default a record stores a digest of the redacted state. No goal text and no raw provider payload are written unless you opt in.
- State you send is processed by TypeSafe. Read their Data Processing Agreement and Privacy Policy first; zero data retention is offered for enterprise customers.
- Recorded logs contain TypeSafe Output. Do not use them to train or distill a model: TypeSafe's Master Customer Agreement (section 2.3(b)) prohibits it.
- This is a decision gate, not a sandbox. Stack OS-level isolation underneath. See SECURITY.md.
Measuring performance
This README makes no latency, accuracy, or cost claims for Jev, and the sample data in
examples/replay.jsonl is hand-written (model mock-fixture), not measured. To measure your
own setup:
jev-reactor bench # policy and Reactor overhead on this machine, no network
jev-reactor bench --live # a small number of real requests through your network and account
The engineering target for local policy evaluation is p95 under 5 ms; bench prints whether
your machine meets it. Nothing about real-time behavior against Jev is asserted until you run
--live. Thresholds in every pack are initial defaults, not calibrated values; calibrate
them on your own labelled traces (docs/policy-writing.md).
Documentation
- Architecture: lifecycle, failure semantics, concurrency, persistence
- Providers: the TypeSafe adapter, the mock, writing your own
- Question packs: writing questions Jev answers well, and the linter
- Policy writing: bands, confidence, rule chains, calibration
- MCP gateway: setup for Claude and Cursor, config reference, security model
- Deployment: modes, deadlines, rate limits, privacy
- Design notes: what the docs and community changed, with sources
Status and roadmap
Version 0.2.0 adds the MCP gateway to the local runtime and developer CLI. The API may change
before 1.0. Live Jev has not been exercised by this project's automated tests or CI (they
run offline against recorded and scripted answers); run jev-reactor bench --live and the
opt-in live test with your own key before depending on it.
| Next | Not planned |
|---|---|
| Advisory MCP decision tools, downstream resources and prompts in the gateway, TypeScript runtime, LangGraph middleware, OpenTelemetry spans, provider fallback chains, speculative prefetch | Anything that trains or distills a model on Jev output (prohibited by TypeSafe's terms), a hosted dashboard in this repository |
License
Apache-2.0. See LICENSE, NOTICE and docs/dependency-notices.md. Jev and TypeSafe are TypeSafe AI's products; this project is independent and not affiliated with or endorsed by TypeSafe AI.
Release files for jev-reactor 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jev_reactor-0.2.0.tar.gz | 180.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jev_reactor-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 312.5 kB
Release files / jev_reactor-0.2.0.tar.gz
| Download URL | jev_reactor-0.2.0.tar.gz |
|---|---|
| Size | 180.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58e932d947f36b2660431c86499fcca473ead31924de1c1431f153f12ec4d1af
|
|
BLAKE2b-256 checksum How to use checksums |
266b71f2d90e3ec92d4ad5c508ba7382a7d68ef7546f4a54f06bd3eb362596b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jev_reactor-0.2.0-py3-none-any.whl
| Download URL | jev_reactor-0.2.0-py3-none-any.whl |
|---|---|
| Size | 132.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6799bcfe3c4fdb608b2f7fb3d799095b491ce79810c0fe7806755365793219b
|
|
BLAKE2b-256 checksum How to use checksums |
c9fe68f48986659fe8218e3ce5e325e380f5748e39e043ad6b7c62d4fb8de3c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|