# Compose decisions with Jev

**AgentRun describes how the work fits together. Jev supplies focused judgments inside that workflow.** Exact code handles rules such as prices and limits; ordinary agent sessions handle jobs that need tools and several turns.

You can use a few decision nodes as a tool inside an existing agent loop, or connect them to agent steps in a larger workflow. The useful boundary is explicit: what goes in, what comes out, and what code does with the answer.

For movie night, four operations have different jobs:

| Operation | Everyday question | What the workflow receives |
| --- | --- | --- |
| Classifier (`judge`) | Does this movie fit our mood? | `good_fit`, `too_scary`, or `unknown`. |
| Router (`route`) | Choose a movie, or plan the whole evening? | A selected branch, including fallback information. |
| Sift (`sift`) | Which movies fit? | Every original movie record that passes the check. |
| Pick (`pick`) | Which one should we choose? | One record from the shortlist, or none. |

Jev answers typed questions through `runJudge`. The DSL compiles those questions, retains their answers, and executes the declared branches and selection rules. Your host continues to own its agents and tools.

## Try the decisions without a key

```sh
npm ci
npm run build
node examples/jev-decisions.mjs
```

**This offline example uses fictional movies and scripted responses, not measured Jev responses.** The real interpreter executes all four node kinds and retains their actual questions, answers, and events. The outer agent loop is not executed or simulated as a successful live run.

The supplied catalog has four movies:

| Movie             | Description                                             | Listed price |
| ----------------- | ------------------------------------------------------- | -----------: |
| Space Pancakes    | Friends and a silly robot have a funny space adventure. |           $8 |
| Bicycle Band      | Friends make music in a cheerful comedy.                |           $6 |
| The Haunted House | Ghosts, jump scares, and frightening chases.            |           $8 |
| The Mystery Pick  | No description supplied.                                |           $8 |

The classifier labels the comedy, horror movie, and missing description separately. It does not guess the mood from a title.

The router distinguishes a movie-only question from a request for pizza and a whole-evening plan. In this small example, each branch records its handler name; it does not order food or run the whole evening's tasks. A separate [movie-night workflow](movie-night.md) shows that larger composition.

Sift asks one suitability question for each movie in **one request**. Both comedies stay on the list; the scary movie and the movie without a description do not. This is a filter, so more than one item can pass.

Pick then chooses one item from that shortlist. The friends prefer a space adventure, so the fixture chooses **Space Pancakes**. Pick returns the original catalog record instead of generating a new title or price.

If none fits, `allowNone: true` permits `none_of_these`. The example demonstrates that with a $1 movie budget. An empty shortlist returns none without making a pick request.

Add `--json` to inspect the complete lesson evidence:

```sh
node examples/jev-decisions.mjs --json
```

## Build one decision tool from those pieces

The example exports each small workflow and a composed `decisionWorkflow`:

```text
classify the suggested movie
  → route the request
  → sift the supplied catalog
  → pick from that shortlist
  → enforce the movie-price limit in code
```

These steps have explicit inputs and named outputs. The classifier writes `classification`; the router records `scope`; sift writes `shortlist`. Pick reads `shortlist.items`, so it can choose only from the items that survived. Its selected item is copied from the original data, not generated as a new title or price.

The first classification describes the suggested movie. The route uses the user's request. Sift considers every supplied candidate, and pick uses the shortlist and stated preference. Keeping those inputs separate makes the dependencies visible. Use only the operations your task needs; the four-step example exists to show how they compose.

The final code node checks the selected movie's price against the supplied budget. A model can judge the movie's mood; it cannot overrule that arithmetic check.

## Evaluate the part you are changing

An isolated evaluation should keep the same node definition and complete question schema used in the composed workflow. Feed it saved inputs, preserve all of its rubric, and compare its answer with an expectation outside the candidate. Do not replace a rich rubric with a shorter test prompt and call that the same component.

The standalone classifier and composed tool here share their classification definition and schema, with their input mappings pointing at the appropriate movie record. Tests inspect the actual state, compiled questions, typed answers, and retained events. The [movie-night primer](movie-night.md) goes further: it isolates its full classifier and deliberately injects a wrong answer to show an evaluation catching a semantic regression.

Then test the composition. A passing classifier does not prove that the fallback route ran, the filter kept the right records, the picker received that filtered list, or a too-expensive result was rejected. Those are separate workflow checks.

## Keep uncertainty visible

A route result preserves both the model's selected branch and the branch actually taken. The example's uncertain answer looks like this:

```json
{
  "branch": "movie_only",
  "taken": "whole_evening",
  "unsure": true
}
```

The workflow's explicit fallback takes `whole_evening` when reported choice confidence is below `0.7`. That is a conservative branch selection, not evidence that the user requested food. A host can check `routeDecision.unsure` and ask a clarifying question before acting.

Sift's keep rule uses the raw yes probability for each movie, with an example threshold of `0.7`. This is separate from the general boolean decoding threshold of `0.5`: a `0.6` answer can decode to `true` while still failing the stricter keep rule. The raw answer remains available. These thresholds are example policy choices to evaluate on your own task, not universal quality guarantees.

<a id="connect-jev"></a>

## Connect Jev

The source checkout already includes the optional Jev adapter. After `npm ci` and `npm run build`, configure authorized TypeSafe access in your **server environment** through your existing credential setup. The adapter reads `TYPESAFE_API_KEY`; an organization using a gateway can provide its configured `TYPESAFE_BASE_URL`. Do not put credentials in browser code or this source tree.

From the repository root, this code makes live Jev requests using the adapter's default configuration:

```js
import { createJevRunner } from "./packages/jev/dist/index.js";
import {
  createMovieDecisionTool,
  decisionInput,
} from "./examples/jev-decisions.mjs";

const runJudge = createJevRunner();
const chooseMovie = createMovieDecisionTool({ runJudge });

const result = await chooseMovie(decisionInput);
if (result.routeDecision.unsure) {
  console.log("Clarify what the group wants before acting.");
} else if (result.none) {
  console.log("No movie fits. Ask for more choices.");
} else {
  console.log(result.selected.title);
}
```

Save that snippet as a module in the repository root and run it with Node. It is an optional live integration; it was not executed to generate this example's results. Live answers may differ from the scripts.

You can also run one small workflow directly:

```js
import { runWorkflow } from "./packages/dsl/dist/index.js";
import { createJevRunner } from "./packages/jev/dist/index.js";
import { siftWorkflow, catalog } from "./examples/jev-decisions.mjs";

const result = await runWorkflow(
  siftWorkflow,
  { candidates: catalog, mood: "lighthearted" },
  { runJudge: createJevRunner() },
);
console.log(result.output.items);
```

The [TypeSafe JavaScript documentation](https://docs.typesafe.ai/sdk/javascript) describes the underlying typed-question API. Its [function-calling cookbook](https://docs.typesafe.ai/cookbooks/function_calling) shows the same principle: a focused judgment can feed an ordinary typed function. AgentRun's adapter supplies that interface to the DSL.

<a id="agent-calls-workflow"></a>

## Let your agent call the workflow

**Agent → workflow:** `createMovieDecisionTool({ runJudge })` returns a normal async function. Register that function under a tool name such as `choose_movie` using your host's existing tool interface. Supply `decisionWorkflow.schemas.Input` as its input schema if your host accepts JSON Schema.

```js
import {
  createMovieDecisionTool,
  decisionWorkflow,
} from "./examples/jev-decisions.mjs";

const chooseMovie = createMovieDecisionTool({ runJudge });
const toolInputSchema = decisionWorkflow.schemas.Input;

// Inside your existing tool-call handler:
const toolResult = await chooseMovie(validatedToolArguments);
// Return toolResult to the SAME agent session through your host's tool API.
```

The last snippet is a host integration pattern: `runJudge` and `validatedToolArguments` come from your application. It does not invent a host-specific registration API or start another coordinating agent.

The composed tool runs classifier → route → sift → pick. A successful result includes:

```json
{
  "route": "movie_only",
  "routeDecision": {
    "branch": "movie_only",
    "taken": "movie_only",
    "unsure": false
  },
  "classification": { "fit": "good_fit" },
  "shortlist": [
    { "id": "space-pancakes", "title": "Space Pancakes", "price": 8 },
    { "id": "bicycle-band", "title": "Bicycle Band", "price": 6 }
  ],
  "selected": { "id": "space-pancakes", "title": "Space Pancakes", "price": 8 },
  "none": false
}
```

This excerpt abbreviates movie records and omits `evidence`. The actual result retains full original records, every request's state and questions, raw answers, events, and the decisions' answer metadata. The first classification describes the suggested movie; sift and pick consider the full supplied catalog. Use the individual workflows when your task needs fewer operations.

The tool throws if a workflow fails. A non-complete result, such as an escalation from a host adapter, is rejected with the original result and recorded evidence in the error's `cause`. The final code guard also rejects a selected price above the supplied movie budget, even if the judge picked it. Semantic suitability remains a model judgment, not something arithmetic can prove.

Your host continues to own the surrounding agent loop, tool permissions, credentials, cancellation, session budgets, and external actions. Returning a movie choice does not buy anything. Keep execution and access control outside the model's judgment.

## Let a workflow call your agent

The reverse direction is **workflow → agent**. An `agent` node calls the host's `runNode` adapter. That adapter runs your ordinary agent loop for the requested job and returns its structured submission. The agent can still use tools and take several turns inside that step.

The [movie-night workflow](movie-night.md#connect-your-agent) uses this direction: movie and food agents run in parallel, a Jev-compatible classifier judges the movie, a problem solver runs only when needed, and a writer can repair a rejected submission. Explicit state mappings and output schemas define each boundary.

```js
import { runMovieNight } from "./examples/movie-night.mjs";

const run = await runMovieNight("repair", {
  runNode: yourAgentAdapter,
  runJudge,
});
```

Here `yourAgentAdapter` is your application's implementation, and `runJudge` can be the function returned by `createJevRunner()` above. The adapter receives instructions, projected JSON state, an output schema, an optional submission-review hook, and a cancellation signal. It must return a valid submission or throw. Wire the review hook into your host's submit tool when the step allows draft repair.

This is an adapter boundary, not automatic conversion of an arbitrary agent loop. The workflow declares branches, parallel work, input/output contracts, and review limits. Your host runs the sessions and enforces their tool access and resource budgets.

## Check the behavior

```sh
node --test examples/jev-decisions.test.mjs
```

Tests cover all four actual node kinds, the uncertain-route fallback, per-item filtering, stricter keep thresholds, explicit no-match and empty-list behavior, raw evidence, and the regular tool function. They also run the composed workflow through the **real Jev adapter with an injected fake client**, verifying the adapter bridge without credentials or network calls. An invalid over-budget pick cannot pass the tool's code guard.

Source: [example and tool](../examples/jev-decisions.mjs), [tests](../examples/jev-decisions.test.mjs). Continue with [the complete movie-night workflow](movie-night.md) or [the DSL reference](guide.md).
