mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Two App Builder spec changes that share the Builder session contract.
Naming (spec N1-N3): an app created from the homepage prompt is no longer named
by the user. core/dify_builder/naming.py cuts the prompt's opening clause at
create time -- instant and infallible, since it runs inside POST /apps -- and
CreateAppPayload.name becomes optional alongside a new prompt field. The Builder
then refines that name once during goal analysis, where a model and the detected
reply language are already in hand, so the judgement calls that are
English-shaped in pure Python (dropping an imperative lead-in, spotting a
contentless prompt) work in any language. Plan approval freezes the name.
Card options: SessionView gains a Decision carrying the gate's choice as options
inside the card plus a fixed confirm/cancel pair, replacing a button row whose
shape changed at every state. Option ids are the old action ids untouched, so
resolve_action_kind and the per-state legality check are unchanged, and
SessionView.actions keeps being populated -- the deployed web client goes on
working until it reads decision instead.
The regenerated console OpenAPI doc also picks up ErrorCard.diagnostics, missed
when cc9ca8b1dd landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
935 lines
25 KiB
Python
935 lines
25 KiB
Python
"""Dify Builder FE<->backend interaction contract.
|
|
|
|
New for the Build/Edit slice (spec: ``docs/superpowers/specs/
|
|
2026-08-21-dify-builder-full-flow-contract-design.md``, §2 enums, §6
|
|
canvas events, §7 state machines). These enums freeze the wire vocabulary
|
|
shared by all entry modes (Fix, Build, Edit) — canned agent today, a future
|
|
ProAgent tomorrow, same shapes (§ Global Constraints: "canned-agnostic").
|
|
|
|
All members are ``StrEnum`` so ``dataclasses.asdict``/``json.dumps`` emit the
|
|
snake_case wire value directly; the member name is just the UPPER_SNAKE of
|
|
that value.
|
|
"""
|
|
|
|
from dataclasses import asdict, dataclass, field
|
|
from enum import StrEnum
|
|
from typing import Any, ClassVar, Literal
|
|
|
|
from core.dify_builder.models import ChangedNode, ConversationItem, EntryMode
|
|
|
|
|
|
class Phase(StrEnum):
|
|
"""Coarse UX phase shown in the panel header (spec §2)."""
|
|
|
|
UNDERSTAND = "understand"
|
|
CLARIFY = "clarify"
|
|
RESOURCES = "resources"
|
|
PLAN = "plan"
|
|
MODIFY = "modify"
|
|
TEST = "test"
|
|
REVIEW = "review"
|
|
PUBLISH = "publish"
|
|
COMPLETE = "complete"
|
|
|
|
|
|
class RunStatus(StrEnum):
|
|
"""Widened run-status vocabulary (spec §2).
|
|
|
|
``processing`` => ``canvas_read_only = true`` (WORKING).
|
|
``waiting_*``/``paused`` => editable (WAITING).
|
|
``complete``/``failed`` => terminal.
|
|
"""
|
|
|
|
PROCESSING = "processing"
|
|
WAITING_INPUT = "waiting_input"
|
|
WAITING_CONFIRMATION = "waiting_confirmation"
|
|
PAUSED = "paused"
|
|
FAILED = "failed"
|
|
COMPLETE = "complete"
|
|
|
|
|
|
class ActionKind(StrEnum):
|
|
"""Action button styling / semantics (spec §2, §5).
|
|
|
|
``automatic`` actions are not buttons — the client must not render
|
|
them; the server auto-advances. They exist in the stream only so the
|
|
FE can show "auto-continuing...".
|
|
"""
|
|
|
|
PRIMARY = "primary"
|
|
SECONDARY = "secondary"
|
|
DESTRUCTIVE = "destructive"
|
|
AUTOMATIC = "automatic"
|
|
|
|
|
|
class CardKind(StrEnum):
|
|
"""The conversation-item/card vocabulary (spec §4)."""
|
|
|
|
USER = "user"
|
|
DECISION = "decision"
|
|
NOTICE = "notice"
|
|
RUN_CONTEXT = "run_context"
|
|
PREFLIGHT_CONTEXT = "preflight_context"
|
|
ASSISTANT_TURN = "assistant_turn"
|
|
PLAN = "plan"
|
|
FORM = "form"
|
|
CHALLENGE = "challenge"
|
|
RESOURCE_SELECT = "resource_select"
|
|
CHECKPOINT = "checkpoint"
|
|
CHANGE_SET = "change_set"
|
|
TEST_RESULT = "test_result"
|
|
ERROR = "error"
|
|
SUMMARY = "summary"
|
|
PUBLISH = "publish"
|
|
BUILD_LEARNING = "build_learning"
|
|
|
|
|
|
class CanvasEvent(StrEnum):
|
|
"""Granular canvas-mutation signals (spec §6).
|
|
|
|
Exactly 22 members, snake_cased from the mock's ``DifyBuilderCanvasEvent``
|
|
union. Presentation of committed backend state: replaying these from a
|
|
snapshot must reconstruct the same canvas.
|
|
"""
|
|
|
|
RESET_BUILD_CANVAS = "reset_build_canvas"
|
|
ADD_START_NODE = "add_start_node"
|
|
ADD_KNOWLEDGE_NODE = "add_knowledge_node"
|
|
ADD_LLM_NODE = "add_llm_node"
|
|
ADD_OUTPUT_NODE = "add_output_node"
|
|
FOCUS_WORKFLOW = "focus_workflow"
|
|
HIGHLIGHT_EDIT_TARGET = "highlight_edit_target"
|
|
APPLY_EDIT_PLAN = "apply_edit_plan"
|
|
START_TEST_RUN = "start_test_run"
|
|
MARK_TEST_ERROR = "mark_test_error"
|
|
FOCUS_ERROR_NODE = "focus_error_node"
|
|
FOCUS_CHECKLIST_NODE = "focus_checklist_node"
|
|
CREATE_CHECKPOINT = "create_checkpoint"
|
|
APPLY_ERROR_FIX = "apply_error_fix"
|
|
MARK_REPAIR_APPLIED = "mark_repair_applied"
|
|
APPLY_PREFLIGHT_FIX = "apply_preflight_fix"
|
|
START_RETEST = "start_retest"
|
|
MARK_TEST_SUCCESS = "mark_test_success"
|
|
MARK_REVIEW_READY = "mark_review_ready"
|
|
REVERT_CHECKPOINT = "revert_checkpoint"
|
|
CANCEL_PUBLISH = "cancel_publish"
|
|
PUBLISH_WORKFLOW = "publish_workflow"
|
|
|
|
|
|
class ConflictPolicy(StrEnum):
|
|
"""Resource-conflict handling in Build (spec §2)."""
|
|
|
|
AUDITED = "audited"
|
|
ASK = "ask"
|
|
|
|
|
|
class SkillLearningPolicy(StrEnum):
|
|
"""Governance tail (spec §2). Deferred — contract-only for now."""
|
|
|
|
ASK = "ask"
|
|
AUTOMATIC = "automatic"
|
|
DISABLED = "disabled"
|
|
|
|
|
|
class RecoveryClass(StrEnum):
|
|
"""How the draft drifted from what the dify_builder last knew, when a session
|
|
is reopened after a hand-edit (spec §8 / C-1)."""
|
|
|
|
UNCHANGED = "unchanged"
|
|
CONFIG_ONLY = "config_only"
|
|
STRUCTURAL_COMPATIBLE = "structural_compatible"
|
|
STRUCTURAL_INVALIDATING = "structural_invalidating"
|
|
|
|
|
|
class BuilderErrorCode(StrEnum):
|
|
"""Stable HTTP error codes returned by the Builder console routes."""
|
|
|
|
BAD_REQUEST = "bad_request"
|
|
NOT_FOUND = "not_found"
|
|
CONFLICT = "conflict"
|
|
SESSION_BUSY = "session_busy"
|
|
FEATURE_UNAVAILABLE = "feature_unavailable"
|
|
MODEL_UNAVAILABLE = "model_unavailable"
|
|
|
|
|
|
@dataclass
|
|
class Action:
|
|
"""A UI action the FE renders (spec §5).
|
|
|
|
DISTINCT from ``core.dify_builder.models.Action`` (the submit DTO
|
|
the FE POSTs back) -- this one is what the server *sends* describing a
|
|
renderable button.
|
|
"""
|
|
|
|
id: str
|
|
label: str
|
|
kind: ActionKind
|
|
next_state: str | None = None
|
|
canvas_event: str | None = None
|
|
|
|
|
|
# The only two buttons an interactive card shows; every real choice is an option
|
|
# inside it. One interaction shape at every gate, not one per state.
|
|
CONFIRM_ACTION_ID = "confirm"
|
|
CANCEL_ACTION_ID = "cancel"
|
|
|
|
|
|
@dataclass
|
|
class OptionInput:
|
|
"""Free text an option reveals once selected ("Something else", "Reject with a reason").
|
|
|
|
Bounds are declared for the client to enforce; see ``Decision`` for why the
|
|
server does not reject on them yet.
|
|
"""
|
|
|
|
placeholder: str = ""
|
|
min_length: int = 0
|
|
max_length: int = 100
|
|
required: bool = True
|
|
|
|
|
|
@dataclass
|
|
class CardOption:
|
|
"""One answer a gate offers, inside the card instead of as a button.
|
|
|
|
``id`` is the former standalone action id, unchanged, so an option resolves
|
|
to exactly the handler kind its button used to.
|
|
"""
|
|
|
|
id: str
|
|
label: str
|
|
# The second line under an option. Empty for now: the per-state action table
|
|
# carries labels only, and writing the copy here is a product decision.
|
|
description: str = ""
|
|
# Shown as a DEFAULT badge. Advisory only -- these gates have side effects,
|
|
# so declining to choose must never apply anything.
|
|
is_default: bool = False
|
|
tone: str = "neutral" # neutral | destructive
|
|
input: OptionInput | None = None
|
|
next_state: str | None = None
|
|
canvas_event: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class Decision:
|
|
"""What the current gate asks: card options plus the fixed button pair.
|
|
|
|
Replaces the per-state action bar. The client renders ``options`` in the
|
|
active card, shows only ``confirm`` and ``cancel``, and on confirm posts
|
|
``confirm`` with ``{"option_id": ..., "free_text": ...}``. ``cancel`` is
|
|
presentational -- it dismisses without posting and the gate stays open.
|
|
|
|
While ``SessionView.actions`` is still populated, the server accepts the old
|
|
per-action ids too and does not enforce ``OptionInput`` bounds, so a client
|
|
posting a bare action id keeps working. Enforcement moves server-side when
|
|
``actions`` goes.
|
|
"""
|
|
|
|
options: list[CardOption] = field(default_factory=list)
|
|
confirm: Action | None = None
|
|
cancel: Action | None = None
|
|
default_option_id: str = ""
|
|
|
|
|
|
@dataclass
|
|
class CheckpointRef:
|
|
"""Active restore point on the SessionView (spec §8)."""
|
|
|
|
checkpoint_id: str
|
|
label: str
|
|
created_at: str
|
|
|
|
|
|
@dataclass
|
|
class RecoveryRef:
|
|
"""Draft-drift recovery offer on the SessionView (spec §8 / C-1)."""
|
|
|
|
recovery_class: str
|
|
can_continue: bool
|
|
can_restart: bool
|
|
message: str
|
|
|
|
|
|
@dataclass
|
|
class BuilderError:
|
|
"""Error body returned before an SSE response starts, or by a JSON route.
|
|
|
|
``message`` and ``recoverable`` are optional extension fields. Current
|
|
responses only require the stable ``code`` field.
|
|
"""
|
|
|
|
code: BuilderErrorCode
|
|
message: str | None = None
|
|
recoverable: bool | None = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Card sub-types (spec §4.3). Plain dataclasses -- no ``kind`` discriminant,
|
|
# they nest inside a card's fields rather than standing alone in the
|
|
# conversation.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class FormField:
|
|
"""One field of a ``form`` card, including Start-node input constraints."""
|
|
|
|
key: str
|
|
label: str
|
|
type: str
|
|
options: list[str] = field(default_factory=list)
|
|
required: bool = False
|
|
default: str | int | float | bool | None = None
|
|
max_length: int | None = field(default=None, metadata={"minimum": 1})
|
|
allowed_file_types: list[str] = field(default_factory=list)
|
|
allowed_file_extensions: list[str] = field(default_factory=list)
|
|
allowed_file_upload_methods: list[str] = field(default_factory=list)
|
|
placeholder: str | None = None
|
|
hint: str | None = None
|
|
unit: str | None = None
|
|
json_schema: str | dict[str, Any] | None = None
|
|
number_limits: int | None = field(default=None, metadata={"minimum": 1})
|
|
|
|
|
|
@dataclass
|
|
class ResourceOption:
|
|
"""One recommended resource in a ``resource_select`` card.
|
|
|
|
``kind`` in knowledge|plugin|model|credential; ``readiness`` in
|
|
ready|missing_config|missing_plugin|placeholder|waiting_setup.
|
|
Credentials are referenced by name only -- never a secret.
|
|
"""
|
|
|
|
id: str
|
|
label: str
|
|
meta: str
|
|
kind: str
|
|
readiness: str
|
|
|
|
|
|
@dataclass
|
|
class SessionModel:
|
|
"""The LLM chosen for this session, echoed for the FE model picker.
|
|
|
|
Empty on the wire (``model: null``) means the session uses the tenant default.
|
|
"""
|
|
|
|
provider: str
|
|
name: str
|
|
mode: str = ""
|
|
completion_params: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class AppRevision:
|
|
"""Shared draft-workflow revision associated with a private session.
|
|
|
|
``observed`` is the revision last incorporated by the Builder engine;
|
|
``current`` is read from the app draft when this SessionView is built.
|
|
A mismatch tells the client to refresh/rebase before submitting a
|
|
workflow-dependent action.
|
|
"""
|
|
|
|
observed: str
|
|
current: str
|
|
conflicted: bool
|
|
|
|
|
|
@dataclass
|
|
class ActiveInteraction:
|
|
"""The one persisted card that may accept input at the current version.
|
|
|
|
Historical cards remain renderable, but the client must only enable this
|
|
card. ``valid_at_version`` is an explicit fence in addition to the normal
|
|
action ``base_version`` check.
|
|
"""
|
|
|
|
action_id: str
|
|
card: ConversationItem
|
|
valid_at_version: int
|
|
|
|
|
|
@dataclass
|
|
class ConversationPage:
|
|
"""A group-safe page of durable conversation items.
|
|
|
|
``limit`` at the HTTP boundary counts rendered conversation groups rather
|
|
than raw rows, so a card bundle is never split from its assistant turn.
|
|
Items inside the page stay in ascending ``seq`` order.
|
|
"""
|
|
|
|
data: list[ConversationItem]
|
|
has_more: bool
|
|
first_seq: int | None
|
|
last_seq: int | None
|
|
|
|
|
|
@dataclass
|
|
class SessionView:
|
|
"""Bounded read model returned by Builder session-facing routes.
|
|
|
|
Conversation history deliberately lives behind ``ConversationPage``. This
|
|
object is safe to send at command start/end without payload growth as a
|
|
session gets older.
|
|
"""
|
|
|
|
session_id: str
|
|
app_id: str
|
|
version: int
|
|
state: str
|
|
canvas_read_only: bool
|
|
run_status: RunStatus
|
|
interrupted: bool
|
|
conversation_last_seq: int
|
|
entry_mode: EntryMode = EntryMode.FIX
|
|
phase: Phase = Phase.UNDERSTAND
|
|
# DEPRECATED until the web client reads ``decision``: the same choice, in
|
|
# the standalone-button shape.
|
|
actions: list[Action] = field(default_factory=list)
|
|
decision: Decision | None = None
|
|
active_interaction: ActiveInteraction | None = None
|
|
checkpoint: CheckpointRef | None = None
|
|
recovery: RecoveryRef | None = None
|
|
model: SessionModel | None = None
|
|
app_revision: AppRevision | None = None
|
|
|
|
|
|
@dataclass
|
|
class ConflictPolicyOption:
|
|
"""One selectable conflict-resolution policy in a ``resource_select`` card."""
|
|
|
|
id: str
|
|
label: str
|
|
recommended: bool = False
|
|
|
|
|
|
@dataclass
|
|
class PreflightIssue:
|
|
"""One checklist finding in a ``preflight_context`` card.
|
|
|
|
``kind`` in connection|variable|required_config|availability.
|
|
"""
|
|
|
|
node_id: str
|
|
label: str
|
|
kind: str
|
|
|
|
|
|
ExecutionActivityState = Literal["active", "done", "failed", "stopped"]
|
|
ExecutionActivityKind = Literal["stage", "node"]
|
|
ExecutionProgressStatus = Literal["running", "completed", "error", "stopped"]
|
|
|
|
|
|
@dataclass
|
|
class ExecutionActivity:
|
|
"""One observable action that has started during a Builder operation.
|
|
|
|
Planned future work is intentionally absent. Node activities use
|
|
``parent_id`` to sit under the stage that owns the workflow run.
|
|
"""
|
|
|
|
id: str
|
|
label: str
|
|
state: ExecutionActivityState
|
|
kind: ExecutionActivityKind = "stage"
|
|
parent_id: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class ExecutionProgress:
|
|
"""A snapshot containing only execution activities observed so far."""
|
|
|
|
status: ExecutionProgressStatus
|
|
activities: list[ExecutionActivity] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class TestStat:
|
|
"""One stat tile in a ``test_result`` card."""
|
|
|
|
# Not a pytest test class; ``__test__`` (unannotated, so not a dataclass
|
|
# field) tells pytest's Test*-name collector to skip it.
|
|
__test__ = False
|
|
|
|
value: str
|
|
label: str
|
|
|
|
|
|
@dataclass
|
|
class SummaryRow:
|
|
"""One row of a ``completion``-variant ``summary`` card."""
|
|
|
|
label: str
|
|
value: str
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cards (spec §4.3). Each subclasses ``_Card``: ``kind`` is a ``ClassVar``
|
|
# discriminant (not a dataclass field), so ``dataclasses.asdict(card)`` is
|
|
# exactly the card's wire ``payload`` -- no redundant ``kind`` inside it.
|
|
# ``to_item`` wraps the payload into the shipped ``ConversationItem``
|
|
# envelope (§4.1): ``{seq, kind, payload, at_version}``.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class _Card:
|
|
kind: ClassVar[CardKind]
|
|
|
|
def to_item(self, seq: int, at_version: int) -> ConversationItem:
|
|
return ConversationItem(seq=seq, kind=str(self.kind), payload=asdict(self), at_version=at_version)
|
|
|
|
|
|
@dataclass
|
|
class UserItem(_Card):
|
|
"""Right-aligned bubble: raw user text."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.USER
|
|
|
|
text: str
|
|
turn_id: str
|
|
|
|
|
|
@dataclass
|
|
class DecisionItem(_Card):
|
|
"""Right-aligned bubble: a committed-choice summary."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.DECISION
|
|
|
|
text: str
|
|
|
|
|
|
@dataclass
|
|
class NoticeItem(_Card):
|
|
"""A neutral/informational system notice."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.NOTICE
|
|
|
|
text: str
|
|
tone: str = "neutral"
|
|
|
|
|
|
@dataclass
|
|
class RunContextCard(_Card):
|
|
"""Fix-run injected context (spec §4.3). ``trace_ref`` is redacted."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.RUN_CONTEXT
|
|
|
|
run_id: str
|
|
title: str
|
|
error_code: str
|
|
message: str
|
|
trace_ref: str = ""
|
|
|
|
|
|
@dataclass
|
|
class PreflightContextCard(_Card):
|
|
"""Checklist-fix injected context (spec §4.3)."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.PREFLIGHT_CONTEXT
|
|
|
|
node_count: int
|
|
issue_count: int
|
|
issues: list[PreflightIssue] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class AssistantTurnItem(_Card):
|
|
"""The streamable unit (spec §4.2). ``stage_id`` is the PcState this
|
|
turn ran in; ``cards`` lists which cards (in order) this turn attaches.
|
|
``card_state`` is ``None`` or ``"invalidated"`` (frozen after task exit).
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.ASSISTANT_TURN
|
|
|
|
turn_id: str
|
|
stage_id: str
|
|
execution: ExecutionProgress
|
|
reasoning_text: str | None = None
|
|
reply_text: str | None = None
|
|
cards: list[str] = field(default_factory=list)
|
|
card_state: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class PlanCard(_Card):
|
|
"""Versioned plan (spec §4.3). ``items`` are ordered, human-readable
|
|
steps. Build emits v1/v2/v2.x; Edit emits a single change plan.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.PLAN
|
|
|
|
title: str
|
|
version_tag: str
|
|
items: list[str] = field(default_factory=list)
|
|
subtitle: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class FormCard(_Card):
|
|
"""Editable, re-submittable input card. ``variant`` in
|
|
build_requirements|edit_rules|testdata.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.FORM
|
|
|
|
variant: str
|
|
fields: list[FormField] = field(default_factory=list)
|
|
values: dict = field(default_factory=dict)
|
|
frozen: bool = False
|
|
|
|
|
|
@dataclass
|
|
class ChallengeCard(_Card):
|
|
"""High-impact-rule confirmation, distinct from a plain question."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.CHALLENGE
|
|
|
|
title: str
|
|
body: str
|
|
tone: str = "warning"
|
|
|
|
|
|
@dataclass
|
|
class ResourceSelectCard(_Card):
|
|
"""Resource discovery: multi-select ``recommended`` resources +
|
|
single-select ``conflict_policy_options``.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.RESOURCE_SELECT
|
|
|
|
recommended: list[ResourceOption] = field(default_factory=list)
|
|
conflict_policy_options: list[ConflictPolicyOption] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class CheckpointCard(_Card):
|
|
"""Restore point, backed by create-checkpoint/revert-checkpoint canvas
|
|
events. Restoring invalidates prior approvals.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.CHECKPOINT
|
|
|
|
checkpoint_id: str
|
|
label: str
|
|
created_at: str
|
|
|
|
|
|
@dataclass
|
|
class ChangeSetCard(_Card):
|
|
"""Read-only change summary. ``scope`` in annotation|configuration|structure."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.CHANGE_SET
|
|
|
|
count: int
|
|
changes: list[str]
|
|
scope: str
|
|
nodes: list[ChangedNode] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class TestResultCard(_Card):
|
|
"""Structured test/verify result. ``tone`` in success|error."""
|
|
|
|
# Not a pytest test class; ``__test__`` (unannotated, so not a dataclass
|
|
# field) tells pytest's Test*-name collector to skip it.
|
|
__test__ = False
|
|
|
|
kind: ClassVar[CardKind] = CardKind.TEST_RESULT
|
|
|
|
title: str
|
|
subtitle: str
|
|
tone: str
|
|
stats: list[TestStat] = field(default_factory=list)
|
|
run_ids: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class ErrorCard(_Card):
|
|
"""A found/fixed problem."""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.ERROR
|
|
|
|
title: str
|
|
body: str
|
|
tone: str = "danger"
|
|
node_id: str | None = None
|
|
# Machine-readable failure breadcrumbs (server timestamp + generator codes /
|
|
# node ids). Not rendered: it rides along in the item payload so the exported
|
|
# debug log retains what would otherwise only exist in a pod log.
|
|
diagnostics: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class SummaryCard(_Card):
|
|
"""Polymorphic by phase. ``variant`` in context|review|completion --
|
|
``items`` is used for context/review, ``rows`` for completion.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.SUMMARY
|
|
|
|
variant: str
|
|
title: str | None = None
|
|
items: list[str] = field(default_factory=list)
|
|
rows: list[SummaryRow] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class PublishCard(_Card):
|
|
"""Publish confirmation. Build -> v1.0, Edit -> v2.4-style bump, Fix ->
|
|
patch bump.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.PUBLISH
|
|
|
|
version: str
|
|
badge: str = "live"
|
|
|
|
|
|
@dataclass
|
|
class BuildLearningCard(_Card):
|
|
"""Post-build "sink this experience into System Skills?" card.
|
|
|
|
Deferred -- contract-only for now. ``policy`` is a
|
|
``SkillLearningPolicy`` value; ``state`` in pending|accepted|skipped.
|
|
"""
|
|
|
|
kind: ClassVar[CardKind] = CardKind.BUILD_LEARNING
|
|
|
|
policy: str
|
|
state: str
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Typed submit payloads (spec §5, ``SubmitPayloadKind``). Plain dataclasses
|
|
# -- no ``kind`` discriminant, no ``to_item``: these are POSTed by the FE as
|
|
# ``Action.payload``, never persisted as conversation items.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class GoalPayload:
|
|
text: str
|
|
|
|
|
|
@dataclass
|
|
class EditGoalPayload:
|
|
text: str
|
|
|
|
|
|
@dataclass
|
|
class RequirementsPayload:
|
|
report_types: str
|
|
audience: str
|
|
currency: str
|
|
metrics: str
|
|
output: str
|
|
prefer_audited: bool = False
|
|
|
|
|
|
@dataclass
|
|
class EditRulesPayload:
|
|
risk_threshold: str
|
|
review_team: str
|
|
timeout_behavior: str
|
|
preserve_summary: bool = False
|
|
|
|
|
|
@dataclass
|
|
class ResourcesPayload:
|
|
conflict_policy: str
|
|
resource_ids: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class RunContextPayload:
|
|
run_id: str
|
|
node_name: str
|
|
error_code: str
|
|
|
|
|
|
@dataclass
|
|
class CheckpointPayload:
|
|
checkpoint_id: str
|
|
|
|
|
|
@dataclass
|
|
class TestPayload:
|
|
# Not a pytest test class; ``__test__`` (unannotated, so not a dataclass
|
|
# field) tells pytest's Test*-name collector to skip it.
|
|
__test__ = False
|
|
|
|
run_ids: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class PublishPayload:
|
|
version: str
|
|
|
|
|
|
@dataclass
|
|
class TestdataPayload:
|
|
"""``mode`` in upload|self_test|mock."""
|
|
|
|
mode: str
|
|
inputs: dict | None = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SSE payloads. ``_SseEventData`` is the shared wire-payload marker. Each payload
|
|
# declares its envelope event name as the ``sse_event`` ClassVar. Bus-level
|
|
# ``kind`` remains on payloads while the HTTP transport wraps each value in a
|
|
# generated ``{ event, data }`` response envelope.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _SseEventData:
|
|
sse_event: ClassVar[str]
|
|
|
|
|
|
@dataclass
|
|
class CommandStartedEventData(SessionView, _SseEventData):
|
|
"""Bounded command/reconnect handshake; never contains conversation history."""
|
|
|
|
sse_event: ClassVar[str] = "command_started"
|
|
|
|
kind: Literal["command_started"] = "command_started"
|
|
|
|
|
|
@dataclass
|
|
class NodeEventData(_SseEventData):
|
|
sse_event: ClassVar[str] = "node"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
stage_id: str
|
|
at_version: int
|
|
revision: int
|
|
node_id: str
|
|
title: str
|
|
status: str
|
|
error: str
|
|
kind: Literal["node"] = "node"
|
|
|
|
|
|
@dataclass
|
|
class CanvasEdge:
|
|
source: str
|
|
target: str
|
|
|
|
|
|
@dataclass
|
|
class CanvasEventData(_SseEventData):
|
|
sse_event: ClassVar[str] = "canvas"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
stage_id: str
|
|
at_version: int
|
|
revision: int
|
|
event: CanvasEvent
|
|
kind: Literal["canvas"] = "canvas"
|
|
node_id: str | None = None
|
|
edge: CanvasEdge | None = None
|
|
|
|
|
|
@dataclass
|
|
class AgentMessageEventData(_SseEventData):
|
|
"""One incremental assistant-text chunk for an in-flight chat turn.
|
|
|
|
``answer`` follows the existing Dify ``agent_message`` convention: it is
|
|
a delta, not the accumulated answer. ``seq``/``at_version`` identify the
|
|
durable assistant item that replaces the transient stream item when the
|
|
turn commits.
|
|
"""
|
|
|
|
sse_event: ClassVar[str] = "agent_message"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
id: str
|
|
answer: str
|
|
seq: int
|
|
at_version: int
|
|
revision: int
|
|
stage_id: str
|
|
kind: Literal["agent_message"] = "agent_message"
|
|
|
|
|
|
@dataclass
|
|
class ReasoningEventData(_SseEventData):
|
|
"""One model-provided reasoning delta for the current operation.
|
|
|
|
Reasoning is independent from curated execution progress. ``span_id``
|
|
identifies the cognitive call within an operation so clients can preserve
|
|
stable rendering while deltas arrive.
|
|
"""
|
|
|
|
sse_event: ClassVar[str] = "reasoning"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
stage_id: str
|
|
at_version: int
|
|
revision: int
|
|
span_id: str
|
|
delta: str
|
|
kind: Literal["reasoning"] = "reasoning"
|
|
|
|
|
|
@dataclass
|
|
class ProgressEventData(_SseEventData):
|
|
"""A replaceable execution snapshot for an in-flight operation.
|
|
|
|
``revision`` is monotonic within ``operation_id``. ``at_version`` points
|
|
at the next durable transition that supersedes this snapshot, allowing a
|
|
client to discard delayed progress after it has already applied a commit.
|
|
The payload contains observable actions only. Model-provided reasoning is
|
|
delivered separately through ``ReasoningEventData``.
|
|
"""
|
|
|
|
sse_event: ClassVar[str] = "progress"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
stage_id: str
|
|
at_version: int
|
|
revision: int
|
|
execution: ExecutionProgress
|
|
kind: Literal["progress"] = "progress"
|
|
|
|
|
|
@dataclass
|
|
class CommitEventData(_SseEventData):
|
|
"""One CAS-backed conversation increment emitted before terminal state."""
|
|
|
|
sse_event: ClassVar[str] = "commit"
|
|
|
|
session_id: str
|
|
operation_id: str
|
|
stage_id: str
|
|
at_version: int
|
|
version: int
|
|
state: str
|
|
settled: bool
|
|
items: list[ConversationItem]
|
|
kind: Literal["commit"] = "commit"
|
|
|
|
|
|
@dataclass
|
|
class StateEventData(SessionView, _SseEventData):
|
|
sse_event: ClassVar[str] = "state"
|
|
|
|
kind: Literal["state"] = "state"
|
|
|
|
|
|
@dataclass
|
|
class ErrorEventData(_SseEventData):
|
|
sse_event: ClassVar[str] = "error"
|
|
|
|
error: str
|
|
kind: Literal["error"] = "error"
|
|
code: str | None = None
|
|
message: str | None = None
|
|
recoverable: bool | None = None
|