UAS is the "hands" of Sourcy's sourcing stack. Given a request it generates
specs, drives an agentic search ⇄ verify loop over 1688 / Alibaba, then cleans, matches,
gates and ranks products into a shortlist — and hands it back to the conversational Brain (agent-core).
TL;DR: The LLM agent owns only judgment — which keyword, when to
stop, which tool. The deterministic stages own the craft — price gate, cleanup, spec match, hard
gates, ranking. All state lives in one in-memory RunContext blackboard; every LLM call self-meters
cost into a CostLedger; an eval harness replays the real pipeline against cached fixtures so
quality & spend are measurable without paying per run. Jump to the
yellow-bottle simulation →
1
The two-brain model
Who decides what vs who finds the how
agent-core is the conversational Brain: it understands the customer and decides what to
source. UAS is the Hands: it owns the craft of actually finding strong products. They communicate
over an async HTTP + webhook contract — UAS replies 202 + jobId immediately and posts a
job_completed webhook when the run finishes.
Solid = sync request · dashed = async webhook
2
Module map
Layered src/ — data flows down the stack, results flow up · click a layer
Click any layer for its role ↑ · lower layers never import upper layers
Click a layer above
Each layer's responsibility and key files will appear here.
3
The sourcing pipeline
One product flows left→right · the agent loops the search box until a good batch exists · click a stage
Click any stage for files + the in/out contract ↑
Click a stage above
Each stage's file, function and data contract appears here.
Stage-by-stage contract
Stage
File · function
In → Out
specs
stages/specs/generate.ts · generateSpecs() or sr-context.ts · adopt Brain specs
A Mastra tool-calling agent on OpenAI · stateless tools mutate one shared blackboard
Production (Brain job) runs a 3-tool search agent (generate_specs, search_products,
verify_candidates) then a deterministic back-half. The CLI/eval /source path runs the
full 6-tool SourcingBrain that drives the entire pipeline itself. Either way the model only chooses
which tool, what keywords, when to stop — the craft lives in the stages.
Tools are thin wrappers over phases.ts → stages/* · all state lands in the blackboard
Why a blackboard? Tools return nothing the agent has to thread through —
they mutate RunContext. The model carries only decisions. Because state serializes to
uas_blackboards, a finished run stays resumable: the P2 /guide channel can
relax_veto, expand_keywords, re_source or request_replacements
without re-sourcing from scratch.
5
Deep dive · the price gate
evaluatePrice() — deterministic, no LLM, generous by design
The gate parses the SR's target_price string, converts currencies through an FX table, and
compares the product's cheapest variant against target × (1 + tolerancePct/100)
(default tolerance 10%). It is fail-open: a missing price or an unknown currency is never
excluded. Exclusions are reversible — they sit in run.priceExcluded and can be released.
Three fail-open exits before any exclusion · the only hard "no" needs a comparable, over-limit price
Input
Result
Why
target $1.50, product min 10 CNY
within
≈$1.40 ≤ limit $1.65
target $1.50, product min 12 CNY
excluded
≈$1.68 > limit $1.65 → "exceeds by 12%"
target $1.50, product currency XYZ
kept
unknown currency → not comparable → fail-open
no target_price on SR
kept
gate never runs
Parsing is lenient.parseTargetPrice() reads
"5 USD", "$5", "0.2 CNY" or bare "5"; RMB→CNY is
normalised; a bare number assumes the config default currency. Comparison always uses the product's
minimum price so a product is only excluded when even its cheapest tier is over budget.
6
Deep dive · cleanup
One consolidated Gemini Flash-lite call + a concurrent item-detail fetch
The Python product-processing pipeline ran ~8 sequential Gemini steps per product (translate, extract,
dedupe, normalise, categorise…). UAS collapses those into one structured Flash-lite call that returns
a translated, summarised, categorised product with a deduped attribute list — and runs it concurrently
with a TMAPI item-detail fetch so resolving SKU variants adds no extra wall-clock time.
The LLM call and the SKU/supplier fetch overlap — cleanup costs one Flash-lite call of latency, not two
why weight always ≥ 0.1 kg
contract safety
agent-core's SR-item migration throws "Missing required fields" on a falsy
weight_per_unit_kg. applyEstimatedDimensions() always falls back to
0.1 kg so the migration contract holds even when the LLM estimates nothing.
why the OR on isFactory
supplier signal merge
1688 carries the factory flag in the search payload; Alibaba only exposes it via
item-detail's company_type. Cleanup ORs both sources so Alibaba sellers don't all default to
"trader" — which would silently sink them in supplier scoring.
cache global catalog reuse
skip-clean across SRs
Cleaned products are keyed platform:itemId in uas_products
and gated by SOURCING_CATALOG_VERSION. A product seen in a prior SR returns instantly —
bump the version to force a re-clean of everything.
7
Match · vision recheck · gates · rank
Where verdicts and the final order are decided
matchProduct() scores every veto + rerank spec, then runs a vision recheck: for any
image-source veto whose text verdict is match or unknown, a stronger Gemini Vision model
looks at the product photo — the image's verdict wins on colour / shape / finish. Then the hard gates
fire and ranking orders what survives.
A strong product isn't starved by one unknown veto — only a confirmed NOT_MATCH eliminates
Recall tuning. Because confidence is a weighted ratio of confirmed
vetoes (not a count), a product that confirms its high-weight vetoes can still shortlist even with a single
unconfirmable low-weight veto — instead of being demoted to "possible" forever. The vision recheck exists to
convert those "unknown" colour/shape vetoes into confirmed yes/no from the photo.
8
LLM stack & cost control
Every call flows through one chokepoint that self-meters spend
Stage
Model
Tier · role
orchestrator (agent)
gpt-4.1-mini · OpenAI
reasoning · tool-calling
spec-gen · match
gemini-3.x-flash
structured generation
cleanup · verify · gates
gemini-3.x-flash-lite
cheap bulk work
vision recheck
gemini-2.5-flash
image truth on visual vetoes
All calls go through llm/call.ts (runObject / runText), which enforces
the 60s timeout, retries and per-run token budget, then writes a UsageEntry into the per-run
CostLedger (carried via AsyncLocalStorage). The ledger rolls up byStage and byModel
into SourcingResult.cost — surfaced on the dashboard and in every eval report.
9
The eval harness
Replays the real pipeline against cached fixtures · judges always live
SR buckets test the hard cases: don't fabricate matches for unsourceable/niche · resolve every customer-selection pick
10
Simulation · "a yellow 100 ml water bottle"
Step-by-step, the way the live system would actually behave
Suppose the Brain sends UAS a sourcing job: "yellow 100 ml water bottle" with
target_price: "1.5 USD" and one reference photo. Here is what happens, stage by stage. Note the
twist 100 ml introduces — at that size, marketplaces surface perfume atomisers and
spray bottles, which the type gate is built to catch.
The funnel — counts dropping through the pipeline
Illustrative counts — the shape (broad search → screened → gated → ranked) is the real behaviour
Step by step
The same blackboard accumulates through every step; the cost ledger totals each LLM call as it goes
What the Brain receives. A ranked shortlist (6 confirmed + 10 possible),
a priceExcluded list of 2 withheld-but-recoverable bottles, per-product veto/rerank detail, and a
cost summary. If suppliers later go quiet, the Brain can call /guide request_replacements and UAS
serves more from its retained product bank — no re-sourcing.
If it were unsourceable. Had the request been a niche branded item,
the agent would loop search, hit the keyword-expansion fallback (≤5 new keywords), find nothing confirmable,
and return an honest no_verified / all_eliminated outcome rather than fabricating a
shortlist — exactly what the eval's "unsourceable" bucket checks for.
?
FAQ
Common questions
Why two agents (3-tool vs 6-tool)?
The production Brain job uses a 3-tool search agent then a deterministic back-half (cleanup →
match → gates → rank) — that path is predictable and cheap to reason about. The 6-tool SourcingBrain
(used by CLI/eval//source) lets the model drive the whole pipeline, which is useful for
exploration but less controllable. Same stages underneath.
Why is the price gate reversible instead of just deleting over-budget products?
Budget is the Brain's call, not UAS's. UAS withholds over-budget bottles into priceExcluded
so the Brain (or a human) can release them — e.g. if nothing else qualifies, a 12%-over bottle may be
acceptable. Deleting them would throw away recoverable options.
Why does the image override the text on colour?
Marketplace titles lie or mistranslate ("yellow" for amber/gold). For visual vetoes — colour, shape,
finish — the product photo is ground truth, so the vision recheck's verdict wins. Text-confirmed
NOT_MATCH is never re-checked (already out); "unknown" and "match" get the photo's verdict.
Why do fixtures "self-heal" on replay instead of failing?
Agent tool-choices are non-deterministic, so a perfectly recorded fixture can miss when the agent picks
a slightly different keyword. Rather than fail the eval, the harness goes live for that one call, records
it, and prints a warning — so the suite stays runnable while surfacing drift.
How is cost tracked without instrumenting every stage?
Every model call funnels through llm/call.ts, which writes a usage entry into a per-run
CostLedger carried on AsyncLocalStorage. Stages don't track cost themselves — they just call
the chokepoint, and the ledger rolls up byStage/byModel automatically.