Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/bitworld/decision_trajectory.nim
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type
model*: Option[string]
origin*: AttemptOrigin
inferenceMode*: InferenceMode
actionEvidence*: Option[JsonNode] ## Game-owned scoring provenance; never player-supplied.
prompt*, request*, response*, rawResponse*, parsedAction*, decoder*: JsonNode
accepted*: bool
platformCallId*, rejectionReason*, modelIdentity*, tokenizerIdentity*: Option[string]
Expand Down Expand Up @@ -244,9 +245,19 @@ proc recordDecision*(trajectory: DecisionTrajectory, decisionId, seat: string,
selectedPrompt = attempt.prompt
if not attempt.accepted or attempt.parsedAction != executedAction:
raise newException(ValueError, "selected proposal differs from executed action")
if attempt.actionEvidence.isSome:
let action = attempt.actionEvidence.get()
if action.kind != JObject or action["protocol"].kind != JString or
action["protocol"].getStr().len == 0:
raise newException(ValueError, "game action evidence requires a protocol-tagged object")
if attempt.inferenceMode != imCandidate or attempt.response.kind != JNull or
attempt.sampledTokenIds.isSome or attempt.behaviorLogprobs.isSome:
raise newException(ValueError, "scoring evidence cannot claim generated text or sampled tokens")
encoded.add(%*{
"attempt_id": attempt.attemptId, "policy": attempt.policy,
"inference_mode": $attempt.inferenceMode,
"action_evidence": (if attempt.actionEvidence.isSome:
copy(attempt.actionEvidence.get()) else: newJNull()),
"origin": originName(attempt.origin), "model": jsonOption(attempt.model),
"prompt": attempt.prompt, "request": attempt.request,
"response": attempt.response, "raw_response": attempt.rawResponse,
Expand Down
44 changes: 44 additions & 0 deletions tests/test_decision_trajectory.nim
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,47 @@ suite "private authoritative decision trajectories":
check readAttemptEvidence(wire).responseReaderJoined.isNone
wire["response_reader_joined"] = %"joined"
expect ValueError: discard readAttemptEvidence(wire)

test "game-owned scoring survives original failure and selected action joins":
let record = episode()
var rejected = newDecisionAttempt("rank-rejected", "policy", aoModel, imCandidate)
rejected.actionEvidence = some(%*{"protocol": "fixture.score.v1",
"scoring_result": {"kind": "rejected", "fault": "provider_error"}})
rejected.rejectionReason = some("original provider failure")
var selected = newDecisionAttempt("rank-scored", "policy", aoModel, imCandidate)
selected.actionEvidence = some(%*{"protocol": "fixture.score.v1",
"scoring_result": {"kind": "scored", "selected_index": 0}})
selected.parsedAction = %*{"move": 1}
selected.accepted = true
record.recordDecision("rank", "0", %*{}, @[rejected, selected],
some("rank-scored"), selected.parsedAction, asAccepted)
selected.actionEvidence.get()["scoring_result"]["selected_index"] = %9
record.finish(esCompleted, %*{}, %*[1])
let event = parseJson(record.eventsJsonl().splitLines()[0])
check event["attempts"].len == 2
check event["attempts"][0]["action_evidence"]["scoring_result"]["kind"].getStr() == "rejected"
check event["attempts"][1]["action_evidence"]["scoring_result"]["selected_index"].getInt() == 0
check event["attempts"][1]["response"].kind == JNull
check event["executed_action"] == event["attempts"][1]["parsed_action"]

test "scoring cannot acquire generation or sampled token provenance":
for fault in ["mode", "text", "tokens", "probabilities"]:
var attempt = newDecisionAttempt("rank", "policy", aoModel, imCandidate)
attempt.actionEvidence = some(%*{"protocol": "fixture.score.v1"})
case fault
of "mode": attempt.inferenceMode = imTextAction
of "text": attempt.response = %"generated-looking text"
of "tokens": attempt.sampledTokenIds = some(@[1])
else: attempt.behaviorLogprobs = some(@[-0.1])
expect ValueError:
episode().recordDecision("rank", "0", %*{}, @[attempt], none(string),
%*{"move": 1}, asFallback, fallbackOrigin = some("actual-scripted"))

test "private players cannot inject game-owned scoring evidence":
var attempt = newDecisionAttempt("rank", "policy", aoModel, imCandidate)
attempt.actionEvidence = some(%*{"protocol": "fixture.score.v1"})
let wire = attempt.attemptEvidenceJson()
check not wire.hasKey("action_evidence")
wire["action_evidence"] = %*{"protocol": "fixture.score.v1"}
expect ValueError:
discard readAttemptEvidence(wire)
Loading