Store the refusals, so the chips and the model can see them
`world_delta_of` wrote `delta` and `applied` only. Every consumer that tells a refused change from a successful one reads the two lists it dropped: `Action.world_changes` marks a clamped chip from `clamped` and builds its refusal chips from `rejected`, and `worldstate.refusals` reads both. So no chip could report a limit, no rejection chip could appear, and no correction ever reached the next prompt. The three mechanisms merged last session were live in the code and unreachable in production. Found by playing the Pokemon demo. Turn 3's snapshot held two clamped entries with correct `fix` text, both chips came back `clamped: false`, and turn 4's prompt carried no correction, so the model repeated the same mistake. The 21 tests passed because `action()` built the column by hand with every list present. It now fills the column through `world_delta_of`. Removing the two new lines fails 9 of the 23 tests; that was checked by sabotage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
This commit is contained in:
co-authored by
Claude Opus 5
parent
abbfc61263
commit
1988979aeb
@@ -528,16 +528,27 @@ def world_delta_of(snapshot: dict | None) -> dict | None:
|
||||
"""Returns the bulk-read slice of a context snapshot, for `Action.world_delta`.
|
||||
|
||||
`context_snapshot` is deferred because it holds the whole assembled prompt.
|
||||
The two parts that every action needs, the world-change chips and the emit
|
||||
block replayed into history, get their own small column instead. Update this
|
||||
function wherever a snapshot is written.
|
||||
The parts that every action needs get their own small column instead: the
|
||||
world-change chips, the emit block replayed into history, and the refusal
|
||||
note fed back to the model. Update this function wherever a snapshot is
|
||||
written.
|
||||
|
||||
Carry all three report lists, not just `applied`. `Action.world_changes`
|
||||
marks a chip from `clamped` and builds its refusal chips from `rejected`,
|
||||
and `worldstate.refusals` reads both. Storing `applied` alone left every
|
||||
consumer unable to tell a refused change from one that worked, which is the
|
||||
distinction this column exists to carry. The two extra lists are subsets of
|
||||
one turn's block, so they cost a few hundred bytes per action at most.
|
||||
"""
|
||||
ws = (snapshot or {}).get("world_state")
|
||||
if not isinstance(ws, dict):
|
||||
return None
|
||||
report = ws.get("report") or {}
|
||||
return {
|
||||
"delta": ws.get("delta") or {},
|
||||
"applied": (ws.get("report") or {}).get("applied") or [],
|
||||
"applied": report.get("applied") or [],
|
||||
"clamped": report.get("clamped") or [],
|
||||
"rejected": report.get("rejected") or [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import pathlib
|
||||
|
||||
from app import models
|
||||
from app import worldstate as w
|
||||
from app.routers.adventures import world_delta_of
|
||||
|
||||
SEED = pathlib.Path(__file__).resolve().parents[1] / "app" / "seed_data"
|
||||
|
||||
@@ -37,9 +38,40 @@ SCHEMA = {
|
||||
|
||||
|
||||
def action(delta, index=1):
|
||||
"""Returns an unsaved Action carrying the report `apply_delta` produced."""
|
||||
"""Returns an unsaved Action carrying the report `apply_delta` produced.
|
||||
|
||||
The column is filled through `world_delta_of`, the same function the turn
|
||||
endpoint uses, rather than by assembling the dict here. An earlier version
|
||||
of this helper built the shape by hand with every report list present. That
|
||||
hid a live bug: `world_delta_of` stored `applied` alone, so no refusal ever
|
||||
reached a chip or the next prompt while all of these tests passed.
|
||||
"""
|
||||
ws, report = w.apply_delta(w.instantiate(SCHEMA), SCHEMA, delta, index)
|
||||
return models.Action(world_delta={"delta": delta, **report})
|
||||
snapshot = {"world_state": {"delta": delta, "report": report, "state": ws}}
|
||||
return models.Action(world_delta=world_delta_of(snapshot))
|
||||
|
||||
|
||||
def test_world_delta_of_keeps_every_report_list():
|
||||
"""The stored column must carry the refusals, not just the successes.
|
||||
|
||||
`Action.world_changes` marks a clamped chip from `clamped` and builds its
|
||||
refusal chips from `rejected`, and `worldstate.refusals` reads both. Dropping
|
||||
either list leaves every consumer unable to tell a refused change from one
|
||||
that worked.
|
||||
"""
|
||||
_, report = w.apply_delta(
|
||||
w.instantiate(SCHEMA), SCHEMA,
|
||||
{"player.arrows": 3, "player.nonesuch": 1, "player.hp": -5}, 1,
|
||||
)
|
||||
stored = world_delta_of({"world_state": {"delta": {}, "report": report}})
|
||||
assert [e["path"] for e in stored["applied"]] == ["player.arrows", "player.hp"]
|
||||
assert [e["path"] for e in stored["clamped"]] == ["player.arrows"]
|
||||
assert [e["path"] for e in stored["rejected"]] == ["player.nonesuch"]
|
||||
|
||||
|
||||
def test_world_delta_of_survives_a_snapshot_with_no_world_state():
|
||||
assert world_delta_of({"story": "s"}) is None
|
||||
assert world_delta_of(None) is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user