Trace agent behavior with structured debug log analysis
Introduction
Josh simulations can emit debug messages that log per-agent decisions at each timestep — survival checks, reproduction events, state transitions, and more. These logs are invaluable for understanding why a simulation behaves the way it does.
The joshpy.debug module provides a CLI tool (python -m joshpy.debug) for quick terminal-based investigation with colored output, and a Python API for use in notebooks and scripts.
This tutorial walks through both using a quickstart simulation that models trees growing, maturing at age 7, and then facing yearly survival checks and reproduction coin flips.
Step 1: Run a Simulation with Debug Output
A Josh script needs debugFiles configured to emit debug output. The key additions compared to a standard simulation are debugFiles.patch and debugFiles.organism in the simulation block, plus debug() calls in the entity definitions:
from pathlib import Pathjosh_source = Path("../../examples/quickstart_debug.josh").read_text()print(josh_source)
# Quickstart simulation with debug output enabled
# Extends quickstart.josh with lifecycle events (maturity, reproduction, death)
# and debugFiles configured for tracing agent behavior.
start simulation Main
grid.size = 1000 m
grid.low = 33.7 degrees latitude, -115.4 degrees longitude
grid.high = 34.0 degrees latitude, -116.4 degrees longitude
grid.patch = "Default"
steps.low = 0 count
steps.high = 25 count
exportFiles.patch = "file:///tmp/quickstart_debug_output.csv"
debugFiles.patch = "file:///tmp/quickstart_patch_debug.txt"
debugFiles.organism = "file:///tmp/quickstart_organism_debug.txt"
end simulation
start patch Default
ForeverTree.init = create 10 count of ForeverTree
# Filter dead trees and add offspring from reproducing trees
ForeverTree.step = {
const alive = prior.ForeverTree[prior.ForeverTree.alive]
const offspring = count(prior.ForeverTree[prior.ForeverTree.reproduces])
const newTrees = create offspring of ForeverTree
const d = debug("[patch]", count(alive), "trees alive,", offspring, "new offspring")
return alive | newTrees
}
export.treeCount.step = count(ForeverTree)
export.averageAge.step
:if(export.treeCount > 0 count) = mean(ForeverTree.age)
:else = 0 years
export.averageHeight.step
:if(export.treeCount > 0 count) = mean(ForeverTree.height)
:else = 0 meters
end patch
start organism ForeverTree
age.init = 0 year
age.step = prior.age + 1 year
height.init = 0 meters
height.step = prior.height + sample uniform from 0 meters to 1 meters
mature.step = current.age >= 7 years
# Survival check first — only mature trees face mortality
alive.init = true
alive.step = {
if (current.mature) {
const r = sample uniform from 0.0 to 1.0
const survives = r < 0.9
const d = debug("[survival] age", current.age, "— survives?", survives, "(roll", r, "vs threshold 0.9)")
return survives
} else {
const d = debug("[growing] age", current.age, "height", current.height, "— still young")
return true
}
}
# Reproduction gated on survival — only living mature trees can reproduce
reproduces.init = false
reproduces.step = {
if (current.mature and current.alive) {
const r = sample uniform from 0.0 to 1.0
const result = r < 0.2
const d = debug("[reproduction] reproduces?", result, "(roll", r, "vs threshold 0.2)")
return result
} else {
return false
}
}
end organism
start unit year
alias years
alias yr
alias yrs
end unit
The Python API is useful in notebooks for quick exploration. Load debug output, trace an entity, and find events — all in a few lines.
from joshpy.debug import load_debug_from_scriptstore = load_debug_from_script("../../examples/quickstart_debug.josh", simulation="Main", cli=cli,)print(f"Loaded {len(store):,} messages across {len(store.entity_ids()):,} entities\n")# Trace an entity — just call print_trace()entity_id = store.filter(entity_type="organism")[0].entity_idstore.print_trace(entity_id)
Loaded 1,977,136 messages across 223,708 entities
--- Step 0 ──────────────────────────────
[Step 0, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 1 height 0.5794 — still young
--- Step 1 ──────────────────────────────
[Step 1, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 2 height 1.0302 — still young
--- Step 2 ──────────────────────────────
[Step 2, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 3 height 1.7555 — still young
--- Step 3 ──────────────────────────────
[Step 3, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 4 height 2.1514 — still young
--- Step 4 ──────────────────────────────
[Step 4, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 5 height 2.9065 — still young
--- Step 5 ──────────────────────────────
[Step 5, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 6 height 3.6384 — still young
--- Step 6 ──────────────────────────────
[Step 6, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 7 — survives? true (roll 0.2218 vs threshold 0.9)
[Step 6, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.1036 vs threshold 0.2)
--- Step 7 ──────────────────────────────
[Step 7, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 8 — survives? true (roll 0.0461 vs threshold 0.9)
[Step 7, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? false (roll 0.391 vs threshold 0.2)
--- Step 8 ──────────────────────────────
[Step 8, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 9 — survives? true (roll 0.5498 vs threshold 0.9)
[Step 8, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.1341 vs threshold 0.2)
--- Step 9 ──────────────────────────────
[Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 10 — survives? true (roll 0.285 vs threshold 0.9)
[Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.0751 vs threshold 0.2)
--- Step 10 ──────────────────────────────
[Step 10, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 11 — survives? false (roll 0.9889 vs threshold 0.9)
Find events
find_events() searches all entity traces for a keyword and prints matching entities with context — just like the CLI’s --find flag:
# Trees that diedstore.find_events("survives? false", entity_type="organism", before=3, after=1);
Found 60944 entities matching "survives? false"
=== Entity 5a8aa422 at (72.5, 1.5) — event at step 9 ===
[Step 7, organism @ 5a8aa422 (72.5, 1.5)] [reproduction] reproduces? false (roll 0.8347 vs threshold 0.2)
[Step 8, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 9 — survives? true (roll 0.6791 vs threshold 0.9)
[Step 8, organism @ 5a8aa422 (72.5, 1.5)] [reproduction] reproduces? true (roll 0.0729 vs threshold 0.2)
> [Step 9, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 10 — survives? false (roll 0.9752 vs threshold 0.9) <- event
=== Entity 6e0dbb8a at (17.5, 12.5) — event at step 10 ===
[Step 8, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.1341 vs threshold 0.2)
[Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 10 — survives? true (roll 0.285 vs threshold 0.9)
[Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.0751 vs threshold 0.2)
> [Step 10, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 11 — survives? false (roll 0.9889 vs threshold 0.9) <- event
=== Entity 64553a18 at (42.5, 29.5) — event at step 16 ===
[Step 14, organism @ 64553a18 (42.5, 29.5)] [reproduction] reproduces? false (roll 0.7578 vs threshold 0.2)
[Step 15, organism @ 64553a18 (42.5, 29.5)] [survival] age 16 — survives? true (roll 0.3926 vs threshold 0.9)
[Step 15, organism @ 64553a18 (42.5, 29.5)] [reproduction] reproduces? false (roll 0.4405 vs threshold 0.2)
> [Step 16, organism @ 64553a18 (42.5, 29.5)] [survival] age 17 — survives? false (roll 0.9587 vs threshold 0.9) <- event
... 60941 more (use limit= to adjust)
# Trees that reproducedstore.find_events("reproduces? true", entity_type="organism", before=3, after=1);
Found 55390 entities matching "reproduces? true"
=== Entity 6e0dbb8a at (17.5, 12.5) — event at step 6, 8, 9 ===
[Step 4, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 5 height 2.9065 — still young
[Step 5, organism @ 6e0dbb8a (17.5, 12.5)] [growing] age 6 height 3.6384 — still young
[Step 6, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 7 — survives? true (roll 0.2218 vs threshold 0.9)
> [Step 6, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.1036 vs threshold 0.2) <- event
[Step 7, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 8 — survives? true (roll 0.0461 vs threshold 0.9)
[Step 7, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? false (roll 0.391 vs threshold 0.2)
[Step 8, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 9 — survives? true (roll 0.5498 vs threshold 0.9)
> [Step 8, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.1341 vs threshold 0.2) <- event
[Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 10 — survives? true (roll 0.285 vs threshold 0.9)
> [Step 9, organism @ 6e0dbb8a (17.5, 12.5)] [reproduction] reproduces? true (roll 0.0751 vs threshold 0.2) <- event
[Step 10, organism @ 6e0dbb8a (17.5, 12.5)] [survival] age 11 — survives? false (roll 0.9889 vs threshold 0.9)
=== Entity 5a8aa422 at (72.5, 1.5) — event at step 6, 8 ===
[Step 4, organism @ 5a8aa422 (72.5, 1.5)] [growing] age 5 height 3.1142 — still young
[Step 5, organism @ 5a8aa422 (72.5, 1.5)] [growing] age 6 height 3.2199 — still young
[Step 6, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 7 — survives? true (roll 0.4228 vs threshold 0.9)
> [Step 6, organism @ 5a8aa422 (72.5, 1.5)] [reproduction] reproduces? true (roll 0.0919 vs threshold 0.2) <- event
[Step 7, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 8 — survives? true (roll 0.0482 vs threshold 0.9)
[Step 7, organism @ 5a8aa422 (72.5, 1.5)] [reproduction] reproduces? false (roll 0.8347 vs threshold 0.2)
[Step 8, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 9 — survives? true (roll 0.6791 vs threshold 0.9)
> [Step 8, organism @ 5a8aa422 (72.5, 1.5)] [reproduction] reproduces? true (roll 0.0729 vs threshold 0.2) <- event
[Step 9, organism @ 5a8aa422 (72.5, 1.5)] [survival] age 10 — survives? false (roll 0.9752 vs threshold 0.9)
=== Entity 64553a18 at (42.5, 29.5) — event at step 9 ===
[Step 8, organism @ 64553a18 (42.5, 29.5)] [survival] age 9 — survives? true (roll 0.6789 vs threshold 0.9)
[Step 8, organism @ 64553a18 (42.5, 29.5)] [reproduction] reproduces? false (roll 0.479 vs threshold 0.2)
[Step 9, organism @ 64553a18 (42.5, 29.5)] [survival] age 10 — survives? true (roll 0.8224 vs threshold 0.9)
> [Step 9, organism @ 64553a18 (42.5, 29.5)] [reproduction] reproduces? true (roll 0.1492 vs threshold 0.2) <- event
[Step 10, organism @ 64553a18 (42.5, 29.5)] [survival] age 11 — survives? true (roll 0.894 vs threshold 0.9)
... 55387 more (use limit= to adjust)
Step 3: CLI Usage
For day-to-day investigation, the CLI is the primary tool. It provides colored output, filtering, and all the same capabilities without writing any Python.
# Auto-discovers debug file paths from the simulation configpython-m joshpy.debug examples/quickstart_debug.josh --summarypython-m joshpy.debug examples/quickstart_debug.josh --trace a1b2c3d4
Load debug output from a registry run
When runs are tracked with RunRegistry, joshpy records resolved debug output paths in run_outputs. You can then inspect by label or hash directly:
# By labelpython-m joshpy.debug --registry experiment.duckdb --label baseline --summary# By run hashpython-m joshpy.debug --registry experiment.duckdb --run-hash abc123def456 --find"survives? false"# Target a specific execution when the same run_hash was executed multiple timespython-m joshpy.debug --registry experiment.duckdb --label baseline --run-id<run_id> --trace a1b2c3d4