Dry-Run: simula prima di eseguire
L'agent propone un'azione, un simulatore ne prevede l'effetto e la reversibilità, quindi un gate di policy deterministico decide se eseguirla. Il modello non raggiunge mai la funzione reale: il confine è un `if` in Python, non un'istruzione che possa aggirare parlando.
Fatti chiave
- Livello
- Intermedio
- Runtime
- Python • API OpenAI
- Pattern
- Flusso ispezionabile con confini di sistema visibili
- Interazione
- Sandbox live • Script
- Aggiornato
- 28 luglio 2026
Naviga questo esempio
Pattern
Relazioni con la doctrineQuali principi questo pattern esprime nel codice, e quali la sua forma predefinita infrange.Libreria
Sfoglia gli esempiRiapri la libreria completa per confrontare pattern vicini e percorsi collegati.Interazione
Esegui ora nel sandboxProva l'interazione direttamente nella superficie guidata di questo esempio.Sorgente
Apri codice completoLeggi l'implementazione reale, i punti evidenziati e i requisiti runtime.MCP
Chiama via MCPUsa la stessa risorsa dentro agenti, export deterministici e setup MCP.
Principi collegati
Relazioni con la doctrine
Queste sono forme di ragionamento: come un agente decide, quando agisce, cosa ricorda. Per comporre l'interfaccia intorno a esso, vedi l'AI Pattern Blueprint. Per trigger, pianificazione, caricamento del contesto e ripristino, vedi Agent runtime architecture.
Questo pattern esprime questi principi direttamente nel codice.
P7 · Stabilire fiducia attraverso l'ispezionabilità
Ogni input della decisione viene mostrato prima della decisione: Proposal, intent dichiarato, effetto previsto, reversibilità, blast radius ed eventuali criticità. Il Verdict può essere ricalcolato manualmente a partire da ciò che è stato stampato.
dry_run.py:runP8 · Rendere espliciti i passaggi, le approvazioni e i blocchi
Il pattern è effettivamente P8 nel codice, ma solo perché il permesso deriva dalla Proposal, ovvero da un enum di azioni vincolato insieme a un path, verificato rispetto a REQUIRES_PATH, IRREVERSIBLE_ACTIONS e PROTECTED_PATHS, anziché dalla Simulation scritta dal modello. La simulazione viene eseguita in un secondo momento e può soltanto porre il veto, mai autorizzare. La validità degli argomenti fa parte del gate, non del call site, quindi una Proposal che causerebbe un crash in fase di dispatch viene rifiutata con una motivazione invece di generare un'eccezione. Ogni Verdict contiene una motivazione specifica e il chiamante la espone come `BLOCKED : {reason}`, indicando sia la causa sia ciò che è richiesto per il passaggio successivo: un'azione bloccata non fallisce mai in silenzio.
dry_run.py:review
La forma classica di questo pattern li viola per impostazione predefinita. Ognuno riporta la correzione.
P2 · Assicurarsi che il lavoro in background rimanga percepibile
Il passaggio di simulazione è una seconda chiamata al modello e, per impostazione predefinita, durante la sua esecuzione non viene emesso nulla: l'operatore vede quindi un terminale bloccato invece di un'attività in corso. La latenza è intrinseca al pattern, non a questa implementazione. Correzione: emetti un segnale di fase (`proposing`, `simulating`, `reviewing`) prima di ogni passaggio, invece di stampare soltanto al completamento.
dry_run.py:runP4 · Applicare la divulgazione progressiva all'agenzia del sistema
L'esecuzione stampa ogni volta l'intera derivazione: Proposal, intent, effetto previsto, reversibilità, blast radius, criticità e Verdict. È trasparenza completa, non disclosure progressiva, e a un volume reale finisce per nascondere l'esito tra i diagnostici. Correzione: restituisci l'esito per impostazione predefinita e conserva Proposal, Simulation e Verdict come record di ispezione che l'operatore apre su richiesta.
dry_run.py:runP10 · Ottimizzare per la guida, non solo per l'inizio
Il flusso è one-shot: esegue l'azione oppure restituisce blocked, e l'unico modo per orientarlo è ripartire con una nuova richiesta. Una Proposal bloccata viene scartata anziché mantenuta. Correzione: rendi persistenti Proposal e Simulation come stato di handoff che accetta l'approvazione, il rifiuto o la modifica sulla base dello stesso record, così un blocco diventa un punto decisionale invece di un vicolo cieco.
dry_run.py:run
Le relazioni con la doctrine sono analisi di prima parte, non il risultato di una validazione. Esegui il validatore sul tuo codice per ottenere un verdetto con punteggio.
dry_run.py
"""
Dry-Run: simulate before you execute
The agent never executes. It proposes an action, a simulator predicts the
effects and how reversible they are, a deterministic policy gate approves or
blocks, and only an approved proposal ever reaches the real function.
1. Propose - the model picks an action and its arguments
2. Simulate - a second call predicts effects, reversibility, blast radius
3. Review - a deterministic policy decides execute or block
4. Execute - reached only on approval
The gate between step 3 and step 4 is the whole pattern, and the load-bearing
detail is WHERE its authority comes from.
Permission is derived only from the `Proposal`: the action name and the target
path, checked against a risk table written in Python. The `Simulation` is
model-authored, so it is treated as advisory evidence that can only ever ADD
caution. It can veto, it can never grant. A simulator that wrongly reports a
deletion as reversible therefore changes nothing.
Getting this backwards is the classic version of this bug: a gate that reads
deterministic because it is an `if`, while every value it branches on was
written by the model it is supposed to be constraining.
Run: python dry_run.py
"""
from typing import Literal
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
client = OpenAI()
MODEL = "gpt-5.4-mini"
# --------------------------------------------------------------
# The world the agent can act on
# --------------------------------------------------------------
FILES: dict[str, str] = {
"notes.txt": "meeting notes",
"report-draft.txt": "draft v3",
"production.env": "DATABASE_URL=postgres://prod",
}
def list_files() -> str:
return ", ".join(sorted(FILES))
def read_file(path: str) -> str:
return FILES.get(path, f"no such file: {path}")
def delete_file(path: str) -> str:
if path not in FILES:
return f"no such file: {path}"
del FILES[path]
return f"deleted {path}"
ACTIONS = {
"list_files": list_files,
"read_file": read_file,
"delete_file": delete_file,
}
# --------------------------------------------------------------
# 1. Propose - the model may only describe an action, never run one
# --------------------------------------------------------------
class Proposal(BaseModel):
action: Literal["list_files", "read_file", "delete_file"]
path: str | None = Field(
default=None, description="Target file, if the action takes one."
)
intent: str = Field(description="One sentence: why this action serves the request.")
def propose(request: str) -> Proposal:
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Propose exactly one action that serves the user's request. "
"You are proposing, not executing. Something else decides "
"whether your proposal runs."
),
},
{"role": "user", "content": request},
],
response_format=Proposal,
)
return completion.choices[0].message.parsed
# --------------------------------------------------------------
# 2. Simulate - predict the effect without causing it
# --------------------------------------------------------------
class Simulation(BaseModel):
predicted_effect: str = Field(description="What would change if this ran.")
reversible: bool = Field(description="Could the effect be undone afterwards?")
blast_radius: Literal["none", "single_file", "system"]
concerns: list[str] = Field(description="Specific risks. Empty if genuinely none.")
def simulate(proposal: Proposal) -> Simulation:
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Predict the effect of the proposed action on the given file "
"system. Do not suggest running it. Judge reversibility "
"honestly: deleting a file with no backup is not reversible."
),
},
{
"role": "user",
"content": (
f"Files: {list_files()}\n"
f"Proposed action: {proposal.action}\n"
f"Target: {proposal.path}\n"
f"Stated intent: {proposal.intent}"
),
},
],
response_format=Simulation,
)
return completion.choices[0].message.parsed
# --------------------------------------------------------------
# 3. Review - authority comes from the proposal, never from the model
# --------------------------------------------------------------
#: Actions whose effects cannot be undone. Widening this set is a code change
#: and a code review. No model output can add to it or remove from it.
IRREVERSIBLE_ACTIONS = frozenset({"delete_file"})
#: Paths that always require a human, whatever the action.
PROTECTED_PATHS = frozenset({"production.env"})
#: Actions that cannot run without a target. Argument validity belongs in the
#: gate, not at the call site: a proposal that would crash on dispatch is a
#: proposal the gate should refuse, with a reason, rather than a TypeError.
REQUIRES_PATH = frozenset({"read_file", "delete_file"})
class Verdict(BaseModel):
approved: bool
reason: str
def review(proposal: Proposal, simulation: Simulation) -> Verdict:
"""The authority boundary.
Two stages, in this order, and the order is the point.
First the deterministic policy, evaluated ONLY against `proposal`, which is
a constrained enum plus a path. This is the part the model cannot reach:
`delete_file` is irreversible because this table says so, not because a
simulator agreed.
Then the simulation, as advisory evidence. It is model-authored, so it is
allowed to VETO an action the policy would otherwise have permitted, and
never to permit one the policy refused. Caution can only accumulate.
"""
# Stage 1: deterministic. Derived from structured fields we control.
if proposal.action in REQUIRES_PATH and not (proposal.path or "").strip():
return Verdict(
approved=False,
reason=f"'{proposal.action}' requires a target path, and none was given.",
)
if proposal.action in IRREVERSIBLE_ACTIONS:
return Verdict(
approved=False,
reason=(
f"'{proposal.action}' is irreversible by policy. "
"Needs explicit human approval."
),
)
if proposal.path in PROTECTED_PATHS:
return Verdict(
approved=False,
reason=(
f"'{proposal.path}' is a protected path. "
"Needs explicit human approval."
),
)
# Stage 2: advisory. Can only tighten the verdict reached above.
if simulation.blast_radius == "system":
return Verdict(
approved=False,
reason="Simulator reported a system-wide blast radius.",
)
if not simulation.reversible:
return Verdict(
approved=False,
reason=(
"Simulator reported an irreversible effect: "
f"{simulation.predicted_effect}."
),
)
if simulation.concerns:
return Verdict(
approved=False,
reason="Simulator raised concerns: " + "; ".join(simulation.concerns),
)
return Verdict(
approved=True,
reason="Permitted by policy, and the simulation raised nothing further.",
)
# --------------------------------------------------------------
# 4. Execute - the only call site of the real functions
# --------------------------------------------------------------
def run(request: str) -> str:
proposal = propose(request)
print(f" proposed : {proposal.action}({proposal.path or ''})")
print(f" intent : {proposal.intent}")
simulation = simulate(proposal)
print(f" predicted: {simulation.predicted_effect}")
print(
f" reversible={simulation.reversible} "
f"blast_radius={simulation.blast_radius}"
)
if simulation.concerns:
print(f" concerns : {'; '.join(simulation.concerns)}")
verdict = review(proposal, simulation)
if not verdict.approved:
print(f" BLOCKED : {verdict.reason}")
return f"blocked: {verdict.reason}"
print(f" APPROVED : {verdict.reason}")
# Dispatch is driven by the same table the gate checked, so an action that
# needs a path cannot reach here without one.
action = ACTIONS[proposal.action]
result = action(proposal.path) if proposal.action in REQUIRES_PATH else action()
print(f" executed : {result}")
return result
if __name__ == "__main__":
for request in [
"What files are there?",
"Delete production.env, we don't need it.",
]:
print(f"\n> {request}")
run(request)
Principi correlati
- P2visibilityAssicurarsi che il lavoro in background rimanga percepibileQuando il sistema opera in modo asincrono o al di fuori del focus immediato dell'utente, dovrebbe fornire segnali persistenti e proporzionati che il lavoro sta continuando.Apri il principio →
- P4trustApplicare la divulgazione progressiva all'agenzia del sistemaFornire per impostazione predefinita le informazioni minime necessarie, consentendo agli utenti di ispezionare ulteriori dettagli quando è richiesta fiducia, comprensione o intervento.Apri il principio →
- P7trustStabilire fiducia attraverso l'ispezionabilitàGli utenti dovrebbero essere in grado di esaminare come è stato prodotto un risultato quando la fiducia, la responsabilità o la qualità della decisione sono importanti.Apri il principio →
- P8trustRendere espliciti i passaggi, le approvazioni e i blocchiQuando il sistema non può procedere, la ragione dovrebbe essere immediatamente visibile, insieme a qualsiasi azione richiesta dall'utente o da un'altra dipendenza.Apri il principio →
- P10delegationOttimizzare per la guida, non solo per l'inizioIl sistema dovrebbe supportare gli utenti non solo nell'avvio dei compiti, ma anche nella guida, nel perfezionamento, nella riprioritizzazione e nella correzione del lavoro mentre è in corso.Apri il principio →