Wake Approvisionnement on a stock event instead of a clock

The reactive half of the agent. It consumes product.stock_changed from the
catalog topic and reports only a crossing of the threshold — every further
order line on an already-low reference would otherwise raise the same alarm,
and an agent that repeats itself is one a human learns to ignore. A stock-out
is treated as its own fact even when the reference was already low.

The detection lands on a watchlist readable over a webhook, because a proposal
that reached only an execution log would be lost between two nightly runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-31 07:46:24 +02:00
parent 2bb6faa121
commit 06d0ab5e2d
4 changed files with 210 additions and 0 deletions
+72
View File
@@ -147,3 +147,75 @@ looks exactly like one computed from all of it, so the difference has to be in
the output. A catalog that outgrows one page needs a paging loop **and** the
read-volume cap this repo already lists as mandatory for autonomous agents.
## approvisionnement-stock-watch
The reactive half, and the one the design had been waiting on. It is woken by
`product.stock_changed` on `classic-models.catalog` rather than by a clock, so a
reference that runs low at 14:32 is noticed at 14:32 instead of being reported
at 06:00 the next morning.
Verified end to end against the live catalog:
```
S12_1099 68 → 38 detected: "fell from 68 to 38, below the threshold of 50"
38 → 30 not detected — already below, figures refreshed instead
30 → 0 detected: "is out of stock. Nothing can be promised."
0 → 68 not detected, entry refreshed (a correction, not demand)
POST {"clear":["S12_1099"]} → watching: 0
```
### Only a crossing is news
`before >= threshold && after < threshold`. Without that test, every further
order line on an already-low reference raises the same alarm again, and an agent
that repeats itself is one a human learns to ignore.
A stock-out is treated as its own fact even when the reference was already below
the threshold: *low* and *nothing left to promise* are different problems.
A movement that changes nothing newsworthy still **refreshes** an entry already
being watched. Otherwise the watchlist would show the figure from the moment of
detection, which is the number least likely to still be true.
`reason` carries `order:<n>` for demand and `manual-adjustment` for someone
correcting the books. Both can justify a purchase order; only demand says
anything about the future, so the distinction is recorded rather than filtered
on.
### The detection has somewhere to go
`$getWorkflowStaticData('global')`, keyed by reference, readable over
`GET /webhook/stock-watchlist` and cleared with
`POST {"clear": ["S24_2000"]}`. The purchase *proposal* still has nowhere to be
written — the domain has no purchase order and no supplier entity — but a
detection that only reached an execution log would be lost between two nightly
runs, which would make the reactive half pointless.
The obvious next consumer is `check-availability`: "can we promise X?" is a
better answer when it knows X crossed the threshold at 14:32.
### Kafka details worth keeping
- **The offset moves only on success** (`resolveOffset: onCompletion`,
`allowedStatuses: ["success"]`). A failed execution replays its event rather
than losing it — for an autonomous agent a detection dropped in silence is
worse than one made twice.
- **`fromBeginning: false`.** Otherwise every restart replays the whole
retention window and re-detects crossings dealt with days ago.
- Most messages on the topic are not `stock_changed`, and the workflow says so
explicitly and succeeds. A workflow that threw on an uninteresting event would
replay it forever.
- The consumer has its **own** SCRAM account,
`classic-models-agent-approvisionnement`: Read on that one topic and that one
consumer-group prefix. It cannot read sales or payments, and it cannot write —
so a compromised workflow can raise no event another agent would act on.
### Two traps
- **n8n's Kafka credential has a `ca` field.** The cluster CA goes there, so
there was no need to touch `NODE_EXTRA_CA_CERTS` or to disable verification.
Not obvious from the node's UI, which shows it only once `ssl` is on.
- **A webhook declaring several methods has one output per method**, in the
order declared. Wiring only the first silently drops every POST: the execution
reports **success** and n8n answers `No item to return was found`.