Skip to main content

The encounter bridge — proposer/responder model

The encounter-argumentation crate is the bridge between formal argumentation and the encounter scene engine. This page explains the model — why a bridge crate exists, the proposer/responder split, the dimensions it composes, and the error-latch design.

The encounter scene engine is a separate library (encounter) that orchestrates multi-actor scenes via three trait hooks: ActionScorer (proposer-side), AcceptanceEval (responder-side), and a resolution loop (MultiBeat, SingleExchange, ...). Argumentation provides the semantics; the bridge wires the two together so each scene beat is decided by argumentation reasoning.

Why a bridge crate?

A scene engine and a reasoning library shouldn't know about each other. The bridge crate exists so:

  • The argumentation crates stay pure-formal — no scene-engine concepts leak in.
  • The encounter crate stays scene-engine-pure — no argumentation concepts leak in.
  • Consumers can swap reasoning backends (or use neither) without touching the engine.

The bridge crate (encounter-argumentation) implements ActionScorer and AcceptanceEval against an EncounterArgumentationState — a single state object that composes scheme reasoning, bipolar graph structure, weighted attack strengths, β intensity, per-character audiences, and an error latch.

The proposer/responder split

Each scene beat is the resolution of one proposer's affordance against the responder(s). The bridge handles the two sides differently:

Proposer side (StateActionScorer): scores the affordances available to the proposer. Higher score = more likely to be picked. The scorer reads:

  • The actor's affordances from the scene catalogue.
  • The actor's ArgumentKnowledge (which schemes they invoke for which actions).
  • The current state of the framework (β, audiences, accepted arguments).

It boosts affordances whose backing argument is credulously accepted at current β. Optionally wraps a SchemeActionScorer for scheme-strength boost, and a ValueAwareScorer for audience-conditioned value boost.

Responder side (StateAcceptanceEval): for each proposer's affordance, evaluates whether the responder accepts. The eval reads:

  • The proposer's argument ID for this affordance.
  • The framework state.
  • The responder's actor name.

It returns true iff the responder does NOT have a credulously accepted counter-argument to the proposer's argument. "Has counter" → reject; "no counter" → accept.

The split mirrors how human deliberation works: each speaker proposes the strongest argument they can; each listener decides whether they have grounds to push back.

What the bridge composes

The bridge composes several dimensions into one resolution:

DimensionWhere it livesWhat it shapes
Scene tension βstate.intensity: Mutex<Budget>Which attack weights bind.
Per-character audiencesstate.audiences: Mutex<HashMap<String, Audience>>Which value-promoting affordances each actor scores up.
Scheme strengthargumentation-schemes (Strength enum)How much SchemeActionScorer boosts an affordance.
Argument credulitystate.is_credulously_accepted(...)Whether the proposer's argument survives current β.
Counter-credulitystate.has_accepted_counter_by(...)Whether the responder has a survivor counter.

These compose at scoring time (proposer side) and at acceptance time (responder side). A single beat's outcome is the combined result.

Audiences as character state

Per-character audiences are first-class state on EncounterArgumentationState — same lifecycle as β. A character authored once can carry their value priorities across many scenes:

state.set_audience("alice", Audience::total([Value::new("duty"), Value::new("survival")]));
state.set_audience("bob", Audience::total([Value::new("survival"), Value::new("duty")]));

When the resolution loop calls score_actions for Alice, the wrapped ValueAwareScorer reads state.audience_for("alice") and boosts duty-promoting affordances. When it calls for Bob, it reads Bob's audience and boosts survival-promoting ones instead. Same scene, same arguments — different actors reach for different proposals.

See Wiring per-character values for the runtime integration pattern.

The error latch

A subtle but important design choice: the bridge does NOT return errors from ActionScorer::score_actions or AcceptanceEval::evaluate — both trait methods return owned values, not Result. So when something goes wrong inside a scoring or acceptance call, the bridge has nowhere to put the error.

Instead: the state owns an error latch (Mutex<Vec<Error>>). On internal failure, the bridge appends an error and returns a permissive default (accept the affordance, skip the boost). The scene continues — D5 contract: "permissive on failure."

After resolution returns, callers MUST drain the latch:

let errors = state.drain_errors();
if !errors.is_empty() {
// log, propagate, or ignore — your choice
}

This pattern matches how the rest of the bridge handles state mutation (interior mutability via Mutex, recovery on poisoning) — see the intensity_guard and audiences_guard private helpers for the exact pattern.

Cost note: scoring and acceptance recompute on each beat

StateActionScorer and StateAcceptanceEval query the framework on every beat without caching. For narrative-scale frameworks (≤ 22 arguments per argumentation::ENUMERATION_LIMIT), this is fine — preferred-extension enumeration is bounded.

For longer scenes or larger frameworks, two patterns help:

  1. Pre-seed only the arguments the scene needs (don't load the entire knowledge base).
  2. Reuse a single EncounterArgumentationState across beats; do NOT reconstruct it per beat.

Integration shape

A typical wiring:

let state = EncounterArgumentationState::new(catalog);
state.set_intensity(Budget::new(0.5).unwrap());
state.set_audience("alice", alice_audience);
state.set_audience("bob", bob_audience);

let scorer = ValueAwareScorer::new(
SchemeActionScorer::new(knowledge, registry, baseline_scorer, 0.3),
&state,
0.2,
);
let acceptance = StateAcceptanceEval::new(&state);

let result = MultiBeat.resolve(&participants, &practice, &catalog, &scorer, &acceptance);

// Drain errors after resolution.
let errors = state.drain_errors();

Three layers, additively composed:

  1. Baseline scorer (consumer-supplied — anything that implements ActionScorer).
  2. SchemeActionScorer (boosts scheme-backed affordances by scheme strength).
  3. ValueAwareScorer (boosts value-promoting affordances by audience).

In our library

TypePurpose
EncounterArgumentationStateThe composed state object — schemes + bipolar + weighted + audiences + errors.
StateActionScorer<S>ActionScorer<P> impl wrapping any inner scorer; adds credulous-acceptance boost.
SchemeActionScorer<K, S>Wraps S; adds scheme-strength × preference_weight boost.
ValueAwareScorer<S>Wraps S; adds audience-conditioned value boost.
StateAcceptanceEvalAcceptanceEval<P> impl; checks for credulously-accepted counter-arguments.
ArgumentKnowledgeTrait — supplies per-character argument positions. StaticKnowledge is the default impl for fixtures.

Further reading