Commit Graph
49 Commits
Author SHA1 Message Date
Charles Yao 118d8a9cf2 fix(builder): share output representations and serialize test JSON safely 2026-10-25 19:22:15 -07:00
Charles Yao bd9d7b1de2 fix(builder): validate generated Template syntax before assembly 2026-10-25 06:11:12 -07:00
Charles Yao 006ad2e317 fix(builder): keep LLM prompt within compact budget 2026-10-25 05:08:28 -07:00
Charles Yao 04764709ee fix(builder): separate reasoning for newly generated LLM nodes 2026-10-25 04:36:19 -07:00
Charles Yao 2c8f3041a5 fix(builder): ground tool outputs and repair confirmed business failures 2026-10-22 14:29:35 -07:00
Charles Yao 8e00366280 fix(builder): ground native generation and explicit acceptance evidence 2026-10-22 03:20:00 -07:00
Charles Yao f5050842fe Fix Builder migration portability and typed backend CI contracts 2026-10-20 17:33:54 -07:00
Charles Yao 231e423431 Integrate validated Builder backend testing and CI repairs
Track backend integration, Ruff cleanup and migration reconciliation in ENG-1271. Retain required copy contracts without implementing frontend interactions.
2026-10-20 15:40:06 -07:00
Charles Yao 1c31ae7bf5 fix(builder): ground constant producers and native capabilities 2026-10-14 08:54:24 -07:00
Charles Yao ccf379392d fix(builder): ground native HTTP outputs and numeric conditions 2026-10-14 07:46:09 -07:00
Charles Yao 59b3d57210 fix(builder): ground API Code contracts and string inputs 2026-10-14 04:24:33 -07:00
Charles Yao 12973e91bf fix(builder): enforce native API output contracts 2026-10-14 02:51:02 -07:00
Charles Yao c98b5f9537 fix(builder): discover native workspace API tools 2026-10-14 00:54:18 -07:00
Charles Yao 84bdc5e9bb fix(generator): parse answers outside leading reasoning envelopes 2026-10-13 19:23:18 -07:00
Charles Yao 625fdfd8d1 fix(builder): discover installed tools needing setup 2026-10-11 02:01:36 -07:00
Charles Yao 6bcc64d4ca fix(dify-builder): validate generation and gate publication on durable verification 2026-10-09 19:14:43 -07:00
twwu 39cbe0e627 Merge branch 'main' into feat/dify-builder 2026-09-29 11:52:13 +08:00
e6f1d77ed5 feat: tool fallback observability (#42596)
Co-authored-by: liuns-yang <liuns-yang@users.noreply.github.com>
Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com>
2026-09-28 11:10:48 +00:00
Charles YaoandClaude Opus 5 c4ac3d49bf fix(dify-builder): a proposal is engine-checked before it reaches the gate
`filter_applicable` dry-ran STRUCTURE only. In the live F4 Edit four intents
came back "applicable, 0 rejected", the batch then died at `apply_repair`'s
preflight, and the one corrective re-prompt could never fire -- it is keyed on
`rejected` being non-empty. Every re-approval re-sent a byte-identical prompt
and got a byte-identical batch back.

`filter_applicable` now returns its dry-run graph (`DryRun` NamedTuple) after
running it through the shared `heal_nodes_for_preflight` -- the same
normalizers, at the same point in the sequence, the write chokepoint runs. A
dry run that healed less would report a defect the write was going to fix; one
that healed more would pass a batch the write refuses. It also skips the
intents the port drops as already-present, via a predicate both now share, so
a re-sent duplicate `create_node` no longer burns the re-prompt.

`preflight.vet_intents` then puts that graph through `validate_node_config` and
feeds any NEW problem into the existing re-prompt, quoting the engine's own
message. `new_preflight_problems` is the one definition of that rule, shared
with `apply_repair` so the dry run and the write cannot answer differently.

It is keyed on the set of pydantic error LOCATIONS per node, which is the only
key that gets both halves right. Node identity exempts an already-refused node
from every later verdict -- and since a Fix repair targets a node that is by
definition already invalid, that would leave `propose_repair` with no node-data
coverage on its own culprit. Message text is too fine: pydantic quotes a
truncated repr of the input, so filling one of three missing fields rewrites
the other two messages and an exact-text diff refuses the repair that improved
the node.

The refusal text is rebuilt from a credential-redacted copy of the node. The
verdict still comes from the real one; only the words the model is shown are
withheld, because a model-level pydantic error echoes the node's whole `data`
and that is where a live bearer token sits.

The retry budget is unchanged: one corrective re-prompt, then Edit's existing
fallback to the first attempt's applicable intents and Fix's `_no_fix()`.

Also folds in the Task 1 review: the generator's postprocess calls the shared
heal set plus `derive_if_else_var_types` instead of re-spelling three
normalizers inline, and the valid-operator no-op sweep covers the
list-operator container so `FilterOperator` gets its own check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 19:23:12 -07:00
Charles YaoandClaude Opus 5 342cfc8545 fix(workflow): an ASCII comparison operator becomes the engine's own
An LLM writes `>=` where graphon's `SupportedComparisonOperator` (and the
list-operator's `FilterOperator`) spell `≥`; pydantic refuses it at
`Graph.init`, so F4's whole Edit batch was rejected and the session parked
at `edit.plan_approval` with nothing written.

`_normalize_condition_list` -- the walker that already visits all five
condition containers and already holds `comparison_operator` in hand --
now canonicalizes the two ordering forms (`>=`→`≥`, `<=`→`≤`) BEFORE the
value rule, which keys on that operator.

Only the ordering forms. Every equality-family ASCII form is ambiguous:
graphon's string equality is `is`/`is not` (`_assert_is` takes `str|bool`)
and its number equality is `=`/`≠` (`_assert_equal` takes only
numbers/bools), so `==`, `!=` and `<>` carry exactly the ambiguity that
keeps `equals`/`gte` rejected. Ordering carries none: graphon has no string
ordering operator at all, so `>=` can only mean `≥`.

`heal_nodes_for_preflight` is the one shared definition of the
deterministic heals a graph gets before it is preflighted, so the write
chokepoint and the dry run that decides applicability cannot drift apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:30:54 -07:00
Charles YaoandClaude Opus 5 443de73258 fix(workflow-generator): an iteration publishes item and index, not just output
`_declares_variable` accepted `output` alone for a container, so a CORRECT
`{{#node4.item#}}` written by a node INSIDE an iteration was read as
unresolved and silently rewritten to `{{#node4.output#}}` -- this item's
value replaced by the container's entire array. It fired on 9 of 9 live
attempts:

    Workflow generator: rewrote unresolved reference node4.item
    to declared output node4.output

Silently rewriting a reference the engine already resolves is the one thing
the repair path must never do. It is worse than the loud failure it
replaces, and it is the same contract violation this whole plan exists to
enforce: the normalizer may only turn a graph the engine REFUSES into one it
ACCEPTS.

Verified against graphon rather than assumed, and the two container types
turn out NOT to be the same shape:

  - `iteration` publishes THREE names. `output` is the node's own result
    (`outputs={"output": flattened_outputs}`, nodes/iteration/iteration_node
    .py:551/565/631), read by consumers outside. `item` and `index` are added
    to a deep COPY of the pool that only the child engine sees
    (`_create_graph_engine`, :827-828), so they resolve for a node inside the
    sub-pipeline. Scope is not enforced: the walker carries only the TARGET
    of a reference, never the node that made it. The alternative to accepting
    all three is the silent rewrite above, which is strictly worse than an
    out-of-scope `item` failing loudly at run time.

  - `loop` never publishes `item` or `index`. It adds each declared
    `loop_variables[].label` to the MAIN pool (nodes/loop/loop_node.py
    :279-286, visible inside and outside) and reports those labels plus
    `loop_round` as its outputs (:489-492), alongside whatever `data.outputs`
    already declares. Those names are now accepted. `output` stays accepted
    too although the engine does not publish a loop under that name:
    dropping it would turn graphs that generate today into failures, which is
    a tightening, not this fix.

Both container types leave `_sole_declared_variable` as a consequence, not a
separate decision: a node with three published names has no SOLE output, so
falling back to `output` is exactly the "which value did the workflow mean?"
that helper refuses to answer for every other multi-output type. An invented
name on a container is now reported instead of guessed. Every other
single-output type keeps the rewrite it has always had, which is what the
non-container test guards.

This is a LATENT defect that the gated snippet work made reachable, and it
ships together with that work: the container-output branch predates this
plan, but the plan is what made the generator write iteration bodies whose
inner nodes reference the per-item variable. It is a separate commit from
the JSON-Schema walker fix and reverts on its own.

Evidence: docs/superpowers/triage/live-verify-2026-09-22/FINDINGS.md section
B8, and `e0_required_probe.py`, whose `{#node4.item#}` UNRESOLVED_REFERENCE
disappears with this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:02:45 -07:00
Charles YaoandClaude Opus 5 04f0374b59 fix(workflow-generator): a JSON Schema array is not a variable selector
`_collect_refs_in_data` read ANY two-element list of strings as a
`["node_id", "variable"]` value selector. An llm node's structured-output
schema carries `required: ["content", "speaker_notes"]`, so the walker
harvested a reference to a node called `content`, `_collect_unresolved_refs`
found no such node, and the whole generation aborted with

    UNKNOWN_NODE_REFERENCE: Reference {#content.speaker_notes#}
    points at unknown node 'content'

Arity 2 is the entire trigger -- a `required` of one name or three names
harvests nothing -- and `enum: ["formal", "casual"]` and
`type: ["string", "null"]` are exposed exactly the same way. The defect is
the walker's, not the schema's: the node builder was told to expose those
two names and wrote a perfectly valid JSON Schema.

This is a LATENT defect that the gated snippet work made reachable, and it
ships together with that work. The two-element branch is byte-for-byte
unchanged since before this plan; what changed is that a JSON Schema now
reaches the walker at all. `f466fd64f8` taught the llm snippet to enable
structured output -- before it, an llm node emitted no schema, so the hazard
was unreachable -- and `af6b0db106`'s rule 16 pins that schema to exactly the
names the plan declares, which makes the two-name case modal rather than
rare. Neither commit touched the walker, so neither can be reverted to fix
this; the fix belongs here.

`_NON_SELECTOR_LIST_KEYS` already gates a key's whole subtree out of selector
harvesting and is shared by all three walkers (harvest, rewrite, sys.query
normalize), so both halves of the fix are one line each there:

  - `structured_output` excludes an llm node's entire schema wrapper, which
    covers every keyword such a schema will ever grow;
  - `required` / `enum` / `type` exclude the schema keywords that hold string
    arrays wherever they appear, because the walker is generic and the next
    schema-bearing node type will not keep its schema under
    `structured_output` (a tool's `output_schema`, say).

Placeholder harvesting is untouched -- `allow_selector` gates only the
selector reading -- and so is the 3-segment
`["node2", "structured_output", "field"]` selector, which is a list VALUE
under some other key, not a subtree under the `structured_output` KEY.

The regression guard is the point of the test set: a genuine
`[node_id, variable]` selector sitting beside the schema in the same node's
data is still harvested AND still validated. An exclusion that swallowed real
references would quietly undo the walker's whole job.

Reproduced and fixed deterministically, with no LLM: see
docs/superpowers/triage/live-verify-2026-09-22/FINDINGS.md and its
`e0_required_probe.py`. The test asserting the byte-for-byte production error
string is gone fails before this change and passes after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:55:28 -07:00
Charles YaoandClaude Opus 5 af6b0db106 feat(workflow-generator): the plan declares producer outputs, and an LLM node's are the engine's
Two halves of one contract, gated on the cmd+K owner together.

1. Node configs are generated by isolated parallel LLM calls that share the
plan but not the output names each producer chooses, so a consumer
references `node3.deck` against a code node that declared `slides_json`
and the whole generation dies on UNRESOLVED_REFERENCE. The planner now
declares `node_outputs` -- `{"<node id>": ["<name>", …]}` -- for the only
node types whose output names their own config chooses: code (`outputs`
keys), parameter-extractor (`parameters[].name`), human-input
(`inputs[].output_variable_name`) and an llm meant to emit structured
output (its schema fields).

It reaches the builders the way `start_inputs` already does, but through
one type-aware section rather than the shared plan JSON: the producer is
told to expose exactly those names, every other builder is told they are
the only names it may reference on that node, and an llm producer's names
are listed as `structured_output.<field>` -- serialising them raw would
hand every consumer the one reference form the engine cannot resolve.

The declaration is BOUNDED in the prompt -- at most 6 names per node and at
most 10 declared nodes. cmd+K sign-off consideration: this is an unbounded
map added to the stage with a known live truncation failure (ESQ1-300:
planner output-budget exhaustion, masked by `json_repair`), and the
standing finding there is that `_PLANNER_DEFAULT_MAX_TOKENS` must not move
before the stage is instrumented. Bounding the declaration is the
alternative: the fail-open parse below already makes a truncated or absent
value harmless, so the bound costs nothing a plan needs.

Fail-open throughout: the value comes from an LLM, so `_parse_node_outputs`
keeps only lists of referenceable names (the run time's own
`[a-zA-Z_][a-zA-Z0-9_]{0,29}` grammar) keyed by a planned node of a
declaring type -- honouring a declaration on, say, an http-request node
would point consumers at a name the engine never exposes. A plan without
the key, or with a garbage one, produces byte-identical prompts and
behaviour to before.

2. `_declares_variable`'s llm branch was wrong in both directions. graphon's
`LLMNode._build_run_outputs` always publishes `text`, `reasoning_content`,
`usage` and `finish_reason`, adds `files` when the node saved any, and adds
`structured_output` -- one object, addressed as
`{{#<id>.structured_output.<field>#}}` -- only when structured output is
enabled with a schema. The branch accepted `text` plus any flat
structured-output SCHEMA PROPERTY, and rejected every other real output.

So a legitimate `{{#node2.reasoning_content#}}` failed the generation while
`{{#node2.answer#}}` -- which the engine can never resolve -- was waved
through, saved clean by cmd+K and died at run time. Removing that flat
acceptance is deliberate: a silently broken graph becomes a loud generation
failure. `test_runner_missing.py`'s assertion that froze the old behaviour
is rewritten with the reason.

Because a consumer only ever sees its OWN node's schema snippet, the shared
builder head now names an upstream llm's real outputs and both taught
reference forms, and the reference walker harvests the 3-element
`["node", "structured_output", "field"]` selector it teaches -- previously
invisible, so an end node pointing at a drifted llm passed validation
silently while the equivalent placeholder was repaired. Both forms now
travel one path: validated by `_declares_variable`, repaired to the node's
real output when the node turns out to be schema-less. The harvest is
deliberately type-blind -- only an `llm` publishes a `structured_output`
object, so the selector aimed anywhere else cannot resolve and has to be
REPORTED, `["start", "structured_output", "x"]` being exactly the invented
reference this walk exists to catch. The `llm` check sits on the REWRITE
instead, which is where reading a plain 3-string list as a selector did
damage: an end node's `["n2", "structured_output", "x"]` data tag was
collapsed to `["n2", "a"]` against a code node, a silent edit where a
report would have been loud and recoverable.

`_sole_declared_variable` reads `_llm_is_schema_less` (the enable flag AND
the schema) rather than schema presence alone, so the "flag off, leftover
schema" drift still repairs an invented name to `text` instead of failing
the build; a real structured-output node still refuses to guess.

3. One behaviour change beyond those two halves, and it is deliberate: a
DOTTED reference is now validated by its ROOT segment, the way
`VariablePool.get` resolves one. A `file` start variable's attributes
(`{{#node1.doc.url#}}`, the `FileAttribute` set), a `json_object` start
variable's keys and an object output's keys stay valid -- a code node whose
output really is called `structured_output` keeps its 3-element selector.

On the start node, a dotted name whose root addresses something the pool
cannot traverse is REPAIRED to that root -- `{{#start.docs.url#}}` becomes
`{{#start.docs#}}` -- on the same path as every other reference repair, so
it shows up in the change log. That covers `file-list`, whose
`ArrayFileSegment` subclasses `ArraySegment` rather than `FileSegment`, so
`{{#node1.docs.url#}}` renders as its own literal text at run time instead
of resolving, and every scalar input. The dotted form never resolves and
the root form always does, which keeps this inside the normalizer's
contract; reporting it instead would abort the whole generation, and cmd+K
`/create` / `/refine` never retry. A root that names NOTHING is a genuinely
invented reference with no value to fall back to and is still reported. It
is never injected as a start variable either way: the run time's grammar
has no dot inside a segment, so a form input literally named `topic.url`
could never resolve.

That repair reads the start node's declarations, and the same walk writes
them, so the walk is now TWO passes over one deterministically ordered list
(`_ordered_refs`): every missing start input is declared first, and only
then is each dotted root evaluated against the completed set. One pass over
the harvested SET made the outcome depend on which reference it yielded
first -- a graph carrying both `{{#start.docs#}}` and `{{#start.docs.url#}}`
with `docs` undeclared repaired cleanly under `PYTHONHASHSEED` 0-6 and
aborted on UNRESOLVED_REFERENCE under 7-9, i.e. the abort this repair exists
to remove, arriving at random. `_collect_unresolved_refs` walks the same
ordered list so a given graph always yields the same error list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:18:42 -07:00
Charles YaoandClaude Opus 5 f466fd64f8 fix(workflow-generator): state an LLM node's real outputs, and the operators a filter takes
The llm paragraph claimed text was a schema-less node's only output; the
engine always exposes reasoning_content, usage and finish_reason too, so
the generator was being told to avoid valid references. The list-operator
snippet now carries the comparison-operator rule itself, because each
node build sees only its own snippet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:51:26 -07:00
Charles YaoandClaude Opus 5 928bf7ade0 fix(workflow-generator): node snippets match the engine's schemas
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:50:18 -07:00
Charles YaoandClaude Opus 5 3a52bc18f4 refactor(workflow-generator): one reader for an LLM node's structured-output schema
The schema-properties lookup was written out three times; the new
schema-less gate made it four. It now has one reader. The
structured_output_switch_on branch is dropped: the engine serialises
structured_output_enabled as the wire/JSON key, and switch_on is only
that field's internal attribute name -- it is never a dict key a raw
graph carries, so the read (and the test reinforcing it) was dead
against real data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:50:18 -07:00
Charles YaoandClaude Opus 5 2411d12441 fix(workflow-generator): an invented LLM output name resolves to text
Extend the shared _OUTPUT_ALIASES table so a reference to
__structured_output__ / __structured_output / structured_output / output /
result on an llm node with no usable structured output (the enable flag --
structured_output_enabled or its alias structured_output_switch_on -- is
falsy, or the structured_output schema is empty) resolves to the node's
real default output, text, instead of staying UNRESOLVED_REFERENCE. Gated
in _aliased_output so an LLM node that genuinely has structured output
enabled with a schema is left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:50:18 -07:00
Charles YaoandClaude Opus 5 f2444b3ec1 fix(workflow): a parameter-extractor query is one selector, not a list of them
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:50:18 -07:00
Charles YaoandClaude Opus 5 9d7acf4089 fix(workflow-generator): log every branch edge the repair re-homes
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>
2026-09-22 01:35:43 -07:00
Charles YaoandClaude Opus 5 6b6f50d412 fix(workflow-generator): say what the branch-handle repair does now
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>
2026-09-22 01:28:05 -07:00
Charles YaoandClaude Opus 5 2692243792 feat(workflow-generator): bind branch-node ids to the planner's handles
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>
2026-09-22 01:01:18 -07:00
Charles YaoandClaude Opus 5 b2b306b3d0 feat(workflow-generator): reject undeclared branch handles and bad http bodies
_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>
2026-09-22 00:52:26 -07:00
Charles YaoandClaude Opus 5 1b0b7e77f2 fix(workflow-generator): apply the shared graph normalizers in postprocess
_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>
2026-09-22 00:18:17 -07:00
Charles YaoandClaude Opus 5 917425a81b fix(workflow-generator): guard TOOL output-schema shape in _declares_variable
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>
2026-09-20 22:09:26 -07:00
Charles YaoandClaude Opus 5 00b9629800 fix(workflow-generator): validate tool-node output references
_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>
2026-09-20 22:09:26 -07:00
Charles YaoandClaude Opus 5 69ccf2ad15 fix(dify-builder): wire resource readiness into recommender/gap prompts
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>
2026-09-20 16:41:52 -07:00
Charles YaoandClaude Opus 5 0a228b7c23 feat(dify-builder): report whether a recommended tool is actually authorized
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>
2026-09-20 16:04:48 -07:00
Charles YaoandClaude Opus 4.8 537a38157c fix(workflow-generator): repair options-less select start variables
Dify has no native boolean start-variable type, so a bool requirement the
Builder proposes gets emitted as a `select` — and the LLM often leaves its
`options` empty. An options-less select renders as an empty, unfillable dropdown
in the Builder's test-input form, so the user can never submit test data
(affects every workflow whose requirements include a bool field).

Add _normalize_start_select_variables to the generator postprocess (beside
_normalize_start_file_variables): a `select` start variable with no usable
(non-empty string) options is converted to a `text-input` the user can type
into; a select that declares real options is normalized to those strings and
kept. Idempotent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 20:21:18 -07:00
Charles YaoandClaude Opus 4.8 968e442c10 fix(workflow-generator): repair HTTP-request response refs to body
Builds failed with an empty graph when the generator LLM wired downstream
references to an HTTP-request node's output as `{#node.response#}`. HTTP-request
nodes expose only {body, status_code, headers, files} — never `response` — and
the reference-repair pass only rewrites to a node's SOLE output, so with four
outputs the ref stayed unresolved: structural validation failed
(runner.py:646 UNRESOLVED_REFERENCE), generate_workflow_graph returned no graph,
and build_nodes produced an empty build ("Couldn't build the workflow").

Add a targeted, unambiguous output-name alias (http-request `response` -> `body`,
the response body) applied in _reconcile_variable_references before the
sole-output fallback, guarded so it only rewrites when `body` is actually
declared. Extensible via _OUTPUT_ALIASES for future known-wrong names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 17:49:15 -07:00
lin 1264d29a4f chore: remove unnecessary type conversions (#41454) 2026-08-29 07:04:18 +00:00
Crazywoola 58653f2cdb feat(workflow-generator): prefer installed tools (#40611) 2026-08-14 05:38:37 +00:00
CrazywoolaandClaude Fable 5 9de7e0fe44 perf(workflow-generator): parallelize node config generation (#38975)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:34:15 +00:00
Crazywoolaandautofix-ci[bot] 7865ffd429 chore: Improve workflow generator accessibility and API contracts (#38838)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-13 09:15:54 +00:00
8809cc036d feat(workflow-generator): enhance the AI auto-creation flow end-to-end (#38175)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-07-01 02:28:58 +00:00
CrazywoolaandClaude Fable 5 09bb87d089 feat: harden /create and /refine workflow generation for edge cases (#37336)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 02:41:17 +00:00
Asuka Minatoandautofix-ci[bot] 28cc3fc10d chore: [Refactor/Chore] if isinstance to match case #35902 (#37087)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-09 09:54:04 +00:00
CrazywoolaandClaude Opus 4.8 db1aa683bc feat(web): gate /create and /refine slash commands behind feature preview flag (#37094)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 02:32:52 +00:00
Evan 76fb1b6ea8 refactor(api): remove redundant typing.cast calls (#37124) 2026-06-06 02:39:07 +00:00
0bfbd2061e feat: enhance go to anything (#32130)
Co-authored-by: Claude Opus 4.7 (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>
2026-06-04 11:06:17 +00:00