← All documents · Agent runtime layer · companion to the backend AUDIT-FINDINGS
Verdict. This is not an amateur build. The architecture is clean, payment handling is safe, escalation is safe, and the test suite (379 tests) is green. The “agent keeps tripping” problem traces to a small, precise set of runtime defects — four critical, six high — and every one is fixable inside the go-live week. This document is what we found, how we fix each, and the exact order we do it in.
Method: six parallel deep-dive reviews across the runtime — concurrency, roster freshness, retrieval, output safety, data contract, test coverage — cross-checked against a direct read of the adapter and a full run of the existing 379-test suite. Every finding below is anchored to a specific line of code.
| Category (handover) | Status after code audit |
|---|---|
| A · Runtime infrastructure | The bulk of the open work. C1–C4 and H1–H4 live here. |
| B · Content & retrieval | Reframed (next section). H6 (wrong-tier price) and the output leak (C1) are the real B-class items. |
| C · Data & wiring | Confirmed resolved backend-side. One adapter boundary bug remains: H5. |
Reading the code closely confirmed most of the handover and refined three points. These matter because they move budget away from a layer that would not have shifted the outcome.
| Prior understanding | What the code shows |
|---|---|
| A per-conversation “serialization lock” was added. | There is no lock. Ordering is an optimistic generation-counter — correct only within one process, which is why the single-worker rule matters (C4). |
| “Automatic context compaction” was added. | It does not exist. The live path sends full, untruncated history every turn — the direct cause of context loss on long chats (H1). |
| The top fix is a RAG router + relevance floor (~2–3 days). | The production agent does no retrieval at all — that engine is wired only into an internal CLI. A RAG fix would have near-zero live effect. The real driver is prompt bloat and the missing trivial-turn fast-path — cheaper, in a different place. |
Net: the same budget buys a more durable result by targeting the runtime and the output gate — not the retriever.
prompting.py:412 synthesises the banned phrase; sanitize_answer_output checks none of the 13 strings; no canonical “sei un bot?” reply exists.web.py:3055; the delayed send reuses the captured object and skips re-lookup at web.py:2575. History is refreshed at send time; the roster is not.access_level, the adapter guesses — and can only ever guess “full”. A customer who should be blocked, closed or escalate then gets normal answers and payment links instead of silence or a human handoff. It fires silently — there is no logging in that file.repository.py:207–216 derives only full/upsell_only and ignores status; no logger present.web.py:3229 bypasses the turn cap; no summarisation anywhere.chatwoot_history.py:77–81 excludes by content instead of by message id.web.py:3260–3288).web.py:2297–2301).prompting.py:162–189 omits ~10 fields for non-current members.Quality and hardening — folded into the same waves, none block go-live on their own.
| ID | Finding | Fix |
|---|---|---|
| M1 | The “second-pass review” is an LLM editor, not a guard; content-safety failures are invisible (only crashes escalate). | Pair it with the deterministic gate from C1. |
| M2 | The phone-wording normaliser can corrupt a correct consultation-call reference into the banned phrase. | Widen its guard; removed with C1. |
| M3 | No fast-path for trivial turns — “ciao”/“ok” triggers two model calls over the full prompt (cost, latency, dilution). | Add a lightweight greeting/ack router. |
| M4 | The count disambiguation signal is validated then ignored; a related quantity is used instead. | Thread count through, or document intent. |
| M5 | The stored schema can hold only 2 of the 6 access-level values — a latent trap. | Widen to all six; unify with C3. |
| M6 | Tool appointment_date reaches the system of record as unvalidated free text. | Validate ISO date before the call. |
| M7 | An empty history fetch can produce a cold greeting mid-conversation. | Fall back to stored history; fail loud. |
Important for scoping: several things the tests worried about are genuinely solid. We are not re-building these.
We work reproduction-first: for each defect we write the failing test that proves it, then fix until green. “Ready” is only said with a green suite and this report attached — the discipline Niek proposed. Nearly all critical fixes are deterministic and need no live model, so they are fast and low-risk.
pdf_generation_failed — referenced as a test scenario but not a real state in the code. We need what signals it (product decision).nie-wa-read always include access_level and group_call_booked_at? The fallback (C3) suggests it sometimes doesn’t.Definition of done. Every fix ships with the failing test that first reproduced it, turned green. No behaviour change to the three instruction files without Christian — they stay the single source of truth. The word “ready” is used only once the full suite is green and this report is attached.
← Tutti i documenti · Layer di runtime dell’agente · complementare all’AUDIT-FINDINGS del backend
Verdetto. Non è un lavoro amatoriale. L’architettura è pulita, la gestione dei pagamenti è sicura, l’escalation è sicura e la suite di test (379 test) è verde. Il problema del “l’agente si perde” si riduce a un insieme piccolo e preciso di difetti di runtime — quattro critici, sei alti — e ognuno è risolvibile entro la settimana del go-live. Questo documento è ciò che abbiamo trovato, come correggiamo ciascun punto e in quale ordine lo facciamo.
Metodo: sei revisioni approfondite in parallelo sul runtime — concorrenza, freschezza del roster, retrieval, sicurezza dell’output, contratto dati, copertura dei test — verificate contro una lettura diretta dell’adapter e un’esecuzione completa della suite esistente da 379 test. Ogni riscontro è ancorato a una riga di codice precisa.
| Categoria (handover) | Stato dopo l’audit del codice |
|---|---|
| A · Infrastruttura di runtime | La gran parte del lavoro aperto. Qui vivono C1–C4 e H1–H4. |
| B · Contenuti & retrieval | Riformulata (sezione seguente). I veri item di classe B sono H6 (prezzo del tier sbagliato) e il leak in output (C1). |
| C · Dati & wiring | Confermata risolta lato backend. Resta un bug di confine nell’adapter: H5. |
La lettura attenta del codice ha confermato gran parte dell’handover e ha precisato tre punti. Contano perché spostano il budget lontano da un layer che non avrebbe cambiato il risultato.
| Convinzione precedente | Cosa mostra il codice |
|---|---|
| È stato aggiunto un “lock di serializzazione” per conversazione. | Non c’è nessun lock. L’ordinamento è un contatore di generazione ottimistico — corretto solo dentro un singolo processo, ed è per questo che la regola “un solo worker” conta (C4). |
| È stata aggiunta una “compattazione automatica del contesto”. | Non esiste. Il percorso live invia tutta la cronologia, senza troncamento, a ogni turno — causa diretta della perdita di contesto nelle chat lunghe (H1). |
| La fix a più alto impatto è un router RAG + soglia di rilevanza (~2–3 giorni). | L’agente in produzione non fa alcun retrieval — quel motore è collegato solo a una CLI interna. Una fix RAG avrebbe effetto quasi nullo dal vivo. Il vero driver è il gonfiore del prompt e l’assenza di una corsia rapida per i turni banali — più economico, in un altro punto. |
In sintesi: lo stesso budget produce un risultato più duraturo agendo sul runtime e sul filtro di output — non sul retriever.
prompting.py:412 sintetizza la frase vietata; sanitize_answer_output non controlla nessuna delle 13 stringhe; non esiste una risposta canonica a “sei un bot?”.web.py:3055; l’invio ritardato riusa l’oggetto catturato e salta la ri-lettura a web.py:2575. La cronologia è aggiornata all’invio; il roster no.access_level, l’adapter indovina — e può indovinare solo “full”. Un cliente che dovrebbe essere blocked, closed o escalate riceve invece risposte normali e link di pagamento anziché silenzio o handoff a un umano. Scatta in silenzio — in quel file non c’è alcun logging.repository.py:207–216 deriva solo full/upsell_only e ignora lo status; nessun logger presente.web.py:3229 aggira il limite di turni; nessuna sintesi da nessuna parte.chatwoot_history.py:77–81 esclude per contenuto invece che per id del messaggio.web.py:3260–3288).web.py:2297–2301).prompting.py:162–189 omette ~10 campi per i membri non correnti.Qualità e hardening — inseriti nelle stesse ondate, nessuno blocca il go-live da solo.
| ID | Riscontro | Fix |
|---|---|---|
| M1 | La “revisione di seconda passata” è un editor LLM, non un guardiano; i fallimenti di sicurezza sui contenuti sono invisibili (solo i crash fanno escalation). | Abbinarla al filtro deterministico di C1. |
| M2 | Il normalizzatore del linguaggio telefonico può corrompere un riferimento corretto alla chiamata di consulenza nella frase vietata. | Allargare il controllo; rimosso con C1. |
| M3 | Nessuna corsia rapida per i turni banali — “ciao”/“ok” attiva due chiamate al modello sull’intero prompt (costo, latenza, diluizione). | Aggiungere un router leggero saluti/conferme. |
| M4 | Il segnale di disambiguazione count è validato e poi ignorato; si usa una quantità correlata. | Portare count fino in fondo, o documentare l’intento. |
| M5 | Lo schema salvato può contenere solo 2 dei 6 valori di access-level — una trappola latente. | Allargare a tutti e sei; unificare con C3. |
| M6 | Il campo tool appointment_date arriva al sistema di record come testo libero non validato. | Validare la data ISO prima della chiamata. |
| M7 | Una lettura di cronologia vuota può produrre un saluto “a freddo” a metà conversazione. | Ripiegare sulla cronologia salvata; fallire in modo rumoroso. |
Importante per lo scoping: diverse cose che i test temevano sono davvero solide. Non le ricostruiamo.
Lavoriamo prima-riproduzione: per ogni difetto scriviamo il test che lo riproduce, poi correggiamo fino al verde. “Pronto” si dice solo con la suite verde e questo report allegato — la disciplina proposta da Niek. Quasi tutte le fix critiche sono deterministiche e non richiedono il modello live, quindi sono rapide e a basso rischio.
pdf_generation_failed — citato come scenario di test ma non è uno stato reale nel codice. Serve sapere cosa lo segnala (decisione di prodotto).nie-wa-read include sempre access_level e group_call_booked_at? Il fallback (C3) suggerisce che a volte no.Definizione di “fatto”. Ogni fix arriva con il test che l’ha riprodotta per primo, portato al verde. Nessun cambio di comportamento ai tre file di istruzioni senza Christian — restano l’unica fonte di verità. La parola “pronto” si usa solo quando l’intera suite è verde e questo report è allegato.