Debug Output Inspection

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 Path

josh_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
from joshpy.cli import JoshCLI, RunConfig
from joshpy.jar import JarMode

cli = JoshCLI(josh_jar=JarMode.DEV)

result = cli.run(RunConfig(
    script=Path("../../examples/quickstart_debug.josh"),
    simulation="Main",
    replicates=1,
))

if not result.success:
    raise RuntimeError(f"Simulation failed:\n{result.stderr}")
print(f"Exit code: {result.exit_code}")
Exit code: 0

Step 2: Python API

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_script

store = 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_id
store.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 died
store.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 reproduced
store.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.

Summary

python -m joshpy.debug /tmp/quickstart_organism_debug.txt --summary

Trace an entity

# Full entity ID
python -m joshpy.debug /tmp/quickstart_organism_debug.txt --trace a1b2c3d4

# Prefix match (like git short SHAs)
python -m joshpy.debug /tmp/quickstart_organism_debug.txt --trace a1b2

Find events

The --find flag searches all entity traces for a keyword and displays matching entities with context:

# Find organisms that died, show 3 messages before the death
python -m joshpy.debug /tmp/quickstart_organism_debug.txt \
    --find "survives? false" --before 3 --after 0

# Find organisms that reproduced, with surrounding context
python -m joshpy.debug /tmp/quickstart_organism_debug.txt \
    --find "reproduces? true" --before 2 --after 2

# Combine with step filters
python -m joshpy.debug /tmp/quickstart_organism_debug.txt \
    --find "survives? false" --step-range 7 10

# Count mode: how many entities died?
python -m joshpy.debug /tmp/quickstart_organism_debug.txt \
    --find "survives? false" --count

Multiple files

Pass multiple debug files — they are merged into a single store. Use --entity-type to focus on one type:

python -m joshpy.debug /tmp/quickstart_patch_debug.txt \
    /tmp/quickstart_organism_debug.txt \
    --find "survives? false" --entity-type organism

Point at a .josh script directly

# Auto-discovers debug file paths from the simulation config
python -m joshpy.debug examples/quickstart_debug.josh --summary
python -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 label
python -m joshpy.debug --registry experiment.duckdb --label baseline --summary

# By run hash
python -m joshpy.debug --registry experiment.duckdb --run-hash abc123def456 --find "survives? false"

# Target a specific execution when the same run_hash was executed multiple times
python -m joshpy.debug --registry experiment.duckdb --label baseline --run-id <run_id> --trace a1b2c3d4

Message count per step

python -m joshpy.debug /tmp/quickstart_organism_debug.txt --count

Color control

Output is automatically colored when writing to a terminal and plain when piped. You can also control it explicitly:

# Force no color
python -m joshpy.debug /tmp/quickstart_organism_debug.txt --no-color --limit 5

# Respects NO_COLOR environment variable (https://no-color.org/)
NO_COLOR=1 python -m joshpy.debug /tmp/quickstart_organism_debug.txt --limit 5

Template Variables in Debug Paths

When debug file paths contain template variables like {run_hash} (common in sweep workflows), pass the value to resolve them:

# Python API
store = load_debug_from_script(
    "simulation.josh",
    run_hash="abc123def456",
)

# CLI
# python -m joshpy.debug simulation.josh --run-hash abc123def456 --summary

What’s Next?