Architecture¶
Rhylthyme is a modular system with clear separation of concerns across five packages.
System Overview¶
┌─────────────────────────────────────────────────────────────────────┐
│ Rhylthyme Ecosystem │
├─────────────┬─────────────┬──────────────┬────────────┬────────────┤
│ rhylthyme- │ rhylthyme- │ rhylthyme- │ rhylthyme- │ rhylthyme- │
│ spec │ cli-runner │ web │ importers │ examples │
│ │ │ │ │ │
│ Schemas │ CLI │ Flask App │ TheMealDB │ Programs │
│ Validation │ Execution │ DAG Viz │ protocols │ Environ- │
│ Types │ Interactive │ MCP Server │ .io │ ments │
│ │ UI │ AI Chat │ │ │
└─────────────┴─────────────┴──────────────┴────────────┴────────────┘
Packages¶
rhylthyme-spec¶
Schema definitions and validation logic.
schemas/- JSON schema files for programs and environmentsvalidation.py- Schema and semantic validationtypes.py- Python type definitions
rhylthyme-cli-runner¶
Command-line interface and interactive program execution.
cli.py- Click-based CLI with commands: validate, run, plan, environmentsprogram_runner.py- Real-time program execution engine with terminal UIprogram_planner.py- Schedule optimizationvalidate_program.py- Program validationenvironment_loader.py- Environment catalog loadingenvironment_schemas.py- Environment type definitions
Web App¶
The web app at www.rhylthyme.com provides browser-based visualization, AI chat, and program execution. It is a Flask application with D3.js visualization and an MCP server for AI tool integration.
The visualizer page itself is generated by web/web_visualizer.py, whose generate_dag_html() assembles one self-contained HTML document. Two browser modules are kept as separate files and inlined verbatim into that document rather than linked, so a generated page still works when saved to disk or served from another origin:
| File | Contents |
|---|---|
static/css/tailwind.min.css | Pre-built Tailwind stylesheet |
static/js/auto-plan.js | The four auto-plan strategies, the in-flight (maxInFlight) enforcement, and the post-optimisation conflict sweep |
auto-plan.js is a UMD module: in the page it defines window.RhylthymeAutoPlan, and under Node it exports the same functions with module.exports, which is how rhylthyme-timeline/test/auto-plan.test.js exercises them headlessly. The page keeps thin wrappers (applyAutoPlanStrategy, detectPostOptimizationConflicts, calculateOptimizedResourceUsage) that pass the page globals -- targetDurationSeconds, resourceConstraints and originalProgram, the replicate-expanded program -- as options. Because it must survive inlining it has no imports; the in-flight helpers (inFlightGroups, inFlightWindows, inFlightConflicts) are therefore duplicated from rhylthyme-server/mcp-api/schedule.js, which stays the source of truth for them.
tools/export_player_template.py copies auto-plan.js next to the exported player template so rhylthyme-timeline/player/build.js can fill the same _AUTO_PLAN_JS slot and produce byte-identical HTML; tools/check_mirrors.sh asserts the two copies stay identical. After editing auto-plan.js or the template, re-run export_player_template.py and then player_parity_fixture.py.
rhylthyme-importers¶
Plugins for importing programs from external sources.
base.py- BaseImporter class and ImporterRegistrythemealdb.py- Import recipes from TheMealDB APIprotocolsio.py- Import lab protocols from protocols.io API
rhylthyme-examples¶
Working examples of programs and environment catalogs.
programs/- Example program JSON filesenvironments/- Environment catalog JSON files
Data Flow¶
CLI Execution¶
User Input → CLI Parsing → Program Loading → Validation → Execution → Interactive UI
↑
Environment Loading (optional)
Web Flow¶
Import Flow¶
The LLM import path¶
Two importers read meaning rather than structure, and both live in rhylthyme-server rather than in rhylthyme-importers, because they need a model client, an API key and a rate limiter that BaseImporter has no business knowing about (rhylthyme-importers must keep working offline):
rhylthyme_server/
_universal/ universal-recipe: URL → fetch → extract → build
page_fetcher.py URL → cleaned page snapshot
extractor.py snapshot → recipe, or "not a recipe"
program_builder.py recipe → program JSON (pure)
rate_limit.py per-user rolling-24h cap, per kind
_prompting/ the four-turn structure, server-side
templates.py byte copy of mcp-api/prompts.js (T1-T4 + slots)
turns.py reply checkers for T1-T3, and the retry turn
enrich.py T4 alone, over a structural import's step list
import_text.py all four turns, over pasted text
llm_text_importer.py llm-text: BaseImporter wrapper for import_text
Where llm-text sits. Structural importers (themealdb, protocolsio, cooklang, opentrons, recipe-scrapers) claim URLs through can_import, and universal-recipe is registered last as the fallback for any other URL. llm-text is outside that ordering entirely: its can_import always returns False, so find_for_url never lands on it, and it is reachable only by name (POST /api/import {source: "llm-text", text}). It takes text, not a URL, and has nothing to fetch.
The turns. templates.py is a byte-for-byte copy of the templates mcp-api/prompts.js sends MCP hosts, checked by a parity test that runs node against the JS, so the structure that runs server-side is the structure that ships (rhylthyme-server must not import rhylthyme-cli-runner, where the evaluation harness keeps its own copy). import_text runs T1 (read-back) and T2 (model check) as a conversation, scoring each 2/1/0 with one retry; T3 (extraction with source spans) once, or once per chunk; then T4 (tracks, triggers, question refinement) as a forced submit_program tool call with a bounded validator fix loop. enrich reuses the same T4 half over a structural import's step list.
Chunking. T1's read-back is the check on whether the whole source arrived. A source over ~6k estimated tokens is packed into paragraph chunks; a source whose read-back never mentioned its last paragraph is split one chunk per paragraph. Either way T3 runs per chunk and the step lists are merged — deduplicated by source span, ids made unique, chunk order preserved — before a single T4.
Limiter kinds. All of these share the universal_import_log audit table and ImportRateLimit, separated by kind so they do not cross-account: import (20/day), shopping_merge (40/day), enrich (20/day), import_text (20/day). import_text also carries a per-call output-token ceiling, since one call is four or more model calls.
Execution history and duration prediction¶
History is a second document type, never part of the program: a run record per execution (see Runs Schema), stored beside the program. Reading it back is split into small pure modules, because two runtimes write runs and two languages read them.
Python — rhylthyme_cli_runner.history: hash (the canonical programVersion), recorder (builds records from runner events), store (the on-disk layout and schema validation), usable (the shared usable-run filter), report (the inferentiality verdicts), calibrate (duration proposals), synth (synthetic corpora with known generating laws) and predict (the duration lookup).
JavaScript — rhylthyme-server/mcp-api/history.js is the twin of usable.py + report.py + predict.py, and schedule.js threads it into analyze_schedule.
The lookup order in predict_durations / predictDurations follows Badosa et al. (2019) §3:
- identical — runs of the same
programVersion,environmentIdand declared factor answers; median and P10/P90 of their measurements. The caller's own runs are preferred over everyone's when there are at leastminIdenticalof them, because one kitchen is a more relevant distribution than two hundred. - model — otherwise every usable measurement of the same program, fitted by ordinary least squares on the numeric factors and one-hot enums whose
|Pearson r|clears a threshold; the median when too few measurements or no factor survives. - none — no measurements, or the inferentiality report says the executor decides this step's length.
Three copies of the same rule, and how they are kept honest. The usable-run filter and the prediction lookup exist once per language, and the Python lookup exists twice: rhylthyme-server must not import rhylthyme-cli-runner (the same rule that produced the copied prompt templates in _prompting/templates.py), but Phase 7 needs the lookup inside the runner. So rhylthyme_server/rhylthyme/predict.py is the source of truth and rhylthyme_cli_runner/history/predict.py is a byte-identical copy, checked by tools/check_mirrors.sh. The module is standalone — it imports nothing from either package and carries its own copy of the usable-run filter and the percentile definition, which tests/test_predict.py asserts against the real usable.py case by case. Python and JavaScript are pinned to each other by two shared fixtures, tests/fixtures/history/usable-cases.json and predict-cases.json, read by tests/test_usable.py / test_predict.py and by mcp-api/history.test.js. Prediction is tested on synthetic histories with known generating laws, so the expected output is exact rather than a snapshot.
Planning against predictions never touches the timing engine. The engine reads every duration out of the program, so withDurations (JS) / _with_durations (the Python MCP analyzer) produces a copy of the program whose durations are the predicted ones and the same resolver, critical-path walk and conflict sweep run over it. That is why useDurations: "predicted" changes the makespan, the itinerary, the critical path and the binding constraints together, with no second implementation of any of them.
Key Design Decisions¶
- Schema-First: JSON schemas define all data structures; validation is automatic
- Optional Environments: Programs run without environments; add them for resource validation
- Modular Packages: Each concern is a separate installable package
- Single-File Web App:
app.pycontains HTML, CSS, JS, and all endpoints for easy deployment - Plugin Architecture: Importers use a registry pattern for extensibility
- LLM Importers Live Server-Side:
universal-recipeandllm-textneed a model client, an API key and a rate limiter, so they stay in rhylthyme-server;BaseImporterremains offline-capable