549 lines
44 KiB
HTML
549 lines
44 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
:root { color-scheme: light dark; }
|
||
html, body { margin: 0; }
|
||
img { max-width: 100%; }
|
||
[hidden] { display: none !important; }
|
||
</style>
|
||
<title>Doormile Agent Layer</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Source+Sans+3:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;600&display=swap">
|
||
<style>
|
||
:root {
|
||
--paper:#F6F4F5; --surface:#FFFFFF; --surface-2:#EFEBED;
|
||
--ink:#17141A; --ink-2:#453E48; --muted:#6E6670;
|
||
--line:#DCD5D8; --line-2:#C6BCC0;
|
||
--accent:#C01227; --accent-ink:#C01227; --accent-soft:#F6E2E5;
|
||
--alt:#10646F; --alt-soft:#DDEEF0;
|
||
--gold:#8A6206; --gold-soft:#F7ECD4;
|
||
--shadow: 0 1px 2px rgba(23,20,26,.06), 0 8px 24px -16px rgba(23,20,26,.35);
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--paper:#121014; --surface:#1A171C; --surface-2:#221E24;
|
||
--ink:#EFEAEC; --ink-2:#C4BBC0; --muted:#968D94;
|
||
--line:#322C34; --line-2:#443C46;
|
||
--accent:#FF6076; --accent-ink:#FF8090; --accent-soft:#3A1B22;
|
||
--alt:#4FC7D4; --alt-soft:#10333A;
|
||
--gold:#E8B84B; --gold-soft:#3A2E10;
|
||
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--paper:#121014; --surface:#1A171C; --surface-2:#221E24;
|
||
--ink:#EFEAEC; --ink-2:#C4BBC0; --muted:#968D94;
|
||
--line:#322C34; --line-2:#443C46;
|
||
--accent:#FF6076; --accent-ink:#FF8090; --accent-soft:#3A1B22;
|
||
--alt:#4FC7D4; --alt-soft:#10333A;
|
||
--gold:#E8B84B; --gold-soft:#3A2E10;
|
||
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
background: var(--paper); color: var(--ink);
|
||
font-family: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
font-size: 16.5px; line-height: 1.62; margin: 0; padding: 0 24px 96px;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
.wrap { max-width: 1180px; margin: 0 auto; }
|
||
.measure { max-width: 68ch; }
|
||
|
||
header.masthead { padding: 64px 0 40px; border-bottom: 2px solid var(--ink); margin-bottom: 40px; }
|
||
.eyebrow {
|
||
font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
|
||
font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase;
|
||
color: var(--accent-ink); margin: 0 0 18px;
|
||
}
|
||
h1 {
|
||
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 700;
|
||
font-size: clamp(2.3rem, 5.4vw, 3.6rem); line-height: 1.03;
|
||
letter-spacing: -.03em; text-wrap: balance; margin: 0 0 20px;
|
||
}
|
||
.standfirst { font-size: 1.16rem; color: var(--ink-2); max-width: 62ch; margin: 0; }
|
||
.standfirst strong { color: var(--ink); font-weight: 600; }
|
||
|
||
nav.toc { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 52px; }
|
||
nav.toc a {
|
||
font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: .05em;
|
||
text-decoration: none; color: var(--ink-2); background: var(--surface);
|
||
border: 1px solid var(--line); border-radius: 3px; padding: 6px 11px;
|
||
}
|
||
nav.toc a:hover { border-color: var(--accent); color: var(--accent-ink); }
|
||
nav.toc a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
|
||
section { margin: 0 0 76px; scroll-margin-top: 20px; }
|
||
h2 {
|
||
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 600;
|
||
font-size: 1.62rem; letter-spacing: -.017em; line-height: 1.2;
|
||
text-wrap: balance; margin: 0 0 6px;
|
||
}
|
||
h3 {
|
||
font-family: Archivo, "Helvetica Neue", Arial, sans-serif; font-weight: 600;
|
||
font-size: 1.1rem; letter-spacing: -.01em; margin: 34px 0 8px;
|
||
}
|
||
.sec-head {
|
||
display: flex; align-items: baseline; gap: 16px;
|
||
border-top: 1px solid var(--line); padding-top: 16px; margin-bottom: 18px;
|
||
}
|
||
.sec-num {
|
||
font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 600;
|
||
color: var(--accent-ink); padding-top: 6px; flex: none;
|
||
}
|
||
p { margin: 0 0 15px; }
|
||
code, .mono { font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-size: .86em; }
|
||
p code, li code, td code {
|
||
background: var(--surface-2); border: 1px solid var(--line);
|
||
border-radius: 3px; padding: .07em .34em; color: var(--ink);
|
||
}
|
||
|
||
figure {
|
||
margin: 26px 0 32px; background: var(--surface); border: 1px solid var(--line);
|
||
border-radius: 4px; box-shadow: var(--shadow); overflow: hidden;
|
||
}
|
||
.fig-scroll { overflow-x: auto; padding: 22px 22px 6px; }
|
||
figure svg { display: block; max-width: 100%; height: auto; min-width: 700px; margin: 0 auto; }
|
||
figcaption {
|
||
border-top: 1px solid var(--line); background: var(--surface-2);
|
||
padding: 12px 22px 14px; font-size: 14px; color: var(--ink-2); line-height: 1.5;
|
||
}
|
||
figcaption b {
|
||
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
|
||
text-transform: uppercase; color: var(--accent-ink); display: block;
|
||
margin-bottom: 4px; font-weight: 600;
|
||
}
|
||
|
||
.table-scroll { overflow-x: auto; margin: 24px 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); }
|
||
table { border-collapse: collapse; width: 100%; font-size: 14.4px; min-width: 720px; }
|
||
th, td { text-align: left; padding: 11px 15px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||
thead th {
|
||
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
|
||
text-transform: uppercase; color: var(--muted); font-weight: 600; background: var(--surface-2);
|
||
}
|
||
tbody tr:last-child td { border-bottom: 0; }
|
||
td.num { font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||
.pill {
|
||
font-family: "JetBrains Mono", monospace; font-size: 10.5px; padding: 2px 7px;
|
||
border-radius: 3px; white-space: nowrap; font-weight: 600;
|
||
}
|
||
.p-high { background: var(--alt-soft); color: var(--alt); }
|
||
.p-mid { background: var(--gold-soft); color: var(--gold); }
|
||
.p-low { background: var(--surface-2); color: var(--muted); }
|
||
|
||
.callout {
|
||
border-left: 3px solid var(--accent); background: var(--accent-soft);
|
||
padding: 16px 20px; border-radius: 0 4px 4px 0; margin: 24px 0; font-size: 15.4px;
|
||
}
|
||
.callout.teal { border-left-color: var(--alt); background: var(--alt-soft); }
|
||
.callout.teal .label { color: var(--alt); }
|
||
.callout.gold { border-left-color: var(--gold); background: var(--gold-soft); }
|
||
.callout.gold .label { color: var(--gold); }
|
||
.callout p:last-child { margin-bottom: 0; }
|
||
.callout .label {
|
||
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
|
||
text-transform: uppercase; font-weight: 600; color: var(--accent-ink);
|
||
display: block; margin-bottom: 5px;
|
||
}
|
||
|
||
ul.plain { padding-left: 1.15em; margin: 12px 0 18px; max-width: 68ch; }
|
||
ul.plain li { margin-bottom: 8px; }
|
||
|
||
pre {
|
||
background: var(--surface-2); border: 1px solid var(--line); border-radius: 4px;
|
||
padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.55;
|
||
font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; color: var(--ink);
|
||
margin: 18px 0;
|
||
}
|
||
|
||
footer { border-top: 1px solid var(--line); padding-top: 20px; color: var(--muted); font-size: 13.6px; }
|
||
|
||
@media (max-width: 640px) {
|
||
body { padding: 0 16px 64px; font-size: 16px; }
|
||
header.masthead { padding: 44px 0 30px; }
|
||
}
|
||
</style>
|
||
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
|
||
<header class="masthead">
|
||
<p class="eyebrow">Integration plan · third in the Doormile series</p>
|
||
<h1>Doormile Agent Layer</h1>
|
||
<p class="standfirst">Where generative AI genuinely earns its place in the platform — auto-assignment included — how it plugs into the Go backend you already have, and the trust ladder that gets it from <strong>logging quietly</strong> to <strong>acting alone</strong> without a bad week in between.</p>
|
||
</header>
|
||
|
||
<nav class="toc">
|
||
<a href="#rule">00 · The rule</a>
|
||
<a href="#map">01 · Capability map</a>
|
||
<a href="#assign">02 · Auto-assign, properly</a>
|
||
<a href="#wiring">03 · Wiring it in</a>
|
||
<a href="#ladder">04 · Trust ladder</a>
|
||
<a href="#loop">05 · The loop you already built</a>
|
||
<a href="#guard">06 · Guardrails</a>
|
||
<a href="#plan">07 · 90 days</a>
|
||
</nav>
|
||
|
||
<!-- ============ 00 ============ -->
|
||
<section id="rule">
|
||
<div class="sec-head"><span class="sec-num">00</span><div><h2>The rule that makes all of this work</h2></div></div>
|
||
|
||
<p class="measure" style="font-size:1.22rem; line-height:1.5; color:var(--ink);"><strong>The model proposes. The solver decides. The system validates.</strong></p>
|
||
|
||
<p class="measure">Every capability below is an application of that one sentence. A language model is exceptional at turning mess into structure — a dispatcher's sentence into a constraint, a WhatsApp forward into a booking, a photo into a condition code, a solver's output into an explanation a rider will actually read. It is poor and expensive at choosing the best of ten thousand arrangements, which is exactly what dispatch is.</p>
|
||
|
||
<p class="measure">So "auto-assign with GenAI" does not mean a model picking riders. It means a model doing the four things around the pick that were previously impossible to automate, while a solver does the pick itself in milliseconds and a validator refuses anything that breaks an invariant. That is a more capable system than an LLM choosing alone, and a cheaper one.</p>
|
||
|
||
<div class="callout teal">
|
||
<span class="label">You already got the hard part right</span>
|
||
<p>The payload your backend sends to <code>routemate</code> is entirely structured — <code>miler_id</code>, <code>distance_km</code>, <code>rating</code>, <code>on_time_rate_30d</code>, <code>hub_load</code>, <code>hub_capacity</code>, plus hour, peak flag and zone. No customer-supplied free text goes into it. That closed payload is a security property, and §06 explains why keeping it is the single most important rule as you expand.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 01 ============ -->
|
||
<section id="map">
|
||
<div class="sec-head"><span class="sec-num">01</span><div><h2>Where GenAI actually earns its place</h2></div></div>
|
||
|
||
<p class="measure">Ten capabilities, ranked by what they return for what they cost. Note how few of them are "the AI decides" and how many are "the AI turns something unstructured into something the system can already handle."</p>
|
||
|
||
<div class="table-scroll">
|
||
<table>
|
||
<thead><tr><th>#</th><th>Capability</th><th>Leg</th><th>What the model does</th><th>Value</th><th>Effort</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="num">1</td><td><b>Address resolution</b></td><td>last</td><td>Embeds messy Indian address text, clusters duplicates, snaps to confirmed <code>DeliveryProof</code> coordinates. Builds you a private geocoder from your own delivered history.</td><td><span class="pill p-high">highest</span></td><td><span class="pill p-low">low</span></td></tr>
|
||
<tr><td class="num">2</td><td><b>Decision explanation</b></td><td>all</td><td>Turns solver output into a sentence for the dispatcher and a line for the rider. Drives adoption more than accuracy does.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-low">low</span></td></tr>
|
||
<tr><td class="num">3</td><td><b>Unstructured intake</b></td><td>first</td><td>A client's WhatsApp message, email or PDF manifest becomes structured bookings. You support Excel; clients don't always send Excel.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
|
||
<tr><td class="num">4</td><td><b>NL constraint compiler</b></td><td>all</td><td>"Don't give Ravi the mall runs" becomes a typed solver constraint a human approves once and the solver honours forever.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
|
||
<tr><td class="num">5</td><td><b>Exception agent</b></td><td>all</td><td>Rider absent, vehicle down, forty parcels stranded. Tool-calling over a bounded action space, proposing a recovery plan.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">medium</span></td></tr>
|
||
<tr><td class="num">6</td><td><b>Damage detection at inbound</b></td><td>mid</td><td>Vision on the inbound scan photo auto-populates <code>Condition</code>, which is a manual dropdown today and therefore mostly says <code>Good</code>.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-low">low</span></td></tr>
|
||
<tr><td class="num">7</td><td><b>POD verification</b></td><td>last</td><td>Vision checks the delivery photo actually shows a parcel at a door, and that the signature isn't blank. Catches soft fraud.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-low">low</span></td></tr>
|
||
<tr><td class="num">8</td><td><b>Vernacular voice</b></td><td>first, last</td><td>Rider status by speech in Tamil, Telugu, Kannada or Hindi; an outbound call confirming the consignee will be home. Directly attacks first-attempt failure.</td><td><span class="pill p-high">high</span></td><td><span class="pill p-mid">high</span></td></tr>
|
||
<tr><td class="num">9</td><td><b>Operator copilot</b></td><td>all</td><td>Extends the existing console assistant from read-only queries to proposing actions with confirmation.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-mid">medium</span></td></tr>
|
||
<tr><td class="num">10</td><td><b>Scenario simulation</b></td><td>all</td><td>Replays historical days against a proposed policy before it touches production. Your safety net for everything above.</td><td><span class="pill p-mid">medium</span></td><td><span class="pill p-mid">medium</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<p class="measure">Four hubs across Coimbatore, Hyderabad, Bengaluru and Chennai means four language regions. Item 8 is not a nicety in that footprint — a rider who can speak a status update is a rider who actually files one.</p>
|
||
</section>
|
||
|
||
<!-- ============ 02 ============ -->
|
||
<section id="assign">
|
||
<div class="sec-head"><span class="sec-num">02</span><div><h2>Auto-assign, properly</h2></div></div>
|
||
|
||
<p class="measure">This is the thing you asked about, drawn end to end. The deterministic spine runs down the left and never waits on a model. The three GenAI touch points sit beside it, feeding in out of band.</p>
|
||
|
||
<figure>
|
||
<div class="fig-scroll">
|
||
<svg viewBox="0 0 1120 780" role="img" aria-label="Auto-assign architecture. A deterministic spine runs down the left: booking enqueued on NATS, adaptive candidate generation from Redis, constraint set assembled, a vehicle routing solver using the Valhalla matrix, a validator enforcing closed-set selection and tenant isolation, then a committed transaction. Three generative AI services sit to the right and feed in out of band: a natural-language constraint compiler that a human approves once, an exception agent invoked only when the solve is infeasible, and an explainer that writes reasoning after the commit. None of them sit on the critical path.">
|
||
<defs>
|
||
<marker id="g1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
|
||
<marker id="g1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
|
||
<marker id="g1t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/></marker>
|
||
</defs>
|
||
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
|
||
|
||
<rect x="0" y="34" width="580" height="716" rx="4" fill="var(--surface-2)" opacity=".55"/>
|
||
<text x="20" y="22" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" font-weight="600" fill="var(--alt)">DETERMINISTIC SPINE · never waits on a model</text>
|
||
<text x="660" y="22" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" font-weight="600" fill="var(--accent-ink)">GENERATIVE · out of band</text>
|
||
|
||
<!-- spine boxes -->
|
||
<rect x="40" y="52" width="380" height="52" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
|
||
<text x="230" y="83" text-anchor="middle" font-size="12.5" fill="var(--ink)">booking enqueued · NATS</text>
|
||
<line x1="230" y1="104" x2="230" y2="128" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="130" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
|
||
<text x="230" y="154" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">candidate generation</text>
|
||
<text x="230" y="173" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Redis GEOSEARCH · adaptive k</text>
|
||
<text x="230" y="189" text-anchor="middle" font-size="10.5" fill="var(--muted)">widen radius until k found</text>
|
||
<line x1="230" y1="200" x2="230" y2="224" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="226" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
|
||
<text x="230" y="250" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">constraint set assembled</text>
|
||
<text x="230" y="269" text-anchor="middle" font-size="10.5" fill="var(--muted)">capacity · slots · skills · tenant scope</text>
|
||
<text x="230" y="285" text-anchor="middle" font-size="10.5" fill="var(--muted)">+ approved rules from the compiler</text>
|
||
<line x1="230" y1="296" x2="230" y2="320" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="322" width="380" height="82" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.8"/>
|
||
<text x="230" y="348" text-anchor="middle" font-size="14" font-weight="600" fill="var(--alt)">VRP solver</text>
|
||
<text x="230" y="368" text-anchor="middle" font-size="11" fill="var(--alt)">assign + sequence, jointly</text>
|
||
<text x="230" y="386" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">Valhalla matrix · milliseconds</text>
|
||
<line x1="230" y1="404" x2="230" y2="428" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="430" width="380" height="88" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="2"/>
|
||
<text x="230" y="454" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">VALIDATOR — hard gate</text>
|
||
<text x="230" y="474" text-anchor="middle" font-size="10.5" fill="var(--muted)">miler_id ∈ candidate set?</text>
|
||
<text x="230" y="490" text-anchor="middle" font-size="10.5" fill="var(--muted)">tenant scope intact? capacity respected?</text>
|
||
<text x="230" y="506" text-anchor="middle" font-size="10.5" fill="var(--muted)">reject → greedy nearest, always</text>
|
||
<line x1="230" y1="518" x2="230" y2="542" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="544" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
|
||
<text x="230" y="568" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">commitAssignment</text>
|
||
<text x="230" y="587" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">one transaction · Idempotency-Key</text>
|
||
<text x="230" y="603" text-anchor="middle" font-size="10.5" fill="var(--muted)">booking + assignment + miler status</text>
|
||
<line x1="230" y1="614" x2="230" y2="638" stroke="currentColor" stroke-width="1.4" marker-end="url(#g1)"/>
|
||
|
||
<rect x="40" y="640" width="380" height="70" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
|
||
<text x="230" y="664" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">AgentDecision written</text>
|
||
<text x="230" y="683" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Context · Decision · Reasoning</text>
|
||
<text x="230" y="699" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Outcome ← filled later, see §05</text>
|
||
|
||
<!-- GenAI 1: constraint compiler -->
|
||
<rect x="640" y="130" width="440" height="98" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
|
||
<text x="660" y="156" font-size="12.5" font-weight="600" fill="var(--accent-ink)">① NL constraint compiler</text>
|
||
<text x="660" y="176" font-size="11" fill="var(--accent-ink)">dispatcher types: "Ravi shouldn't take mall runs"</text>
|
||
<text x="660" y="194" font-size="11" fill="var(--accent-ink)">→ typed constraint → <tspan font-weight="600">human approves once</tspan></text>
|
||
<text x="660" y="214" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">offline · stored · reused every solve</text>
|
||
<path d="M636,261 H470 V261 H424" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#g1a)"/>
|
||
<path d="M860,228 V261 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4"/>
|
||
|
||
<!-- GenAI 2: exception agent -->
|
||
<rect x="640" y="322" width="440" height="112" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
|
||
<text x="660" y="348" font-size="12.5" font-weight="600" fill="var(--accent-ink)">② Exception agent</text>
|
||
<text x="660" y="368" font-size="11" fill="var(--accent-ink)">invoked only when the solve is infeasible</text>
|
||
<text x="660" y="386" font-size="11" fill="var(--accent-ink)">or an operator escalates</text>
|
||
<text x="660" y="406" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">tools: reassign · split · defer · widen · page human</text>
|
||
<text x="660" y="424" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">proposals go back through the validator</text>
|
||
<path d="M424,378 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#g1a)"/>
|
||
<text x="530" y="370" text-anchor="middle" font-size="10" font-weight="600" fill="var(--accent-ink)">infeasible</text>
|
||
<path d="M860,434 V474 H424" fill="none" stroke="var(--accent)" stroke-width="1.4" marker-end="url(#g1a)"/>
|
||
<text x="600" y="466" text-anchor="middle" font-size="10" font-weight="600" fill="var(--accent-ink)">proposal — never a direct write</text>
|
||
|
||
<!-- GenAI 3: explainer -->
|
||
<rect x="640" y="596" width="440" height="98" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
|
||
<text x="660" y="622" font-size="12.5" font-weight="600" fill="var(--accent-ink)">③ Explainer</text>
|
||
<text x="660" y="642" font-size="11" fill="var(--accent-ink)">solver output → a sentence for the dispatcher,</text>
|
||
<text x="660" y="660" font-size="11" fill="var(--accent-ink)">a line for the rider, in their language</text>
|
||
<text x="660" y="680" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">async · after commit · failure is cosmetic</text>
|
||
<path d="M424,600 H636" fill="none" stroke="var(--accent)" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#g1a)"/>
|
||
|
||
<!-- timing note -->
|
||
<rect x="40" y="726" width="1040" height="30" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
|
||
<text x="560" y="746" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">critical path budget: candidate generation + solve + validate + commit, with no network call to a model in it</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<figcaption><b>Figure 1 — auto-assign with generative AI in its right places</b>Compare with today: a model on the critical path with a 5-second cap, choosing one rider for one parcel. Here the model never blocks a booking, and the thing it produces — a constraint, a recovery plan, an explanation — is something no solver could have produced instead.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Why the constraint compiler is the sleeper</h3>
|
||
<p class="measure">Dispatchers hold dozens of rules in their heads that never reach the system: which rider is slow at a particular gate, which client insists on the same face, which lane floods in monsoon. Today the only way to encode those is a developer ticket, so they stay in the dispatcher's head and the optimiser looks stupid. A model that turns a typed sentence into a reviewable constraint — which a human approves before it ever binds — converts tacit knowledge into solver input at conversational speed. That is a capability the solver cannot have on its own, and it is where generative AI is doing something genuinely irreplaceable.</p>
|
||
|
||
<pre>// what the compiler emits — reviewed by a human, then stored
|
||
{
|
||
"type": "exclude_miler_from_location_class",
|
||
"miler_id": 412,
|
||
"location_class": "mall_service_gate",
|
||
"reason": "slow at gate handover",
|
||
"author": "dispatcher:coimbatore:7",
|
||
"expires_at": null
|
||
}</pre>
|
||
</section>
|
||
|
||
<!-- ============ 03 ============ -->
|
||
<section id="wiring">
|
||
<div class="sec-head"><span class="sec-num">03</span><div><h2>Wiring it into what you have</h2></div></div>
|
||
|
||
<p class="measure">You do not need new infrastructure. Every piece below already exists in the estate; the work is contracts and placement.</p>
|
||
|
||
<div class="table-scroll">
|
||
<table>
|
||
<thead><tr><th>Piece</th><th>Already there</th><th>What changes</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Async invocation</td><td>NATS JetStream, the booking worker, a 60 s budget</td><td>Model calls become subscribers, not inline HTTP. Nothing in a request path waits on one.</td></tr>
|
||
<tr><td>Fallback discipline</td><td><code>AI_LAYER_FALLBACK</code> and the 5 s cap in <code>selectMilerWithAI</code></td><td>Keep the pattern exactly; apply it to every new capability. Best-effort or it does not ship.</td></tr>
|
||
<tr><td>Audit</td><td><code>AgentDecision</code> with a <code>DecisionType</code> discriminator</td><td>One row per model call, whatever the capability. The discriminator was clearly built for this.</td></tr>
|
||
<tr><td>Idempotency</td><td>Redis-backed <code>Idempotency-Key</code> on payment, pickup and deliver</td><td>Extend to every agent-initiated write. An agent that retries must not double-act.</td></tr>
|
||
<tr><td>Vector store</td><td>pgvector, in the database already</td><td>Address embeddings land here. No new dependency.</td></tr>
|
||
<tr><td>Road distance</td><td>Valhalla behind <code>routes.workolik.com</code></td><td>Becomes the solver's cost matrix rather than a post-hoc sequencer.</td></tr>
|
||
<tr><td>Object storage</td><td>presigned uploads for signatures and photos</td><td>The same URLs feed the vision capabilities. Images never transit your API.</td></tr>
|
||
<tr><td>Console surface</td><td>the existing deterministic assistant and intents</td><td>Gains an action tier behind a confirmation step.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<p class="measure">One deliberate constraint: <b>the agent layer never writes to the database directly.</b> It calls the same internal endpoints a human operator would, so tenant scoping, idempotency, validation and audit apply identically whether the caller is a person or a model. If an agent can reach a table your dispatcher cannot, you have built two systems and will secure only one.</p>
|
||
</section>
|
||
|
||
<!-- ============ 04 ============ -->
|
||
<section id="ladder">
|
||
<div class="sec-head"><span class="sec-num">04</span><div><h2>The trust ladder</h2></div></div>
|
||
|
||
<p class="measure">Every capability climbs the same four rungs, and each rung has a numeric gate. Nothing goes to the next rung on a good feeling.</p>
|
||
|
||
<figure>
|
||
<div class="fig-scroll">
|
||
<svg viewBox="0 0 1120 480" role="img" aria-label="A four-rung trust ladder ascending left to right. Rung one, shadow: the model runs and logs but changes nothing, gated on a sufficient volume of logged decisions and a measured agreement rate. Rung two, advisory: the recommendation appears in the console and a dispatcher clicks to apply it, gated on acceptance rate and zero invariant violations. Rung three, auto with veto: the system acts and a dispatcher can undo within a window, gated on a low veto rate and outcome parity with the human baseline. Rung four, autonomous: the system acts and humans handle escalations only.">
|
||
<defs>
|
||
<marker id="l1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
|
||
</defs>
|
||
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
|
||
|
||
<!-- rungs ascending -->
|
||
<rect x="0" y="300" width="250" height="140" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
|
||
<rect x="0" y="300" width="250" height="5" fill="var(--muted)"/>
|
||
<text x="20" y="330" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--muted)">RUNG 01</text>
|
||
<text x="20" y="356" font-size="15" font-weight="600" fill="var(--ink)">Shadow</text>
|
||
<text x="20" y="380" font-size="11.5" fill="var(--ink-2)">runs on every booking,</text>
|
||
<text x="20" y="396" font-size="11.5" fill="var(--ink-2)">logs, changes nothing</text>
|
||
<text x="20" y="422" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: zero</text>
|
||
|
||
<rect x="286" y="230" width="250" height="210" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
|
||
<rect x="286" y="230" width="250" height="5" fill="var(--gold)"/>
|
||
<text x="306" y="260" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--gold)">RUNG 02</text>
|
||
<text x="306" y="286" font-size="15" font-weight="600" fill="var(--ink)">Advisory</text>
|
||
<text x="306" y="310" font-size="11.5" fill="var(--ink-2)">recommendation shown in</text>
|
||
<text x="306" y="326" font-size="11.5" fill="var(--ink-2)">the console; a dispatcher</text>
|
||
<text x="306" y="342" font-size="11.5" fill="var(--ink-2)">clicks to apply it</text>
|
||
<text x="306" y="368" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: a wasted click</text>
|
||
|
||
<rect x="572" y="160" width="250" height="280" rx="3" fill="var(--gold-soft)" stroke="var(--gold)"/>
|
||
<rect x="572" y="160" width="250" height="5" fill="var(--gold)"/>
|
||
<text x="592" y="190" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--gold)">RUNG 03</text>
|
||
<text x="592" y="216" font-size="15" font-weight="600" fill="var(--ink)">Auto with veto</text>
|
||
<text x="592" y="240" font-size="11.5" fill="var(--ink-2)">the system acts; a dispatcher</text>
|
||
<text x="592" y="256" font-size="11.5" fill="var(--ink-2)">can undo inside a window</text>
|
||
<text x="592" y="272" font-size="11.5" fill="var(--ink-2)">before the rider is notified</text>
|
||
<text x="592" y="298" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: bounded & reversible</text>
|
||
|
||
<rect x="858" y="90" width="250" height="350" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-width="1.8"/>
|
||
<rect x="858" y="90" width="250" height="5" fill="var(--alt)"/>
|
||
<text x="878" y="120" font-family="'JetBrains Mono', monospace" font-size="10.5" letter-spacing=".1em" fill="var(--alt)">RUNG 04</text>
|
||
<text x="878" y="146" font-size="15" font-weight="600" fill="var(--ink)">Autonomous</text>
|
||
<text x="878" y="170" font-size="11.5" fill="var(--ink-2)">the system acts; humans</text>
|
||
<text x="878" y="186" font-size="11.5" fill="var(--ink-2)">handle escalations only</text>
|
||
<text x="878" y="212" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">risk: managed by §06</text>
|
||
|
||
<!-- gates -->
|
||
<line x1="254" y1="290" x2="282" y2="290" stroke="currentColor" stroke-width="1.5" marker-end="url(#l1)"/>
|
||
<rect x="150" y="252" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
|
||
<text x="278" y="272" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: 2 000 logged + agreement measured</text>
|
||
|
||
<line x1="540" y1="220" x2="568" y2="220" stroke="currentColor" stroke-width="1.5" marker-end="url(#l1)"/>
|
||
<rect x="436" y="182" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
|
||
<text x="564" y="202" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: acceptance > 80% · 0 violations</text>
|
||
|
||
<line x1="826" y1="150" x2="854" y2="150" stroke="currentColor" stroke-width="1.5" marker-end="url(#l1)"/>
|
||
<rect x="722" y="112" width="256" height="30" rx="15" fill="var(--surface)" stroke="var(--ink)"/>
|
||
<text x="850" y="132" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--ink)">gate: veto < 5% · outcome parity</text>
|
||
|
||
<line x1="0" y1="458" x2="1108" y2="458" stroke="var(--line)" stroke-width="1"/>
|
||
<text x="0" y="476" font-size="11.5" fill="var(--muted)">Any capability can sit on a different rung. Address resolution may be autonomous while assignment is still advisory — that is the point of separating them.</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<figcaption><b>Figure 2 — four rungs, three numeric gates</b>Shadow mode is free and tells you more than any amount of design discussion. Run every new capability there first — including the ones you are confident about, because that is where the surprises are.</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ============ 05 ============ -->
|
||
<section id="loop">
|
||
<div class="sec-head"><span class="sec-num">05</span><div><h2>The loop you already built</h2></div></div>
|
||
|
||
<p class="measure">This is the sharpest finding in the codebase, and it makes everything above cheaper.</p>
|
||
|
||
<figure>
|
||
<div class="fig-scroll">
|
||
<svg viewBox="0 0 1120 420" role="img" aria-label="The AgentDecision feedback loop. Solid lines show what exists: the assignment writes context, decision and reasoning into an AgentDecision row, and an internal PATCH endpoint for recording an outcome is already routed and implemented. A dashed line shows the one missing connection: nothing in the deliver or skip handlers calls that endpoint, so the outcome and outcome-recorded-at columns stay empty and no decision is ever scored.">
|
||
<defs>
|
||
<marker id="m1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
|
||
<marker id="m1a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/></marker>
|
||
</defs>
|
||
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
|
||
|
||
<rect x="0" y="60" width="250" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
|
||
<text x="125" y="92" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">assignment made</text>
|
||
<text x="125" y="112" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">selectMilerWithAI</text>
|
||
<line x1="254" y1="100" x2="300" y2="100" stroke="currentColor" stroke-width="1.5" marker-end="url(#m1)"/>
|
||
|
||
<rect x="304" y="40" width="330" height="120" rx="3" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.8"/>
|
||
<text x="469" y="68" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">AgentDecision row</text>
|
||
<text x="322" y="92" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Context jsonb</text>
|
||
<text x="322" y="110" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Decision jsonb</text>
|
||
<text x="322" y="128" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--alt)">✓ Reasoning text</text>
|
||
<text x="322" y="148" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">✗ Outcome always null</text>
|
||
|
||
<line x1="638" y1="100" x2="684" y2="100" stroke="currentColor" stroke-width="1.5" marker-end="url(#m1)"/>
|
||
<rect x="688" y="60" width="392" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
|
||
<text x="884" y="88" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">PATCH /internal/agent-decisions/:id/outcome</text>
|
||
<text x="884" y="110" text-anchor="middle" font-size="11" fill="var(--alt)">routed · implemented · working</text>
|
||
<text x="884" y="128" text-anchor="middle" font-size="11" font-weight="600" fill="var(--accent-ink)">called by nothing</text>
|
||
|
||
<!-- the missing wire -->
|
||
<rect x="0" y="266" width="250" height="80" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
|
||
<text x="125" y="298" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">parcel resolves</text>
|
||
<text x="125" y="318" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">deliver · skip · RTO</text>
|
||
|
||
<path d="M250,306 H884 V148" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="7 5" marker-end="url(#m1a)"/>
|
||
<rect x="380" y="292" width="330" height="30" rx="15" fill="var(--accent-soft)" stroke="var(--accent)"/>
|
||
<text x="545" y="312" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">the one wire that is missing</text>
|
||
|
||
<text x="0" y="386" font-size="12.5" fill="var(--ink-2)">Column, index, endpoint and route all exist. One call from the resolution handlers turns every past assignment into</text>
|
||
<text x="0" y="406" font-size="12.5" fill="var(--ink-2)">a labelled training example — and gives you the replay corpus every gate in §04 is measured against.</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<figcaption><b>Figure 3 — 90% built, 0% connected</b><code>AgentDecision.Outcome</code>, <code>OutcomeRecordedAt</code>, the controller and the route are all in place. Nothing writes to them. This is the cheapest high-value change available in the whole system.</figcaption>
|
||
</figure>
|
||
|
||
<div class="callout">
|
||
<span class="label">Do this in week one</span>
|
||
<p>Call <code>UpdateDecisionOutcome</code> from the deliver, skip and RTO handlers with a simple verdict — delivered first attempt, delivered late, failed, reassigned. From that day forward every dispatch decision is scored, you can replay history against any new policy offline, and every claim anyone makes about the AI dispatcher becomes checkable. Until then, no gate in §04 can be measured at all.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 06 ============ -->
|
||
<section id="guard">
|
||
<div class="sec-head"><span class="sec-num">06</span><div><h2>Guardrails</h2></div></div>
|
||
|
||
<ul class="plain">
|
||
<li><b>Closed-set selection, enforced server-side.</b> A model returning <code>chosen_miler_id</code> must have that ID checked against the candidate set before commit. Models hallucinate identifiers; a validator costs three lines and removes the entire failure class.</li>
|
||
<li><b>Keep the payload structured.</b> Your current request to <code>routemate</code> carries no customer free text. The moment you pass raw <code>Notes</code>, address strings or client emails into a prompt that also drives actions, you have built a prompt-injection channel — anyone who can type into a booking form can try to instruct your dispatcher. If unstructured text must be read, do it in a separate call that only <em>extracts</em>, and never let that call's output reach an action without validation.</li>
|
||
<li><b>Tenant isolation is an invariant, not a preference.</b> <code>HubStaffAccount.Tenantid</code> already isolates partner freight. Agents must be scoped identically, and cross-tenant leakage should be a hard validator failure, not a scoring penalty. This is the one mistake that is a breach rather than a bad route.</li>
|
||
<li><b>Every agent action is idempotent.</b> You already have the Redis-backed key infrastructure. Agents retry more than humans do; a duplicated reassignment or a double COD entry is far worse than a slow one.</li>
|
||
<li><b>Budget per booking, enforced.</b> Set a token ceiling per decision and per day, with a hard cutoff to the deterministic path. Cost overruns in agentic systems come from retry storms, not from steady state.</li>
|
||
<li><b>Mind what leaves the estate.</b> Consignee phone numbers, COD amounts and precise home coordinates are in scope for whatever model endpoint you call. Decide deliberately what is sent, and prefer IDs over identities wherever the model does not need the human detail.</li>
|
||
<li><b>Escalate rather than improvise.</b> Give the exception agent an explicit "page a human" tool and reward using it. An agent with no honourable way to stop will invent an action instead.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- ============ 07 ============ -->
|
||
<section id="plan">
|
||
<div class="sec-head"><span class="sec-num">07</span><div><h2>Ninety days</h2></div></div>
|
||
|
||
<div class="table-scroll">
|
||
<table>
|
||
<thead><tr><th>Phase</th><th>Ship</th><th>Rung</th><th>Proves</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="num">Wk 1–2</td><td>Wire <code>UpdateDecisionOutcome</code> from deliver / skip / RTO. Build the replay harness over <code>AgentDecision</code>.</td><td>—</td><td>Every later claim becomes measurable.</td></tr>
|
||
<tr><td class="num">Wk 3–4</td><td>Real ETA: Valhalla road time × time-of-day factor + learned dwell, replacing <code>calculateETA</code>.</td><td>direct</td><td>No model needed; immediate accuracy win on all three legs.</td></tr>
|
||
<tr><td class="num">Wk 3–6</td><td>Address resolution on pgvector. Embed delivered addresses, cluster, snap to <code>DeliveryProof</code> coordinates.</td><td>shadow → advisory</td><td>Highest-leverage capability, lowest risk.</td></tr>
|
||
<tr><td class="num">Wk 5–8</td><td>VRP solver behind a flag: batched joint assign + sequence, running in shadow beside today's path.</td><td>shadow</td><td>Head-to-head on the replay corpus against live decisions.</td></tr>
|
||
<tr><td class="num">Wk 7–9</td><td>Explainer on solver output; dispatcher and rider-facing, in-language.</td><td>advisory</td><td>Adoption. Riders follow routes they understand.</td></tr>
|
||
<tr><td class="num">Wk 9–11</td><td>NL constraint compiler with a human approval queue.</td><td>advisory</td><td>Tacit dispatcher knowledge starts reaching the solver.</td></tr>
|
||
<tr><td class="num">Wk 10–12</td><td>Exception agent with a bounded tool set, proposals only.</td><td>advisory</td><td>The escalation queue starts shrinking.</td></tr>
|
||
<tr><td class="num">Wk 12</td><td>Promote whichever capabilities have cleared their gates. Not the ones that haven't.</td><td>→ auto with veto</td><td>The ladder does the deciding, not the roadmap.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="callout teal">
|
||
<span class="label">What is deliberately not in the first ninety days</span>
|
||
<p>Vision on inbound condition and POD, vernacular voice, and the operator copilot's action tier are all worth building — and all of them are easier once the outcome loop, the replay harness and the validator exist. Building those three foundations first is what makes the rest take weeks instead of quarters.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
Grounded in <code>internal/assignment/ai_layer.go</code>, <code>internal/assignment/crm_assignment.go</code>, <code>internal/routing/optimizer.go</code>, <code>models/agentdecision.go</code>, <code>controllers/agentDecisionController.go</code> and <code>routes/routes.go</code> in the nearle-de workspace. Field names, endpoints and existing behaviour are quoted as they appear there.
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
</body>
|
||
</html>
|