# Una risposta rifiutata deve comunque addebitare i token

> Se il runtime rifiuta la risposta, conservo i token della chiamata produce già fatturata. Onestà del ledger per agenti governabili, da mklang 1.3.7 / pull 111.

Published: 2026-10-04
Canonical: https://gianlucamazza.it/it/blog/mklang-output-rejected
Tags: mklang, LLM, Cost, Agents, Production

## Perché conta

Non posso governare un agente se una risposta rifiutata sparisce dal ledger. Il provider ha già fatturato la chiamata. Se `RunResult.usage` e il costo dello step leggono 0, mento a me stesso su quanto ha speso il run.

È contabilità, non un halt più veloce. Una risposta troncata sotto `on_truncate: halt`, o un fallimento di `parse:`, resta una produce che è successa. La policy può rifiutare il testo. Non può fingere che i token non siano mai stati ordinati.

La parte sul linguaggio è in [mklang: il documento è il programma](/it/blog/mklang). Questa nota riguarda il ledger. La ricevuta pubblica è [gianlucamazza/mklang#111](https://github.com/gianlucamazza/mklang/pull/111), uscita come pacchetto 1.3.7 il 2026-09-29. Sto leggendo quell'albero. Non aggiungo un benchmark.

## Che cosa registra la ricevuta

Il [CHANGELOG 1.3.7](https://github.com/gianlucamazza/mklang/blob/main/CHANGELOG.md) dice il bug in una frase: una risposta prodotta che il run rifiuta viene addebitata dopo la fix, e prima no.

Prima della fix, `deps.llm.produce(...)` poteva tornare una risposta troncata o un testo che falliva `parse:`. Il motore alzava un `ValueError` nudo. L'`except Exception` generico del loop lo trasformava in `state-error: …` e perdeva i token della chiamata già fatta e fatturata. Il changelog registra l'osservazione in produzione: una risposta da ~4096 token sotto `on_truncate="halt"`. Quel numero è il wording della ricevuta. Non è un claim di throughput.

Il motivo di halt era già corretto. Il ledger no.

[`OutputRejected`](https://github.com/gianlucamazza/mklang/blob/main/src/mklang/errors.py) è l'errore tipizzato che chiude il buco. Porta i token della chiamata come `CallFailed` già faceva per una sotto-macchina:

```python
class OutputRejected(MklangError):
    """A produce call answered (and was billed) but its answer was refused."""

    def __init__(self, error: str, input_tokens: int = 0, output_tokens: int = 0):
        super().__init__(error)
        self.error = error
        self.input_tokens = input_tokens
        self.output_tokens = output_tokens
```

`_exec_produce` lo alza per l'halt da troncamento e per i fallimenti di parse. Il percorso a stato singolo lo cattura prima dell'handler generico, addebita lo step, lo registra con il suo `cost` e si ferma con il motivo **invariato**: `state-error: output-truncated`, `state-error: parse-list-truncated`, o `state-error: parse-json: …`. Un branch fan-out rifiutato allo stesso modo tiene i suoi token nel marker `[branch-error: …]`, come già faceva un branch `call` fallito.

La [scheda progetto](/it/progetti#mklang) è l'indice pubblico. Spec 0.4 e pacchetto 1.3.7 sono le versioni che dichiara l'albero. Non le tratto come certificato di maturità.

## Che cosa spariva

L'addebito mancante era un incidente di control flow. Dopo il ritorno di produce, due percorsi di rifiuto condividevano un raise sbagliato:

1. **Halt da troncamento.** `on_truncate: halt` vedeva `Produced.truncated` e alzava `ValueError("output-truncated")`.
2. **Fallimento di parse.** JSON parziale da uno stop per lunghezza, o testo che non era JSON valido, alzava `ValueError` da `_parse_structured`.

Entrambi sono rifiuti di una risposta che esisteva già. L'handler generico non sapeva che erano stati fatturati. `CallFailed` lo sapeva già: una sotto-macchina in halt tiene `input_tokens` e `output_tokens` perché il parent possa addebitarli. L'output rifiutato non aveva quel tipo, così i token morivano in `except Exception`.

È il buco che mi interessa in un agente governabile. La macchina può fermarsi. La macchina può riparare. La macchina non può perdere il conto del lavoro già ordinato e poi dirmi che il run era economico.

Non pretendo che il vecchio percorso fosse raro. Pretendo che fosse disonesto quando succedeva. Il changelog nomina un caso di produzione. I test nominano il contratto.

## Che cosa l'halt deve conservare

Due fatti devono sopravvivere insieme: il motivo e il costo.

[`tests/engine/test_truncation.py`](https://github.com/gianlucamazza/mklang/blob/main/tests/engine/test_truncation.py) è il fixture. Quattro test fallivano su `usage == 0` prima della fix:

- `test_halt_on_truncation_still_charges_the_call` — motivo `state-error: output-truncated`; usage e il `cost` dello step in halt tengono i token della chiamata.
- `test_parse_truncated_halt_still_charges_the_call` — `state-error: parse-list-truncated`; stesso addebito.
- `test_unparseable_output_halt_still_charges_the_call` — `state-error: parse-json: output is not valid JSON (`; stesso addebito.
- `test_refused_branch_keeps_its_tokens` — un fan-out `sample: 2` finisce `done` con marker `[branch-error: parse-json: …]`; i token di entrambi i branch restano sullo step.

L'LLM di fixture restituisce `input_tokens=1200` e `output_tokens=4096`. Sono valori di test, non una distribuzione misurata in produzione. Il caso a due branch li raddoppia perché sono girate due produce. Non pubblico un modello di costo da lì.

```python
r = run(m, {}, {m.name: m}, _billed("cut", truncated=True), TIERS, on_truncate="halt")
assert (r.status, r.error) == ("halt", "state-error: output-truncated")
assert r.usage == {"input_tokens": 1200, "output_tokens": 4096}
assert r.trace[-1]["cost"] == {"input_tokens": 1200, "output_tokens": 4096}
```

Il commento nel motore in [`src/mklang/engine.py`](https://github.com/gianlucamazza/mklang/blob/main/src/mklang/engine.py) è la policy che voglio sul muro: dal momento in cui produce torna, la chiamata è stata fatturata; una risposta rifiutata si ferma via `OutputRejected`, che porta i token perché l'halt li addebiti comunque.

`_halt_output_rejected` addebita, scrive `policy="state-error"`, registra lo step e torna `_halt(f"state-error: {e.error}")`. La stringa su cui una dashboard era già agganciata non cambia. Il ledger sì.

## Che cosa non ho cambiato

La pull request è esplicita sul confine.

**I judge call restano fuori.** Gli adapter alzano `JudgeUnparseable` prima di esporre l'usage. È un percorso diverso. Non l'ho piegato in `OutputRejected`. Un install verde di 1.3.7 non significa che un gate-judge fallito adesso addebiti allo stesso modo. Se mi servirà, è un'altra ricevuta.

**I motivi di halt restano gli stessi.** `state-error: output-truncated` è ancora `state-error: output-truncated`. Non ho inventato uno status nuovo per festeggiare l'onestà. Il punto è che il vecchio motivo ora sta accanto a un `cost` diverso da zero.

**Nessun claim di performance, sicurezza o maturità.** Addebitare una chiamata rifiutata non rende il modello migliore, il gate più saggio o il pacchetto pronto per la produzione. Rende vero il libro. Le [eval di livello bancario](/it/blog/bank-grade-agent-evals) restano dopo: un halt addebitato non prova che la policy fosse giusta.

Non tratto nemmeno questo come un trucco di consenso o un segnale di mercato. I token su uno step rifiutato sono un conto. Non sono un biglietto.

## Pratica: il conto accanto al rifiuto

La pratica che posso sostenere è più stretta di un pitch da prodotto sul costo.

Quando eseguo una macchina che può fermarsi su truncate o parse, voglio tre cose visibili su quello step: il motivo di halt, i token di input e i token di output. Se usage è 0 dopo una produce che ha restituito testo, il ledger mente. mklang 1.3.7 è il cambio dell'interprete che ferma quella menzogna per quei due percorsi di rifiuto. Non butto il pacchetto in un host e lo chiamo governato.

Se un branch fan-out viene rifiutato, voglio i suoi token nello step come già faceva una `call` fallita. Se un judge non parsa una scelta, non pretendo che 1.3.7 l'abbia coperto. Se non so dire queste cose, non so rendere conto del run.

L'[articolo mklang](/it/blog/mklang) resta il posto in cui descrivo il documento. Questa pagina è il posto in cui descrivo il conto che sopravvive a un rifiuto.

## FAQ

### Perché addebitare una risposta che il runtime rifiuta?

Perché la produce è già successa e il provider l'ha già fatturata. Rifiutare il testo è una decisione di policy. Cancellare i token è una menzogna sul costo. Non posso governare un run il cui ledger perde il lavoro che ha ordinato.

### Quali motivi di halt restano invariati?

`state-error: output-truncated`, `state-error: parse-list-truncated` e `state-error: parse-json: …`. La pull request tiene quelle stringhe. Cambia solo l'addebito.

### I judge call addebitano allo stesso modo?

No. La ricevuta lascia i judge call come erano. Gli adapter alzano `JudgeUnparseable` prima di esporre l'usage. Non è il pattern di `OutputRejected`.

### È un vantaggio di performance o di sicurezza?

No. È onestà del ledger per due percorsi di rifiuto nell'interprete di riferimento. Non pretendo un halt più veloce, un agente più sicuro o un salto di maturità.

### Dov'è la ricevuta?

[mklang#111](https://github.com/gianlucamazza/mklang/pull/111), [CHANGELOG 1.3.7](https://github.com/gianlucamazza/mklang/blob/main/CHANGELOG.md), [`errors.py`](https://github.com/gianlucamazza/mklang/blob/main/src/mklang/errors.py) e [`tests/engine/test_truncation.py`](https://github.com/gianlucamazza/mklang/blob/main/tests/engine/test_truncation.py). La [scheda progetto](/it/progetti#mklang) è l'indice pubblico.
