Files
factory/vibe/PRD/ai-back-office/agent-architecture.md
arcodangeandClaude Fable 5 42bc8ee42c docs(prd): backlog sharpened for cold sessions — execution footers, splits, D9 parked
- STATUS resume protocol: milestones ordered by due date, skip
  human-gated tops, named entry points (erp#38 / #51 / write-skill
  quartet); every issue now carries an Execution footer.
- Backlog map: +erp#59 (T14 split from #48), +erp#60 (T11 loop split
  from #54), retitles, post-replay markers.
- D9 meeting capture parked (nice-to-have; calls are iPhone-first).
- prd_check.py preserved from the session scratchpad into scripts/
  (the closure protocol references the pattern — now it's runnable).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-12 14:07:16 +02:00

198 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[vibe](../../README.md) > [PRD](../README.md) > [AI back-office](README.md) > **Agent architecture**
# Agent architecture — atoms, contracts, gates
> **Status:** In design
> **Last Updated:** 2026-07-11
> **Up:** [AI back-office hub](README.md)
> **Related:** [Task inventory](task-inventory.md) · [Model fleet](model-fleet.md) · [Challenges](challenges.md) · [ADR 0003 — sandbox state lifecycle](../../ADR/0003-sandbox-state-lifecycle.md)
## Design principles
1. **Atoms, not monoliths.** Each capability (classify, extract, validate, record, reconcile, report, remind) is one narrow agent with a strict I/O contract. Workflows are compositions of atoms with explicit gates — never one prompt that "does the accounting".
2. **The LLM proposes, code disposes.** Formats, arithmetic, checksums, dedup, and referential integrity are enforced by deterministic validators. A model output that fails validation is quarantined, never auto-corrected.
3. **Data is never instructions.** Inbound content (mails, PDFs, bank labels) flows through typed fields; extraction atoms hold zero credentials and zero action tools.
4. **Writes are rehearsed, gated, and replayable.** The only path to prod mutation is manifest → sandbox rehearsal → human approval → gated promote ([ADR 0003](../../ADR/0003-sandbox-state-lifecycle.md)).
5. **Silence is an alert.** Every standing loop heartbeats; a quiet fleet must be provably quiet, not possibly dead.
6. **Earn autonomy.** Levels ([A0A3](README.md#the-autonomy-ladder)) are granted per-atom from measured evals and revoked on incident ([QA strategy](qa-strategy.md)).
## Atom contract
Every atom is registered in a versioned YAML registry (git) with:
| Field | Meaning |
| --- | --- |
| `name`, `version` | Identity; version bumps on any behavioral change (re-triggers evals). |
| `input_schema` / `output_schema` | JSON Schema; enforced at runtime (constrained decoding where the tier supports it). |
| `invariants` | Deterministic post-conditions (e.g. `HT + TVA == TTC ± 0.01`). |
| `side_effect_class` | `read` · `draft` · `write-sandbox` · `write-prod` · `outbound` — drives which gates apply. |
| `idempotency_key` | How a replay is recognized (e.g. supplier + `ref_supplier` + TTC). |
| `autonomy` | Current earned level (A0A3) + link to the eval evidence. |
| `model_policy` | Preferred tier, fallbacks, escalation rule ([model fleet](model-fleet.md)). |
| `eval_ref` | Golden set + scoring script for this atom. |
The registry is the source of truth for what the fleet may do; an atom absent from the registry does not run. File layout, prompt syntax, and the full agent-facing document surface (`AGENTS.md`, `SKILL.md`, `atom.yaml`, `prompt.md`, profile files) are specified in the [agent catalog](agent-catalog.md#the-document-surface-agents-read).
## The pipeline shape
Every workflow instantiates the same stage skeleton (skipping stages it doesn't need):
**watch → classify → extract → validate → stage → approve → apply → verify → journal**
The flagship instance — supplier invoice end-to-end ([T01](task-inventory.md#t01--mailbox-triage--routing)→[T03](task-inventory.md#t03--supplier-invoice-recording), POC-1):
```mermaid
%%{init: {'theme':'base'}}%%
flowchart TB
mail["Zoho books@<br>new message"]:::src
triage["T01 classify<br>(Pi tier, constrained)"]:::proc
extract1["T02 extract A<br>(M4 local)"]:::proc
extract2["T02 extract B<br>(Mistral EU)"]:::proc
agree{"critical fields<br>agree?"}:::gate
escal["escalate<br>(Claude tier)"]:::proc
valid["deterministic validators<br>arithmetic · rates · SIREN · IBAN · dedupe"]:::gate
quarantine["quarantine queue<br>(review in digest)"]:::store
manifest["T03 manifest + sandbox rehearsal<br>predicted-delta check"]:::proc
card["Telegram approval card"]:::gate
promote["gated promote to prod<br>(human key + confirm)"]:::gate
ged["attach PDF (GED)<br>re-read + snapshot delta"]:::proc
journal["run journal<br>+ golden-set feedback"]:::store
mail --> triage --> extract1
triage --> extract2
extract1 --> agree
extract2 --> agree
agree -- "no" --> escal --> valid
agree -- "yes" --> valid
valid -- "fail" --> quarantine
valid -- "pass" --> manifest --> card --> promote --> ged --> journal
quarantine --> journal
classDef src fill:#2563eb,stroke:#1e40af,color:#fff
classDef proc fill:#059669,stroke:#047857,color:#fff
classDef store fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef gate fill:#b45309,stroke:#92400e,color:#fff
```
1. A new message on `books@` is classified by the **T01 sentinel** (Pi tier, schema-constrained output).
2. The PDF is extracted **twice independently** — locally on the M4 and on the Mistral EU cloud.
3. Critical fields (amounts, IBAN, ref, dates) must **agree exactly**; disagreement escalates to the Claude tier; still-ambiguous items stop here.
4. **Deterministic validators** check arithmetic, VAT rates, SIREN/IBAN checksums, and duplicates; any failure lands in the **quarantine queue**, surfaced in the digest.
5. A **write manifest** is rehearsed on the sandbox and its result re-read and compared to the draft (predicted-delta check).
6. The human gets a **Telegram approval card**; approval triggers the **gated promote** to prod (human-held key + explicit confirm).
7. The source PDF is **attached in the GED** (Dolibarr's document store), the write is verified by re-read + snapshot delta, and the full run is **journaled** — rejections and corrections feed the golden set.
## Write safety (inherited, not reinvented)
[ADR 0003](../../ADR/0003-sandbox-state-lifecycle.md) already delivers the hard part, proven live on the erp repo:
- **Sandbox host-guard**: the write skill structurally refuses any host that is not `erp-sandbox` — a sandbox atom *cannot* mutate prod.
- **Manifests with portable refs**: `@ref` (created earlier in the run) and `#entity:field=value` business-key lookups (aborts on 0 or >1 match — never guesses ids).
- **Gated promote**: `promote-plan` (human-readable review) → `promote-apply --target prod` requiring the prod write key from ENV only (never stored) + an explicit confirm variable.
- **Iso-prod checkpoints**: the sandbox is re-seedable from prod at will, so rehearsals run against *today's* real state.
This PRD adds around it: idempotency keys on every write atom, predicted-delta assertions (rehearse → re-read → compare *before* asking for approval), pre/post snapshots ([T13](task-inventory.md#t13--erp-snapshot--drift-detection)), a **compliance linter** in `promote-plan` (a manifest with any operation outside the [ledger grammar](compliance.md#the-ledger-grammar-production) never reaches the approval card), and approval cards as the human interface to the gate.
### Anti-hallucination contract for agent writes
No value reaches the books because a model "remembers" it. Four mechanical layers, all deterministic:
1. **No free-text critical fields.** Amounts, dates, refs, IBANs and transaction ids are *copied by code* from the validated extraction payload or the bank feed into the manifest — the orchestrating model routes and assembles; it never re-types a value it read.
2. **Provenance per critical field.** Write manifests carry a source anchor per critical field — `{source_sha256, raw_excerpt}` — and a deterministic checker re-extracts the source text (pdftotext / feed pull) and asserts the excerpt exists and parses to the same value (locale-normalized: `219,50``219.50`, `2,147``2147.00`). A value not literally present in its source cannot be promoted.
3. **Cross-system corroboration.** Every payment amount must equal its bank-feed movement to the cent, against a **fresh** pull at check time (never a cached copy); arithmetic (`HT + TVA = TTC ± 0.01`), checksums (SIREN, IBAN mod-97) and dedupe keys apply regardless of source.
4. **Read-back closes the loop.** Predicted-delta on the sandbox and post-write verification on prod prove that what was *written* equals what was *checked* — source → manifest → ERP, corroborated at every hop.
A failed check refuses; it never repairs. Proven in practice: the 2026-07 books-regularization pack shipped with a standalone `verify-provenance` checker (36 field-level checks against the source PDFs and fresh Qonto/Wise pulls, run before the human gate) — [POC-1](poc-plan.md#poc-1--supplier-invoice-end-to-end) industrializes it as a linter stage alongside the ledger grammar.
## Environments — sandbox vs production
The environment split is not an implementation detail — it is both the **safety** device (ADR-0003) and the **compliance** device ([compliance](compliance.md)): the sandbox may host any experiment because its state is disposable; production is held to append-only ledger discipline because it *is* the books.
| | **Production** (`erp.arcodange.lab`) | **Sandbox** (`erp-sandbox.arcodange.lab`) |
| --- | --- | --- |
| Role | the ledger — book of record | rehearsal, POCs, evals, drills |
| State | permanent, append-shaped only | disposable; re-seeded **iso-prod** on demand (`arcodange sandbox checkpoint refresh`) |
| Credentials | read-only `ai_agent`; prod write key human-held, ENV-only at promote time | write-scoped `ai_agent_sandbox`, host-guarded (structurally cannot reach prod) |
| Ledger grammar | **enforced** (linter + locking + snapshot detection) | exempt — but manifests destined for prod are linted *before* rehearsal |
| Third parties | real (Qonto/PA, Zoho, Telegram) | **never wired to production externals**: no PA emission, no outbound mail — side channels are stubbed or blackholed |
Every atom's `side_effect_class` maps to an environment posture:
| `side_effect_class` | Runs against | Credential |
| --- | --- | --- |
| `read` | prod (and sandbox for evals) | read-only `ai_agent` |
| `draft` | no ERP at all | none |
| `write-sandbox` | sandbox only | `ai_agent_sandbox` (host-guarded) |
| `write-prod` | prod, **only** through the promote gate | human-held key + explicit confirm |
| `outbound` | production channels | allowlisted recipients, human-gated |
Standing rules: **every POC's write legs run on the sandbox** and enter prod only through the gate with a real approval; ERP-dependent **eval runs target a fresh checkpoint** (iso-prod refresh = a reproducible fixture); restore drills and game-days land on the sandbox by construction ([T14](task-inventory.md#t14--backup--restore-verification), [QA strategy](qa-strategy.md#ops-qa)); anything designed to be irreversible in prod (e.g. Dolibarr's BlockedLog module) is trialed on a checkpoint first, because the sandbox provides exactly the reversibility production denies.
## Security model
- **Least privilege per atom.** Extraction and classification atoms hold no credentials at all. Read atoms use the read-only `ai_agent` key. Sandbox writes use the sandbox-only agent. The prod write key exists only in the human's hands at promote time.
- **Ephemeral scoped ERP workers.** For orchestrated batches, the orchestrator mints short-lived Dolibarr users scoped to the subtask (`supplier-ingest`, `bank-reconciler`, `readonly` — the `PERMISSION_SCOPES` pattern prototyped in erp `test/orchestratorExample.ts` + `test/scripts/admin/permissions.ts`), and deletes them when the batch ends. A leaked worker key is narrow and already dead.
- **Secrets discipline.** All standing credentials live in Vault (house pattern, VSO-injected); skill `.env` files are mode-600 and gitignored; agents never echo credentials into journals or prompts.
- **Blast-radius honesty.** Bank access is read-only by construction (no payment-initiation scopes are ever requested). The mailbox OAuth is read-only. The single irreversible surface is prod ERP writes — hence the gate.
## Prompt-injection defenses
Inbound documents are adversarial by default — an invoice PDF or a mail body can contain text addressed to an LLM. Defense in depth:
1. **No-tool extraction**: atoms that read untrusted content can only emit schema-constrained JSON — there is nothing to hijack.
2. **Typed handoffs**: downstream atoms receive extracted *fields*, never raw document text; the raw source travels as an opaque attachment (hash-addressed) for human eyes.
3. **Instruction-shaped content is a finding**: validators flag imperative/LLM-addressed text in extracted fields; such items are quarantined and surfaced verbatim to the human.
4. **Action allowlists**: outbound mail only to allowlisted recipients; calendar mutations sourced from mail content require human confirmation ([T11](task-inventory.md#t11--compliance-calendar--reminders)).
5. **Injection fixtures in evals**: every extraction atom's golden set includes adversarial documents; a regression here blocks autonomy promotion ([QA strategy](qa-strategy.md)).
## Runtimes & scheduling
| Runtime | Runs | Scheduling | Notes |
| --- | --- | --- | --- |
| **k3s cluster (Pis)** | T01 sentinel inference, T11 reminders, T13/T14 verifications, queue + gateway | CronJobs + long-running Deployments (ArgoCD apps per the lab's `<app>` join-key convention) | Proven pattern: the erp backup CronJob. No LLM heavier than the Pi tier. |
| **M4 MacBook** | T02/T16 local extraction, T09 report, T17 vault capture/retrieval, interactive Claude Code sessions (the atom factory) | **hermes cron ticker** (already driving the vault jobs) + on-wake queue drain | **Not a server**: availability model in [model fleet](model-fleet.md); time-critical work must not depend on it. hermes = the local agent runtime (skills, cron, the Ornith model). |
| **Cloud APIs** | Mistral extraction/OCR; Claude reasoning steps (headless `claude -p` / Agent SDK) | invoked by pipeline stages | Budget-capped; degraded modes defined. |
| **telegram-gateway** | digests, approval cards, human commands | webhook-driven | Roadmapped phases (durable Postgres queue, async handlers) are exactly what the fleet needs — see open decisions. |
**Work queue.** Pipeline stages communicate through a durable queue with dead-letter semantics (an item that fails N times parks in the DLQ and appears in the digest). Start minimal; the queue technology is an open decision below.
**Graduation path.** New atoms are prototyped as Claude Code skills (fast iteration, human in the loop), then frozen into deterministic scripts + tests once stable — the house already does this (`.claude/skills/` scripts wrapped by `bin/arcodange`). Claude-tier involvement in a mature atom shrinks to escalation handling.
## Human channel
The channel is **hermes's telegram-gateway whenever it is available** (operator direction, 2026-07): the gateway runs on the cluster, so digests and approval cards are served 24/7 without depending on the laptop being awake — the M4-side hermes runtime consumes the same gateway for its own jobs. When the gateway is down, the fleet keeps queueing, the digest falls back to plain email, and the [degraded-modes table](model-fleet.md#degraded-modes) applies.
- **One daily digest** (Telegram, morning): items awaiting approval, quarantined items, aging unresolved work, heartbeat summary, upcoming deadlines (D-30/D-7/D-1). An empty day still sends "all green" — silence must be distinguishable from failure.
- **Approval cards**: one decision per card (approve / edit / reject-with-reason); rejection reasons are first-class data feeding golden sets.
- **Escape hatch**: every automated lane has a documented manual runbook fallback (the fleet augments the operator; it never becomes the only way to run the company).
## Knowledge layer — the second brain
The operator's second brain is already in place and already agent-integrated: a **PARA Obsidian vault** (`00-Inbox``06-Zettel`), git-synced to the forge ([arcodange/SecondBrain](https://gitea.arcodange.lab/arcodange/SecondBrain)) via obsidian-git, exposed to agents through `mcp-obsidian` (local REST API), and automated by `.automation/sb.py` (weekly digest, inbox triage, daily prefill, idempotent Gitea→Inbox ingest) scheduled on the **hermes cron ticker** — with **Ornith**, hermes's local reasoning model (`127.0.0.1:18080`), as the confidential/offline lane. The vault even declares its own AI routing doctrine — *Claude by default, Mistral for well-defined tasks, Ornith/hermes for the confidential* — which is precisely the policy the [model fleet](model-fleet.md) generalizes.
The integration contract ([T17](task-inventory.md#t17--knowledge-capture--retrieval-second-brain)):
- **Division of truth:** the ERP is the *book of record*; the vault is *context and institutional memory* (contract nuances, client history, decisions, REX). No accounting fact is authoritative in the vault.
- **Capture:** fleet outputs worth remembering land as **append-only inbox/area notes with idempotent frontmatter** — the pattern the Gitea ingest already proves; human-authored notes are never edited in place.
- **Retrieval:** context-hungry atoms query the vault and carry facts *with their note dates* — notes are **trusted-but-stale**: anything contradicting the ERP, or older than its subject's last change, triggers re-verification rather than belief.
- **Rails reused, not rebuilt:** M4-side access is direct filesystem + `mcp-obsidian`; the weekly digest and the human's PARA filing ritual remain the curation loop; cluster-side access is open decision [D7](#open-decisions).
- **Client dossiers — the junction of the agent families.** `01-Projects/Clients/<client>/` is where this fleet meets the sales chain (prospection → proposal → contract) and the future **delivery agents** working on client projects (the KM architecture notes already live there). Deposits are **agent-owned files** (e.g. a regenerated billing-snapshot note with `ai_generated` frontmatter), never edits of human notes; each family both feeds and reads the dossier — the back-office deposits billing state and contract facts and retrieves dunning tone; delivery agents deposit decisions, meeting notes and **new-business sightings** (contract clauses like the KM 4 % settlement make this a billing input) and retrieve contract scope and billing state. Client-project content is confidential by default — the vault's own routing doctrine applies (Ornith/local first).
## Open decisions
To be settled by POC evidence, each closing with a short ADR:
| # | Decision | Options (leaning) |
| --- | --- | --- |
| D1 | Work queue | telegram-gateway's planned Postgres durable queue (**leaning, operator-endorsed 2026-07** — already roadmapped, transactional, one less system) vs. flat files in git vs. Redis |
| D2 | Orchestration runtime | Claude Agent SDK headless for cluster-triggered jobs + **hermes** for M4-side lanes (**leaning** — hermes already runs skills + cron there) vs. bespoke TS orchestrator (erp `test/` Deno codebase) vs. pure CronJobs + scripts |
| D3 | KM monthly invoice firing | enable Dolibarr template auto-fire (`frequency>0`) vs. agent-fired via sandbox+promote (**leaning** — keeps the gate + mention audit in-line) |
| D4 | PA — e-invoicing platform (*plateforme agréée*, ex-PDP) | **Leaning: Qonto** (operator direction, 2026-07 — the capital-deposit bank, DGFiP-registered PA, e-invoicing included in every plan, and the fleet's richest existing API integration); POC-6 validates reception + API pull before the ADR — **must close before 2026-09-01** ([C12](challenges.md#c12--e-invoicing-reform-unknowns)) |
| D5 | OCR provider for scanned docs | Mistral OCR (EU cloud) vs. local vision model on M4 vs. Tesseract baseline |
| D6 | Pi inference serving | llama.cpp server vs. Ollama on arm64, resource limits, node pinning ([C5](challenges.md#c5--slm-capability-ceiling-on-pi-hardware)) |
| D7 | Cluster↔vault access | git clone/pull of the SecondBrain remote (**leaning** — the Gitea remote exists, offline-friendly, reviewable) vs. tunneled Obsidian REST API (M4-only today) vs. keeping vault access M4-exclusive |
| D8 | Fleet code home | erp repo `fleet/` next to the skills (**leaning** — the atoms are ERP-domain today; revisit into a dedicated repo when a second domain joins) vs. dedicated fleet repo vs. scattered per existing repo |
| D9 | Meeting capture tool (client-project notes lane) — **parked 2026-07-12, nice-to-have** (first client calls happen on the iPhone: a phone call precedes the Meet, which desktop capture doesn't cover; revisit when desktop meetings become routine) | **Leaning: Hyprnote free tier** (rebranded "Char" — local capture + transcription with **speaker diarization and Google Calendar sync both on the free plan**; manual trigger only, auto-record stays off by consent stance) vs. **Meetily** as OSS fallback (MIT, diarization in the community core, no calendar sync — `sb.py` can compensate by matching recording timestamps to the calendar ICS) vs. bare Whisper-class + Ornith |
D4D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest); D8 rides POC-1 — the first atoms need their home on day one; D9 is **parked** (nice-to-have; calls are iPhone-first today) — erp#49 carries the wake-up steps (gate: diarization quality on a real bilingual call).