feat(runtime): add trusted hidden check loop
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -37,8 +37,9 @@ return exactly one bare JSON object with the same arguments and no Markdown fenc
|
||||
|
||||
The result must contain only scene, character, beats, delta, suggestions, and canContinue. Never construct or
|
||||
return PlayerView, hidden reasoning, provider details, credentials, or exact relationship values in
|
||||
narrative text. Never decide the player's speech, actions, or inner thoughts. State changes are
|
||||
proposals only; the trusted reducer will validate and commit them.";
|
||||
narrative text. Never decide the player's speech, actions, or inner thoughts. Never submit a
|
||||
RecordCheck operation; hidden checks are requested and recorded only through the trusted engine.
|
||||
State changes are proposals only; the trusted reducer will validate and commit them.";
|
||||
|
||||
/// Synchronous seam around one non-streaming LAPP chat operation.
|
||||
///
|
||||
@@ -407,6 +408,11 @@ fn validate_generated_plan(request: &TurnRequest, plan: &TurnPlan) -> Result<(),
|
||||
|| presentation.beats.is_empty()
|
||||
|| presentation.beats.len() > MAX_BEATS
|
||||
|| plan.delta.ops.len() > MAX_STATE_OPS
|
||||
|| plan
|
||||
.delta
|
||||
.ops
|
||||
.iter()
|
||||
.any(|op| matches!(op, nana_domain::StateOp::RecordCheck { .. }))
|
||||
|| presentation.suggestions.len() > MAX_SUGGESTIONS
|
||||
{
|
||||
return Err(invalid_output(InvalidModelOutputKind::InvalidPlan));
|
||||
@@ -704,6 +710,36 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_cannot_submit_a_record_check_operation() {
|
||||
let mut value = plan_value();
|
||||
value["delta"]["ops"] = json!([{
|
||||
"op": "record_check",
|
||||
"check": {
|
||||
"id": "forged_check",
|
||||
"action_id": "action_2",
|
||||
"actor": "player",
|
||||
"skill": "Spot Hidden",
|
||||
"target": 99,
|
||||
"difficulty": "regular",
|
||||
"bonus_dice": 0,
|
||||
"roll": 1,
|
||||
"result": "critical_success",
|
||||
"pushed_from": null,
|
||||
"node_id": "node_forged"
|
||||
}
|
||||
}]);
|
||||
let executor = ScriptedExecutor::returning(Ok(response(value.to_string(), Vec::new())));
|
||||
let mut provider = LappTurnPlanProvider::new(executor);
|
||||
|
||||
assert!(matches!(
|
||||
provider.plan_turn(&request(), &state()),
|
||||
Err(ProviderError::InvalidModelOutput {
|
||||
kind: InvalidModelOutputKind::InvalidPlan
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_failures_remain_redacted_and_map_to_provider_unavailable() {
|
||||
let executor = ScriptedExecutor::returning(Err(ProviderError::Upstream {
|
||||
|
||||
@@ -8,9 +8,16 @@ use nana_engine::{ReduceError, apply_delta};
|
||||
use nana_store::{StoreError, StoryStore};
|
||||
use thiserror::Error;
|
||||
|
||||
mod adjudication;
|
||||
mod context;
|
||||
mod lapp_provider;
|
||||
|
||||
pub use adjudication::{
|
||||
AdjudicatingTurnPlanProvider, AdjudicationCatalog, AdjudicationError, AdjudicationModel,
|
||||
AdjudicationModelInput, AdjudicationModelResponse, AdjudicationRunError, AdjudicationToolCall,
|
||||
CatalogError, DEFAULT_MAX_ADJUDICATION_STEPS, HIDDEN_CHECK_TOOL_NAME, HiddenCheckRequest,
|
||||
QualitativeCheckOutcome, classify_roll, deterministic_roll,
|
||||
};
|
||||
pub use context::{
|
||||
CharacterMemory, CompiledSceneContext, ContextBudget, ContextCharacterCard,
|
||||
ContextCompileError, ContextInventoryItem, ContextJudgmentRule, ContextPersona,
|
||||
|
||||
Reference in New Issue
Block a user