7.9 KiB
ADR-0038: Hedge Injection as a Runtime-Level Affordance
Status: Accepted (2026-05-17)
Author: Joshua Shay + planner pass
Companion docs: ADR-0028-surface-preferences.md, ADR-0036-safety-refusal-policy.md, ADR-0037-per-predicate-ethics-refusal.md
Context
ADR-0036 chose typed refusal over hedge injection for safety violations because conflating refusal with hedging would blur audit:
Hedge injection would blur the boundary between hedging (alignment-score driven) and refusing (predicate-driven). The same surface change could mean two different things. Audit becomes ambiguous.
That choice was correct for refusal. But it left an open question:
once EthicsCheck predicates fire runtime-checkably for low alignment
(acknowledge_uncertainty) or ungrounded scope (disclose_limitations),
a deployment might want softer remediation than full refusal —
some way to qualify the surface without replacing it.
ADR-0037 introduced refusal_commitments as the opt-in for per-predicate
escalation to refusal. This ADR introduces its sibling
hedge_commitments: opt-in for runtime-level hedge prepend.
Decision
Add an optional hedge_commitments field to the ethics pack JSON
schema. Each entry must be a declared commitment_id. When any
runtime-checkable violation of a commitment in hedge_commitments
fires this turn, the runtime prepends the manifold's preferred hedge
phrase (preferred_hedge_soft, falling back to
preferred_hedge_strong) to ChatResponse.surface.
Mutual exclusion with refusal
A commitment cannot appear in both refusal_commitments and
hedge_commitments. This is enforced at load time:
overlap = refusal & hedge
if overlap:
raise EthicsPackError("commitments cannot appear in both ...")
The two remediations are escalation siblings, not stackable layers. Pack authors pick one per commitment: hedge (soft) or refuse (hard).
Refusal supersedes hedge in code path order
Even though pack schema forbids per-commitment overlap, the runtime still gives refusal priority globally: if any safety boundary or opted-in ethics commitment fires refusal, the surface is the typed refusal — hedge injection is skipped for the turn. This preserves the invariant "refusal is total."
Stub path does not hedge
The stub-path surface (_UNKNOWN_DOMAIN_SURFACE = "I don't know — insufficient grounding for that yet.") is already a disclosure
surface. Prepending a hedge ("Perhaps I don't know — …") would read
as a confused double-disclosure. Hedge injection runs only on the
main articulation path. Stub-path refusal does still fire (per
ADR-0036) because refusal is a hard stop, not a qualifier.
Evidence preservation
Same discipline as ADR-0036: hedge changes only the user-facing
surface field. walk_surface (token-walk evidence) and
articulation_surface (realizer output) are preserved unchanged.
An auditor reading a hedged turn sees:
- original surface (walk_surface / articulation_surface),
- hedged user-facing surface (with prepended hedge),
- ethics_verdict (with the violating commitment).
Idempotent on prefix
inject_hedge() is idempotent: if the surface already begins with
the hedge phrase (case-insensitive match), no double-prepend occurs.
This is a defensive property — the assembler's existing
SurfaceContext-driven hedge logic (ADR-0028) may have already
hedged the surface, and runtime injection should not duplicate.
No effect on refusal bookkeeping
Hedge injection does not set _last_refusal_was_typed. Hedging
is not a refusal — the no_silent_correction safety predicate cares
about typed refusals specifically, and a hedge should not be miscounted
as one.
Consequences
Positive
- Soft remediation channel. A medical-domain pack can opt
acknowledge_uncertaintyinto hedging without committing to full refusal. Deployment authors get a middle tier between audit-only and refuse. - Schema-enforced mutual exclusion. Load-time error makes it impossible to ship a pack where the same commitment claims both remediations.
- Runtime path stays minimal. Three helper functions
(
should_inject_hedge,build_hedge_prefix,inject_hedge), all pure. ChatRuntime adds a single conditional after the refusal branch. - Evidence preserved. Same audit discipline as refusal: original surfaces retained on the response and turn event.
- Backward compatible. Default pack ships
hedge_commitments: []; no behavior change for unmodified deployments.
Negative / risks
- Hedge phrase source is the identity manifold, not the ethics pack.
This means swapping ethics packs while keeping identity packs fixed
produces the same hedge phrasing. Acceptable today: the manifold's
surface_preferencesis the canonical hedge home (ADR-0028). A future ADR could let ethics packs override phrasing per commitment. - Hedge runs only on main path. Stub-path hedge would be a
double-disclosure. Tests gate runtime-end-to-end hedge assertions
on
rt.turn_logpopulated. - Idempotent-on-prefix means assembler hedges suppress runtime hedges. Correct (no double-hedge), but it means the signal of "did the runtime inject this hedge or did the assembler?" is lost from the surface alone. Audit consumers should rely on the ethics_verdict, not on the surface, to determine whether the injection path fired.
- Ratification round-trip on schema change. Same cost as
ADR-0037: adding
hedge_commitmentsto the default pack required re-ratifying its mastery report.
Verification
tests/test_hedge_injection.py— 22 tests covering: loader bounds (empty default, unknown id rejected, mutual exclusion rejected, split-allocation OK); pure helpers (should_inject_hedgewith pack/verdict/opt-in/evidence combinations;build_hedge_prefixwith default manifold + None;inject_hedgehappy path, empty prefix, empty surface, idempotent on prefix, case-insensitive idempotency); ChatRuntime integration (default pack does not inject; opt-in pack injects on violation; walk_surface preserved; refusal supersedes hedge; hedge does not flip_last_refusal_was_typed).- Combined pack-layer suite: 154 tests, all green (safety pack + safety check + ethics pack + ethics check + turn-loop verdicts + safety refusal + ethics refusal opt-in + hedge injection).
- CLI suites unchanged: smoke 67, runtime 19, cognition 121.
core eval cognition: intent_accuracy 100%, versor_closure_rate 100% — baseline preserved.
Open questions deferred to a future ADR
- Per-commitment hedge phrases sourced from the ethics pack.
Today the manifold owns hedge phrasing. A future commitment-keyed
override would let ethics packs say "for
defer_high_stakes_to_human_review, use 'Before proceeding,' instead of 'Perhaps'." - Hedge strength tiers. Today a single hedge fires regardless
of how many commitments violated. A pack could opt commitments
into specific strength tiers (
hedge_softvshedge_strong). - Verdict surface for "was hedge injected this turn." Today
only the ethics_verdict carries the signal; downstream consumers
inferring "hedge fired" must inspect both the verdict and the
prefix. A
hedge_injected: boolfield onChatResponse/TurnEventwould make audit simpler. - Stub-path soft disclosure with hedge. The current "I don't know — insufficient grounding" surface is fixed. A pack might want to inject domain-specific disclosure phrasing on stub. Deferred until packs need it.
- Interaction with assembler hedges (ADR-0028). Today idempotent-on-prefix prevents double-hedging; a future ADR could make the relationship explicit (e.g., assembler is responsible for alignment-score-driven hedges; runtime is responsible for ethics-violation-driven hedges; never both fire on the same turn).