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
- Propose; do not commit. Resolution names a possible remedy but cannot issue a refund, reship, or message.
- Carry the evidence forward. Every proposed action links to Intake or ledger evidence IDs.
- Block on missing inputs. If the information needed for a customer-facing draft is unknown, return
escalate. - Label the path.
standardandexceptiongive Policy and reviewers a concrete decision surface. - Surface costly decisions. Refund and reship thresholds set
requires_approvalfor HITL (Ch 29). - 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
| Failure | Symptom | Fix |
|---|---|---|
| Propose send directly | SoD break | Action kinds exclude send |
| Empty evidence_ids | Audit failure | Require ids before can_proceed |
| Treat inference as certain | Wrong track | Expose kind to Policy/HITL |
| Ignore opt-out | Illegal contact path | Hard block in propose() |
| Plan without profile refresh | Stale order/shipment | Checkpoint 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
- Mechanical. Force opted_out on profile; assert escalate with
opted_out. - Mechanical. Clear shipment exception signals; assert
Track.STANDARDand nopropose_refund. - Design. Add a
propose_store_creditaction kind; what evidence ids must it carry? - Hostile. Resolution proposes full refund with empty evidence_ids — write the gate that fails CI.