Tutorials
Jev + LangChain: Routing and Guardrails Around createAgent
LangChain's createAgent gives you a configurable agent harness: model, tools, prompt, middleware. What it does not give you is a trustworthy way to decide which harness configuration should run for a given request. That is a decision problem, and it belongs to Jev.
The shape of the integration
Jev is not a chat model, so it never goes into the model field. You call it from your own code, and the result configures or gates the LangChain agent:
request
│
▼
Jev decision (Choice / Noul)
│
├── route: which agent or tool set
├── gate: should the tool call run at all
└── escalate: hand off when confidence is low
Step 1: A baseline LangChain agent
Per the LangChain docs, the current API is createAgent, with tool() taking a function, a name, a description, and a zod schema:
import { createAgent, tool } from "langchain";
import * as z from "zod";
const kubectl = tool((input) => runKubectl(input.command), {
name: "kubectl",
description: "Inspect Kubernetes cluster state: pods, logs, events",
schema: z.object({ command: z.string().describe("kubectl arguments") }),
});
const agent = createAgent({
model: "gpt-5.5",
tools: [kubectl],
});
const result = await agent.invoke({
messages: [{ role: "user", content: request }],
});
Step 2: Route with a Jev Choice question
Define your agents as criteria and let Jev pick:
const AGENTS = {
kubernetes: "Cluster, pods, deployments, CrashLoopBackOff, resource limits",
coding: "Writing or refactoring application code, tests, code review",
research: "Finding and summarizing external information, docs, papers",
} as const;
async function route(request: string) {
const response = await fetch("https://api.typesafe.ai/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
state: request,
model: "jev-latest",
questions: {
agent: {
type: "choice",
instructions: "Which agent should handle this request",
criteria: AGENTS,
},
},
}),
});
const { answers } = await response.json();
return {
agent: answers.agent.choice as keyof typeof AGENTS,
confidence: answers.agent.confidence as number,
};
}
The same decision can drive which tools you expose. Build one agent per capability and select the agent, rather than handing every tool to one agent and hoping the model chooses well. Fewer tools per prompt also means a smaller, cheaper context.
Step 3: Guard tool calls before they execute
LangChain documents middleware as the place for guardrails, retries, routing, and custom tool policies. A Jev Noul question fits the guardrail role: a cheap check that runs before a side-effecting tool call.
async function isDestructive(command: string): Promise<number> {
const response = await fetch("https://api.typesafe.ai/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
state: command,
model: "jev-latest",
questions: {
destructive: {
type: "noul",
instructions: "This command deletes, restarts, or mutates production state",
},
},
}),
});
const { answers } = await response.json();
return answers.destructive.noul as number;
}
Then gate the call in your tool wrapper:
const kubectl = tool(
async (input) => {
const destructive = await isDestructive(input.command);
if (destructive > 0.7) {
return { status: "blocked", reason: "destructive command requires confirmation" };
}
return runKubectl(input.command);
},
{
name: "kubectl",
description: "Inspect Kubernetes cluster state: pods, logs, events",
schema: z.object({ command: z.string() }),
},
);
The guardrail is deterministic code rather than a prompt instruction, which is the point. Prompt-based guardrails are suggestions. A Noul check is a number your code branches on.
Step 4: Keep decisions out of the prompt
The temptation is to put routing in the system prompt: "If the user asks about Kubernetes, use the kubectl tool." That works until it does not, and when it fails the model is making a judgment call you cannot measure, threshold, or debug.
Splitting it out buys you three things:
- Measurability. Log
choiceandconfidenceper request; you get a distribution instead of a vibe. - A threshold you control. Route at 0.85, confirm between 0.6 and 0.85, escalate below. Change the numbers, not the prompt.
- Cheaper loops. The decision is one fast call, and the LangChain agent only runs for requests that earned it.
Step 5: Escalate with the LLM you already have
Low-confidence decisions are the interesting ones. Hand them to a reasoning model or a human, and keep the trace:
const { agent: chosen, confidence } = await route(request);
if (confidence < 0.6) {
return escalateToHuman(request, confidence);
}
const result = await agents[chosen].invoke({
messages: [{ role: "user", content: request }],
});
return result;
A note on LangGraph
If you need durable execution, persistence, or human-in-the-loop, LangChain's docs point to LangGraph as the lower-level orchestration layer. A Jev decision node fits there the same way it fits here: a node that returns a typed answer your graph routes on. Nothing about the decision layer changes, only where you place it.
Try it
Both scenarios in the playground map onto this article: the agent router and the guardrail. Run them with your own key and watch the probabilities before you commit to thresholds. The official TypeSafe playground shows the raw primitives.