The test form and mock data skipped every start variable an http node
read, so a search query or a language header was never mocked. Only a
host-position URL template, credential-keyed header/param lines and an
api-key auth now count, with one shared is_credential_key in core. A
bare "key" param, an empty credential and a bare "Bearer" are grounded
too, and an existing Dify template is never re-grounded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- bind_resources fixtures use the production model label, the full
provider/model id (langgenius/tokener/tokener/deepseek-v4-flash), and the
real Code Interpreter id (code/simple_code); the docstring's ESQ1-302 quote
now shows that shape too.
- The run_test prefill uses without_endpoint_values instead of re-evaluating
endpoint_variable_names(graph) for every key.
- _ground_placeholder_endpoints: two placeholder http-request nodes get two
start variables, and a start variable that already exists is not duplicated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Build, Edit and Fix mock call sites passed generate_mock_inputs through
unfiltered, so an endpoint variable got a mocked URL. The engine accepts any
URL at launch; the http-request node then fails on connection, which
is_input_failure reads as a config failure, so the flow fed the config-repair
loop (the ESQ1-302 thrash). without_endpoint_values drops the endpoint keys at
every mock call site: the missing required key now fails the launch as
"<var> is required in input form", which routes back to the test-data gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A placeholder endpoint is re-pointed to a required start variable, but
the test-data form pre-filled every text field with a mocked value, so
one click ran against another invented URL. Start variables an
http-request node reads its URL from now stay empty, like upload fields.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inline repair the shared graph_normalizers replaced logged each
re-homed edge at info; the delegated version returns (and logs) only
the edges it could NOT re-home, so a successful re-home -- the planner's
"else" moved onto "false", a default-handle edge moved onto a class --
left no trace in the generator logs.
_repair_branch_edge_handles now snapshots each edge's sourceHandle (by
edge object) before the shared repair and logs every edge whose handle
changed (source -> target, old -> new) at info; the unresolved warnings
are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps in the fail-closed branch-handle repair:
- The failure-arm aliases lacked fail_branch / failbranch / error_branch
/ on_fail / fallback. A planner's "fail_branch" edge on a fail-branch
http-request stayed unresolved (only the hyphenated spelling matched
the declared handle exactly). They are looked up through _canon_name
like every other name, so any spacing / hyphen / underscore spelling
hits.
- On an if-else / question-classifier / human-input node with
error_strategy fail-branch and that arm unwired, the fan-out rule
eliminated an unrecognized name onto the one free branch arm (ELSE, a
class, an action) -- even when the edge meant the failure arm. Rule 2
no longer runs for such a node while its fail-branch arm is unwired;
the edge is left for a validator instead. Once fail-branch is wired
the rule applies as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_normalize_conditions called its value normalizer as (value, operator)
and, on any TypeError, retried it as (value) -- the fallback that let
the one-argument list-operator normalizer through. It also swallowed a
genuine TypeError raised inside a normalizer, replacing it with an
unrelated "missing argument" error, and needed two type: ignores.
normalize_filter_condition_value now takes (value, operator="") and
ignores the operator (a FilterCondition scalar is never list-wrapped),
so both normalizers share Callable[[Any, str], Any]; the walker calls
it once, with no try/except and no type: ignore.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The _repair_branch_edge_handles docstring claimed an ambiguous edge is
left "so _validate_structure fails the graph closed" -- true only while
the INVALID_BRANCH_HANDLE check (its own, separately revertable commit)
is in. It now says the edge is left as it was, logged, and returned by
the shared function so a validator can fail closed when that check is
enabled. The comment above the call in _postprocess_graph still
described the old repair (if-else / question-classifier, default
handle only); it now names every branch type the shared repair covers
and the named-but-wrong handles it re-homes, fail-closed.
Comments only; no behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon runs an if-else whose `cases` is absent or None (the legacy
top-level `conditions` shape) as one case with id "true"
(IfElseNodeData.iter_cases; if_else_node routes "true" when
uses_legacy_shape), and the canvas migrates it to case_id "true".
declared_branch_handles declared only "false" for it, so the engine's
own IF arm looked undeclared: apply_connect refused a connect on "true",
and with only "true" wired the fan-out rule re-homed the IF arm onto
ELSE in the generator postprocess.
The case ids now mirror iter_cases (legacy -> ["true"]; an explicit
`cases: []` stays case-less), and the one-case IF-alias rule counts the
legacy shape as one case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The planner names a branch's outgoing source_handle values (rule 12) in
one LLM call; the node builder chooses the case / class / action ids in
another, and nothing told it the planner had already chosen. ESQ1-303's
if-else was built with case "true" against planned edges on
"score_equals_60" / "else", so both arms were skipped.
The builder already receives the planned edges with their handles in
"Normalized plan and topology". The if-else, question-classifier and
human-input snippets now state the contract: declared ids MUST be those
planned handles; the ELSE arm is implicit ("false") and is never a case.
human-input also gets a matching clause for its implicit timeout arm
("__timeout"), which must never be declared as a user action. Shared
with cmd+K /create and /refine; the postprocess repair stays as a
fallback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_validate_structure fails closed on two defects that reached the canvas
silently: an edge leaving a branch node on a handle postprocess could
not re-home unambiguously (INVALID_BRANCH_HANDLE -- the arm would never
run while the run reports "succeeded", ESQ1-303), and a json / raw-text
/ binary http-request body with a number of items other than one
(INVALID_HTTP_BODY -- the executor rejects it at run time, ESQ1-302, and
no automatic collapse of several key/value items is correct).
cmd+K /create and /refine see these as structured errors with the node
id, like every other structural error; the Builder retries once with the
error fed back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_postprocess_graph now runs core.workflow.graph_normalizers: condition
values written as JSON numbers become strings, if-else varType is
derived from a declared start-variable type, http-request body items
get the type BodyData cannot default, and _repair_branch_edge_handles
delegates to the shared fail-closed repair (so a named-but-wrong handle
such as "else" is re-homed, not only the default "source").
Shared with cmd+K /create and /refine: every change turns a graph the
engine would have refused into one it accepts, and none alters a graph
the engine already accepted. Nothing is rejected here.
Validated on the real ESQ1-303 dev draft: value "60", varType "number",
handles true/false, edge ids derived from the repaired handles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A missing sourceHandle is "source" to the engine, so a fail-branch node's
unkeyed success edges are valid and must not be re-homed. The error and
timeout arms exist because of the node's configuration, not because the
author wired a branch, so elimination and default assignment never guess
onto them; named edges reach them through explicit aliases instead.
Ambiguous names (duplicate class names, an alias that disagrees with a
stem match) now fail closed, the one-case IF aliases count case ids
rather than handles, and name keys fold _ - and spaces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The planner names a branch's outgoing source_handle values in one LLM
call; the node builder chooses the case / class / action ids in another.
ESQ1-303's edges left its if-else on "score_equals_60" and "else"
against a case named "true", so the engine skipped both arms and the
run succeeded with nothing.
declared_branch_handles lists what a node routes on; repair_branch_edge_
handles re-homes an edge only when it is forced (exact / alias / name /
stem match, one arm fanning out under a single invented name, default-
source edges onto unused handles in order) and returns everything
ambiguous untouched; undeclared_branch_handles reports what is left so a
validator can reject it. A wrong guess would silently swap IF and ELSE.
Aliases are trusted as names -- the documented limit -- which is why the
node-builder handle contract, not this repair, is the primary fix.
A fail-branch node's success path stays on "source" (what graphon's
NodeRunResult.edge_source_handle actually emits by default), not an
invented "success" handle nothing ever routes on; a fail-branch
branch-type node (if-else / question-classifier / human-input) just
gains the extra "fail-branch" arm. human-input also always declares its
implicit "__timeout" arm, since every human-input node has a timeout
config and the FE always renders that handle; that arm is excluded from
the "unused declared handles" pool the fan-out/default-order repair
rules count against, since it is routinely left unwired by design and
must not block or absorb repair of the arms the author did configure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon turns a falsy body.data into an empty list before the executor
counts items, so a json / raw-text / binary body whose data is "" fails
at run time. http_request_body_errors counted any string as one item;
only a non-empty string is one now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon's BodyData.type (Literal["file","text"]) has no default and the
shared builder prompt only ever shows an empty "none" body, so the LLM
writes {key, value} items and the graph dies at Graph.init (ESQ1-302).
normalize_http_request_bodies fills the type (file when the item carries
a file selector, else text) and empties a "none" body.
The executor sends json / raw-text / binary bodies as ONE rendered
template. Two {key, value} items under a json body have no correct
automatic collapse -- quoting breaks a JSON-valued variable (ESQ1-302's
node3.text is JSON), not quoting breaks plain text -- so
http_request_body_errors reports the node instead of guessing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varType is a frontend-only hint the canvas uses to pick the operator
list; the runtime ignores it. Builder-made if-else nodes never carried it
(ESQ1-303), which steers the panel to string operators on a number and
reproduces ESQ1-285's runtime error. Derive it only when a start
variable's declared type makes it certain; never overwrite, never guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
list-operator's filter conditions are FilterCondition (value: str |
Sequence[str] | bool, default ""), and filtering an array of strings or
numbers requires a plain string. The normalizer treated them like
if-else's Condition: it wrapped an in/not-in scalar into a list and left
None alone, both of which graphon rejects. They now get their own rule:
None becomes "", numbers become text, nothing is wrapped. List items use
the same integral-float rule as scalars.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon's Condition.value is str | Sequence[str] | bool | None. The node
builder wrote "value": 60 for the ESQ1-303 draft; Graph.init builds every
node up front, so that one number stopped the whole workflow. pydantic
also coerces int 1 / 0 to True / False, so bool must be checked before
int.
core/workflow/graph_normalizers.py is the one place both the shared
generator's postprocess and the Builder's write chokepoint will call, so
the two paths cannot disagree. This commit adds the module with the
condition-value normalizer only (if-else cases, sub-variable conditions,
loop break conditions, list-operator filters); nothing calls it yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build's and Edit's re_fix branches (handle_review's continue_adjusting,
handle_reverted's retry_after_revert) reset repair_attempts and
last_repair_error but not unknown_outcome_count. After the cap (count=2)
-> keep_draft -> review -> re_fix, the new cycle's FIRST unknown outcome
re-capped with "couldn't be determined twice in a row". Reset it at all
four sites, as Fix's re_fix already does.
Also pins that Edit's unknown-outcome cap drops a stale diagnosis and
staged repair (seeded, so the assertion is not vacuous).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apply_repair's preflight raises PreflightError when the graph it is about
to write would fail at Graph.init. Every handler caught it as a plain
ValueError, so the five apply sites could not tell it apart from a
graph_ops rejection:
- plan approval (Build, Edit) called ANY ValueError "The workflow can't
start", including "node not found: x", sending the user hunting for a
broken node that does not exist;
- the repair-apply gates (Build/Edit await_repair, Fix apply) called a
preflight rejection "The proposed fix no longer applies to the current
draft" -- the fix applied fine; it would leave an unstartable draft.
Add core DraftWouldNotStartError(ValueError) and make PreflightError
subclass it (services imports core; core still imports no services).
Each site now catches it before the ValueError branch, with the same
control flow and the true reason: plan approval keeps "The workflow
can't start" for it and says "Couldn't apply the workflow" otherwise;
the repair gates say "The workflow can't start" for it and keep
"Couldn't apply the fix" otherwise. The shared card/turn construction is
factored (_graph_not_applied, _change_not_applied, and
drop_unapplied_repair in handlers_fix) and the new literals are
catalogued in strings.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build resets the breaker counter when the user re-enters the repair
loop from review or after a revert; Edit's equivalent branches did not,
so a count left over from an earlier cycle could trip the breaker one
round early.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Edit's affected-path test diagnosed and staged a repair for the same
engine failure with no counter at all, so the loop 6877532f8e closed for
Build stayed open for Edit. The breaker (engine-keyed signature,
consecutive-repeat counter, MAX_REPEATED_REPAIRS) moves to handlers_fix
under public names, Build keeps its underscore names as aliases, and
Edit stops at its gate on the third identical failure. A new edit goal
resets the counter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix's re_fix action resets the rest of the repair loop's state but kept
unknown_outcome_count, so after the cap tripped the first unknown
outcome of the new repair cycle re-capped immediately instead of being
re-runnable once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unknown-outcome branch (a stream that ended with neither a terminal
nor an error frame) returned the user to the re-run state with no
limit in all three flows. With error frames now reported as failures
that branch is rare, but a stream that keeps ending early could still be
re-run forever. DifyBuilderContext.unknown_outcome_count counts
consecutive unknown outcomes (reset by any determinate one); the second
in a row stops at the gate with a card telling the user to look at the
run itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first cut flagged any succeeded run where a branch node ran and no
End did, which also caught a legitimate side-effect-only arm that ran
to completion, and treated any fail-branch node as a router. The rule
is now the ESQ1-303 mechanism itself: a branch node ran and none of its
edge targets ran (the engine skipped every arm), in a graph that has an
End that was never reached. The card points at that branch node.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ESQ1-303: both edges of a Builder-made if-else used handles the node
does not declare, so the engine skipped both arms, reached no End node,
and reported "succeeded" with outputs={}. Every verify handler took any
"succeeded" as "All checks passed".
A succeeded run that executed a branch node (if-else, question-
classifier, human-input, or a fail-branch node) and no End node is now
"Finished without output": an error-toned test card plus a card pointing
at the branch node, waiting at the gate with no staged repair -- the
engine reported no error, so there is nothing to diagnose. The rule is
deliberately that narrow: requiring an End node for every succeeded run
would false-fail side-effect-only branches, which the structural
validator does not forbid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apply_repair streams a canvas marker per applied intent before its
preflight runs, so a rejected build left the client with markers for
mutations that were never written. The plan-approval catch now emits
revert_checkpoint: nothing was written and the draft is still the
checkpoint taken at approval.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apply_repair now rejects a graph Graph.init would refuse (PreflightError).
Build's and Edit's plan approval called apply_repair with no guard, so
the rejection would have escaped the handler and left the operation
interrupted mid-step. Both now catch it: the step is marked failed, an
error card names the node and the reason, nothing has been written, and
the plan stays at its gate -- re-approving regenerates the graph.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
validate_node_config promises to raise exactly when Graph.init would.
That only holds while it and DifyNodeFactory.create_node run the same
validation steps, so both now call one helper instead of two copies.
No behavior or error-text change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A graph-init failure surfaced as "2 validation errors for
HttpRequestNodeData ..." -- pydantic names the model class, never the
node. graphon logs the node id and re-raises the raw error, so the error
frame the Builder received (ESQ1-302) could not say which node was
wrong, and its repair agent had to guess.
DifyNodeFactory.create_node now re-raises every ValueError from the
validation stage (shared-shape validation, class resolution, concrete
NodeData re-validation) prefixed "node '<id>' (<type>): ". This runs for
EVERY workflow start, so the error text of every graph-init failure
changes, not only the Builder's. The wrapped error is a plain ValueError,
so app_generator's handling takes its ValueError branch (log only under
DEBUG) instead of the ValidationError branch; the published error is the
same.
validate_node_config runs the same three validation steps with no
factory instance and no runtime state -- the dry Graph.init a draft can
be put through before it is written or run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run_draft populates Run.error, and all three consumers threw it away:
Build and Edit assigned run_error = "" and Fix's Run(...) literal had no
error= at all. A launch failure therefore persisted with an empty error
(the long-open "TRUNCATED_STREAM_ERROR not persisted, err_len=0" item),
diagnose saw nothing, and the repair breaker -- which keys a launch
failure on run.error -- could never fire on one.
Fix's handle_verify also gains the try/except Build and Edit already
have, so a run_draft that raises degrades to a failed Run carrying the
launch error instead of killing the advance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The streaming pipeline reports a run that threw as one ``event: error``
frame with top-level message/code and no data (ESQ1-302: Graph.init
rejected a node before workflow_started, so there is no run id either).
run_mapping had no representation for it: _TERMINAL_STREAM_EVENTS lists
only workflow_finished/paused, so the frame was neither terminal nor a
node event and the run came back "running" with the truncated-stream
notice.
error_from_stream_chunk reads such a frame; map_error_frame_run turns it
into a FAILED Run carrying the message (whitespace collapsed, so it is a
stable repair-breaker signature) and the code. map_run_result now keeps
the engine's run-level error text for a failed run instead of dropping
it -- a launch failure has no per-node rows, so that text is the only
evidence diagnose and the breaker have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run workflow streaming inside the Builder worker to avoid queue deadlocks. Forward native events to shared editor callbacks and wait for graph refresh before retesting repairs.
Keep command cleanup stable, preserve batched canvas events, and allow failed or unfinished historical runs to refresh again.
Starting a NEW Build session on an app whose draft already holds a previous
build's graph silently changed nothing and still reported "graph built".
_already_present matches a create_node by node id ALONE, and the generator
always emits node1, node2, ... So a new plan whose node2 is a
parameter-extractor was filtered out against an existing node2 that is an
llm: to_apply became empty, apply_repair([]) no-oped, change_set fell back
to "graph built", and the session advanced to build.execution over an
untouched canvas. Observed live -- the snapshot taken before apply was
byte-identical to the draft after it, and the subsequent test run "passed"
because it ran the OLD graph.
The id filter itself is deliberate and stays: it is what makes a loop-back
re-approve (continue_adjusting / revert) idempotent instead of raising on a
colliding id. The guard therefore keys on built_node_ids, which names what
THIS session built -- empty only on a session's first build, which is
exactly the broken case. A loop-back still no-ops silently and advances.
Does NOT clear the canvas on the user's behalf: that would destroy an
existing workflow. It reports honestly and stays in plan_approval.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_repair_is_repeating could never fire in production. _note_repair_error
counted repeats by exact string equality on diagnosis.root_cause, which is
LLM prose regenerated on every diagnosis call. Observed live across four
turns of one session: same failure, reworded every time, one turn switching
to Chinese. Equality therefore never held, repair_attempts reset forever,
and the guard was unreachable -- that session burned 9 failed verify runs
and 8 repair approvals with the guard in place.
The call site's own comment reads "ESQ1-285 burned nine approvals this way,
ESQ1-290 six". The reproduction burned nine again.
Key on the ENGINE's output instead: _failure_signature(run) returns the
first failed node's id plus its error text (whitespace-collapsed), falling
back to the launch error, and "" when the run identifies nothing -- so two
unknown failures never compare equal.
Why the existing tests passed while production was broken: every one of them
fed _note_repair_error the SAME literal string twice, and StubAgent.diagnose
always returns one fixed root_cause. They encoded the premise that is false
in production. The new handler-level test drives three turns with an agent
that rewords its root_cause each call while the engine fails identically,
and fails against the old implementation with repair_attempts == 0.
last_repair_error now holds that signature rather than prose. It was never
displayed -- its only use was this comparison.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four findings from the whole-branch review, all in the repair loop:
I1 -- repair_attempts/last_repair_error were added to the context and to
serde but not to recovery._reset_working_fields (which clears eleven sibling
fields) nor to the re_fix/restart re-plan branches, so a fresh build could
inherit repair_attempts >= the ceiling and decline a repair on its FIRST
failure. They are cleared with their siblings now. The guard also ANDed a
monotonic counter with a consecutive-error test, so A -> B -> B tripped it
although B had survived one repair. _note_repair_error now makes the counter
mean what the docstring says: consecutive repeats of ONE error, reset when
the error changes.
I2 -- the repeated-failure return dropped run/run_id_sink, so repo.save_run
never ran while fc.verify_run_id already pointed at that run: a dangling
pointer and the third failing run lost from the debug export.
I3 -- ESQ1-291's empty-staged-repair guard existed in Build only. Edit's
model-config branch GUARANTEES an empty staged repair and routes to a state
whose approve applied it blindly; handlers_fix.handle_apply had the same
shape. Both are guarded now, reusing Build's notice.
I5 -- apply_repair re-validates intents against the draft as it is NOW, so a
dotted path that was applicable at propose time can raise ValueError at
approve time. Uncaught at all three approve sites, it reached the runner and
drove the session to FAILED -- the exact ESQ1-271 outcome this branch set out
to remove. Each site now degrades to a "Couldn't apply the fix" error card
naming the reason and clears the staged repair (engaging the I3 guard).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-1 review defect: the TOOL branch added for ESQ1-296/ESQ1-289 assumed
output_schema was always a dict once truthy. A non-dict output_schema (or a
non-dict `properties` inside it) raised AttributeError instead of failing
open. That crash is new -- the old branch never read output_schema at all --
and it happens outside the _run_stage try/except and outside the
controller's narrow exception allowlist, so it would take down the SSE
generator mid-stream for both cmd+K and the Builder.
Reachable via refine mode: _hydrate_tool_nodes only stamps a schema when it
finds a matching catalogue entry, so a hand-edited or legacy graph can carry
forward an output_schema of any shape.
Fix: shape-check before every .get() -- output_schema must be a dict and its
properties must be a dict, or the branch returns True.
SHARED WITH CMD+K: fails open whenever the schema is absent, empty, or an
unreadable shape, so a tool we cannot describe behaves exactly as before.
Only provably-wrong references, from a schema we can actually read, are
rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_declares_variable returned True unconditionally for TOOL nodes, so any
invented downstream reference survived generation. ESQ1-296 and ESQ1-289
are the same three lines: code/simple_code is a TOOL node, so the real
code-node validation directly above never saw it. They differ only in
consumer -- End tolerates the missing variable and yields a silent null,
if-else fails at run time and feeds the repair loop.
Tool outputs are not opaque: tool_node.py fixes text/files/json and the
generator already copies the provider's output_schema onto the node.
Validate against both, and alias the invented `result` to `text` so the
common mistake self-heals.
SHARED WITH CMD+K: fails open when no output_schema is present, so a tool
we cannot describe behaves exactly as before. Only provably-wrong
references are rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handle_await_repair offered "Apply the fix" unconditionally, so an empty
staged_repair produced approve -> apply nothing -> retest -> same failure
-> offer again, with no LLM in the loop. ESQ1-291's trace shows five such
approvals, every change set {count: 0, changes: []}.
Adds repair_attempts / last_repair_error to the context (both defaulted,
so persisted contexts deserialize unchanged) and _repair_is_repeating for
the follow-on gate. An empty staged repair is now declined with a decision
the user can act on instead of a run that cannot help.
_repair_is_repeating is also wired in at the point a repair is OFFERED
(the config-failure path in handle_test_and_repair, right after
diagnose()): a third identical failure clears staged_repair and surfaces
a "Repeated failure" card instead of proposing a fourth repair
(ESQ1-285/290 burned nine and six approvals this way).
Also catalogs the two new user-facing literals in core/dify_builder/
strings.py so test_catalog_covers_handler_literals keeps passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An LLM-supplied int node_id reached diff_graphs, which drops falsy and
non-string ids from its node maps, so result.changes came back empty and
build_change_set's fallback joined a list containing an int. The
TypeError took the whole session to `failed` (ESQ1-271) after the
mutation had already been committed to the draft.
validate_intent_args now rejects a non-string or empty node id, so
filter_applicable turns it into a rejection reason the agent can act on.
build_change_set str()s defensively -- that join must never be what kills
a session.
Also extends the same guard to set_node_config's `path` arg: a non-string
path used to reach _resolve_path's str.split() and raise AttributeError,
surfacing "'int' object has no attribute 'split'" to the repair agent
instead of an actionable rejection reason. Same boundary, same defect
shape, deliberately included here rather than deferred.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mocking the inputs removed the work of inventing test data, but it also
removed the user's sight of what was actually tested. A green check
produced by {"topic": "test"} is weak evidence about a workflow, and an
invisible mock is a quieter version of the same false confidence a
passing-but-unverified test gives.
So keep the gate and pre-fill it: the form arrives with the mocked values
already in it, unfrozen, so the common case is one click rather than N
fields to type, and anyone who cares can edit before running. The cost
item 5 removed was PRODUCING the data, not confirming it.
Upload fields stay empty and still ask, because nothing can mock a file --
and the mock generator falls back to a plain string for types it does not
know, so it would otherwise pre-fill a file field with "test". New
``upload_variable_names`` and ``without_upload_values`` strip those values;
``needs_upload_inputs`` is re-expressed on top of the first rather than
duplicating the type check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The run id already reaches a client over SSE, which is enough to open the
run on the canvas while the page is up. It is not enough afterwards: SSE
frames do not survive a reload, and the persisted card's ``run_ids`` holds
BUILDER run ids -- uuid4s the engine mints, which resolve to nothing
outside it. So on reopening a session the card was the only record of the
result, and the canvas view was unreachable.
Put ``dify_run_id`` on TestResultCard, beside ``run_ids``, wired at all
nine construction sites across the Build, Edit and Fix flows. Now the
canvas is reachable from the durable conversation too, which is what makes
the card's ``output`` genuinely the client's choice to render or skip
rather than the only surviving copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>