Redouble AI Nucleo documentation

Set up and run the demo

You are a coding agent, and a person asked you to get Nucleo running. This file is written for you. It is also the quickstart a person follows by hand; every step is a command whose result you can check. A person reading this: what Nucleo is and why it is built this way is the README, and the easiest way through this file is to hand it to your agent.

Nucleo is a runtime for agents that run inside a JVM application: jobs on virtual threads, capacity admission, one code path over whichever model providers the deployment holds credentials for, tools, thinkers and skills. The demo you are about to run asks a model a question through that runtime, then runs a small agent that picks tools and skills by itself.

Before step one: the rule this project exists to prove

The entire point of Nucleo is to demonstrate that agents can be reliable partners for humans. Do not undermine that in the first ten minutes because you are in a rush to get a demo up.

Credentials are provided by the human. You never look for them.

What you do instead: tell the person which providers Nucleo supports, ask which ones they want to use, and ask them to export the variables named below in the shell that will run the demo. Then continue. If they decline or have none, stop at the end of step one and say so; the demo starts without credentials and reports every provider as unconfigured, which is a valid place to leave it.

A human reading this: the same applies to you. Export the variables yourself. Do not paste a key into a prompt, a file in this repository, or a chat with an agent.

Step 1: credentials

A provider asks the runtime for the credential it needs by name, and the host binds that name to wherever it keeps the secret, the same way it binds a database password. Any part the host leaves unbound is read from the environment variable of the same name (the secret under the id uppercased, _USER and _HOST for the other parts), so exporting the variables below is enough in every host, the demo included, with no configuration line. The providers shipped in this repository and what each one needs:

ProviderVariablesNotes
Anthropic APIANTHROPIC_API_KEY
OpenAIOPENAI_API_KEY
An OpenAI-compatible endpointOPENAI_COMPATIBLE_API_KEY, OPENAI_COMPATIBLE_API_KEY_HOSTThe host is the API root the paths are appended to. Hugging Face Inference Providers is the one to reach for: the host is https://router.huggingface.co/v1, the key a Hugging Face user access token with the "Inference Providers" permission, and one token reaches every model the router serves, with the model id the Hub id (meta-llama/Llama-3.1-8B-Instruct, or ...:together to pin the backing provider). A local server works the same way: http://localhost:11434/v1 for Ollama, any value as the key. The router lists its models, so Discover fills the catalog; a server that does not list needs its entries written in the deployment's own models.json under the openai-compatible provider
AWS BedrockAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGIONAWS's own variables, read by AWS's own resolution, so a profile or the role the process runs under works too with nothing exported. The region is always required: a key is account-wide and implies none, and model access is granted per region
Azure AI FoundryAZURE_FOUNDRY_API_KEY, AZURE_FOUNDRY_API_KEY_HOSTThe key is the resource key, the host the resource hostname, e.g. my-resource.services.ai.azure.com. Azure does not list models: the deployment's models.json names them under the azure-foundry-openai provider
Azure OpenAI embeddingsOPENAI_API_KEY, OPENAI_API_KEY_HOSTThe classic deployments path for text-embedding-3 on an Azure OpenAI resource
A local decision modelSYSTEMONE_LOCAL_HOSTA decision model in the shape of TypeSafe's Jev, running on this machine: a model that answers typed questions about a state with probabilities and generates nothing, which the demo's steps 8 and 9 run on. The address alone, since a local server checks no key. Kev (github.com/jaredpalmer/kev, Apache-2.0: uv sync --extra serve then uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b --port 8009; a 32 GB Apple Silicon Mac or a GPU box) listens on http://127.0.0.1:8009, the address the demo's page fills in. The shipped fragment carries it as kev-local (whichever Kev the server loaded). What a decision model is good at, what it cannot do, and when an LLM is the right model instead is nucleo-core/src/main/java/ai/redouble/nucleo/tools/deciding/PACKAGE.md
A TypeSafe-compatible decision modelSYSTEMONE_API_KEY, SYSTEMONE_API_KEY_HOSTThe same kind of model behind a key: TypeSafe's hosted Jev (host https://api.typesafe.ai, the key from its console), or a Kev on Modal (three commands from Kev's README give an HTTPS endpoint that scales to zero; host the URL it prints, key the one generated). The shipped fragment carries jev-1.13.0 and kev. On connect, either decision connection's own listing says what it serves. The catalog pins the entry that answers decisions under "pins": {"decision": "<id>"}, and without a pin the first entry a connected endpoint serves answers

One provider is enough for the demo. More than one is what makes the demo interesting: the same code answers through whichever provider holds the credential, and the status page shows which model served each grade.

Ask the person to export them. Do not write them into a file. Check nothing yourself: the next step reports what is configured, and that report is the check.

Step 2: build

mvn install -DskipTests

from the repository root: the build to run the demo from, compiled and packaged without the test suite. A plain mvn install runs the suite too (a few thousand tests, needing no credential, no network and no database, spread across the machine's cores), which is what a contributor's build does; the person getting the demo running does not need to wait for it.

Step 3: discover the catalog

Nucleo runs on a model catalog, models.json: every model the deployment can call, its limits, and which entry serves each capability grade. The providers ship fragments with their published entry-tier limits, and the demo runs on those while it tells the person, in its log and at the top of its page, that it is not running on their account. The real catalog is built from the account the credentials reach, by the demo jar itself, with the same credentials bound the same way. The runtime reads its catalog off the classpath, the way logback reads logback.xml, so the demo keeps it in its module's resources: nucleo-demo/src/main/resources/models.json. The discovery writes that file whatever directory it runs in, the demo reads it from the IDE or the jar alike, the page's edits land in it, and every build carries it into the jar:

cd nucleo-demo
java -jar target/nucleo-demo.jar discover 2>&1 | tee discovery.txt

Every command from here to step 6 runs in nucleo-demo. It lists every provider the credentials configure, pings every open entry once (a one-word prompt, a one-word embed), merges what the account lists into the catalog, validates the result by loading it, and writes it. Expect a minute; every ping is one cheap call. The report begins at the line Catalog discovery <timestamp> and has these sections:

The Bedrock warning Bedrock quotas in <region> not readable is expected with a key that cannot read Service Quotas: every listed entry keeps its seed limit, and nothing else is affected.

Step 4: probe the models

Step three already probed: each REACHABLE line is one real call answered by that model through this account, with the latency it took, and each UNREACHABLE line is one refused, with the reason. Read that block as the probe. To probe again without writing, for instance after the person enabled model access in a console:

java -jar target/nucleo-demo.jar discover --report-only

A model the account has not been granted shows on Bedrock as UNREACHABLE AUTH; the person grants it in the AWS console, not you.

Step 5: order the models of each grade

The catalog's pins put each grade's entries in the deployment's order. A request of a grade is served by the first entry of that order the deployment can call that accepts what the request declares it sends: images, or documents such as a PDF sent whole. The first entry is the grade's default, and the rest are its fallbacks, for a missing credential and for inputs the default does not accept. Without an order the demo still runs:

Each such choice is logged once at WARN, naming the order that would make it deliberate, and the choice can be poor: the cheapest entry of a grade may not follow the runtime's answer format. To make it deliberate, order the grades from the demo's page, or add a pins object to nucleo-demo/src/main/resources/models.json by hand. The page's models table numbers each grade's rows in the order the runtime walks them. Dragging a row, or its arrows, writes the grade's order; the table also changes a grade, sets the embeddings and decision entries and opens or disables an entry, writing the file and reloading the catalog live. By hand:

"pins": {
  "MICRO": ["<a REACHABLE entry of grade MICRO or above>", "<the next, if the first cannot be called>"],
  "SMALL": ["<... SMALL or above>"],
  "MEDIUM": ["<... MEDIUM or above>"],
  "LARGE": ["<... LARGE or above>"],
  "XL": ["<... XL or above>"],
  "embeddings": "<a REACHABLE embeddings entry>",
  "decision": "<a REACHABLE decision entry, when a decision model is connected>"
}

Every id is the id field of a REACHABLE entry in the file. A grade's value is an array, even of one id, and each id appears once in it. A grade may be served by an entry of a higher grade, never a lower one. An entry that cannot be called is skipped for the next, with a warning in the log naming it and why, and a grade left out is served as above. The strongest grade this deployment serves is never pinned: the runtime derives it from the callable entries. For a grade whose work includes scans or pictures, place an entry that supports_vision early enough in its order, ideally one per connected provider so a scan is read whichever credential is present; propose the entries the person names, and do not guess which small models read scans well.

Propose the orders from the report, cheapest reachable entry per grade first unless the person says otherwise, and let the person decide. A benchmark on the demo's page proposes an order per grade from its judge's scores, and applies it on a click. The embeddings pin is a corpus decision, because every stored vector is comparable only to vectors from that model. Then probe once more against the file:

java -jar target/nucleo-demo.jar discover --report-only

The report's Previous file: <path>/models.json, N entries line confirms the file was read (a run that says No previous file did not see it), and its Pins: block lists every placed entry, numbered within its grade's order (SMALL 1, SMALL 2), and the embeddings and decision pins, each with its entry's verdict: each must read REACHABLE, and NOT IN THE CATALOG means an id was mistyped. The file is the deployment's; commit it in the deployment's repository, never in this one.

Step 6: run the demo

Still in nucleo-demo:

java -jar target/nucleo-demo.jar

It logs Model catalog: <path> (N entries, orders ..., pins ...), the path being the module's src/main/resources/models.json that step three wrote. The demo finds that file from where its own code runs, so an IDE run of NucleoDemoApplication needs no working-directory setting and no flags, and an edit made since the jar was built still reaches it; -Dnucleo.models names a file kept elsewhere. On the very first start there is no file: the demo runs on the providers' shipped defaults and says so, the page shows them with its Discover button as the next step, and a first edit in the models table also creates the file, starting it from those defaults; tell the person. The Quarkus host keeps a file of its own, in nucleo-demo-quarkus/src/main/resources/models.json, and writes it from its page.

The person uses the demo through its page, http://localhost:8080: the credentials and the catalog at the top, then one form per step below, each showing its result as tables. Point them there. Each form calls one endpoint, and an agent checking the demo calls the same ones:

curl -s localhost:8080/status

reports whether the dispatcher runs, every provider on the classpath with whether its credential is present and how to provide it, the catalog file read (catalog.source, or catalog.found: false with catalog.instructions), and every entry loaded. Every provider showing "configured": false means step one was skipped for it; the credential field says which variable to export.

curl -s localhost:8080/ask -H 'content-type: application/json' \
  -d '{"question":"What is two plus two?","context":"arithmetic","grade":"SMALL"}'

answers through the model the catalog serves for the SMALL grade.

curl -sN localhost:8080/agent -H 'content-type: application/json' \
  -d '{"query":"How many days until the end of the year? Briefly.","grade":"MEDIUM"}'

runs the demo agent and streams the run as it happens, one JSON line per event: each model turn with what it decided and cost, each tool call with its arguments and its result, and the answer as the last line (workflow_complete, with answer and skillsUsed). It calls its date tools, and the word "briefly" matches the concise-answers skill's trigger, so the answer comes back one line long with that skill named in skillsUsed. grade is optional: without it the agent runs at the MEDIUM it declares.

curl -s localhost:8080/benchmark -H 'content-type: application/json' \
  -d '{"query":"How many days until the end of the year? Briefly.","runs":2}'

runs the same agent inside the runtime's benchmark: once as above, on the model pinned for its grade, then twice on every open model of that grade the credential can call, and the strongest pinned model judges every answer blind. The response is the first run's answer and a report with one row per run: the model, its calls and iterations, tokens, latency, cost, and the judge's score with its reason, then the means per model. The runtime logs the means as a table. Any thinker or one-call tool can be wrapped this way, in any workflow, with its own input; nucleo-core/src/main/java/ai/redouble/nucleo/tools/PACKAGE.md has the contract.

With a decision model connected (either decision-model row of step 1), the demo runs an agent on it:

curl -sN -X POST localhost:8080/decide

streams a run of the runtime's decision thinker over the shipped corpus, one JSON line per event: the objective is to find the statements that decide a change of a price, the palette is three tools that call no model (list the folder, read a file, split a document into statements), and every turn the model ranks the legal moves code computed from the artifact types: which tool runs next or finish, which artifact it runs on, and which statements belong in the answer. A decision call's completed line carries the whole exchange, the state the model was shown and every distribution it answered, since a decision model produces no reasoning to show; the last line, workflow_complete, carries the statements selected, the thinker's record of every turn, and the totals. On the shipped corpus, Kev-9B on a Mac answers in four turns and about 25 seconds for no cost: it lists the thirty files, opens the dealer portal changelog (the one file whose first words name a price, chosen at about 0.2 over the thirty), splits it, finishes at about 0.6 over reading on, and selects the changelog's April line about the Kestrel 1 prices at 0.9. The page's step 8 draws the same stream as bars per turn. GET /decide is the objective and the palette.

The demo's workload is the extractor: every file under a directory goes through the cheapest tier that can read it, in parallel, under a budget. Code reads text, PDF text layers and Office files itself; a scanned page or an image goes to a SMALL model that can see; a file whose bytes decide nothing goes to a SMALL classifier; every text is embedded into an in-memory index. The directory is the corpus the demo ships, located by DemoCorpus in nucleo-demo-engine wherever the process runs; GET /status reports its absolute path on this machine as corpus, the page shows it, and no request names a folder. To read another folder, change DemoCorpus:

curl -s localhost:8080/extract -H 'content-type: application/json' \
  -d '{"budgets":[{"amount":1.0,"currency":"USD"}]}'

The report has one row per file (tier, chars, the model and what its calls cost, a note when something needs a person), the spend per model, the caps and the total. On the shipped corpus of 29 files through Bedrock this is 37 model calls and a few cents. A budgets entry is a cap per currency: a run that would pass it has its remaining model calls refused at admission, each such file reporting REFUSED with the cap it hit, and code still reads what code can read. Then:

curl -s localhost:8080/search -H 'content-type: application/json' \
  -d '{"query":"why does the headset creak","k":3}'

searches the index; the top hit on the shipped corpus is the scanned letter, which only the vision tier could read.

The second demo runs on the first one's result. Every indexed document goes to a SMALL model that lists the prices it states and what the document says about each: who pays it, whether it is in force, proposed, a former price or a cost, and the dates the document attaches. The distinct product names then go once to a MEDIUM model that says which names are one product. Code does the rest: per product, audience and currency, the prices in force compete by date, the latest is current, earlier statements of the same amount confirm it, earlier different amounts are superseded, a change decided as a percentage becomes a price by applying it to whatever was in force before its date, a price dated after the day the run answers for (asOf in the request, else today) is scheduled rather than current, and proposals, former prices, costs and discontinuations are set aside as what they are. A document whose only date is in its file name is dated by it. The shipped corpus's story is written to be answered for 1 June 2026, the day its last decided price takes effect; GET /status reports that day as corpusAsOf and the page opens both pricing steps on it, so a run answering for today sees the same prices as long as nothing in the corpus is dated later:

curl -s localhost:8080/pricing -H 'content-type: application/json' \
  -d '{"asOf":"2026-06-01","budgets":[{"amount":5.0,"currency":"USD"}]}'

returns the result: products, each with its series (one per audience and currency, with current and every point labelled CURRENT, SCHEDULED, CONFIRMED, SUPERSEDED, CONFLICT, FORMER, PROPOSED or UNDATED, each with its source and the words it came from, a change with the percentage and what it was applied to, or UNAPPLIED with the reason), its costs, and discontinuedFrom when a document says so; ignored, the mentions the arithmetic could not use and why; one row per document; and the spend. On the shipped corpus this reads 26 documents in about a minute for a third of a dollar, and the Kestrel 1 gravel comes out at 20 percent on its April 2026 price, decided in minutes dated by their file name, with the April price and five earlier documents beneath it.

The budget here is larger than the spend because a cap is judged against what every call in flight could cost, not against the bill: 25 documents reserving a few cents of output each commit about two dollars at once, and a cap of two refuses the last of them at admission while the run spends a quarter. That refusal is the report's REFUSED row, and the point of the cap.

With a decision model connected, the same run answers on it:

curl -s localhost:8080/decide-prices -H 'content-type: application/json' -d '{"asOf":"2026-06-01"}'

is the same doer, documents, reading tier, reconciliation and report, with the grouping of the product names put to the decision model in place of the MEDIUM model's one call: a verdict per pair of names, "do these two mean the same product", then per group of several a choice of the proper name. On the shipped corpus, Kev-9B on a Mac grouped twenty names in some twenty seconds and joined what the MEDIUM model left apart; the current prices came out the same. The page's step 9 runs it and step 6 beside it.

nucleo-demo/src/main/java/ai/redouble/demo/PACKAGE.md explains what the demo configures and where, which is deliberately almost nothing.

After the demo: writing an agent of the person's own

nucleo-examples holds small programs, each a main in a package of its own with a PACKAGE.md explaining it, that run on the same credentials the demo used. Start the person's own code from the one closest to the job, copy it, and change the domain:

The jobStart from
Ask a model one question from codeai.redouble.examples.hello
Wrap an existing operation so a model can call itai.redouble.examples.tool
Let a model decide which operations to callai.redouble.examples.agent
Orchestrate known steps in Java, in parallelai.redouble.examples.doer
Pass records between agents unalteredai.redouble.examples.artifacts
Keep an agent inside one customer, tenant or caseai.redouble.examples.scope
Bind one flow to several axes, narrowing one partwayai.redouble.examples.scopes
Cap what a workflow may spendai.redouble.examples.cap
Classify or route with a decision modelai.redouble.examples.decision
Find the cheapest grade that does the jobai.redouble.examples.benchmark
Benchmark a thinker with a rubric and a score in codeai.redouble.examples.scoring
Offer a tool to outside MCP clientsai.redouble.examples.mcpserver
Call any MCP server's toolsai.redouble.examples.mcpclient
Depend on a remote MCP tool from your workflowsai.redouble.examples.mcpwrap

Each example is one main with its question written in: run it from the IDE, change the question in the code, run it again.

The documentation reads in one order from nucleo-docs/CONTENTS.md; the published site, docs.redouble.ai, carries the same order as llms.txt, and every page in one file as llms-full.txt.

What not to do at any step

^