95b6e3a1c9
Recording where the work lands and the two facts about that codebase that shape it: every write passes through one BaseModelViewSet, so one hook covers every domain; and the models are managed = False, so a transactional outbox introduces the estate's first Django-managed table beside the pre-existing schema. Emission belongs in the API rather than a database CDC or a gateway plugin, because only the API knows which change is a business event and only it holds the sub/azp that the event must carry for a downstream agent to act under the same authority. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
6.7 KiB
Markdown
155 lines
6.7 KiB
Markdown
# Flows — the autonomous agents
|
|
|
|
**Runtime:** n8n · **Mode:** autonomous · Two agents in one repo.
|
|
|
|
| Agent | Client id | Trigger |
|
|
|---|---|---|
|
|
| Approvisionnement | `classic-models-agent-approvisionnement` | event-driven, plus a periodic ordering run |
|
|
| Recouvrement | `classic-models-agent-recouvrement` | weekly, Monday |
|
|
|
|
They share a repo because they are not code: n8n workflow exports, same tool,
|
|
same owner. Splitting them would buy nothing.
|
|
|
|
## Why low-code for these two
|
|
|
|
Their work is rules, thresholds and due dates — a sequence of conditions and
|
|
calls, which a visual editor expresses better than code. The LLM is optional on
|
|
their critical path, which makes them cheaper and more predictable than the
|
|
conversational agents.
|
|
|
|
They remain full A2A agents: they publish an Agent Card, accept tasks, answer.
|
|
|
|
## Approvisionnement
|
|
|
|
| | |
|
|
|---|---|
|
|
| MCP | catalog, sales |
|
|
| Scopes | `catalog:read`, `catalog:write`, `sales:read` |
|
|
|
|
Detect coming stock-outs, propose reorders — **two jobs with two different
|
|
triggers**, which an earlier revision of this file collapsed into one nightly
|
|
run.
|
|
|
|
*Detection is reactive.* It depends on something happening: an order placed, a
|
|
stock level crossing a threshold. Finding that out up to a day later is latency
|
|
chosen for nothing, and a nightly sweep re-reads the whole catalog to rediscover
|
|
what the domain already knew at the moment it changed.
|
|
|
|
*Proposing a purchase order is periodic.* Nobody reorders on every unit sold;
|
|
orders are grouped by supplier lead time and buying cycle. A clock is the right
|
|
trigger there, and that run is periodic on purpose rather than a batch standing
|
|
in for events.
|
|
|
|
Worth naming, because it changes what has to be built: **the autonomous mandate
|
|
is a cost of the trigger, not of the business need.** `offline_access`, a
|
|
refresh token stored encrypted and an exchange at trigger time are required only
|
|
because nobody is present at 3am. An agent woken by a domain event runs moments
|
|
after a human acted.
|
|
|
|
**Exposes** `check_availability` — called by Ventes before promising a delivery
|
|
date, and by Catalogue before discontinuing a reference. Returns a decision and a
|
|
date, never the stock table.
|
|
|
|
## Recouvrement
|
|
|
|
| | |
|
|
|---|---|
|
|
| MCP | customers, payments |
|
|
| Scopes | `customers:read`, `payments:read`, `payments:write` |
|
|
|
|
Weekly: unpaid invoices past 30 days, dunning, escalation.
|
|
|
|
**Exposes** `check_creditworthiness` — called by Ventes before accepting a large
|
|
order.
|
|
|
|
**Dunning is prepared automatically and sent only after human validation.**
|
|
Sending a letter to a customer is an outward-facing action; a rule bug would
|
|
otherwise write to the entire customer base in one run.
|
|
|
|
## The A2A server gap
|
|
|
|
The available n8n community nodes ([n8n-nodes-a2a](https://github.com/agentic-layer/n8n-nodes-a2a),
|
|
[n8n-nodes-agent2agent](https://github.com/pjawz/n8n-nodes-agent2agent)) make n8n
|
|
an A2A **client**: `Discover Agent`, `Send Task`.
|
|
|
|
But both agents here must be **servers** — Ventes calls them. That needs an
|
|
Agent Card on `/.well-known/agent.json` and a JSON-RPC endpoint with the task
|
|
lifecycle. n8n webhooks can do it by hand, at which point it stops being no-code.
|
|
|
|
Preferred approach: **Kong in front of n8n**, exactly as it fronts every other
|
|
resource. Kong serves the Agent Card statically and routes JSON-RPC to the
|
|
webhook, and token verification lands where it already lives. No new component.
|
|
|
|
Alternative: a small FastA2A adapter, written once for both — see
|
|
`classic-models-agent-catalogue`, which uses FastA2A already.
|
|
|
|
## The autonomous mandate
|
|
|
|
No user session exists when these run, so authority must be pre-established: the
|
|
user obtains `offline_access` when creating the mandate, the refresh token is
|
|
stored encrypted, and the exchange happens at trigger time. `sub` stays the user
|
|
who mandated, `azp` says which agent acted.
|
|
|
|
The MCP servers see no difference between an autonomous and a conversational
|
|
call — same `sub`, same `azp`, same audiences. That is deliberate: downstream
|
|
policy must not need to know the trigger mode.
|
|
|
|
## Backlog
|
|
|
|
**Events over Kafka, emitted by `classic-models-api`.** The target for the
|
|
reactive half. The estate emits no domain event today — the API has no signals
|
|
and no outbound webhook, and there is no bus in the cluster — which is what made
|
|
a nightly sweep look like the only option.
|
|
|
|
The emission belongs **in the API**, not in a database CDC and not in a gateway
|
|
plugin. Only the API knows which change is a business event rather than a row
|
|
update, and only it holds the identity that made it — `sub` and `azp` from the
|
|
caller's token, which the event has to carry if a downstream agent is to act
|
|
under the same authority as the person who triggered it.
|
|
|
|
Two facts about that codebase shape the work:
|
|
|
|
- Every write goes through one `BaseModelViewSet` in
|
|
`api/v1/classicmodels/views.py`. One hook there covers products, orders,
|
|
payments and the rest — Django signals scattered across models would be the
|
|
harder version of the same thing.
|
|
- **The models are `managed = False`.** The tables are the pre-existing
|
|
classicmodels schema and Django owns no migrations for them. A transactional
|
|
outbox means introducing the estate's first managed table beside unmanaged
|
|
ones — which works, and is worth deciding deliberately rather than
|
|
discovering.
|
|
|
|
An outbox is the part that matters: emitting from a viewset after `save()` drops
|
|
the event if the process dies between the two, and an agent that never hears
|
|
about a stock movement is worse than one that hears late.
|
|
|
|
Until this exists, detection stays unbuilt rather than being approximated by a
|
|
periodic scan: a batch that pretends to be reactive is worse than an absent
|
|
feature, because it looks finished.
|
|
|
|
The periodic ordering run does not wait on any of it.
|
|
|
|
## Two controls that become mandatory here
|
|
|
|
They were deferrable with a human in the loop. They are not, once an agent
|
|
loops on its own.
|
|
|
|
- **Read volume caps per `(azp, sub)`**, enforced on Kong. An agent reads a
|
|
thousand records in seconds where a human clicks ten; a scope authorises the
|
|
read but says nothing about volume.
|
|
- **Authorisation on tool-call parameters**, not just the tool name. `write`
|
|
authorises writing, not "this €40k transfer".
|
|
|
|
An autonomous agent hitting a rule that requires approval must **fail cleanly
|
|
and escalate**, never decide. And any approval screen must be rendered from the
|
|
structured tool-call parameters — never from a sentence the model produced, or a
|
|
prompt injection writes what the human validates.
|
|
|
|
## Before writing workflows
|
|
|
|
Read `classic-models-agent-platform/CLAUDE.md` for the identity model, the
|
|
endpoints and the known blockers.
|
|
|
|
Deliverables in English. Export workflows as JSON and commit them; do not rely
|
|
on the n8n instance as the source of truth.
|