Dry-Run: simulate before you execute
The agent proposes an action, a simulator predicts its effect and reversibility, and a deterministic policy gate decides whether it runs. The model never reaches the real function; the boundary is an `if` in Python, not an instruction it can talk past.
Fatti chiave
- Livello
- intermediate
- Runtime
- Python • API OpenAI
- Pattern
- Flusso ispezionabile con confini di sistema visibili
- Interazione
- Sandbox live • Script
- Aggiornato
- 27 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à
Every input to the decision is surfaced before the decision: the proposal, the stated intent, the predicted effect, reversibility, blast radius, and any concerns. The verdict can be re-derived by hand from what is printed.
dry_run.py:runP8 · Rendere espliciti i passaggi, le approvazioni e i blocchi
The pattern IS P8 in code, but only because permission is derived from the Proposal (a constrained action enum plus a path, checked against REQUIRES_PATH, IRREVERSIBLE_ACTIONS and PROTECTED_PATHS) rather than from the model-authored Simulation. The simulation runs second and can only ever veto, never permit. Argument validity is part of the gate rather than the call site, so a proposal that would crash on dispatch is refused with a reason instead of raising. Every Verdict carries a specific reason and the caller surfaces it as `BLOCKED : {reason}`, naming both the cause and what is required next, so a blocked action never fails silently.
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
The simulate step is a second model call, and by default nothing is emitted while it runs, so the operator sees a stalled terminal rather than work in progress. Latency here is inherent to the pattern, not to this implementation. Fix: emit a phase signal ('proposing', 'simulating', 'reviewing') before each step rather than printing only on completion.
dry_run.py:runP4 · Applicare la divulgazione progressiva all'agenzia del sistema
The run prints its whole derivation every time: proposal, intent, predicted effect, reversibility, blast radius, concerns, verdict. That is full disclosure, not progressive disclosure, and at any real volume it buries the outcome in diagnostics. Fix: return the outcome by default and keep the Proposal, Simulation and Verdict as an inspection record the operator opens on demand.
dry_run.py:runP10 · Ottimizzare per la guida, non solo per l'inizio
The flow is one-shot: it executes or it returns blocked, and the only way to steer is to restart with a new request. A blocked proposal is discarded rather than held. Fix: persist the proposal and simulation as a handoff state that accepts approve, reject, or revise against the same record, so a block becomes a decision point instead of a dead end.
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 →