Appearance
TypeSafe Classifier
Jev is TypeSafe’s System One classifier. You call it from your own code, a hook, or an interrupt when the agent loop needs a typed decision: which team owns this ticket, whether a tool may run, whether the work can wait.
It answers Noul (yes/no), Choice (one label), and Score (a number on a rubric) questions about a state you pass in. It does not generate chat text, call tools, or stream tokens. The agent still uses Gemini, OpenAI, or another chat model. Jev does not sit in Agent(model=...).
Classifier constructors and classify / aclassify are on TypeSafe Classifier in Core.
Default model is Amazon Bedrock
Agent() with no model= uses Amazon Bedrock (Claude Sonnet 4.6 in us-west-2). Configure AWS credentials (aws configure or AWS_* env vars), or pass an explicit model — see Installation and Model Providers.
Install
Install elsai-model[typesafe], set TYPESAFE_API_KEY, and import TypeSafeClassifier. AgentKit does not ship a Jev plugin. That is the integration.
bash
pip install --extra-index-url https://core-packages.elsai.ai/root/elsai-model/ "elsai-model[typesafe]==2.1.3"
export TYPESAFE_API_KEY=sk-...python
from elsai import Agent
from elsai_model.gemini import GeminiModel
from elsai_model.typesafe import Choice, Noul, Score, TypeSafeClassifier
agent = Agent(model=GeminiModel(...), tools=[...]) # the LLM
jev = TypeSafeClassifier(model_id="jev-latest") # the decision layerQuestion types
Use Noul when your code needs an if. Use Choice when options map to different handlers. Use Score when you compare a number to a threshold. Ask several questions in one call; they share the same state. See TypeSafe Classifier for the objects and fields.
| User wants | Ask Jev | Then |
|---|---|---|
| Which agent / team? | Choice | Start that agent or enqueue |
| Skip the LLM? | Score or Noul | Return a static reply or run the agent |
| May this tool run? | Noul | cancel_tool or interrupt |
| Which graph node? | Choice | Run that node |
| Act automatically? | Noul + confidence | Code acts, or fall through to the LLM |
Bring Jev into the agent loop
This is a working example of how you bring Jev into an agent’s loop.
Before the turn starts, a BeforeInvocationEvent hook asks Jev which team owns the ticket and stores that label on invocation_state. When the chat model proposes send_email, a BeforeToolCallEvent hook asks Jev whether the send is risky. If the probability is high, the hook cancels the tool with a fixed sentence. Jev names the lane and gates the tool. It does not write the customer reply.
Build state from event.messages or event.agent.messages, or from a dict you control. Mutate event.invocation_state in place; do not assign a new dict.
This is not a replacement for Permissions on file_read, file_write, editor, shell, python_repl, or execute. Use Jev on app tools those plugins do not already own.
python
import os
from elsai import Agent, tool
from elsai.agent import AgentConfig
from elsai.hooks import BeforeInvocationEvent, BeforeToolCallEvent, HookProvider
from elsai.plugins import hook
from elsai_model import LLM, Provider
from elsai_model.typesafe import Choice, Noul, TypeSafeClassifier
jev = TypeSafeClassifier(model_id="jev-latest")
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a customer."""
return f"Queued email to {to}: {subject}"
def state_from_messages(messages):
parts = []
for message in messages or []:
texts = [
block["text"]
for block in message.get("content") or []
if isinstance(block, dict) and "text" in block
]
if texts:
parts.append({"role": message.get("role"), "text": "\n".join(texts)})
return {"messages": parts}
class TicketPolicy(HookProvider):
@hook
def route(self, event: BeforeInvocationEvent) -> None:
response = jev.classify(
state=state_from_messages(event.messages),
questions={
"team": Choice(
instructions="Which team should handle this?",
criteria={
"billing": "Payments, invoices, refunds",
"technical": "Bugs, outages, integrations",
"sales": None,
},
),
},
)
event.invocation_state["team"] = response.choices["team"].choice
@hook
def gate_email(self, event: BeforeToolCallEvent) -> None:
if event.tool_use["name"] != "send_email":
return
response = jev.classify(
state={
"messages": state_from_messages(event.agent.messages),
"tool": event.tool_use["name"],
"input": event.tool_use.get("input"),
},
questions={
"risky": Noul(
instructions="Is this send_email call risky or unauthorized given the last messages?",
criteria={
"true": "The send is unauthorized, off-policy, or would expose sensitive data",
"false": "The send is an expected follow-up",
},
),
},
)
noul = response.nouls["risky"].noul
if noul >= 0.5:
event.cancel_tool = f"send_email blocked (noul={noul:.2f})"
model = LLM(
provider=Provider.GEMINI,
model=os.getenv("GEMINI_MODEL_NAME", "gemini-2.5-flash"),
api_key=os.environ["GEMINI_API_KEY"],
)
invocation_state = {}
agent = Agent(
model=model,
tools=[send_email],
system_prompt="You help with customer tickets. Use send_email only when the customer asked to be emailed.",
config=AgentConfig(hooks=[TicketPolicy()]),
)
result = agent(
"I was charged twice. Email me a refund confirmation.",
invocation_state=invocation_state,
)
print(invocation_state["team"], result)Your app reads invocation_state["team"] and starts the billing agent, the infra agent, or a human queue. The chat model proposed send_email; Jev only gates it.
Pause for a human
This is a second working example. Same send_email tool, same Noul, but the hook picks proceed / ask / stop instead of only cancel.
Low probability: the tool runs. High probability: cancel_tool with a template. A middle band (about 0.4–0.7) calls event.interrupt(...) so a reviewer decides. Jev does not write the approval UI. See Interrupts.
python
from elsai.hooks import BeforeToolCallEvent, HookProvider
from elsai.plugins import hook
from elsai_model.typesafe import Noul, TypeSafeClassifier
jev = TypeSafeClassifier(model_id="jev-latest")
class EmailReview(HookProvider):
@hook
def review(self, event: BeforeToolCallEvent) -> None:
if event.tool_use["name"] != "send_email":
return
response = jev.classify(
state={
"messages": state_from_messages(event.agent.messages),
"tool": event.tool_use["name"],
"input": event.tool_use.get("input"),
},
questions={
"risky": Noul(
instructions="Is this send_email call risky or unauthorized given the last messages?",
criteria={
"true": "The send is unauthorized, off-policy, or would expose sensitive data",
"false": "The send is an expected follow-up",
},
),
},
)
noul = response.nouls["risky"].noul
if noul < 0.4:
return
if noul >= 0.7:
event.cancel_tool = f"send_email blocked (noul={noul:.2f})"
return
approval = event.interrupt(
"send-email-review",
reason={"noul": noul, "input": event.tool_use.get("input")},
)
if approval != "approve":
event.cancel_tool = f"send_email blocked (noul={noul:.2f})"Register EmailReview the same way as TicketPolicy: AgentConfig(hooks=[EmailReview()]).
Other uses
These follow the same pattern: classify a state, then branch in your code.
Skip the LLM
Ask a Score for urgency (can wait / this week / today). If the score is low and confidence is high, return a canned “we’ll get to this” and do not start the agent. Jev sorts the ticket. It does not write the support reply.
Act when you are sure
Ask a Noul or Choice and read confidence. High confidence: your code refunds, tags, or closes. Low confidence: send the same ticket to the LLM agent.
Ask several questions in one call
At the start of a ticket, ask billing (Noul), team (Choice), urgency (Score), and needs-a-human (Noul) together. They share one state. Your code then branches. You do not ask the LLM four classification questions first. The Core example shows that call.
Stop after a tool
After a tool result, ask a Noul: is this resolved enough to stop? If yes, do not spend another model turn. If no, the existing agent continues. Jev is a stop/go bit, not a summary.
Dispatch a graph node
Before a node runs, ask a Choice over research, writer, reviewer. Your orchestrator starts that node. Each node still has its own chat model. See Multi-Agent.
Keep policy in your repo
Ask several atomic Scores (impact, reversibility, authorization). Weight them in Python, then cancel, interrupt, or proceed. Jev returns the numbers. The rule lives in your code.
Do not
- Do not pass Jev to
LLM(...)orAgent(model=...). - Do not expect it to stream tokens, call tools, or write
Guide(reason)prose. If you cancel a tool, use a template. - Do not flatten chat messages for them. Build
statefromevent.agent.messagesor a dict you control. - Do not stack Jev in front of
file_read/file_write/editor/shell/python_repl/executeunless you want a second, semantic policy on top of the existing permission plugins.