
A technical tutorial covering architecture, decision logic, implementation, and performance analysis.
Quick Answer
JEV is a narrow decision layer that sits in front of a browser automation agent. Instead of one general-purpose LLM reasoning, choosing a tool, executing it, and reasoning again in a single loop, JEV's only job is to decide the next high-level action (navigate, click, type, press_key, scroll, wait, or finish). The existing LangGraph agent and Chrome DevTools MCP still handle the actual execution. Separating those two responsibilities, deciding what to do next versus figuring out how to do it, is the core idea behind this architecture.
1. Introduction
LLM-powered browser agents commonly use a ReAct-style loop, where the same general-purpose agent repeatedly reasons, selects a tool, executes it, observes the result, and reasons again. This is flexible, but it also lets the agent wander: nothing stops it from chaining several unrelated actions together inside what should have been one small step, which makes the loop harder to predict, harder to bound, and harder to debug when something goes wrong.
The implementation presented here introduces a separate JEV decision layer. JEV decides what the browser agent should do next, while the existing LangGraph/LangChain agent and Chrome DevTools MCP remain responsible for executing the actual browser action.
User Request
↓
JEV Decision Layer
↓
LangGraph Agent
↓
Chrome DevTools MCP
↓
Browser
↓
Updated State
↓
JEV (loop continues)
2. Prerequisites
- Python
- httpx
- python-dotenv
- LangChain
- LangGraph
- LangChain MCP adapters
- MCP
- Chrome DevTools MCP
- OpenRouter
- A JEV-compatible model endpoint
The JEV implementation loads OPENROUTER_API_KEY from the environment and validates that it exists and is not empty.
OPENROUTER_API_KEY=your_openrouter_key
The implementation configures ~typesafe/jev-latest and the OpenRouter decisions endpoint.
3. Architecture Overview
3.1 JEV
JEV is the decision layer. Its responsibility is deliberately narrow: given the user goal, current browser state, previous action, and available tools, determine the single high-level browser action required next.
3.2 LangGraph Agent
The LangGraph agent is responsible for execution. It receives JEV's high-level decision and determines the concrete MCP operation and arguments required to perform it.
3.3 MCP and Browser
Chrome DevTools MCP provides the execution bridge between LangGraph and Chrome. The MCP tools remain executable by LangGraph; JEV receives compact tool information for decision context only, it never calls the tools directly.
JEV
→ high-level action
LangGraph
→ concrete MCP operation
Chrome DevTools MCP
→ browser command
Browser
4. The JEV Mechanism
The central mechanism is a constrained decision request. JEV receives state and answers a structured question: which single high-level browser action should happen next?
- navigate
- click
- type
- press_key
- scroll
- wait
- finish
JEV also evaluates whether the user's requested task has been completely completed. The response therefore provides both an action decision and completion information.
4.1 Decision schema
questions = {
"next_action": {
"type": "choice",
"criteria": {
"navigate": "Navigate to a required URL or page.",
"click": "Click a visible browser element.",
"type": "Type text into a visible input, textarea, or editor.",
"press_key": "Press a keyboard key such as Enter, Escape, Tab, or another required key.",
"scroll": "Scroll the browser to find required content or an element.",
"wait": "Wait because the browser is loading or the UI needs time to update.",
"finish": "The user's requested task has been completed successfully.",
},
},
"finished": {
"type": "bool",
"instructions": "Has the user's requested browser task been completely completed?",
},
}
The structured output is parsed into next_action, finished_probability, confidence, probabilities, and the raw response.
5. Step-by-Step Implementation
5.1 Create the JEV class
class JEV:
"""
JEV decision layer.
JEV decides WHAT the browser agent should do next.
JEV does NOT execute browser actions.
Existing LangGraph/LangChain agent + Chrome DevTools MCP
remain responsible for executing the actual browser action.
"""
This class creates a clean boundary between decision-making and execution.
5.2 Configure the model and endpoint
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.model = "~typesafe/jev-latest"
self.url = "https://openrouter.ai/api/alpha/decisions"
The class exposes the implementation as a LangChain Runnable using RunnableLambda, allowing synchronous and asynchronous invocation.
5.3 Build the JEV state
def _build_state(self, state, mcp_tools):
tools_text = json.dumps(mcp_tools, indent=2)
return f"""
{state}
============================================================
AVAILABLE MCP TOOLS
============================================================
{tools_text}
JEV DOES NOT EXECUTE MCP TOOLS.
The existing LangGraph/LangChain agent is responsible
for executing the appropriate MCP tool.
"""
JEV receives tool descriptions as context rather than receiving direct authority to execute them.
5.4 Invoke JEV through LangChain
self.runnable = RunnableLambda(self._invoke)
async def ainvoke(self, state, mcp_tools=None):
return await self.runnable.ainvoke({
"state": state,
"mcp_tools": mcp_tools or [],
})
This gives the surrounding agent service a normal asynchronous LangChain-style interface.
5.5 Construct the browser decision state
def _build_jev_state(message, browser_state, previous_action=None, step=1):
return f"""
USER GOAL:
{message}
PREVIOUS JEV ACTION:
{previous_action or "None"}
CURRENT BROWSER / AGENT STATE:
{browser_state}
AVAILABLE HIGH-LEVEL ACTIONS:
navigate
click
type
press_key
scroll
wait
finish
"""
The state is the bridge between execution and the next decision. After each bounded browser action, the updated state is fed back into JEV.
5.6 Load and describe MCP tools
tools = await load_mcp_tools(self.mcp_session)
self.mcp_tools = tools
self.mcp_tool_info = []
for tool in tools:
self.mcp_tool_info.append({
"name": tool.name,
"description": tool.description or "",
})
The actual tools remain available to LangGraph, while JEV receives a compact name-and-description representation.
5.7 Execute the selected action
decision = await self.jev.ainvoke(
state=jev_state,
mcp_tools=self.mcp_tool_info,
)
next_action = decision["next_action"]
If the action is finish, the orchestration layer can terminate. Otherwise, it tells LangGraph to perform that single high-level action.
5.8 Bound each execution step
JEV_STEP_RECURSION_LIMIT = 5
config = {
"configurable": {"thread_id": thread_id},
"recursion_limit": JEV_STEP_RECURSION_LIMIT,
}
The recursion limit prevents the underlying ReAct loop from silently completing an entire multi-step task inside one JEV step.
5.9 Recover from the recursion boundary
try:
result = await agent.ainvoke(
{"messages": [HumanMessage(content=execution_prompt)]},
config=config,
)
except GraphRecursionError:
snapshot = agent.get_state(config)
result = snapshot.values
The graph state is preserved through the checkpointer, so hitting the execution boundary does not discard progress.
5.10 Repeat the JEV loop
for step in range(1, self.MAX_JEV_STEPS + 1):
decision = await self._get_jev_decision(
message=message,
browser_state=browser_state,
previous_action=previous_action,
step=step,
)
next_action = decision["next_action"]
if next_action == "finish":
return {"status": "completed"}
last_agent_result = await self._execute_jev_action(
agent=agent,
user_id=user_id,
message=message,
browser_state=browser_state,
decision=decision,
)
browser_state = self._extract_agent_response(
last_agent_result
)
previous_action = next_action
This creates the closed-loop controller: decide, execute, observe, and decide again.
6. Example Decision Cycle
Consider a browser task such as filling out a contact form.
User:
Fill out the contact form with name "Alex Rivera"
and message "Looking forward to connecting."
A first JEV state could lead to:
{
"next_action": "navigate",
"confidence": 0.96
}
After navigation, the resulting browser state is sent back to JEV. The next decision might be click, followed by type, press_key, or another action dictated by the current state. Eventually JEV can return finish.
7. Why Separate Decision-Making From Execution?
Decision: "What should happen next?" → JEV
Execution: "How do I perform that action?" → LangGraph + MCP
This allows the decision layer to operate over a small, predictable action space while the execution layer handles detailed MCP tool selection and arguments.
8. Structured Decision Output
return {
"next_action": next_action,
"finished_probability": finished_probability,
"confidence": confidence,
"probabilities": probabilities,
"raw": result,
}
The orchestration code therefore does not need to parse free-form natural language to discover the intended action.
8.1 Action validation
allowed_actions = {
"navigate", "click", "type", "press_key",
"scroll", "wait", "finish",
}
if next_action not in allowed_actions:
raise RuntimeError(
f"Unknown JEV action: {next_action}"
)
This establishes a strict interface between the decision model and the execution system.
9. Schema Sanitization
The agent service sanitizes MCP schemas once when tools are initialized. The implementation removes unsupported schema keys such as additionalProperties and $schema.
_UNSUPPORTED_GEMINI_SCHEMA_KEYS = (
"additionalProperties",
"$schema",
)
def _strip_unsupported_schema_keys(schema):
if isinstance(schema, dict):
for key in _UNSUPPORTED_GEMINI_SCHEMA_KEYS:
schema.pop(key, None)
for value in schema.values():
_strip_unsupported_schema_keys(value)
elif isinstance(schema, list):
for item in schema:
_strip_unsupported_schema_keys(item)
return schema
The source implementation describes this as avoiding repeated stripping and warnings during every LLM call in the agent loop.
10. Maximum JEV Steps
MAX_JEV_STEPS = 10
The outer loop is also bounded. If the task does not reach finish within the configured number of JEV iterations, the manager returns a max_steps_reached status.
{
"status": "max_steps_reached",
"steps": self.MAX_JEV_STEPS
}
This provides two controls: a per-step LangGraph recursion limit and an overall task-level JEV step limit.
11. Streaming Architecture
yield {
"type": "jev_decision",
"step": step,
"decision": decision,
}
async for chunk in agent.astream(
{"messages": [HumanMessage(content=execution_prompt)]},
config=config,
stream_mode="updates",
):
yield {
"type": "agent_update",
"step": step,
"data": chunk,
}
This allows an application to expose the decision and execution stages independently to a frontend or monitoring system.
12. Performance Analysis
12.1 What the implementation optimizes
Traditional:
LLM → reason + choose + execute → LLM → reason + choose + execute → ...
JEV architecture:
JEV → choose ONE action → execution agent → ONE bounded execution → JEV → choose ONE action → ...
The implementation optimizes the control flow of agent decisions, not the raw speed of an individual HTTP request.
12.2 Reduced tool chaining
The recursion limit prevents the execution agent from chaining unrelated operations during one JEV step.
12.3 Smaller decision space
JEV chooses from seven high-level actions: navigate, click, type, press_key, scroll, wait, and finish. Detailed MCP execution remains with LangGraph.
12.4 Structured evaluation
The response provides next_action, confidence, action probabilities, and finished probability, making downstream orchestration deterministic and easier to validate.
13. Complete Architecture
User Request
↓
AgentManager
↓
JEV
↓ next_action
LangGraph
↓
Chrome DevTools MCP
↓
Browser
↓ new state
JEV (loop continues)
14. Final Takeaway
The central idea is not simply adding another model to an agent. It is separating two fundamentally different responsibilities.
Decision: "What should happen next?" → JEV
Execution: "How do I perform that action?" → LangGraph + MCP
JEV receives the user objective, current browser state, previous action, and available tool information. It returns a structured high-level decision such as click, type, navigate, or finish. LangGraph then translates that decision into an actual MCP operation.
The execution is deliberately bounded, so the underlying ReAct loop cannot silently execute an arbitrary sequence of actions before JEV receives control again.
The resulting architecture is a closed-loop decision and execution system, in which planning, execution, state transitions, and termination are explicit architectural boundaries, rather than being hidden inside one unrestricted ReAct loop.
For the supplied code, that architectural separation is the primary mechanism demonstrated by the implementation, not an unverified numerical latency claim.
Frequently Asked Questions
What is JEV? JEV is a decision layer that sits in front of a browser automation agent. Given the user's goal, the current browser state, the previous action, and the available tools, it decides a single high-level action (navigate, click, type, press_key, scroll, wait, or finish) for the execution layer to carry out.
How is this different from a standard ReAct agent loop? In a standard ReAct loop, one general-purpose LLM reasons, picks a tool, executes it, and reasons again, all in one unbounded cycle. JEV separates that into two roles: JEV decides what should happen next, while LangGraph and Chrome DevTools MCP handle how to actually do it, within a bounded, per-step recursion limit.
Does JEV execute browser actions itself? No. JEV only produces a decision. The existing LangGraph/LangChain agent, using Chrome DevTools MCP, is responsible for translating that decision into the actual browser operation.
Is there benchmark data showing JEV is faster? Not from the supplied implementation. The source code doesn't include a controlled comparison against a baseline ReAct agent, so no specific speed number can be claimed. A benchmark table is included above for anyone who wants to run that comparison themselves.
What are the seven actions JEV can choose from? navigate, click, type, press_key, scroll, wait, and finish. This fixed, small action space is what makes JEV's output structured and easy to validate, rather than requiring free-form natural language parsing.
Does this require Chrome DevTools MCP specifically? The implementation described here uses Chrome DevTools MCP as the execution bridge between LangGraph and the browser. The decision and execution separation itself isn't inherently tied to that one MCP server, but this tutorial's code assumes it.
What happens if a task never reaches "finish"?
Two limits apply. JEV_STEP_RECURSION_LIMIT bounds how much a single execution step can do before returning control to JEV. MAX_JEV_STEPS bounds the whole task, if that many JEV iterations pass without reaching finish, the manager returns a max_steps_reached status instead of looping indefinitely.
Atif Ali
Full Stack Engineer
Full-Stack Engineer at Groooh with extensive expertise in cross-platform mobile development and cloud systems. Specializes in building unified digital ecosystems using React Native, Next.js, and PostgreSQL. Known for writing maintainable, test-driven code and optimizing app performance from database query tuning down to 60fps mobile UI interactions.
Add Groooh to Preferred Sources
Top Stories & AI Overviews
Prioritize our engineering & AI guides in your Google Top Stories & AI Overviews.
Ready to build your next breakthrough product?
Let’s collaborate on your architecture roadmap, MVP sprint, or full product build with our senior team.




