Vai al contenuto principaleVai al footer
PatternscriptintermediateEseguibileguided-flow

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

Vista rapida del flusso

Come questo esempio si muove tra input, esecuzione e risultato rivedibile

Dry-Run: simulate before you execute -> Route with explicit logic -> Load environment keys -> Initialize OpenAI client -> Constrain output schema -> Send message payload

Avvio

Dry-Run: simulate before you execute

Checkpoint

Route with explicit logic

Esito

Load environment keys

Perché esiste questa pagina

Questo esempio è mostrato sia come codice sorgente reale che come pattern di interazione orientato al prodotto, così i discenti possono collegare implementazione, UX e doctrine senza lasciare la libreria.

Flusso visivoCodice realeSandbox o walkthroughAccesso MCP

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.

Strutturale

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:run

  • P8 · 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

Gap predefinito

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:run

  • P4 · 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:run

  • P10 · 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.

Come dovrebbe essere usato questo esempio nella piattaforma?

Usa prima la sandbox per comprendere il pattern di esperienza, poi ispeziona il sorgente per vedere come il confine del prodotto, il confine del modello e il confine della doctrine sono effettivamente implementati.

UX pattern: Flusso ispezionabile con confini di sistema visibili
Assicurarsi che il lavoro in background rimanga percepibile
Applicare la divulgazione progressiva all'agenzia del sistema
Stabilire fiducia attraverso l'ispezionabilità

Riferimenti sorgente

Voce di libreria
pattern-dry-run
Percorso sorgente
content/patterns/safety-routing/dry-run/dry_run.py
Librerie
openai, pydantic, python-dotenv
Requisiti di runtime
OPENAI_API_KEY
Principi correlati
Assicurarsi che il lavoro in background rimanga percepibile, Applicare la divulgazione progressiva all'agenzia del sistema, Stabilire fiducia attraverso l'ispezionabilità, Rendere espliciti i passaggi, le approvazioni e i blocchi, Ottimizzare per la guida, non solo per l'inizio

dry_run.py

python
"""
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)

Cosa dovrebbe ispezionare il discente nel codice?

Cerca il punto esatto in cui lo scope del sistema è delimitato: definizioni di schema, impostazione del prompt, configurazione di runtime e il punto di chiamata che trasforma l'intenzione dell'utente in un'azione concreta del modello o del workflow.

Cerca i contratti di output e la validazione
Cerca la chiamata di esecuzione esatta
Cerca cosa il prodotto potrebbe esporre all'utente

Come si relaziona la sandbox al sorgente?

La sandbox dovrebbe rendere leggibile l'UX: cosa vede l'utente, cosa sta decidendo il sistema e come il risultato diventa revisionabile. Il sorgente mostra poi come quel comportamento è effettivamente implementato.

Leggi il riepilogo dell'implementazione.
Esplora gli stati utente e di sistema.
Ispeziona il codice sorgente tenendo a mente le decisioni di doctrine evidenziate.
SandboxFlusso ispezionabile con confini di sistema visibili

Guida all'interazione

Usa la sandbox per esplorare l'esperienza visibile all'utente, il lavoro del sistema e la scelta di doctrine che l'esempio sta facendo.

Spiegazione UX

La sandbox spiega cosa dovrebbe vedere l'utente, cosa sta facendo il sistema e dove il controllo o l'ispezionabilità devono rimanere espliciti.

Spiegazione AI Design

La pagina trasforma il codice sorgente in un pattern orientato al prodotto: cosa può decidere il modello, cosa dovrebbe esporre il prodotto e dove il codice deterministico o la revisione devono subentrare.

Guida all'interazione

  1. 1Leggi il riepilogo dell'implementazione.
  2. 2Esplora gli stati utente e di sistema.
  3. 3Ispeziona il codice sorgente tenendo a mente le decisioni di doctrine evidenziate.

Visibile all'utente

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.

Lavoro del sistema

Il prodotto prepara un task delimitato per il modello o il workflow.

Perché è importante

L'interfaccia dovrebbe rendere il task delegato leggibile prima che avvenga l'automazione.

Usato in corsi e percorsi

Questo esempio attualmente è indipendente nella libreria, ma si connette comunque al sistema dei principi e alla famiglia di esempi più ampia.

Principi correlati

Runtime architecture

Usa questo esempio nei tuoi agenti

Questo esempio è disponibile anche tramite il layer agent-ready del blueprint. Usa la pagina Per agenti per recuperare MCP pubblico, export deterministici e setup per Claude o Cursor.

Definisci trigger, contesto e confini prima di aumentare l'autonomia
Rendi espliciti controllo, osservabilita e recovery nel runtime
Scegli i pattern operativi giusti prima di delegare ai workflow