feat(observation): expose spell cast time in the snapshot

The per-spell Observation dict gains the additive cast_time key: the
registry spell's budget code on a hit, "standard" on a miss (mirroring
the engine's resolver fallback), so an LLM policy can schedule casts
against the action budget it sees reported back.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-11 14:13:13 +02:00
parent c5426ffe34
commit b3398c16ce
4 changed files with 9 additions and 2 deletions
+3 -1
View File
@@ -977,7 +977,9 @@ Règles modélisées :
spells (résumés enrichis du registre : name, level, school, range_ft,
save, dc, summary, half_on_save, save_negates (E4), area {shape,
radius_ft} | None (U7a : premier AreaEffect de niveau supérieur du sort,
gabarit de zone structuré, None si le sort n'a pas de zone)),
gabarit de zone structuré, None si le sort n'a pas de zone), cast_time
(code de temps d'incantation, « standard » pour un sort inconnu du
registre)),
spell_resistance (int), resistances {type: montant}, immunities
(tuple triée des types immunisés), sneak_dice (dés de précision disponibles :
(niveau + 1) // 2 avec « Sneak Attack », sinon 0), engaged_in_melee (bool —
+2
View File
@@ -4574,6 +4574,7 @@ class CombatEngine:
"half_on_save": False,
"save_negates": False,
"area": None,
"cast_time": "standard",
}
half_on_save = _spell_half_on_save(spell)
return {
@@ -4587,6 +4588,7 @@ class CombatEngine:
"half_on_save": half_on_save,
"save_negates": spell.save != "none" and not half_on_save,
"area": _spell_area_dict(spell),
"cast_time": spell.cast_time,
}
def _cast_spell(
+1
View File
@@ -532,6 +532,7 @@ def test_observation_enriched_spells_unknown_graceful() -> None:
"half_on_save": False,
"save_negates": False,
"area": None,
"cast_time": "standard",
},
)
+3 -1
View File
@@ -127,7 +127,7 @@ def test_area_snapshot_existing_keys_unchanged() -> None:
"""Given: a known Fireball snapshot dict
When: all keys and values are checked
Then: the nine pre-U7a keys are present with unchanged values, and the
exact key set is the old nine plus the additive "area"."""
exact key set is the old nine plus the additive "area" and "cast_time"."""
s = _spell_dict_for(_FIREBALL, "Fireball")
assert s["name"] == "Fireball"
assert s["level"] == 3
@@ -140,6 +140,7 @@ def test_area_snapshot_existing_keys_unchanged() -> None:
assert s["summary"] == "area 20ft"
assert s["half_on_save"] is True
assert s["save_negates"] is False
assert s["cast_time"] == "standard"
assert set(s.keys()) == {
"name",
"level",
@@ -151,6 +152,7 @@ def test_area_snapshot_existing_keys_unchanged() -> None:
"half_on_save",
"save_negates",
"area",
"cast_time",
}