Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 37 — The Resolution Agent

Intake finished O-1001. The pack says: email preferred, not opted out, shipment stalled (exception_likely inference at 0.7), language still unknown. Resolution is next — it chooses a proposed path, not an executed one.

Resolution turns the intake pack into a standard, exception, or escalate proposal: draft email outlines, reship/refund actions, evidence links, and requires_approval flags. It does not issue refunds, reships, or customer messages.

typed intake pack → Resolution.propose → standard | exception | escalate → Policy (+ HITL if flagged)

Without this separation, one model call “decides” reship and writes send-ready copy in the same breath. Policy never gets a clean decision surface. A plan without evidence IDs is persuasive text — useless in review and untestable in CI.

On O-1001, Resolution points at stalled tracking, proposes the exception track with a draft status email and reship options, and leaves Policy and Maya to decide what is permitted. If required contact fields are still unknown, Resolution returns escalate and Outreach never runs.

This chapter adds Resolution: propose with evidence; commit nowhere.

First principles

  1. Propose; do not commit. Resolution names a possible remedy but cannot issue a refund, reship, or message.
  2. Carry the evidence forward. Every proposed action links to Intake or ledger evidence IDs.
  3. Block on missing inputs. If the information needed for a customer-facing draft is unknown, return escalate.
  4. Label the path. standard and exception give Policy and reviewers a concrete decision surface.
  5. Surface costly decisions. Refund and reship thresholds set requires_approval for HITL (Ch 29).
  6. Keep side effects elsewhere. Resolution has no channel or order-management write capability.

Concrete example — exception vs standard

O-1001 profile: exception_likely inference true (stalled tracking), email preferred, not opted out.

ResolutionProposal
  track: exception
  rationale: stalled/failed shipment suggests exception track (reship or refund)
  actions:
    - kind: draft_email
      channel: email
      amount_cents: 12900
      body_outline: stalled shipment apology + reship-or-refund options + order number
      evidence_ids: [oms:order_total, oms:channel_pref, ship:s1, …]
      requires_approval: true   # above $50 auto-refund teaching cap
    - kind: propose_reship
      requires_approval: true

If language were required for contact in your product rules and still UNKNOWN, Resolution returns track=escalate with blocked_reasons=["missing:language"] — Outreach never runs.

Diagram

flowchart TD
  Pack[Typed intake pack] --> Ch{inputs known?}
  Ch -->|no| Esc[ESCALATE / blocked]
  Ch -->|yes| Opt{opted_out?}
  Opt -->|yes| Esc
  Opt -->|no| H{exception inference?}
  H -->|yes| Exc[EXCEPTION proposal]
  H -->|no| Std[STANDARD proposal]
  Exc --> Pol[Policy]
  Std --> Pol

Caption: Notice send never appears — only draft / propose_reship / propose_refund / await_human kinds.

Implementation

Module: shopops/agents/resolution.py.

from shopops.agents.intake import IntakeAgent, FactKind
from shopops.agents.resolution import ResolutionAgent, Track

profile = IntakeAgent(oms=FakeOms()).build("O-1001", "store_northline")
strat = ResolutionAgent()
proposal = strat.propose(profile)
assert proposal.track == Track.EXCEPTION
assert proposal.can_proceed
assert proposal.actions[0].evidence_ids  # non-empty

# Missing evidence path
profile.fields["preferred_channel"].kind = FactKind.UNKNOWN  # type: ignore
blocked = strat.propose(profile)
assert blocked.track == Track.ESCALATE
assert any(r.startswith("missing:") for r in blocked.blocked_reasons)

Orchestrator records the proposal on the ledger (Ch 16) before Policy reviews it. Model-assisted narrative can fill body_outline later; the structure stays code-owned.

Required evidence invariant

∀ action in proposal.actions:
    action.evidence_ids ≠ ∅
    every id resolves to profile/ledger evidence
otherwise: can_proceed = false

Policy and audit both consume those ids. A resolution that “sounds right” without citations is rejected before Outreach exists.

Thresholds as product config

exception_refund_cents, standard_update_cents, and approval_above_cents belong in tenant config (Ch 33), not hard-coded forever — but they remain code-read numbers, not prompt suggestions the model can nudge.

Failure modes

FailureSymptomFix
Propose send directlySoD breakAction kinds exclude send
Empty evidence_idsAudit failureRequire ids before can_proceed
Treat inference as certainWrong trackExpose kind to Policy/HITL
Ignore opt-outIllegal contact pathHard block in propose()
Plan without profile refreshStale order/shipmentCheckpoint profile version

Production considerations

  • Eval fixtures: golden proposals for standard/exception/opt-out/missing.
  • Human-readable rationale is for reviewers; rule ids come from Policy.
  • Keep amounts in integer cents; never float money.
  • Resolution may call a large model (Ch 34) for narrative — still validate schema.

Chapter summary

  • Resolution emits proposals with tracks and evidence links.
  • Missing evidence blocks contact actions.
  • Exception vs standard is explicit (reship/refund vs status update).
  • Approval flags bridge to HITL.
  • No sends from Resolution.
  • Opt-out short-circuits to escalate.
  • Ledger the proposal before policy review.
  • Structure is code; prose is optional model fill.

Exercises

  1. Mechanical. Force opted_out on profile; assert escalate with opted_out.
  2. Mechanical. Clear shipment exception signals; assert Track.STANDARD and no propose_refund.
  3. Design. Add a propose_store_credit action kind; what evidence ids must it carry?
  4. Hostile. Resolution proposes full refund with empty evidence_ids — write the gate that fails CI.