Files
dify/api/core/dify_builder/handlers_edit.py
T
Charles YaoandClaude Opus 4.8 97b8354173 fix(dify-builder): capture verify-launch errors instead of swallowing them
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>
2026-09-10 14:24:51 -07:00

783 lines
30 KiB
Python

"""Edit-flow handlers: pure ``(env, turn, session, fc) -> StepResult`` steps.
Mirrors ``handlers_build.py`` for the Edit entry mode (spec: docs/superpowers/
specs/2026-08-23-dify-builder-slice3-edit-design.md). Cards for a state are
emitted by the handler transitioning INTO it; ``edit.test_affected_paths`` and
``edit.publish`` are working states that auto-advance. Publishing is separated
from the review decision so its progress and side effect precede the terminal
``edit.complete`` commit. Keeping the draft reaches that terminal directly.
"""
import logging
import uuid
from core.dify_builder.changes import describe_changed_nodes, describe_proposed_nodes
from core.dify_builder.contract import (
AssistantTurnItem,
ChallengeCard,
ChangeSetCard,
CheckpointCard,
DecisionItem,
ErrorCard,
ExecutionProgress,
FormCard,
PlanCard,
PublishCard,
SummaryCard,
SummaryRow,
TestResultCard,
TestStat,
)
from core.dify_builder.handlers_fix import (
action_kind,
action_string,
append_card,
build_change_set,
build_form_fields,
emit_canvas,
first_failed_node,
is_input_failure,
launch_error_text,
merge_known_keys,
mint_checkpoint,
perform_revert,
start_schema,
testdata_form_fields,
)
from core.dify_builder.models import DifyBuilderContext, NodeEvent, Run, Session, TestInput, Turn
from core.dify_builder.progress import ProgressReporter
from core.dify_builder.runner import Env, Handler, StepResult
from core.dify_builder.state import PcState
logger = logging.getLogger(__name__)
__all__ = [
"edit_registry",
"handle_apply_changes",
"handle_await_repair",
"handle_await_testdata",
"handle_capability_check",
"handle_impact_analysis",
"handle_plan_approval",
"handle_publish",
"handle_reverted",
"handle_review",
"handle_test_affected_paths",
]
def handle_capability_check(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) Entry state. On ``send_edit_goal`` read the existing graph,
emit a read-only context summary, analyze impact into edit_rules + target
nodes (highlighting them), and transition to edit.impact_analysis emitting
its form + challenge + change_set(preview). The canvas is read only here,
after the goal is sent (mock 02-edit.txt:3,9)."""
kind = action_kind(turn)
if kind != "send_edit_goal":
return StepResult(next=PcState.EDIT_CAPABILITY_CHECK, context=fc)
text, ok = action_string(turn, "text")
if ok and text:
fc.goal_text = text
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[
("edit-inspect-workflow", "Inspect the current workflow"),
("edit-analyze-impact", "Analyze the requested change"),
("edit-prepare-impact", "Prepare the impact summary"),
],
)
progress.activate("edit-inspect-workflow")
graph, graph_hash = env.dify.read_graph(s.app_id, turn.actor)
fc.last_snapshot_hash = graph_hash
fc.last_structure_fingerprint = env.dify.structural_fingerprint(graph)
node_count = len(graph.get("nodes", []))
edge_count = len(graph.get("edges", []))
progress.activate("edit-analyze-impact")
impact = env.agent.analyze_impact(fc.goal_text, graph)
fc.form_fields = list(impact.get("fields") or [])
fc.edit_rules = dict(impact.get("values") or {})
fc.edit_target_node_ids = list(impact.get("target_node_ids") or [])
for node_id in fc.edit_target_node_ids:
emit_canvas(env, "highlight_edit_target", node_id=node_id)
progress.activate("edit-prepare-impact")
summary_items = append_card(
fc,
SummaryCard(
variant="context",
title="Current workflow",
items=[f"Read {node_count} nodes", f"{edge_count} connections"],
),
)
form_items = append_card(
fc,
FormCard(
variant="edit_rules", fields=build_form_fields(fc.form_fields), values=dict(fc.edit_rules), frozen=False
),
)
challenge_items = append_card(
fc,
ChallengeCard(
title="High-impact rules",
body="These rules change branching and output; review before applying.",
tone="warning",
),
)
change_set_items = append_card(
fc,
ChangeSetCard(
count=len(fc.edit_target_node_ids),
changes=[],
scope="configuration",
nodes=describe_changed_nodes(fc.edit_target_node_ids, graph),
),
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="Here's the impact of your change.",
cards=["summary", "form", "challenge", "change_set"],
),
)
return StepResult(
next=PcState.EDIT_IMPACT_ANALYSIS,
context=fc,
items=[*summary_items, *form_items, *challenge_items, *change_set_items, *turn_items],
)
def handle_impact_analysis(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) On ``submit_edit_rules`` merge the form payload, propose the
change plan, self-mint the pre-edit checkpoint (so the CheckpointCard at
plan_approval carries a real id -- mirrors Build's handle_resource_
recommendation), and transition to edit.plan_approval."""
kind = action_kind(turn)
if kind != "submit_edit_rules":
return StepResult(next=PcState.EDIT_IMPACT_ANALYSIS, context=fc)
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[
("edit-review-rules", "Review confirmed edit rules"),
("edit-draft-plan", "Draft the change plan"),
("edit-create-checkpoint", "Create a pre-edit checkpoint"),
],
)
progress.activate("edit-review-rules")
fc.checkpoint_seq = fc.next_seq
if turn.action is not None and isinstance(turn.action.payload, dict):
keys = [f["key"] for f in fc.form_fields if isinstance(f, dict) and f.get("key")]
fc.edit_rules = merge_known_keys(fc.edit_rules, turn.action.payload, keys)
progress.activate("edit-draft-plan")
graph, graph_hash = env.dify.read_graph(s.app_id, turn.actor)
fc.plan_items = env.agent.propose_edit_plan(dict(fc.edit_rules), graph)
fc.plan_version_tag = "v1"
progress.activate("edit-create-checkpoint")
checkpoint_id = mint_checkpoint(env, s, fc, graph, graph_hash, PcState.EDIT_PLAN_APPROVAL)
decision_items = append_card(fc, DecisionItem(text="Submitted edit rules"))
plan_items = append_card(fc, PlanCard(title="Change plan", version_tag="v1", items=list(fc.plan_items)))
checkpoint_items = append_card(
fc, CheckpointCard(checkpoint_id=checkpoint_id, label="Pre-edit checkpoint", created_at="")
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="Change plan ready for approval.",
cards=["plan", "checkpoint"],
),
)
return StepResult(
next=PcState.EDIT_PLAN_APPROVAL,
context=fc,
items=[*decision_items, *plan_items, *checkpoint_items, *turn_items],
)
_EDIT_EXECUTION_STEPS = [
("edit-prepare", "Prepare canvas changes"),
("edit-highlight", "Highlight edit targets"),
("edit-apply", "Apply the change plan"),
]
def handle_plan_approval(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) THE EDIT. Only ``approve_repair`` (resolved from approve_plan)
applies: read the current graph, get the canned set_node_config intents,
highlight the targets, apply once (on_canvas=None -- Edit narrates its own
coarse apply_edit_plan rather than the Fix-flavored per-intent apply_error_
fix), emit the real change_set + checkpoint + assistant_turn, transition to
edit.apply_changes.
Naturally idempotent on loop-back re-approve: re-applying the same
set_node_config value overwrites the node's data (no ValueError, unlike
Build's create_node); a re-approve simply yields an empty diff."""
kind = action_kind(turn)
if kind != "approve_repair":
return StepResult(next=PcState.EDIT_PLAN_APPROVAL, context=fc)
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=_EDIT_EXECUTION_STEPS,
)
progress.activate("edit-prepare")
emit_canvas(env, "create_checkpoint")
graph, _hash = env.dify.read_graph(s.app_id, turn.actor)
intents = env.agent.build_edit_intents(dict(fc.edit_rules), graph)
fc.staged_repair = list(intents)
progress.activate("edit-highlight")
for node_id in fc.edit_target_node_ids:
emit_canvas(env, "highlight_edit_target", node_id=node_id)
progress.activate("edit-apply")
result = env.dify.apply_repair(s.app_id, turn.actor, intents, on_canvas=None)
fc.last_snapshot_hash = result.new_hash
fc.last_structure_fingerprint = result.structure_fingerprint
emit_canvas(env, "apply_edit_plan")
changes, scope, fc.change_set = build_change_set(result, default_scope="configuration", fallback_diff="no changes")
change_set_items = append_card(
fc,
ChangeSetCard(
count=len(changes),
changes=changes,
scope=scope,
nodes=result.nodes or describe_changed_nodes(result.changed_nodes),
),
)
checkpoint_items = append_card(
fc, CheckpointCard(checkpoint_id=fc.checkpoint_id, label="Pre-edit checkpoint", created_at="")
)
decision_items = append_card(fc, DecisionItem(text="Approved the change plan"))
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="Applied the changes to the canvas.",
cards=["change_set", "checkpoint"],
),
)
return StepResult(
next=PcState.EDIT_APPLY_CHANGES,
context=fc,
items=[*change_set_items, *checkpoint_items, *decision_items, *turn_items],
)
def handle_apply_changes(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) At rest after the edit. ``run_affected_tests`` -> edit.await_
testdata when no test input is prepared yet (gate), else straight to edit.
test_affected_paths; ``revert`` (resolved to ``undo``) -> edit.reverted:
restores the pre-edit draft from the checkpoint and invalidates the
approvals made since it (via perform_revert)."""
kind = action_kind(turn)
if kind == "undo":
perform_revert(env, turn, s, fc)
items = append_card(fc, DecisionItem(text="Requested a revert"))
return StepResult(next=PcState.EDIT_REVERTED, context=fc, items=items)
if kind == "run_affected_tests":
if fc.test_input_ref == "":
graph, _hash = env.dify.read_graph(s.app_id, turn.actor)
form_items = append_card(
fc,
FormCard(
variant="testdata",
fields=testdata_form_fields(start_schema(graph)),
values={},
frozen=False,
),
)
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=str(uuid.uuid4()),
stage_id=str(s.current_state),
execution=ExecutionProgress(status="completed"),
reply_text="Provide test inputs (or use mock data) to run the affected-path test.",
cards=["form"],
),
)
return StepResult(next=PcState.EDIT_AWAIT_TESTDATA, context=fc, items=[*form_items, *turn_items])
emit_canvas(env, "start_test_run")
items = append_card(fc, DecisionItem(text="Run affected-path tests"))
return StepResult(next=PcState.EDIT_TEST_AFFECTED_PATHS, context=fc, items=items)
return StepResult(next=PcState.EDIT_APPLY_CHANGES, context=fc)
def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) Prepare inputs for the affected-path test. mock -> schema-shaped
generate_mock_inputs; provide/upload -> the payload's inputs dict (may carry
file refs). Persists a TestInput and advances to edit.test_affected_paths."""
mode, _ = action_string(turn, "mode")
if mode == "mock":
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[("edit-generate-test-inputs", "Generate affected-path test inputs")],
)
progress.activate("edit-generate-test-inputs")
graph, _hash = env.dify.read_graph(s.app_id, turn.actor)
inputs = env.agent.generate_mock_inputs(start_schema(graph), {})
progress.finish()
else:
inputs = {}
if turn.action is not None and isinstance(turn.action.payload.get("inputs"), dict):
inputs = turn.action.payload["inputs"]
ti = TestInput(session_id=s.id, source=mode or "upload", inputs=inputs)
env.repo.save_test_input(ti)
fc.test_input_ref = ti.id
return StepResult(next=PcState.EDIT_TEST_AFFECTED_PATHS, context=fc)
def handle_test_affected_paths(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(working, auto) Live affected-path test via run_draft. Success ->
edit.review; failure -> real diagnose + propose_repair, staged for the
edit.await_repair approval gate. No auto-apply (human-gated)."""
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[
("edit-prepare-test", "Prepare affected-path tests"),
("edit-run-test", "Run affected workflow paths"),
("edit-evaluate-test", "Evaluate the test result"),
],
)
progress.activate("edit-prepare-test")
graph, _hash = env.dify.read_graph(s.app_id, turn.actor)
if fc.test_input_ref:
inputs = env.repo.get_test_input(fc.test_input_ref).inputs
else: # defensive: the gate normally prepares inputs first
inputs = env.agent.generate_mock_inputs(start_schema(graph), {})
ti = TestInput(session_id=s.id, source="mock", inputs=inputs)
env.repo.save_test_input(ti)
fc.test_input_ref = ti.id
progress.activate("edit-run-test")
def emit(event: NodeEvent) -> None:
progress.observe_node("edit-run-test", event)
if env.emit is not None:
env.emit(event)
try:
raw = env.dify.run_draft(s.app_id, turn.actor, inputs, emit)
status, per_node, dify_run_id, run_error = raw.status, raw.per_node, raw.dify_run_id, ""
except Exception as exc:
# Never crash the advance; capture the launch error (log + store) instead
# of swallowing it, so diagnose/routing have something to act on.
logger.exception("dify_builder verify run failed to launch (session=%s, app=%s)", s.id, s.app_id)
status, per_node, dify_run_id, run_error = "failed", [], "", launch_error_text(exc)
if status == "succeeded":
progress.complete("edit-run-test")
else:
progress.fail_step("edit-run-test")
progress.activate("edit-evaluate-test")
run = Run(
id=str(uuid.uuid4()),
kind="verify",
dify_run_id=dify_run_id,
status=status,
per_node=per_node,
error=run_error,
inputs_ref=fc.test_input_ref,
immutable=True,
)
if status == "succeeded":
emit_canvas(env, "mark_test_success")
test_items = append_card(
fc,
TestResultCard(
title="Affected-path tests",
subtitle="All checks passed",
tone="success",
stats=[TestStat(value="1", label="runs"), TestStat(value="0", label="errors")],
run_ids=[run.id],
),
)
emit_canvas(env, "mark_review_ready")
summary_items = append_card(
fc,
SummaryCard(
variant="review",
title="Review",
items=["Applied the change plan", "Affected paths tested", "Tests passing"],
),
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="Tests passed; ready for review.",
cards=["test_result", "summary"],
),
)
return StepResult(
next=PcState.EDIT_REVIEW,
context=fc,
items=[*test_items, *summary_items, *turn_items],
run=run,
run_id_sink=[run.id],
)
# failure: real diagnosis + proposed repair, staged for the approval gate
run.culprit_node_id = first_failed_node(per_node)
fc.verify_run_id = run.id
emit_canvas(env, "mark_test_error")
if is_input_failure(run):
# the run failed on its INPUT, not the config -- route back to the
# testdata gate instead of the config-repair gate; clear the stale
# input ref (and the verify_run_id we just set) so a fresh
# provide_testdata cycle starts clean.
fc.test_input_ref = ""
fc.verify_run_id = ""
test_items = append_card(
fc,
TestResultCard(
title="Affected-path tests",
subtitle="Failed",
tone="error",
stats=[TestStat(value="1", label="runs"), TestStat(value="1", label="errors")],
run_ids=[run.id],
),
)
form_items = append_card(
fc,
FormCard(
variant="testdata",
fields=testdata_form_fields(start_schema(graph)),
values={},
frozen=False,
),
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="The run failed on its inputs — provide test data and retry.",
cards=["test_result", "form"],
),
)
return StepResult(
next=PcState.EDIT_AWAIT_TESTDATA,
context=fc,
items=[*test_items, *form_items, *turn_items],
run=run,
run_id_sink=[run.id],
)
# config failure: existing diagnose + propose_repair -> EDIT_AWAIT_REPAIR
progress.add_steps(
[
("edit-diagnose-failure", "Diagnose the failed path"),
("edit-prepare-repair", "Prepare a safe repair"),
]
)
progress.activate("edit-diagnose-failure")
diagnosis = env.agent.diagnose(run, graph, per_node)
progress.activate("edit-prepare-repair")
intents, risk = env.agent.propose_repair(diagnosis, graph)
fc.diagnosis = diagnosis
fc.staged_repair = list(intents)
fc.risk = risk
test_items = append_card(
fc,
TestResultCard(
title="Affected-path tests",
subtitle="Failed",
tone="error",
stats=[TestStat(value="1", label="runs"), TestStat(value="1", label="errors")],
run_ids=[run.id],
),
)
error_items = append_card(
fc,
ErrorCard(
title="Test failed",
body=diagnosis.root_cause or "The run failed.",
tone="danger",
node_id=diagnosis.culprit_node_id,
),
)
proposed = [f"{i.op} {i.args.get('node_id', '')}".strip() for i in intents]
cs_items = (
append_card(
fc,
ChangeSetCard(
count=len(intents),
changes=proposed,
scope="configuration",
nodes=describe_proposed_nodes(intents, graph),
),
)
if intents
else []
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text=(
"Test failed — here's a proposed fix to review."
if intents
else "Test failed — no safe automatic fix; edit or keep draft."
),
cards=["test_result", "error"] + (["change_set"] if intents else []),
),
)
return StepResult(
next=PcState.EDIT_AWAIT_REPAIR,
context=fc,
items=[*test_items, *error_items, *cs_items, *turn_items],
run=run,
run_id_sink=[run.id],
)
def handle_await_repair(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) Post-failure gate for Edit, mirroring Build's await_repair.
approve_repair applies the staged repair and re-runs the affected-path
tests (edit.test_affected_paths); keep_draft -> edit.review; undo ->
edit.reverted. apply_repair runs ONLY here, only on approve."""
kind = action_kind(turn)
if kind == "approve_repair":
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[
("edit-apply-repair", "Apply the approved repair"),
("edit-prepare-retest", "Prepare to retest affected paths"),
],
)
progress.activate("edit-apply-repair")
result = env.dify.apply_repair(s.app_id, turn.actor, list(fc.staged_repair), on_canvas=env.emit_canvas)
fc.last_snapshot_hash = result.new_hash
fc.last_structure_fingerprint = result.structure_fingerprint
fc.staged_repair = []
changes, scope, fc.change_set = build_change_set(result, default_scope="configuration", fallback_diff="repair")
cs_items = append_card(
fc,
ChangeSetCard(
count=len(changes),
changes=changes,
scope=scope,
nodes=result.nodes or describe_changed_nodes(result.changed_nodes),
),
)
progress.activate("edit-prepare-retest")
decision_items = append_card(fc, DecisionItem(text="Approved the fix; retesting"))
progress.finish()
return StepResult(next=PcState.EDIT_TEST_AFFECTED_PATHS, context=fc, items=[*cs_items, *decision_items])
if kind == "keep_draft":
items = append_card(fc, DecisionItem(text="Kept the draft despite the failure"))
return StepResult(next=PcState.EDIT_REVIEW, context=fc, items=items)
if kind == "undo":
perform_revert(env, turn, s, fc)
items = append_card(fc, DecisionItem(text="Requested a revert"))
return StepResult(next=PcState.EDIT_REVERTED, context=fc, items=items)
return StepResult(next=PcState.EDIT_AWAIT_REPAIR, context=fc)
def _completion_rows(fc: DifyBuilderContext, status: str) -> list[SummaryRow]:
return [
SummaryRow(label="Change", value="; ".join(fc.plan_items) or "config edit"),
SummaryRow(label="Status", value=status),
]
def handle_review(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) Final user decision before publish or completion."""
kind = action_kind(turn)
if kind == "publish_workflow":
return StepResult(
next=PcState.EDIT_PUBLISH,
context=fc,
items=append_card(fc, DecisionItem(text="Chose to publish")),
)
if kind == "keep_draft":
emit_canvas(env, "cancel_publish")
decision_items = append_card(fc, DecisionItem(text="Kept the draft"))
summary_items = append_card(
fc, SummaryCard(variant="completion", title="Draft kept", rows=_completion_rows(fc, "Draft kept"))
)
return StepResult(next=PcState.EDIT_COMPLETE, context=fc, items=[*decision_items, *summary_items])
if kind == "re_fix": # continue_adjusting -> re-analyze impact
graph, _hash = env.dify.read_graph(s.app_id, turn.actor)
emit_canvas(env, "cancel_publish")
for node_id in fc.edit_target_node_ids:
emit_canvas(env, "highlight_edit_target", node_id=node_id)
fc.test_input_ref = ""
fc.verify_run_id = ""
decision_items = append_card(fc, DecisionItem(text="Continue adjusting"))
form_items = append_card(
fc,
FormCard(
variant="edit_rules", fields=build_form_fields(fc.form_fields), values=dict(fc.edit_rules), frozen=False
),
)
challenge_items = append_card(
fc,
ChallengeCard(
title="High-impact rules",
body="These rules change branching and output; review before applying.",
tone="warning",
),
)
change_set_items = append_card(
fc,
ChangeSetCard(
count=len(fc.edit_target_node_ids),
changes=[],
scope="configuration",
nodes=describe_changed_nodes(fc.edit_target_node_ids, graph),
),
)
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=str(uuid.uuid4()),
stage_id=str(s.current_state),
execution=ExecutionProgress(status="completed"),
reply_text="Let's adjust the change.",
cards=["form", "challenge", "change_set"],
),
)
return StepResult(
next=PcState.EDIT_IMPACT_ANALYSIS,
context=fc,
items=[*decision_items, *form_items, *challenge_items, *change_set_items, *turn_items],
)
if kind == "undo": # revert
perform_revert(env, turn, s, fc)
items = append_card(fc, DecisionItem(text="Requested a revert"))
return StepResult(next=PcState.EDIT_REVERTED, context=fc, items=items)
return StepResult(next=PcState.EDIT_REVIEW, context=fc)
def handle_publish(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(working, auto) Publish the updated workflow and close the edit flow."""
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[("edit-publish-workflow", "Publish the updated workflow")],
)
progress.activate("edit-publish-workflow")
env.dify.publish(s.app_id, turn.actor)
emit_canvas(env, "publish_workflow")
publish_items = append_card(fc, PublishCard(version="2.1", badge="live"))
summary_items = append_card(
fc, SummaryCard(variant="completion", title="Edit published", rows=_completion_rows(fc, "Published"))
)
progress.finish()
return StepResult(next=PcState.EDIT_COMPLETE, context=fc, items=[*publish_items, *summary_items])
def handle_reverted(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
"""(waiting) After a revert. ``retry_after_revert`` (resolved to re_fix)
re-proposes the change plan, self-mints a fresh pre-edit checkpoint, and
returns to edit.plan_approval (spec §7.2)."""
kind = action_kind(turn)
if kind != "re_fix":
return StepResult(next=PcState.EDIT_REVERTED, context=fc)
progress = ProgressReporter.for_session(
emit=env.emit_progress,
operation_id=env.operation_id,
session=s,
stage_id=str(s.current_state),
steps=[
("edit-rebuild-plan", "Rebuild the change plan"),
("edit-create-checkpoint", "Create a new pre-edit checkpoint"),
],
)
progress.activate("edit-rebuild-plan")
fc.checkpoint_seq = fc.next_seq
graph, graph_hash = env.dify.read_graph(s.app_id, turn.actor)
fc.plan_items = env.agent.propose_edit_plan(dict(fc.edit_rules), graph)
fc.plan_version_tag = "v1"
fc.test_input_ref = ""
fc.verify_run_id = ""
progress.activate("edit-create-checkpoint")
checkpoint_id = mint_checkpoint(env, s, fc, graph, graph_hash, PcState.EDIT_PLAN_APPROVAL)
plan_items = append_card(fc, PlanCard(title="Change plan", version_tag="v1", items=list(fc.plan_items)))
checkpoint_items = append_card(
fc, CheckpointCard(checkpoint_id=checkpoint_id, label="Pre-edit checkpoint", created_at="")
)
execution = progress.finish()
turn_items = append_card(
fc,
AssistantTurnItem(
turn_id=progress.operation_id,
stage_id=str(s.current_state),
execution=execution,
reply_text="Re-approve to apply the change.",
cards=["plan", "checkpoint"],
),
)
return StepResult(next=PcState.EDIT_PLAN_APPROVAL, context=fc, items=[*plan_items, *checkpoint_items, *turn_items])
def edit_registry() -> dict[PcState, Handler]:
"""The Edit handler table; ``edit.complete`` is the terminal state."""
return {
PcState.EDIT_CAPABILITY_CHECK: handle_capability_check,
PcState.EDIT_IMPACT_ANALYSIS: handle_impact_analysis,
PcState.EDIT_PLAN_APPROVAL: handle_plan_approval,
PcState.EDIT_APPLY_CHANGES: handle_apply_changes,
PcState.EDIT_AWAIT_TESTDATA: handle_await_testdata,
PcState.EDIT_TEST_AFFECTED_PATHS: handle_test_affected_paths,
PcState.EDIT_AWAIT_REPAIR: handle_await_repair,
PcState.EDIT_REVIEW: handle_review,
PcState.EDIT_PUBLISH: handle_publish,
PcState.EDIT_REVERTED: handle_reverted,
}