Mova0.5 pre-release⌕
☰
home / docs / train / Story worlds: bAbI, StepGame, SpartQA

Story worlds: bAbI, StepGame, SpartQA

Gist

The world crate (world) reads short stories into a temporary story world (who is where, who holds what, which thing relates to which) and answers questions from that world. It does not train on the questions, and its code has no trigger words. When it cannot answer, it prints ? and the reason. A gap like that tells you what knowledge to add next.

Three readers are covered here:

CommandDatasetReaderKnowledge it takes from the global level
world babibAbI tasks 1-20world/src/babi.rs: a UD tree from en, then verb class / relation lookupsglobal::verb_class (verbs blocks) and `global::relation` (relation blocks)
world stepgameStepGame k=1..10world/src/stepgame.rs: an averaged perceptron with integer weights reads each sentence into a vector, and the world adds vectors along a pathnone. The nine direction vectors are a constant in the code (LABELS)
world spartqaSpartQA-humanworld/src/spartqa.rs: object mentions plus relation words between mentions, with three-valued inferenceglobal::concept_words (```category blocks in scene.md)

Global knowledge lives in the seed files global/seeds/**/*.md. At compile time, global/build.rs turns their fenced blocks into Rust tables (RELATIONS, verb_class(), CONCEPTS, ...). The seed file is the source of the knowledge, and the compiled tables are what the readers query.

When to use

Setup

The commands below use these placeholder paths: data/babi/en, data/stepgame, data/SpartQA_Human/human_test.json.

Commands (verified against world/src/main.rs)

All arguments are positional. The code has a home-directory fallback when the folder argument is missing, so always pass the folder explicitly.

W="cargo run --release --manifest-path world/Cargo.toml --"

$W babi <en-dir> [test|train] [N]   # all files *_<split>.txt in the folder; N = errors shown per task (default 0)
$W babi-probe "<sentence>" ... "<question?>"   # any argument containing '?' is a question; the others are read into the story
$W babi-learn <qa20_..._train.txt>  # learn "state -> go=place" from training data; prints relation lines
$W babi-contrast <src-dir> <dst-dir>  # writes contrast copies: new names, places, things, synonym verbs
$W stepgame <dir> [epochs] [N]      # trains on qa1_train.json (default 10 epochs), tests qa1..qa10_test.json
$W spartqa <file.json> [N]          # accuracy per question type (YN / FB / CO / FR)

Environment switches (all read in the source):

VariableEffect
BABI_DEBUG=1prints the UD tree of each sentence (form, lemma, UPOS, head, relation) to stderr
STEPGAME_ERR=1prints reader errors on level-1 test sentences, grouped by template (agents shown as A1/A2)
STEPGAME_EXCLUDED=<file.tsv>writes the list of questions excluded from the "clean" number, one per line, with the reason
SPARTQA_SCENE=1on each error that is shown, prints the story, the objects read, the facts, and the block facts
SPARTQA_DEBUG=<substring>dumps the scene of every story that contains the substring
GLOBAL_EXCLUDE=a,bbuild time: seeds whose path contains any of these substrings are left out of the build (ablation)

The key output words are total, solved (≥95%), without doubtful, and gap (missing knowledge).

Seed blocks the readers use

Verb classes (```verbs, in Possession and changes in quantity). Each line has three parts: <class> <effect> <lemma>.... The effect is one of =, +, -, ±, which mean no change, more, less, and transfer of quantity. Multi-word verbs are written with an underscore (pick_up), and the reader forms them from a verb plus its particle (compound:prt).

get + get grab pick_up get_on buy receive find collect ...
give ± give pass hand_over sell lend pay send ...
lose - lose eat spend use break drink throw ... drop discard abandon put_down ...
move = move hurry choose come start gnaw wake run walk drive travel journey ...

The bAbI reader acts on these classes:

Bare be with in or at is also read as a location. Any other verb gives verb "<lemma>" has no class at the global level.

Relations (```relation, one per line as name: key=value ...). From Relations of space, size and time:

north_of: inverse=south_of vec=0,1 step=n
bigger_than: inverse=smaller_than transitive=yes
fit_inside: same=smaller_than
morning: order=1

From State → where they go:

hungry: go=kitchen
tired: go=bedroom

Keys that babi.rs actually reads:

KeyMeaning in the reader
inverseThe opposite relation. A word plus its preposition (north+of, bigger+than, fit+inside) is recognised as a relation only if the relation has inverse or same
sameAn alias, rewritten to the canonical relation (fit_inside → smaller_than)
vecA 2-D direction, used for left/right/above/below questions
stepA path step (n, s, e, w). Path finding needs step on both the relation and its inverse
transitiveyes: chain facts ("bigger than" through intermediate things)
orderA time word and its rank within one story (yesterday=0 ... evening=3)
goMotivation: the state → where the person will go. It also answers "why did X go ..."

symmetric=yes (in scene.md) is documentation only: no reader looks it up at the time of writing. The SpartQA relation words (left, above, near, touching, ...) are a match in spartqa.rs. SpartQA does read the scene_color, scene_size, scene_shape and scene_any categories from scene.md.

Build gate. global/build.rs panics, so the build fails, and the message names the seed file, if any of these hold:

When a word appears in two verb classes, the first one in build order wins, and nothing warns you. Files are walked in sorted path order.

Steps: add a verb or a relation

  1. Find the gap.
    $W babi-probe "Mary sprinted to the attic." "Where is Mary?"
    
    Output:
    Mary sprinted to the attic. → Some("verb «sprint» has no class at the global level")
    ? Where is Mary? → ("?", "where mary is — unknown")
    
    Use BABI_DEBUG=1 to make sure the parse is sane (here sprinted is lemmatised to sprint and is the VERB root). If the tree is wrong, the fix belongs in en, not in a seed.
  2. Edit the seed. Append the lemma to the right class line in Possession and changes in quantity, for example move = move hurry ... sprint. For a new relation, add a line to a ```relation block. Give it inverse= (or same=) so the reader recognises it. Add step= on both directions if path questions need it.
  3. Rebuild and probe again. build.rs re-runs when anything under seeds/ changes, so the next cargo run recompiles global and world. Rerun the same probe. The sentence should now read as None (understood) and the question should be answered.
  4. Check for regressions. Run cargo test --release --manifest-path global/Cargo.toml. It includes verb_class assertions, such as eat → lose. Then run the full evals below, on both the original files and the contrast files. Verb classes are also used by math, which reads quantity word problems, so run its checks too.
  5. Write down why in the seed's prose (gist, conditions, examples, sources). The block is the data, and the prose is the explanation.

Steps: learn knowledge from training data into a seed

world babi-learn reads a bAbI task-20 training file. For every Where will X go? question, it takes X's last stated attribute ("Jason is thirsty") and counts each attribute → gold-place pair. Then it prints the majority place for each attribute, with its support. It never opens the test file.

$W babi-learn data/babi/en/qa20_agents-motivations_train.txt

Output on the 1k training split:

bored: go=garden    # 98/98
hungry: go=kitchen    # 89/89
thirsty: go=kitchen    # 97/97
tired: go=bedroom    # 91/91

To turn it into a seed:

  1. Decide. A person (or a gate) decides what to keep. Keep only rows with clean support (here 375/375). A split count is a signal to look at the data, not a rule to freeze.

  2. Strip the # n/all comments. Inside a block, # is a field without =, so the build fails with relation "...": "#" without "=". Move the counts into the prose.

  3. Check the names are new. For example, hungry already exists in motivations.md. A second definition makes the build fail with relation "hungry" is repeated.

  4. Write the seed at global/seeds/shortcuts/<topic>.md:

    # State → where people go
    
    **Gist.** A person's state suggests a goal: hungry or thirsty → kitchen, tired → bedroom, bored → garden.
    
    **Conditions and exceptions.** A house with typical rooms; another world (forest, city) has other goals.
    Learned with `world babi-learn` from the bAbI task-20 training split: 375 of 375 questions, no exceptions;
    the test split was not opened.
    
    **Examples.** "Jason is thirsty. Where will Jason go?" → kitchen.
    
    **Sources.** Weston et al. 2015, bAbI task 20 (CC BY 3.0), training split.
    
    ```relation
    hungry: go=kitchen
    thirsty: go=kitchen
    tired: go=bedroom
    bored: go=garden
    ```
    
  5. Confirm the seed is what answers. Rebuild, then probe "Jason is thirsty." "Where will Jason go?". The answer should be ("kitchen", "thirsty → kitchen (first level)"). Then do an ablation: build with GLOBAL_EXCLUDE=motivations and qa20 should drop. This shows the score comes from the seed. The run only needs the env var; do not delete the seed file.

babi-learn handles only the task-20 question shape. To learn another table, copy its pattern from learn_motivations in babi.rs: read the story, count (state, gold) pairs on training data only, print a block line with its support, and let a person decide what to keep.

Evaluate

# bAbI: test split; also run train (should be close: nothing was fitted to test)
$W babi data/babi/en test 0
$W babi data/babi/en train 0
# Contrast set: same stories with new names, places, things and synonym verbs (some deliberately not in the seeds)
$W babi-contrast data/babi/en data/babi-contrast
$W babi data/babi-contrast test 0

# StepGame: full test, plus the list of excluded items
STEPGAME_EXCLUDED=stepgame-excluded.tsv $W stepgame data/stepgame 10 0

# SpartQA-human test
$W spartqa data/SpartQA_Human/human_test.json 5

Output formats:

Results for release 0.4 (2026-10-16; the full table is in the README). These runs reproduce them:

Runtime is seconds: about 14 s for the bAbI test, about 8 s for StepGame, and under 1 s for SpartQA.

Pitfalls

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/train/story-worlds/