Chapter 36 — The Intake Agent
Jordan Lee emailed about order O-1001 — headphones still “in transit” after eight days. Maya opened the case in ShopOps. Before Resolution can propose a reship, refund, or status email, something has to turn OMS and carrier records into a case the pipeline can trust.
That first step is Intake. It reads order and shipment systems and emits a typed intake pack: facts with evidence IDs, inferences with confidence, and explicit UNKNOWN where the records are silent.
Maya opens O-1001 → Intake reads OMS + shipments → facts / inferences / unknowns → typed pack → Resolution
Skip strict Intake and the rest of the chain guesses. If language preference is missing, a sloppy step invents English so the workflow keeps moving — and Outreach later sends the wrong template with high confidence. If a stalled shipment is narrated as a fact instead of an inference, Policy cannot tell what is proven versus assumed.
This chapter adds Intake: the agent that starts every case from records, not from chat prose, and passes a compact contract downstream instead of a raw OMS dump.
First principles
- Start with records, not narration. Order and shipment systems supply facts and evidence IDs.
- Keep reasoning visible. A stalled-shipment assessment is an inference, never a fact in disguise.
- Make absence explicit.
UNKNOWNtells the next step to block or escalate instead of guessing (Ch 13). - Stop at the boundary. Intake reads and organizes; it has no customer-contact capability (Ch 15, 27).
- Pass a compact contract downstream. Resolution receives the typed intake pack, not a raw OMS dump.
- Do not let confidence make decisions. Confidence helps Resolution choose a track; Policy still decides what is permitted.
Concrete example — O-1001 intake pack
| Field | Value | Kind | Provenance |
|---|---|---|---|
| customer_id | C-7781 | fact | oms.get_customer |
| order_total_cents | 12900 | fact | oms.get_customer |
| preferred_channel | fact | oms.get_customer | |
| opted_out | false | fact | oms.get_customer |
| exception_likely | true | inference (0.7) | shipment_pattern |
| language | null | unknown | missing |
Resolution (Ch 37) may proceed on exception track using inference + facts. If preferred_channel were unknown, Resolution blocks contact proposals.
Diagram
flowchart LR
R[Order and shipment records] --> I[Intake.build]
I --> F[Facts]
I --> N[Inferences]
I --> U[Unknowns]
F & N & U --> P[Typed intake pack]
P --> S[Resolution]
P --> M[Optional memory write]
raw OMS JSON ──► classify field ──┬── FACT (+ evidence_id)
├── INFERENCE (+ confidence)
└── UNKNOWN (value=null)
Caption: Notice UNKNOWN is an output, not a failure to parse — it is a decision to not invent.
Implementation
Module: shopops/agents/intake.py.
from shopops.agents.intake import IntakeAgent, FactKind
class FakeOms:
def get_customer(self, case_id: str) -> dict:
return {
"customer_id": "C-7781",
"order_total_cents": 12_900,
"preferred_channel": "email",
"opted_out": False,
# language intentionally missing
}
def list_shipments(self, case_id: str) -> list[dict]:
return [
{"id": "s1", "status": "in_transit_stalled"},
{"id": "s2", "status": "failed_delivery"},
{"id": "s3", "status": "delivered"},
]
agent = IntakeAgent(oms=FakeOms())
profile = agent.build("O-1001", tenant_id="store_northline")
assert profile.get("language").kind == FactKind.UNKNOWN
assert profile.get("exception_likely").kind == FactKind.INFERENCE
missing = agent.require_known(profile, ["preferred_channel", "language"])
assert missing == ["language"]
Authz: run under agent:intake scopes only (intake:read, order_pii:read, memory:write_intake). Never attach outreach:send.
What “unknown” forces upstream
language = UNKNOWN
→ Resolution may still propose if language not required
→ OR product rule: require_known(["language"]) before any draft
→ Orchestrator escalates to human data repair — model does not invent "en"
The Intake agent’s highest-value output is often the list of unknowns. That list is how you stop the rest of the pipeline from improvising.
Memory write (optional)
If you persist profile facts via the memory controller (Ch 10), write only FactKind.FACT with provenance. Inferences can be stored as inferences with decay; unknowns are usually omitted rather than stored as null spam.
Failure modes
| Failure | Result | Fix |
|---|---|---|
| Fill unknowns with model guesses | Wrong channel / language | FactKind.UNKNOWN + escalate |
| Raw tool JSON as memory | Poison / bloat | Memory controller (Ch 10) |
| Outreach from profile | SoD break | No channel tools in process |
| Inference as fact | Overconfident strategy | Kind checks downstream |
| Missing provenance | Unauditable | Require evidence_ids |
Production considerations
- Snapshot golden profiles in eval fixtures (Ch 20).
- PII minimization: Outreach may receive a redacted view, not full order + payment PII.
- Refresh policy: when do we rebuild vs reuse checkpointed profile?
- Trace every CRM read with tool spans; link evidence ids into audit packs.
Chapter summary
- Intake agent produces typed facts/inferences/unknowns.
- Provenance is mandatory.
- Unknowns must not be hallucinated away.
- No customer contact from this agent.
- Downstream blocks on missing required keys.
- Inferences carry confidence and evidence.
- Least-privilege authz applies.
- O-1001 profile pack is the contract for later chapters.
Exercises
- Mechanical. Remove
preferred_channelfrom FakeOms; assertrequire_knownlists it. - Inference. Tune the stalled-shipment threshold; show an exception signal flip between UNKNOWN and INFERENCE.
- Design. Specify which profile fields Outreach is allowed to see vs only Intake/Policy.
References
- Chapters 9–10, 13, 15, 27
- Provenance / evidence patterns in knowledge bases. [VERIFY]