jev_agent_router
A pure Dart package from brainovatech.in, the same
publisher as
flutter_app_functions.
It decides what should happen, then hands the work to a registered tool.
Jev (package:jev_dart) is the decision layer
over a closed set of options. It does not call an LLM, and it does not depend
on Flutter or on flutter_app_functions.
| Runtime | Dart 3.5 or newer. Flutter, CLI, server, and agents |
| Decisions | Jev choice questions through jev_dart |
| LLM | Not required, and not included |
| Publisher | brainovatech.in |
| License | MIT, Copyright (c) 2026 Mohit Koley |
Use it from a Flutter app, a Dart CLI, a backend, or any tool-calling agent that can supply:
- the user request
- the tools that are legal right now
- known state and candidate entities
User Input
↓
JevAgentRouter
↓
Jev Decision Engine
↓
Tool
↓
Argument Resolver Chain
↓
Validated Arguments
↓
Tool Executor
↓
Result
Why Jev fits this job
Jev answers typed questions about state you already have. A choice question lists at most 255 labels, each with a description. The response is the winning label, a probability for every label, and a confidence from 0 to 1 that describes how peaked that distribution is.
That is the whole decision this package needs:
- which tool
- which domain, then which tool
- which known product, order, or other entity
Jev never has to invent a tool name, an id, or a sentence.
Jev is not an LLM
| Jev | LLM | |
|---|---|---|
| Output | One label from your list, plus probabilities | Free text or tool JSON the model authors |
| Options | Closed. An unknown id cannot be returned | Open. It can name a tool you did not register |
| Confidence | A number you can threshold | Usually hidden or uncalibrated |
| When it is enough | Every argument is state or a known candidate | You need open-ended language or a value that is not in the candidate list |
No LLM is required when every decision is one of those bounded choices. If a required argument is missing and there is no candidate list, the router asks the user. It does not guess.
An LLM resolver can be added later as a separate package that implements
ArgumentResolver. This package does not include one.
Install
dependencies:
jev_agent_router: ^0.1.0
jev_dart: ^0.1.5
Set TYPESAFE_API_KEY or JEV_API_KEY before constructing the real engine.
Tests and the example below use a fake engine and do not need a key.
Add repository and homepage to this package's pubspec.yaml before
publishing.
Route a request
import 'package:jev_agent_router/jev_agent_router.dart';
import 'package:jev_dart/jev_dart.dart';
final engine = JevDecisionEngine(
client: TypeSafeClient(),
);
final router = JevAgentRouter(
decisionEngine: engine,
tools: [
SearchProductsTool(),
AddToCartTool(),
TrackOrderTool(),
],
argumentResolvers: [
StateArgumentResolver(),
JevChoiceArgumentResolver(engine),
],
);
final result = await router.route(
AgentContext(
userInput: 'Add the black Nike shoe to my cart',
state: {'currentScreen': 'search'},
candidates: {
'productId': productCandidates,
},
),
);
switch (result) {
case ToolExecuted():
print(result.value);
case ClarificationRequired():
showOptions(result.candidates);
case NoMatchingTool():
showUnsupportedMessage();
case ArgumentResolutionFailed():
showArgumentError();
case ToolConfirmationRequired():
showConfirm(result);
case ToolExecutionFailed():
showFailure(result.error);
}
JevDecisionEngine is the only type that imports Jev. Everything else depends
on DecisionEngine, so tests substitute a fake and never open a socket.
A choice with one option returns confidence 1 and does not call the API.
Choice criteria for a stable option list are cached on that list, so repeated
routes do not rebuild the description map. Provider tools are loaded once
until refresh.
Architecture
lib/
jev_agent_router.dart public exports
src/router/ route(), outcomes, config
src/tools/ AgentTool, registry, providers, groups
src/decisions/ DecisionEngine and JevDecisionEngine
src/arguments/ resolver chain
src/context/ user input, state, candidates
src/errors/ registration and argument failures
JevAgentRouter.route does five things:
- Load providers if this is the first call.
- Ask the decision engine which tool or group fits. Groups add another decision inside the chosen domain.
- Apply the confidence policy.
- Walk the argument resolver chain for each parameter.
- Invoke the tool, or stop for confirmation when the risk level requires it.
Tool descriptions are sent as Jev choice criteria. The user utterance, known state, and an optional conversation transcript are the choice state. Jev is not asked to write the argument object.
Tools
abstract interface class AgentTool {
String get id;
String get name;
String get description;
List<AgentParameter> get parameters;
ToolRiskLevel get riskLevel;
Future<ToolExecutionResult> invoke(Map<String, Object?> arguments);
}
const AgentParameter(
name: 'productId',
description: 'Product to add.',
type: AgentParameterType.string,
);
Built-in parameter types are string, integer, double
(AgentParameterType.doubleValue), boolean, stringList, integerList,
and json. A custom AgentParameterType('sku') is accepted and passed
through, so adapters can extend the set without changing this package.
final registry = ToolRegistry();
registry.register(AddToCartTool());
registry.register(SearchProductsTool());
registry.get('addToCart');
registry.unregister('addToCart');
registry.all;
Duplicate ids throw DuplicateToolException unless you pass override: true.
CallbackAgentTool covers a one-off implementation. Prefer a small class when
the tool has dependencies.
Providers
final router = JevAgentRouter(
decisionEngine: engine,
providers: [EcommerceToolProvider()],
);
await router.refresh();
abstract interface class ToolProvider {
Future<List<AgentTool>> getTools();
}
The same seam is how a later package can expose
FlutterAppFunctionsProvider, McpToolProvider, or RestApiToolProvider
without this package importing those hosts.
Argument resolution
Resolvers run in order. The first one that resolves, asks, or fails stops the chain.
argumentResolvers: [
StateArgumentResolver(),
JevChoiceArgumentResolver(engine),
UserInputArgumentResolver(),
],
That default order is:
- Read a typed value from structured state.
- Choose one entity from the candidate list.
- Ask the user. This does not parse the utterance.
StateArgumentResolver checks state[name], then any stateKeys, then
current{Name} and selected{Name}. currentProductId fills productId.
selectedVariantId fills variantId. No Jev call is made. A present value of
the wrong type fails instead of falling through.
JevChoiceArgumentResolver reads context.candidates[parameter.name]. One
candidate is used directly. Several candidates become a Jev choice, and the
selected entity id becomes the argument. Confidence below the execute
threshold does not bind that id.
UserInputArgumentResolver reads metadata['arguments'] when the caller
already collected a value. Otherwise a required parameter becomes
ClarificationRequired.
Confidence
const ConfidencePolicy(
executeThreshold: 0.85,
clarificationThreshold: 0.60,
);
| Confidence | Result |
|---|---|
>= 0.85 |
Execute, after arguments are valid |
>= 0.60 and < 0.85 |
ClarificationRequired with the leading options |
< 0.60 |
NoMatchingTool, or ArgumentResolutionFailed when a candidate choice is this uncertain |
Jev's confidence is used as returned. The package does not recompute it.
probabilities are kept on DecisionResult when the engine provides them.
ConfidencePolicy.requireExecutable throws LowConfidenceException for
callers that want an exception instead of an outcome. route itself returns
an outcome.
Risk
enum ToolRiskLevel { readOnly, low, medium, high }
| Tool | Risk |
|---|---|
searchProducts, viewCart |
readOnly |
addToCart |
low |
cancelOrder |
medium |
checkout |
high |
const RouterConfig(
requireConfirmationFor: {ToolRiskLevel.high},
);
A matching tool returns ToolConfirmationRequired with the resolved
arguments and does not call invoke. After the user agrees:
final executed = await router.executeConfirmed(pending);
Hierarchical routing
One Jev choice accepts 255 labels. Group tools when the catalog is larger, or when domains make the decision easier.
router.registerGroup(
AgentToolGroup(
id: 'orders',
description: 'Order management',
tools: [
TrackOrderTool(),
CancelOrderTool(),
OpenOrderTool(),
],
),
);
Jev decision 1 → orders
Jev decision 2 → cancelOrder
Ungrouped tools stay in the first decision beside groups. Nested groups
add another level. The router works with no groups at all: one decision over
the flat tool list.
Ecommerce example
example/ecommerce_example registers search, open, add-to-cart, view cart,
track, and cancel. It uses an offline engine so it runs without a key.
User: Add the black Nike shoe to my cart
Candidates: P100 Nike Air Max Black, P101 Nike Pegasus Blue, P102 Adidas Ultraboost White
addToCart (0.93)
→ productId P100 (0.94)
→ addToCart(productId: P100)
User: Remove the Nike
Cart: Nike Air Max, Nike Pegasus
confidence 0.72
→ ClarificationRequired
cd example/ecommerce_example
dart pub get
dart run bin/main.dart
In Flutter, build the same AgentContext from the current screen and catalog,
call route, and switch on the outcome to push a route, fill a form, or show
a chooser. The package does not import Flutter.
To use live Jev, replace EcommerceDecisionEngine with JevDecisionEngine.
A local timing probe for 200 tools lives at benchmark/route_benchmark.dart.
flutter_app_functions
Do not import flutter_app_functions here. Wrap each function as an
AgentTool in a separate package (jev_agent_router_flutter_app_functions
or flutter_app_functions_jev):
class AppFunctionAgentTool implements AgentTool {
AppFunctionAgentTool(this.definition);
final AppFunctionDefinition definition;
@override
String get id => definition.id;
@override
String get name => definition.name;
@override
String get description => definition.description;
@override
List<AgentParameter> get parameters => [
for (final parameter in definition.parameters)
AgentParameter(
name: parameter.name,
description: parameter.description,
type: AgentParameterType(parameter.typeName),
),
];
@override
ToolRiskLevel get riskLevel => ToolRiskLevel.low;
@override
Future<ToolExecutionResult> invoke(Map<String, Object?> arguments) {
return FlutterAppFunctions.invoke(id, arguments);
}
}
A ToolProvider can list every registered app function and return those
wrappers. The router then selects and fills arguments the same way it does
for a hand-written tool.
Later: an optional LLM
Add a package that implements ArgumentResolver and place it after
JevChoiceArgumentResolver. It should run only when state and candidates
cannot fill a parameter. Keep tool selection on Jev so the model cannot call
a tool that was not registered. Gate that resolver with the same
ConfidencePolicy, and keep high-risk tools behind
ToolConfirmationRequired.
Publishing releases
Maintainer-only. Skip this if you are consuming the package.
Publishing is automated by
.github/workflows/publish.yml using
OpenID Connect. No long-lived secret is stored in the repo. The same
pattern publishes
flutter_app_functions.
The official guide is at
dart.dev/tools/pub/automated-publishing.
Publish under the brainovatech.in publisher, the same pub.dev publisher
as flutter_app_functions.
One-time setup (do this once on pub.dev)
- Sign in to pub.dev with the Google account that is a member of brainovatech.in.
- On the first upload, create
jev_agent_router, then transfer it to that publisher from the package admin page. Later uploads already belong to the publisher. - Open
pub.dev/packages/jev_agent_router/admin/automated-publishing. - Click Enable publishing from GitHub Actions and set the repository to
mohitkoley/jev_agent_routerwith tag patternv{{version}}. The form's{{version}}placeholder accepts tags likev0.1.0andv1.2.3-rc.1. - Tick the form's checkboxes as follows:
- Enable publishing from push events — on. This workflow is triggered
by
push: tags:. - Enable publishing from workflow_dispatch events — off. pub.dev rejects branch-typed refs.
- Require GitHub Actions environment — off, unless you add an approval environment later.
- Enable publishing from push events — on. This workflow is triggered
by
- Save. The next job triggered by a matching tag will be trusted.
Pull requests and pushes to main run
.github/workflows/ci.yml: dart analyze and
dart test, plus analysis of example/ecommerce_example.
Cutting a release
# 1. Bump the version and update the changelog
# pubspec.yaml version: and CHANGELOG.md
# 2. Commit and push to main
git add pubspec.yaml CHANGELOG.md
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
git commit -m "Release ${VERSION}"
git push origin main
# 3. Tag and push the tag — this triggers publish.yml
git tag "v${VERSION}"
git push origin "v${VERSION}"
Tests do not call Jev. The example under example/ecommerce_example is the
same offline demo.
Re-running a failed publish
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')
git tag -d "v${VERSION}"
git push origin ":refs/tags/v${VERSION}"
git tag "v${VERSION}"
git push origin "v${VERSION}"
References
Libraries
- jev_agent_router
- Route user requests to registered tools with Jev as a bounded decision layer.