NOULI

Guide

  1. Install
  2. Configure a provider
  3. Interactive mode
  4. Make a decision
  5. Add a turn
  6. Branch and blend
  7. See the graph
  8. Run a workflow
  9. Output and exit codes
  10. Troubleshooting

Nouli is a multi-turn decision harness: it gives a decision model a memory. You send facts and a question with a fixed set of answers; the model returns a probability for each answer. Nouli records the turn in a session, and the next turn only needs the new facts.

01Install

On macOS (Apple silicon), use Homebrew:

brew install standing-desks/tap/nouli

On Linux, use the download installer:

curl -fsSL https://standing-desks.github.io/nouli/install.sh | sh

The script installs the latest published binary into ~/.local/bin after verifying its checksum. It also works on Apple silicon macOS. Linux needs glibc 2.38 or newer.

If your shell cannot find Nouli, add the installation directory to your PATH:

export PATH="$HOME/.local/bin:$PATH"
nouli --version

If you previously installed with Cargo, run cargo uninstall nouli-cli before switching so the old executable does not take precedence.

To update a Homebrew installation:

brew update
brew upgrade standing-desks/tap/nouli

For a script installation, run the download installer again. Uninstall with brew uninstall standing-desks/tap/nouli for Homebrew, or remove ~/.local/bin/nouli for the script. Your configuration and saved sessions are preserved.

See the installation guide for selecting a version, changing the installation directory, inspecting the script, and building from source.

02Configure a provider

Nouli reads ~/.nouli/config.toml. The first time you make a decision without one, Nouli creates an empty file and asks you to define a provider. For TypeSafe:

default_provider = "typesafe"
default_model = "jev-latest"

[providers.typesafe]
protocol = "typesafe"
base_url = "https://api.typesafe.ai"

Then log in once. Nouli asks for the key without showing it and keeps it in ~/.nouli/auth.toml, a file only you can read, so every command works from any directory:

nouli auth login typesafe
nouli auth list

nouli auth list shows where each provider's key comes from and never prints a key. nouli auth logout typesafe removes it.

For CI and agents, use an environment variable instead. Name it in the provider with api_key_env, and Nouli reads it from your shell or from a .env in the directory you run it in. The shell wins over .env, and both win over the stored key:

[providers.typesafe]
protocol = "typesafe"
base_url = "https://api.typesafe.ai"
api_key_env = "TYPESAFE_TOKEN"
TYPESAFE_TOKEN="your-api-key"

Use --env-file FILE to read a different file. The other supported protocols are openai-decisions and openrouter-decisions. A local Laya service uses the typesafe protocol with a loopback URL and needs a context budget, because Nouli can't look up its limits:

[providers.local]
protocol = "typesafe"
base_url = "http://localhost:8000"

[providers.local.models."english"]
context_budget_tokens = 8192
reserved_output_tokens = 128

Pick a provider or model for one call with --provider NAME and --model MODEL. Each call uses your provider account and may cost money.

03Interactive mode

The quickest way to use Nouli is a session you work in. nouli open trip opens a prompt on a session, creating it if you agree, and a bare nouli in a terminal lists your sessions first. Type a fact to queue it, then ask a question with ?: the question goes to the model right away, together with the queued facts. Questions stay with the session, so the next question asks them again, and so does return on an empty line:

$ nouli open trip
new session "trip" · /help for commands

trip › The train costs 40 USD and arrives 30 minutes before the meeting.
trip › A taxi costs 70 USD and also arrives in time.
trip › ?transport: How should I get to the meeting? [Take the train, Take a taxi]
  turn 1 · 2 facts · 1 question
  transport   ▶ Take the train                   ████████████████████ 1.00
                Take a taxi                                           0.00
trip › The train has been cancelled.
trip › ?on_time: Will I get to the meeting on time?
  turn 2 · 1 fact · 2 questions · re-asked 1
  transport     Take the train                   ██▎                  0.11
              ▶ Take a taxi                      █████████████████▊   0.89   → was Take the tr… 1.00
  on_time       yes                              ███████▎             0.36
trip › /replace 3 The train is running 10 minutes late but still arrives 20 minutes before the meeting.
  queued: replace fact 3
trip › ↵
  turn 3 · 1 fact (1 replaced, kept in history) · 2 questions · re-asked 2
  transport   ▶ Take the train                   ███████████▎         0.56   → was Take a taxi 0.89
                Take a taxi                      ████████▊            0.44
  on_time       yes                              █████████████▎       0.66   → was 0.36
trip › /whatif My budget for the trip is 50 USD.
  (not saved) · 1 fact · 2 questions
  transport   ▶ Take the train                   ███████████████████▍ 0.97   → was Take the tr… 0.56
                Take a taxi                      ▋                    0.03
  on_time       yes                              ██████████████▋      0.73   → was 0.66
trip › /quit
  bye

A question is one line: an ID, a colon, the prompt, and then the answers. [a, b] makes a choice, <low, medium, high> makes a score with levels in order, and no list makes a yes/no question. Quote an answer that contains a comma.

Slash commands do the rest: /whatif FACT asks without saving, /replace N TEXT and /drop ID queue a correction, /branch NAME and /blend A B as NAME start new sessions, and /help lists them all. The prompt only shows what the store holds. Ctrl-C clears the line or what you have queued, and on an empty prompt it quits, as Ctrl-D and /exit do.

04Make a decision

Everything you can do at the prompt is also a command, for scripts and agents. nouli new starts a saved session. Give it the facts, then the questions with -q. A question is one line: an ID, a prompt, and the options in brackets:

nouli new travel \
  'The train costs 40 USD and arrives 30 minutes before the meeting.
A taxi costs 70 USD and also arrives in time. Prefer the cheaper option
that arrives on time.' \
  -q 'transport: How should I travel to the meeting? [Take the train, Take a taxi]'
  turn 1 · 1 fact · 1 question
  transport   ▶ Take the train                   ████████████████████ 1.00
                Take a taxi                                           0.00
  typesafe/jev-1.14.0 · session 01a18762-58ce-726d-83d9-70cf6801a1ff · turn 01a18762-58d4-71ca-9344-e84e399dedd0

Every option gets a probability, and ▶ marks the answer. Each question has one of three answer types:

typeanswerhow to write it
choiceOne option, with a probability for every optionid: prompt [option, option]
noulThe probability that the answer is yesid: prompt
scoreThe expected position on an ordered rubric, which can be fractionalid: prompt <poor, fair, good>

Put an option in double quotes if it contains a comma or a bracket. A question can sit in a file too: pass --input request.json (JSON or TOON) for the full form with rubrics and structured evidence. Answers are model judgments, not actions: Nouli never books, sends, or changes anything.

05Add a turn

A session keeps the facts you gave it and the questions it asks. nouli add sends only what changed, and the earlier facts and answers come along. A question with an ID the session already has takes the place of the old one; --drop ID stops asking one, and nouli ask travel asks again without new facts:

nouli add travel \
  'The train has been cancelled. The taxi is still available at 70 USD.' \
  -q 'on_time: Will the train get me to the meeting on time?' \
  -q 'transport: How should I travel to the meeting now? [Take the train, Take a taxi]'
  turn 2 · 1 fact · 2 questions · re-asked 1
  transport     Take the train                   ▎                    0.01
              ▶ Take a taxi                      ███████████████████▊ 0.99   → was Take the tr… 1.00
  on_time       yes                              ██▋                  0.13
  typesafe/jev-1.14.0 · session 01a18762-58ce-726d-83d9-70cf6801a1ff · turn 01a18762-58e6-77f4-b3ff-6f0834a4344f

The arrow shows what changed since the last answer to the same question. Facts are numbered, and --replace 1 'new text' corrects fact 1: the model sees only the new text, and the old one stays in the history. nouli whatif travel 'The taxi fare has doubled to 140 USD.' asks the same questions of a hypothetical fact and saves nothing.

Sessions are saved under ~/.nouli and work from any directory. --store DIR uses a different store.

Long histories are trimmed to fit the model's context budget: older decisions shrink to their question and answer first, then the oldest optional records drop out. The full record stays in the store either way. nouli context SESSION shows exactly what reached the model for a turn.

06Branch and blend

--from starts a new session from saved history. One --from branches, and the branch keeps the source's questions; several blend, and a blend starts with none:

nouli new travel-late --from travel '…' -q '…'
nouli new plan --from travel --from budget '…' -q '…'

A new session captures its sources when you create it. Later turns in travel don't change plan; make a new blend to reconsider with the updated history. To start from an older point, name the turn: --from travel@TURN_ID.

07See the graph

nouli view plan
nouli view plan --mermaid > plan.mmd
nouli facts plan
nouli questions plan
nouli sessions list
nouli sessions show plan
nouli sessions export plan --format json > plan.json

nouli view draws the session and every turn it captured as boxes in your terminal, with ▶ marking each selected answer. --mermaid prints the same graph as Mermaid source; paste it into a ```mermaid block and GitHub draws it. nouli facts lists the numbered facts, marking those inherited from another session, and nouli questions prints the session's questions in the one-line form. None of these commands call a model or change the store.

08Run a workflow

When later questions depend on earlier answers, describe the turns as a graph and let Nouli run it. Nodes with no dependency between them run in parallel:

{
  "state": [],
  "nodes": [
    {"id": "choose", "turn": {
      "state": [{"type": "text", "text": "The train takes 4 hours; the flight takes 2 hours."}],
      "questions": [{"id": "route", "prompt": "Which route should I take?",
        "answer_space": {"type": "choice", "options": [
          {"id": "train", "description": "Take the train"},
          {"id": "flight", "description": "Take the flight"}]}}]}},
    {"id": "book", "dependencies": ["choose"], "turn": {
      "state": [],
      "questions": [{"id": "book_now", "prompt": "Given the preferred route, should I book it now?",
        "answer_space": {"type": "noul"}}]}}
  ]
}
nouli workflow run workflow.json --name review --parallel 2
nouli workflow show RUN_ID
nouli workflow resume RUN_ID

A run saves its graph and settings before it starts. If it's interrupted, resume reuses finished nodes and runs the rest. --jsonl streams progress events.

09Output and exit codes

Output is text in a terminal and JSON when piped. Choose explicitly with --format text|json|jsonl|toon, --json, or --jsonl.

exitmeaning
0Completed, including replays and per-question refusals
2Invalid input or configuration
3Provider, authentication, capability, contract, or capacity failure
4Partial or failed workflow
5Request, writer, context, or advanced-head conflict
6Storage, internal, or output failure
130Interrupted; recovery needed

10Troubleshooting

messagewhat to do
providers_requiredOpen the configuration file named in the error and add a [providers.NAME] section.
provider_requiredSet default_provider or pass --provider NAME.
model_requiredSet default_model or pass --model MODEL.
unknown_providerUse a provider name defined in your configuration.
credential_unavailableRun nouli auth login PROVIDER, or set the environment variable named in the error, in your shell or the env file.
auth_store_insecureOthers can read ~/.nouli/auth.toml. Run chmod 600 on it.
questions_requiredThe session has no questions yet. Add one with -q.
invalid_standing_question--drop named a question the session does not have, or a one-off question repeats an ID it already asks.
env_file_not_foundCheck the path passed to --env-file.
invalid_env_fileCheck assignments, quotes, and UTF-8 encoding in the env file.
invalid_model_budgetAdd context_budget_tokens under [providers.NAME.models."MODEL"] or pass --context-budget TOKENS.
invalid_configuration_syntaxCheck the TOML. Top-level settings go before provider sections.

A provider can refuse a single question; that's recorded as a refusal, not an error. Run nouli --help for every option. The reference docs cover each subsystem in detail.