Streaming the test run gives the user a live progress bar but no results:
NodeEvent carries node_id/title/status/error and nothing else, so a client
watching the stream learns which node is running, never what it produced.
The results already exist -- the run is a normal InvokeFrom.DEBUGGER draft
run, its node executions are in the database, and the editor's canvas
already knows how to render exactly that view. It was simply never told
which run to open.
So put the Dify run id where a client can reach it:
- NodeEvent gains run_id, filled from the streaming frame. Every non-ping
frame is stamped with workflow_run_id, so this arrives with the FIRST
node event -- while the run is still going, which is the point.
- The node SSE frame publishes it alongside the existing fields.
- mark_test_success / mark_test_error carry dify_run_id in both the Build
and Edit flows, so the terminal state names its run too.
This is what makes per-node results land on the graph, where a node's
inputs and outputs already have a home, instead of only as a JSON blob on
a conversation card.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three minor whole-branch-review fixups:
- ResourceOption.readiness ("ready"/"missing_config") was computed but never
shown to the model: discover_resources's recommender prompt listed ids and
labels only, so it could recommend an unauthorized tool; _GAP_SYSTEM showed
readiness in the listing (assess_capability_gap) but never defined what
missing_config means. Both prompts now show readiness and state that
missing_config is installed-but-not-authorized, i.e. not usable -- pinned
with tests asserting the literal "missing_config" in the constructed
prompts.
- strings.py: dropped three orphaned catalogue entries ("Here's the initial
plan.", "Ask each time", "Prefer audited") -- the conflict-policy concept
they backed was removed end to end; nothing in the handlers emits them.
- core/workflow/generator/tool_catalogue.py: provider.need_credentials was
read OUTSIDE the per-provider try/except that already guards
get_tools()/tool parsing. This module is shared with the cmd+K /create and
/refine flows, so a provider whose property raised would break those too,
not just the Builder. Moved the read inside the try/except so a bad
provider is skipped like any other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_terminal_output json.dumps'd a node's outputs with no length limit. Node
outputs are only truncated upstream at ~100,000 chars per string, so a
report-generating workflow -- the Builder's showcase case -- could push
hundreds of KB of JSON into a dify_builder_conversation_items row, the SSE
frame, and the localizer walk on every successful build.
Cap the rendered output at 2000 chars with a visible truncation marker
before it reaches TestResultCard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run_draft's streaming path introduced Run.status="running" (outcome
genuinely unknown -- the progress stream ended before the run did) as a
third status alongside "succeeded"/"failed", but neither
handle_test_and_repair (Build) nor handle_verify (Fix) knew about it: both
treated anything short of "succeeded" as a failure, so a truncated run got
labeled "Test run -- Failed" and, in Build, had a graph repair diagnosed
and staged against it while the run might still be executing.
Both handlers now branch on status=="running" before the failure path: a
neutral notice explains the outcome couldn't be determined, and the flow
returns to a re-runnable waiting state (build.execution / fix.await_testdata)
without ever calling diagnose or propose_repair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add assess_capability_gap: one plain sentence naming a plan step nothing
installed can perform, or "" when the plan is covered. Grounded only in
installed resources -- there is no marketplace lookup, so it names a missing
CAPABILITY, never a product to install. A missing_config tool (installed but
unauthorized, from the prior task's real readiness) doesn't count as
covering a step either, since the model is shown each option's readiness
alongside its label and kind.
Wired through the full agent seam: the DifyBuilderAgent protocol, the real
LlmBuilderAgent (delegates to build.assess_capability_gap), and the
PlaceholderAgent test double (returns ""). handlers_build.py calls it
after discover_resources and, when non-empty, appends a NoticeItem beside
the existing empty-resources notice -- the two are independent and can
both fire.
Like its sibling cognition functions, it degrades to "" (never raises) on
no model, an LLM error, or an unparseable reply: a gap that can't be
assessed isn't a gap worth inventing. The sentence is LLM-generated prose
already in the user's language (json_language_instruction), not a
catalogue entry, so no static string was added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Builder recommended every installed tool as ready to use without
ever checking whether its provider actually has credentials configured.
A credentialed tool with no tool_builtin_providers row got bound into a
plan and then failed at test time with an authorization error the user
can't act on.
tool_catalogue.py now carries provider_controller.need_credentials onto
each ToolCatalogueEntry as needs_credentials. resources.py resolves
which providers are actually authorized for the tenant (mirroring
ToolTransformService.builtin_provider_to_user_provider's need_credentials
/ db_provider rule exactly, so the Builder and the console's tools page
can never disagree) and sets ResourceRef.readiness to "missing_config"
for a credentialed tool with no authorized provider row, "ready"
otherwise. Datasets and models are unaffected. If the authorization
query itself fails, readiness fails open to "ready" rather than
mass-labeling every credentialed tool unauthorized on a transient error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Streaming introduced a correctness regression: if the event stream ended
without a terminal frame (idle timeout, dead publishing worker), run_draft
synthesised status="failed" with an empty dify_run_id. A run that had in fact
SUCCEEDED would then be reported as a failed build and sent into the repair
loop. The blocking path would have waited and returned the truth.
The stream is the fast path, not the authority. Every non-ping frame is
stamped with workflow_run_id by convert_stream_full_response, so the run id
is known from the first frame onwards -- long before the terminal frame that
may never arrive. run_draft now captures it from any frame and, when no
terminal frame arrives, reads the run's real status from the workflow-run row
instead of inventing one.
Note that data["id"] is the node-execution id on node frames, not the run id,
so it is only read as a run id on workflow-level frames.
When even the row cannot settle it -- no run id, no row, or a row still
marked running because the worker died mid-run -- the result is reported as
status="running" with an explicit Run.error, which is what Run.status already
documents for "not over / not known". A truncated stream is now either the
truth or a visible unknown, never a fabricated failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run_draft blocked on AppGenerateService.generate(streaming=False) and then
replayed every finished node-execution row through on_event, so the user
watched a spinner for the whole run and then saw every node light up at once.
It now consumes the streaming generator and fires on_event per frame.
Two things the plan assumed that turned out not to hold:
- The stream does NOT yield dicts. convert_stream_full_response yields dicts,
but that runs in the Celery worker; on our side
BaseAppGenerator.convert_to_event_stream (not overridden) re-wraps every
event as "data: {json}\n\n" and every keep-alive as "event: ping\n\n". An
isinstance(chunk, dict) guard would have dropped every frame. New
stream_chunk_as_mapping accepts both shapes and rejects keep-alives.
- The streaming workflow_paused frame carries workflow_run_id where the
blocking response carried id, so map_run_result would KeyError on it.
Adapted at the call site via run_result_data_from_terminal_chunk;
map_run_result's contract is unchanged for the fix/edit flows.
Session lifetime: _dispatch_generate is a plain function, so all of its
Session work is eager, and the generator it returns only reads a Redis
subscription while the run executes in a Celery worker with its own session.
The consumption loop therefore sits outside the `with` block rather than
pinning a DB connection for the length of a workflow run; a test pins this.
The node-execution read stays at the end -- map_run_result still needs those
rows for per_node[].outputs. Only the progress streams.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The task-5 change to handle_execution's run_test branch (gate only on
needs_upload_inputs) had no test driving the file-declaring branch through
the real Runner: the two nearest tests called handle_execution /
handle_await_testdata directly and never connected into one flow, and no
existing full-flow test's generated graph declares a file variable.
Add test_full_build_flow_file_schema_routes_through_testdata_gate_via_fsm:
builds a real session through runner.advance(), mutates the built graph's
Start node to declare a file variable, then asserts run_test lands at
BUILD_AWAIT_TESTDATA with a testdata form card actually emitted, and that
submitting provide_testdata through the Runner (not a direct handler call)
advances it to build.review. Together with the existing (task-5-updated)
fileless full-flow tests, both branches of the conditional are now
exercised through the state machine end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A passing build test reported "All checks passed" and threw away the
run's actual node outputs. TestResultCard gains an `output: str` field
(pretty-printed JSON of the last node that produced anything, "" when
none); handle_test_and_repair's success card now populates it via a new
_terminal_output(per_node) helper.
_terminal_output is deliberately status-agnostic (no node.status filter):
per-node status spellings differ by source ("success" vs "succeeded"), and
a status filter that silently misses would defeat the point of the card.
FakeDifyPort (tests/unit_tests/core/dify_builder/fakes.py) gains a
settable run_outputs dict, seeded onto the success-path NodeOutput's
outputs, so tests can assert on what the card shows without hand-rolling
a Run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handle_execution's run_test branch used to stop and ask a human for test
data any time none was prepared yet, even when every declared start
variable was something the agent could invent. Add
needs_upload_inputs(schema) -> bool (true only for a declared file /
file-list variable) and gate BUILD_AWAIT_TESTDATA on it: everything else
is now mocked inline via env.agent.generate_mock_inputs and the flow goes
straight to build.test_and_repair. A schema mixing text and file
variables still stops at the gate, since the file can't be invented.
Updates three full-build-flow tests whose generated graph has no file
start variable, so run_test now lands directly at build.review instead of
pausing at the testdata gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code review finding: handle_review's re_fix branch and handle_reverted both
emitted PlanCard("Build plan", "v1") before falling through to
build.initial_plan's unconditional resource discovery, which falls through
again to confirm_resources -- which emits PlanCard("Build plan", "v1") a
SECOND time, now with resources bound. Before this task those two cards were
tagged v1 then v2, so they were visibly different plans; now both say v1,
so the loop-back shows the same plan card twice in one pass. Task 2 already
removed this exact duplicate from the straight-through path
(handle_goal_analysis); it should not survive on the loop-back and not on
the straight-through path.
Fix: handle_review's re_fix branch and handle_reverted still draft
fc.plan_items/fc.plan_version_tag (needed by discovery and the eventual
confirm_resources card) but no longer emit a PlanCard or claim cards=["plan"]
on their AssistantTurnItem (now cards=[], matching the existing
handlers_fix.py precedent for a turn with no card). There is now exactly ONE
plan card per pass, tagged v1, on every path: straight-through,
continue_adjusting, and retry_after_revert.
Updated the two direct-handler unit tests that asserted the now-removed
card, and extended test_review_continue_adjusting_then_reapprove_is_
idempotent (an end-to-end Runner-driven test) to assert re_fix adds zero new
plan cards and the subsequent confirm_resources adds exactly one, tagged v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code review finding: handle_goal_analysis built its own ProgressReporter
(build-review-requirements/build-draft-plan) and finish()ed it, then
tail-called _discover_and_offer_resources, which built a SECOND reporter
under the SAME env.operation_id. ProgressReporter's revision counter starts
at 0 per instance, so the second reporter's activate/finish calls restarted
revision at 1 -- two progress events with revision 1 (and 2, and 3) under
one operation_id. contract.py's ProgressEventData docstring states revision
is monotonic within an operation_id; every other handler across the three
handler files uses exactly one reporter per step -- this was the sole
exception, on the most-travelled build path.
Fix: _discover_and_offer_resources now takes an optional `progress`
parameter. handle_goal_analysis passes its live (still-open) reporter
through instead of finishing it early; the helper adds the discovery steps
onto that SAME reporter via add_steps() and is the one that finish()es it.
handle_initial_plan (the continue_adjusting/retry_after_revert loop-back,
which has no reporter of its own -- a separate CAS commit/operation) still
gets progress=None and the helper builds a fresh one, unchanged.
Added test_submitting_requirements_progress_is_one_operation_with_monotonic_
revisions: drives the real Runner with submit_requirements, captures every
emitted ProgressEventData, and asserts a single operation_id with strictly
increasing, non-repeating revisions across all four progress steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code review finding: the plan card's version_tag was renumbered v2 -> v1
in an earlier commit, but handle_resource_recommendation's own reply_text
still read "Plan v2 ready for approval." -- a card that says v1 next to
prose that says v2 is worse than leaving both at v2, since it's exactly
the user-visible inconsistency the renumber exists to eliminate.
Updated the reply_text to "Plan v1 ready for approval." and its matching
entry in the strings.py literal catalog, so test_catalog_covers_handler_
literals keeps passing for the right reason (a registered v1 string, not
a stale v2 one). Grepped the whole of core/dify_builder/ for any other
quoted v2/v2.1 string this renumber might have missed -- this was the
only one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code review finding: build.initial_plan was left in _WAITING with zero
_ACTIONS_FOR entries but a handler still gated on find_resources. Since a
waiting state settles the runner's advance loop (is_waiting(next_state) ->
settled), continue_adjusting from build.review landed a session there with
no button the FE could render and no action the handler would accept --
the loop-back could never advance.
Reclassify build.initial_plan as working (state.py _WORKING, not _WAITING):
it is now a genuine pass-through step, matching build.test_and_repair /
build.publish / build.governance_feedback. handle_initial_plan drops its
find_resources gate and runs resource discovery unconditionally on entry;
the runner drives it with the action already consumed and falls straight
through to build.resource_recommendation within the same advance() call.
Removed the now-stale BUILD_INITIAL_PLAN entry from service.py's
_BACKEND_ACTIONS_FOR (no action to gate on any more) and the compensating
exemption it required in test_every_backend_waiting_action_has_a_ui_path
(the invariant now holds with no special case).
Added a state-classification test (test_build_initial_plan_is_working_not_
waiting) and an end-to-end regression driving the Runner from build.review
through continue_adjusting with no find_resources action ever sent
(test_continue_adjusting_reaches_resources_without_find_resources_action),
which is the scenario this fix exists to unwedge. Fixed the two existing
idempotency flow tests that asserted the old (broken) two-step loop-back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handle_capability_check now advances a session that already carries a goal
(fc.goal_text set at creation) on ANY turn, not only send_goal -- the
composer already carried the goal in, so requiring a further send_goal
click was a second decision-free gate. A goal-less (create-from-blank)
session still waits for send_goal to supply one; that action id stays in
_ACTIONS_FOR for exactly that case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With build.initial_plan's plan card gone (previous commit), the plan card
emitted by handle_resource_recommendation on confirm_resources is now the
FIRST and only plan card a build shows -- so it becomes v1 instead of v2.
The build-complete plan card in handle_plan_approval follows suit,
v2.1 -> v1.1. A build no longer displays "the same plan twice under two
version numbers".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
submit_requirements now tail-calls a shared _discover_and_offer_resources
helper straight into build.resource_recommendation, instead of stopping at
build.initial_plan to show a plan card the user must click through with no
real decision to make. The resource-discovery body (progress reporting,
discover_resources, ResourceSelectCard, empty-options notice, assistant
turn) is extracted verbatim into that helper so the straight-through path
and the continue_adjusting/retry_after_revert loop-back (which still
re-enters at build.initial_plan and still requires find_resources) share
one implementation instead of drifting apart.
build.initial_plan survives as the loop-back target but now offers no UI
action (_ACTIONS_FOR entry removed); its handler still accepts
find_resources at the backend for that loop-back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
api/openapi/markdown/console-openapi.md still documented the removed
ConflictPolicyOption schema and ResourceSelectCard.conflict_policy_options
field as live API contract after e8b14b50ff. Regenerated via
api/dev/generate_swagger_markdown_docs.py and kept only the two hunks
that remove the conflict-policy schema; discarded unrelated churn the
regeneration also picked up (the already-merged /workflow-generate/improve
endpoint's docs, never regenerated by that earlier commit).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The resource-recommendation step made the user pick a "conflict
policy" that never decided anything. Remove it end to end: the
ConflictPolicyOption card option, ResourceSelectCard's
conflict_policy_options field, the third bind_resources() parameter
across the port/placeholder/LLM-agent implementations, and the dead
conflict_policy field on the (unused) ResourcesPayload contract type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The App Builder composer's "Add missing details" link needs a backend. It
fires in the pre-creation composer, so there is no app, no draft workflow and
no Builder session yet -- it is tenant-scoped only, which puts it beside
/workflow-generate/suggestions rather than under console/dify_builder.
POST /console/api/workflow-generate/improve expands a short build instruction
into one that states what a builder would otherwise have to ask about: the
trigger and inputs, the steps, the rules that decide between outcomes, the
output and its destination, and the failure path.
Soft-fail like suggestions: no default model, an LLM error, an echo of the
input, or a rewrite shorter than the input all return "" from the generator,
and the route answers 200 with the caller's own text and changed=false. The
action can never strand what the user typed, so there is deliberately no
402/429 mapping.
Deliberately not grounded in the tenant's tools or knowledge bases. Naming
tools here would prejudice the workflow generator's own tool router, which
selects from the full installed catalogue against the instruction it is
actually given -- and it keeps the endpoint free of DB and plugin-daemon
calls.
The boundary guard is split out rather than shared: _workflow_instruction_guard
names ideal_output in its wording, which this payload does not carry. The
existing endpoints' error strings stay byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>