The build/edit test handlers wrapped run_draft in a bare, non-logging
`except Exception` that turned any launch-time failure into a blank failed
run (dify_run_id="", per_node=[], no error). Diagnose then had nothing to
act on and the repair loop ran blind — the prod symptom where a stale test
input (ValueError "<var> is required in input form") produced an
undiagnosable await_repair loop.
Now the launch error is logged, captured, and persisted so diagnose/routing
can act on it:
- Run.error field + dify_builder_runs.error column (migration a9cc8d552db4)
- handlers_build/edit: logger.exception + launch_error_text(exc) -> Run.error
- is_input_failure() also inspects Run.error (+ "in input form" signal), so a
stale-input launch failure routes to the testdata gate, not blind repair
- diagnose/_degraded_diagnosis surface the launch error when no node executed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render change sets directly in plan-style cards with affected node names,
IDs, and change details. Capture node metadata for proposed and applied
changes and persist it with conversation history.
Remove the view_changes action and expand/collapse state. Update generated
contracts, translations, and regression coverage.
Validation: 112 frontend tests and 742 backend unit tests passed.
A build for a workflow app whose goal reads like a chatbot produced an empty
canvas while the assistant reported "Workflow built on the canvas.": the plan
was chatbot-shaped, build_nodes fed it to the generator (mode="workflow"), the
LLM emitted a chatflow graph (answer node / no end node), the generator
rejected it as MISSING_TERMINAL -- a class it does NOT retry -- and build_nodes
swallowed the error into []. handle_plan_approval then treated [] as a
successful build: it deleted the placeholder start and advanced to
build.execution, leaving nothing on the canvas.
Two parts:
1. build_nodes now prepends an explicit workflow-topology directive (one
start + >=1 end node, no answer nodes) to the generator instruction, and
retries ONCE on an error/empty result with the specific error fed back as
a corrective nudge -- covering the terminal-node failure the shared
generator skips.
2. handle_plan_approval refuses to fake success: when build_nodes yields zero
create_node intents it surfaces an honest error and stays in
build.plan_approval (retryable) without deleting the placeholder, instead
of emptying the canvas and claiming a build. A loop-back re-approve still
returns the fixed create intents (only filtered as already-present), so
zero create intents unambiguously means a genuine generation failure.
Adds regression tests for both. Note: part 1 improves LLM reliability
(verifiable live); part 2 is the deterministic guarantee against the
false-success empty canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uv run --project selects the venv but doesn't chdir, so invoking
gen-dify-builder-contract from packages/contracts (as autofix.yml does)
raised ModuleNotFoundError: No module named 'core'. Switch to
--directory so uv chdirs into api/ before running the module. Also
update two contract_gen.py docstrings that still pointed at the old
web/app/components/dify-builder/contract/types.ts output path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It was stubbed to [] in LlmBuilderAgent, canned in PlaceholderAgent, and never
called by any handler — the Build failure path reuses Fix cognition (diagnose +
propose_repair). Removed from the DifyBuilderAgent Protocol, both implementations,
and all test doubles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final-review fix wave for the testdata-gate + start-selector slice:
- I1 (Important): is_input_failure's signal list included the bare
"required" (and generic "variable not found"), which also fire on
config/runtime faults unrelated to test inputs -- misrouting those
failures to the *_AWAIT_TESTDATA gate, where the diagnosed auto-repair
is never offered and re-running just loops at the gate. Narrowed
_INPUT_FAILURE_SIGNALS to ("file variable", "not provided", "missing
input"), favoring precision per the spec's ambiguous-failure ->
config-repair safe default. The real file-input E2E error ("File
variable not found for selector: [...]") still matches via "file
variable".
- M2 (Minor, latent): handle_plan_approval's from-scratch delete-then-
create-filter path could drop a generator create_node/connect that
reuses a just-deleted placeholder start's id, since _already_present
compared against the PRE-delete existing_node_ids/existing_edges --
delete with no re-create, silently vanishing the node. Deleted ids are
now excluded from that comparison set; the loop-back path (no deletes)
is unaffected.
- Cleanups: state.py's Build/Edit state-count comments (14->15, 9->10,
verified against the actual enum members), FormCard's docstring now
lists the "testdata" variant, and handle_plan_approval binds
current_graph's nodes to one local instead of calling .get() 3x and
documents the delete-placeholder branch.
Tests: extended test_is_input_failure_matches_input_signals's sibling
with a config "required" case (red before the signal-list fix, green
after), and added a plan_approval test where the generator's create_node
reuses the deleted placeholder start's id (red before the id-exclusion
fix -- a dangling connect ValueError -- green after).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clear fc.test_input_ref and fc.verify_run_id on the four re-plan/revert
escape branches in Build and Edit (handle_review's continue_adjusting
and handle_reverted's retry_after_revert) so a rebuild+retest
regenerates fresh schema-shaped mock inputs instead of reusing the
first build's stale ones -- the approve_repair retest path is left
untouched since it must fairly retest against the same failing case.
Also corrects three doc/comment mismatches (llm_agent's 12->13 method
count, an await_repair docstring describing a branch that doesn't
exist, and state.py's Build state-count banner), and adds tests for
the run_draft-raises degrade path and Build/Edit pass-path canvas
event symmetry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite handle_test_affected_paths from a canned always-success stub to a
live handler mirroring Build's handle_test_and_repair (A5): derive mock
inputs once via start_schema/generate_mock_inputs and persist a TestInput
(reused on retest), run run_draft (degrading to a synthesized failed Run on
exception), and mint a real immutable Run. Pass -> edit.review with a green
TestResultCard + summary. Fail -> real diagnose/propose_repair, stage
fc.diagnosis/staged_repair/risk, emit a red TestResultCard + ErrorCard (+
ChangeSetCard when intents are proposed) -> edit.await_repair (the B1 gate).
No auto-apply; apply_repair still runs only inside handle_await_repair's
approve_repair branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement start_schema(graph: Graph) -> StartSchema that reads the start
node's declared input variables from graph.nodes[].data.variables into
{"variables": [...]}. Pure dict access — stays in core without importing
services. Returns {"variables": []} when there is no start node or it
declares no variables.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
analyze_impact now returns {fields, values, target_node_ids} instead of a
flat edit_rules dict, mirroring Build's goal-shaped form (A2). Rewire
handle_capability_check to read fc.form_fields/edit_rules/edit_target_node_ids
from that shape and render the FormCard via build_form_fields;
handle_impact_analysis merges the submitted payload on the dynamic
fc.form_fields keys instead of a fixed key tuple; handle_review's re_fix
branch re-renders the form the same way. Delete the now-dead
_EDIT_RULE_FIELDS/_EDIT_RULE_KEYS constants. PlaceholderAgent.analyze_impact
returns the new shape (the old fields become canned fields/values), so both
it and the LLM agent (which still delegates to canned) are shape-uniform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
handle_test_and_repair's AssistantTurnItem.cards was unconditional even
though error/change_set cards are now only appended when
propose_build_repair returns intents -- the manifest claimed cards that
were never attached in the neutral (empty-repair) path. Make cards
conditional to match what's actually appended, and assert it in both
branches.
Remove three noqa comments added in the prior commit that suppressed
ARG001/ARG005 on new test code (the global constraint disallows noqa
for non-enabled rules); switch the genuinely-unused monkeypatch
args/lambda params to underscore-prefixed names instead, and drop a
build_nodes monkeypatch that exercised nothing. Add
test_remaining_build_methods_thread_model_and_tenant_args to cover
propose_plan_v1/discover_resources/bind_resources/build_nodes/
learn_from_build's model/tenant_id/model_config threading, matching
the existing analyze_goal coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LlmBuilderAgent's 6 Build methods (analyze_goal, propose_plan_v1,
discover_resources, bind_resources, build_nodes, learn_from_build) now
delegate to services.dify_builder.agent.build instead of the canned
PlaceholderAgent. propose_build_repair returns [] -- there's no real
failure signal to diagnose until the live test step is de-canned.
handle_test_and_repair now conditions the error card + apply_repair on
a non-empty propose_build_repair result: placeholder mode keeps today's
"found and fixed" arc, while real cognition (which never reports a
failure yet) emits a neutral "no issues found" review summary instead
of a fake error/fix on a hardcoded node id that real build_nodes
wouldn't have generated anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
analyze_goal now returns {fields, values} instead of a flat requirements
dict, so the Build capability-check form renders whatever fields the
agent (placeholder or LLM) proposes rather than a hardcoded 6-field
constant. handlers_fix.build_form_fields whitelists/coerces the agent's
raw field specs into FormField (unknown type -> text). handle_goal_analysis
now merges submitted values against the dynamic fc.form_fields keys
instead of the deleted _REQUIREMENT_KEYS constant.
Also updates test_placeholder_agent.py's analyze_goal shape assertion,
which broke as a direct consequence of the new {fields, values} contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Behavior-preserving code-quality cleanup across the Dify Builder engine:
extract emit_canvas, build_change_set, mint_checkpoint, action_kind, and
merge_known_keys into handlers_fix.py (shared by handlers_build.py and
handlers_edit.py); extract _add in repository.py for the id-preservation
idiom; extract _make_edge (and unify the node/edge id-collision loops
into _next_unique_id) in graph_ops.py; extract _sync_graph_only in
dify_port.py. No behavior, generated ids, or wire output changed — same
387-test suite passes, ruff clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FINDING 1: Updated module docstring in handlers_build.py to accurately
reflect that build.complete's completion summary is emitted by the
governance-tail handlers (handle_governance_feedback for automatic/disabled
policies, or handle_await_learning for the ask policy).
FINDING 2: Strengthened two per-policy governance tests to include notice
kind assertions:
- test_governance_automatic_learns_and_reaches_complete: add assert "notice" in kinds
- test_await_learning_accept_learns_and_completes: add assert "notice" in kinds
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework handle_governance_feedback into the 3-policy branch (automatic/
disabled/ask) reading fc.skill_learning_policy, and add handle_await_learning
to resolve the ask-policy accept_learning/skip_learning turn. Both funnel
into a shared _emit_completion helper for the build-complete summary.
Registered BUILD_AWAIT_LEARNING in build_registry().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>