Chapter 38 — The Policy Agent
Resolution proposed an exception path for O-1001: stalled shipment, draft apology with reship-or-refund options, order total above the auto-refund teaching cap. The draft includes store identity and order number. SMS send window does not apply — this is email.
Policy decides whether that proposal is allowed. It runs the deterministic shop_v1 pack first: opt-out, send window (08:00–21:00 local for SMS/push), contact frequency, required message fields (store identity, order number, purpose), return window, refund/reship thresholds. Only if those checks pass may an optional model flag genuinely ambiguous wording for human review.
ResolutionProposal + draft body → deterministic shop_v1 → PASS | FAIL | NEEDS_HUMAN | GRAY → Outreach may draft; send waits on PASS + approval
A prompt that says “follow policy” is not enforcement. The same stalled-shipment evidence and refund amount must produce the same verdict in a test, in Maya’s review UI, and at send time. Without code-first Policy, send-window rules and required-field checks drift with model mood — and a model cannot turn a deterministic FAIL into PASS.
This chapter adds Policy: repeatable permit/deny with inspectable rule IDs, plus an optional gray classifier that may only tighten, never widen.
First principles
- Evaluate deterministic rules first. The fixture suite must be green before model assistance is enabled.
- Let the model narrow, never widen. It may add a
GRAYreview path; it cannot permit a denied action. - Judge the action, not the prose alone. A draft, refund, reship, and customer contact can require different checks.
- Encode send window and required fields in code. SMS/push outside 08:00–21:00 local fails; email drafts can still be written. Missing order-number / store-identity tokens fail.
- Make the verdict inspectable. Store status, rule IDs, reasons, and policy version as data.
- Route uncertainty to a person.
NEEDS_HUMANandGRAYstop automatic execution. - Reuse the same pack at enforcement. Separate rule text for review and execution will drift.
Concrete example
Resolution proposes an exception path for O-1001: the shipment is stalled, the order evidence is complete, the customer has not opted out, and there have been no recent contacts.
PolicyReview
status: PASS
policy_version: shop_v1.0.0
rule_ids: [order_state_ok, contact_ok]
reasons: [evidence complete, remedy within threshold]
The same proposal with an unsupported refund amount → NEEDS_HUMAN. An optional gray classifier that flags a confusing or overly assertive draft produces GRAY, even when the deterministic pack passes.
Diagram
flowchart TD
Prop[ResolutionProposal + body] --> D[Deterministic pack]
D -->|FAIL / NEEDS_HUMAN| Out[PolicyReview]
D -->|PASS| G{gray classifier enabled?}
G -->|no| Pass[PASS]
G -->|yes| C{model ok?}
C -->|yes| Pass
C -->|no| Gray[GRAY → HITL]
100% deterministic suite ──► green required
│
▼
optional gray-text model ──► can only tighten, never loosen
Caption: Notice the model has no edge that turns FAIL into PASS.
Implementation
Module: shopops/agents/policy_agent.py + pack policy/packs/shop_v1.py.
from shopops.agents.policy_agent import PolicyAgent, PolicyReviewStatus
from shopops.agents.resolution import ResolutionAgent
proposal = ResolutionAgent().propose(profile) # from Ch 36–37
body = (
"Your shipment has stalled. We can review a reship or refund for order O-1001."
)
comp = PolicyAgent()
result = comp.review(
proposal,
body=body,
contacts_last_24h=0,
contacts_last_7d=1,
order_evidence_complete=True,
send=False,
)
assert result.status == PolicyReviewStatus.PASS
# Deterministic suite before enabling model assist
fixtures = [
{
"name": "refund_requires_review",
"expect_allow": False,
"case_id": "O-1001",
"channel": "email",
"body": body,
"contacts_last_24h": 0,
"contacts_last_7d": 0,
"order_evidence_complete": True,
"refund_cents": 12_900,
"is_draft_only": False,
"customer_opted_out": False,
},
]
assert all(ok for _, ok, _ in comp.deterministic_suite(fixtures))
Failure modes
| Failure | Risk | Fix |
|---|---|---|
| Prompt-only policy | Injection / drift | Pack engine |
| Model overrides deny | Illegal send | Code structure above |
| Divergent rules in prompt vs pack | False green | Single pack source |
| Order-detail substring games | Weak evidence check | Structured evidence references |
| Skipping suite in CI | Regressions | Gate deploys on suite |
Production considerations
- Jurisdiction packs selected by tenant (Ch 33).
- Policy agent identity has
policy:judgeonly — no send (Ch 27). - Record every verdict on the ledger; CONFLICT if Resolution disputes (Ch 17).
- Gray-text model: version, eval rubrics, never in the hot path until suite green.
Chapter summary
- Deterministic checks first; model only for gray text.
- FAIL cannot be loosened by the model.
- Draft and send differ under the pack.
- Verdicts carry versions and rule ids.
- Suite green is a release gate.
- HITL for NEEDS_HUMAN and GRAY.
- Same pack as executor enforcement.
- Policy is SoD, not a personality prompt.
Exercises
- Mechanical. Run
deterministic_suitewith 10 fixtures covering shipment state, refund threshold, frequency, and opt-out; all green. - Subordinate model. Inject a classifier that always returns
ok=Trueon a denied refund; assert the verdict remainsFAIL. - Design. Define what “gray text” means for your channels; list labels humans must see.
References
- Chapters 28, 17, 27, 29
- Illustrative store return/refund and contact rules — not legal advice. [VERIFY SOURCE]