Files
dify/api/core/dify_builder/errors.py
T
Charles YaoandClaude Opus 5 4082cceb24 fix(dify-builder): never write a batch the agent has already judged wrong
The two semantic guards describe defects the ENGINE ACCEPTS: a new branch wired
into a variable-aggregator that was never told about it, an array re-sent with
an element quietly dropped. Both drafts start, both run green, and neither
produces a `DraftWouldNotStartError` for `apply_repair` to veto. So the guards
had one hole left, and it was in the agent's own fallback:
`build_edit_intents` spent its corrective re-prompt on the guard's reason and
then, if the retry yielded nothing usable, returned the FIRST attempt's
applicable intents -- the very batch the guard had condemned. The guard fired
and the silently-wrong draft was written anyway.

`vet_intents` now carries `would_run_wrong` alongside `rejections`: the subset
of refusals the engine does not make. The two kinds fail differently and a
caller has to tell them apart without reading the text -- a structural refusal
DROPPED its intent, so what survives is a batch nobody judged wrong and keeping
it is a deliberate choice; a semantic verdict drops nothing and condemns what
remains. Reading that off the prose would be the same mistake as keying control
flow on the model's prose.

`build_edit_intents` therefore raises `ProposalWouldRunWrongError` instead of
returning a batch still carrying one, whichever attempt it came from. Nothing is
written and nothing is attempted. `handle_plan_approval` catches it into the
same `_change_not_applied` recovery a refused write gets: the plan stays at its
gate, the reason goes on an error card and into `last_edit_rejection` for the
next attempt.

Surfacing is a real exit now and not the dead end it would have been before this
plan: Task 7 gave the gate a revert and a keep-adjusting action, and Task 6
carries the refusal into the next proposal, so the user can change the rules and
the retry is not blind.

`ProposalWouldRunWrongError` is deliberately neither a `DraftWouldNotStartError`
nor a `ValueError`: the draft WOULD start, and a broad `except ValueError`
around a port write must not swallow it as if the write had been attempted.

`_change_not_applied` takes the activity to mark failed, defaulting to the write
where the usual refusal happens. This exit gives up during "prepare", so failing
the write would have shown a LATER step failed while an earlier one was still
running, with the step between never resolving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 23:14:25 -07:00

64 lines
2.5 KiB
Python

"""Dify Builder domain errors.
Port of dify-enterprise/server/pkg/enterprise/biz/dify_builder/errors.go. Go
models these as sentinel ``error`` values (``errors.New(...)``); Python has
no sentinel-error idiom, so each becomes an ``Exception`` subclass that the
``Repository``/``Runner`` layers raise and callers catch by type.
"""
class ConflictError(Exception):
"""Raised when compare_and_advance loses the version race (stale base_version)."""
class NotFoundError(Exception):
"""Raised when a session/checkpoint/run does not exist."""
class BusyError(Exception):
"""Raised when a session already has an Advance in flight.
The per-session gate is held, so a concurrent SubmitAction is rejected.
"""
class BadRequestError(Exception):
"""A client-supplied value is invalid (e.g. an unresolvable model_config).
Maps to HTTP 400 in services.dify_builder.wiring.dify_builder_error_response.
"""
class ModelUnavailableError(BadRequestError):
"""The chosen or default Builder model cannot be used with its current configuration."""
class DraftWouldNotStartError(ValueError):
"""Raised by the Dify port when the graph it was asked to write would fail
at Graph.init, so nothing was written.
Handlers catch it to say "the workflow can't start" rather than "the
change no longer applies". A ``ValueError`` so older ``except ValueError``
guards around the port's writes still catch it.
"""
class ProposalWouldRunWrongError(Exception):
"""Raised by an agent that has judged its OWN final proposal wrong, so it
refuses to hand it on to be written.
The sibling of ``DraftWouldNotStartError``, for the defects the engine
cannot see. A draft that wires a new branch into a variable-aggregator
without extending that aggregator's ``variables``, or that re-sends an
array having quietly dropped an element the user configured, passes
``Graph.init`` and every node-data check and then runs green while
producing nothing or the wrong thing -- so there is no engine refusal for a
handler to catch and nothing for the write chokepoint to veto.
Deliberately NOT a ``DraftWouldNotStartError`` and not a ``ValueError``:
the draft WOULD start, and a broad ``except ValueError`` around a port
write must not swallow this as if the write had been attempted. Nothing has
been written when it is raised; the message is the engine-grounded reason,
which handlers show at the gate and carry into the next attempt.
"""