Tutorials

Jev + LangChain: Routing and Guardrails Around createAgent

Jev AI Guide Editorial Team
Sep 21, 20264 min readUpdated Sep 22, 2026

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:

  1. Measurability. Log choice and confidence per request; you get a distribution instead of a vibe.
  2. A threshold you control. Route at 0.85, confirm between 0.6 and 0.85, escalate below. Change the numbers, not the prompt.
  3. 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.

What to read next

// Related articles