jev_agent_router

pub package CI

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:

  1. Load providers if this is the first call.
  2. Ask the decision engine which tool or group fits. Groups add another decision inside the chosen domain.
  3. Apply the confidence policy.
  4. Walk the argument resolver chain for each parameter.
  5. 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:

  1. Read a typed value from structured state.
  2. Choose one entity from the candidate list.
  3. 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)

  1. Sign in to pub.dev with the Google account that is a member of brainovatech.in.
  2. 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.
  3. Open pub.dev/packages/jev_agent_router/admin/automated-publishing.
  4. Click Enable publishing from GitHub Actions and set the repository to mohitkoley/jev_agent_router with tag pattern v{{version}}. The form's {{version}} placeholder accepts tags like v0.1.0 and v1.2.3-rc.1.
  5. 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.
  6. 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.