Redouble AI Nucleo documentation
Part 13 · Inside the runtime

Inside the demo

ai.redouble.demo · nucleo-demo

This page is for people working on the demo itself: how the Spring host is configured, every endpoint with its request, its response and the streams the page draws from, the rules of the catalog edits, the pricing arithmetic, the command-line discovery and the skills setup. What the demo shows, step by step, is in The demo and its endpoints.

The demo is one engine, nucleo-demo-engine - the agent, the workflows, the index, the corpus, the page, and the read and write logic behind every endpoint - served by two hosts that add only their framework's wiring and no logic of their own: this one, a Spring Boot process (nucleo-demo), and a Quarkus one (nucleo-demo-quarkus, which runs on the JVM or compiled to a native image). This page describes the Spring host and the endpoints both serve, computed once in the engine so the two can never drift; the Quarkus page describes only what its framework wires differently.

The Spring host is a Spring Boot process running the runtime in-process. It is the shape a host application copies: the dispatcher's lifecycle bound to the application context, credentials from the environment, one HTTP door that submits a job and returns its result.

Running it

mvn -pl nucleo-demo spring-boot:run

or the packaged jar:

java -jar nucleo-demo/target/nucleo-demo.jar

With no credential in the environment the process starts, the page (GET /status) reports every provider on the classpath as unconfigured, and POST /ask is refused with the runtime's message naming what to provide. Set one credential and the same question is answered:

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

The credential ids and their environment variable names are listed in nucleo-core/.../secrets/PACKAGE.md: ANTHROPIC_API_KEY, OPENAI_API_KEY, and for Bedrock AWS's own AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION (or a profile, or the role the process runs under: whatever the AWS default chains resolve).

What is configured where

Endpoints

Every job the demo submits - the agent and the calls it makes, the extract and pricing tools, the ask tool, the benchmark's judges - allows one transparent re-run on an upstream retry signal (DemoPolicy.UPSTREAM_RETRIES, against the runtime's default of three): a person waits behind each in a browser, a blip clears on the second attempt, and an endpoint still failing then shows as a failed row or section within a minute instead of holding a race or a page.

The pricing demo

Every document in the index goes to ExtractPricesTool (SMALL, one call per document, all in parallel): the model lists the prices the document states as PriceMentions, each with the product, the audience (who pays: retail, dealer, unspecified), the kind (LIST, TRANSACTION, PROPOSED, FORMER, COST, CHANGE, DISCONTINUATION), the amount and currency (a CHANGE carries a percentChange and no amount), the dates the document attaches (effectiveFrom, statedOn; a date in the file name is the document's when the text has none) and the words it came from. The distinct product names then go once to CanonicalizeProductsTool (MEDIUM), which says which names mean one product. PriceHistory does the rest with no model in it: a point's date is its effective date, else the statement's, else the document's; per product, audience and currency the LIST and TRANSACTION points compete by date as of the day the run answers for (asOf on the request, else today), the latest on or before that day is CURRENT, one dated after it is SCHEDULED, every other statement of the same amount is CONFIRMED, earlier different amounts are SUPERSEDED, two amounts on the same latest date are a CONFLICT for a person, a point with no date is UNDATED and never ranked; a CHANGE becomes a price first, its percentage applied to the latest price in force before its date (earlier changes first, so a change can build on a change), and competes from its date, or stays UNAPPLIED with the reason when nothing single precedes it; FORMER, PROPOSED and COST never compete; a DISCONTINUATION dates the product's end. PriceHistoryTest pins that arithmetic. ExtractPricingDoer runs the fan-out under the same caps as the extractor and reports one row per document with what it cost.

On the shipped corpus the history is placed on purpose (corpus-build/build-corpus.py says where): a 2025 list that calls itself replaced, the 2026 list effective 1 January in a spreadsheet, a deck and an invoice, a Kestrel 1 change decided in the February meeting for 1 April and repeated by the changelog, an SQL script and a dated portal screenshot, a 20 percent rise from 1 June decided in minutes whose only date is their file name, a 2027 figure floated and not decided, a planning price in the product spec, a sign with the price a tune-up used to be, supplier costs in a parts sheet, a phone note and a photographed note, a discontinued model, and a whiteboard whose figures carry no currency. Read from Bedrock the run finds about 75 mentions in 26 documents, and the Kestrel 1 gravel comes out at 20 percent on its April price, with the April price and five earlier statements beneath it.

POST /decide-prices is the same run with its grouping put to a decision model: the same ExtractPricingDoer, documents, reading tier, reconciliation and report, with the two model-facing tiers given to the doer as constructors and DecideProductGroupsTool in place of CanonicalizeProductsTool. Of the run's two model judgments only the grouping is a verdict over options code already holds, which is a decision model's shape: the first decision is one verdict per pair of names, "do these two mean the same product", and every pair the model puts at or above one half (DecideProductGroupsTool.THRESHOLD) joins their groups; the second, for each group of several names, is a choice of which name is the product's proper name, without a brand in front, a descriptor behind or a size. Every name in a group is one the documents used. The pairs are the cost: a dozen names are sixty-six questions in one decision. DecideProductGroupsTest pins it with a scripted model. On the shipped corpus (2026-09-25, Kev-9B on a Mac, the same index as the run beside it), twenty names were 190 pairs and the run took 39 seconds against step 6's 22, spending the reading tier's 1.6 cents against 2.8 with the MEDIUM call; the decision model joined what the MEDIUM model left apart ("Comet 2 city" with "Comet 2", "Halcyon Meridian 3" with "Meridian 3 trail", "Forks" with "Fork M3", "bearing swap" with "headset bearing replacement") and left apart one pair the MEDIUM model joined ("Kestrel 1" and "Kestrel 1 gravel"); the current prices came out the same.

Discover mode

java -jar target/nucleo-demo.jar discover, run in nucleo-demo, runs CatalogDiscovery instead of the web server, on the same credentials bound the same way, and exits with the discovery's exit code: the demo's main names its catalog file (DemoHome.configure) and then makes one call to the starter's NucleoSpringApplication.run, which provides the command. The starter's NucleoRuntime stays down in that mode: the discovery seals its own permitting envelope to ask what the account can call, and a started dispatcher would have sealed the refusing default first. The discovery writes the file -Dnucleo.models names, which main has set to this module's src/main/resources/models.json, whatever directory the command runs in: the same file the web demo reads and the page edits, gitignored because no account's catalog is a default. AGENTS.md at the repository root walks the setup.

Skills

Two bundles reach the process, through one loader, and GET /skills shows both:

DemoAgent declares two tools of its own (get_current_time, calculate_dates), a tool catalog it may ask for through request_tools (web_fetch), and a skill catalog of everything the registry holds. Each turn the runtime offers request_skill with that catalog as an enumeration in the tool's input schema, so the model admits a skill by name from what it was offered and nothing else; an admitted skill's instructions ride the system preamble of every later call in that conversation. GET /agent is that offer, as the model sees it.

With a credential set:

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

The lines arrive as the run goes: the request_skill tool's queued line carries {"skillNames":["concise-answers"]}, its completed line the skill admitted, and the last line the answer. The word "briefly" is the trigger the concise-answers skill describes, so a model that reads the catalog admits it and the answer comes back one line long, with skillsUsed naming it.

^