Files
dify/api/models/dify_builder_native_tests.py

278 lines
15 KiB
Python

"""Native test persistence. Ledger rows contain hashes/references, never effect payloads.
The request scope is the lock for every child invocation and all shared spend.
Attempts, physical dispatch claims and outcomes are append-only; operations
are a resumable projection.
"""
from datetime import datetime
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from libs.uuid_utils import uuidv7
from models.base import TypeBase
from models.dify_builder import BuilderRowIdentity
from models.types import AdjustedJSON, StringUUID
class RequestOwner(MappedAsDataclass, kw_only=True, eq=False, repr=False):
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
request_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
class InvocationOwner(RequestOwner, kw_only=True, eq=False, repr=False):
invocation_id: Mapped[str] = mapped_column(sa.String(128), nullable=False)
class Identity(MappedAsDataclass, kw_only=True, eq=False, repr=False):
# Keep ledger IDs insert-generated while accepting caller-preallocated identities.
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, insert_default=lambda: str(uuidv7()), default=None)
class Quota(MappedAsDataclass, kw_only=True, eq=False, repr=False):
max_calls: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
max_dispatches: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
max_cost_units: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
spent_calls: Mapped[int] = mapped_column(sa.BigInteger, nullable=False, default=0)
spent_dispatches: Mapped[int] = mapped_column(sa.BigInteger, nullable=False, default=0)
spent_cost_units: Mapped[int] = mapped_column(sa.BigInteger, nullable=False, default=0)
def quota_check(name: str) -> sa.CheckConstraint:
return sa.CheckConstraint(
"max_calls >= 0 AND max_dispatches >= 0 AND max_cost_units >= 0 "
"AND spent_calls >= 0 AND spent_calls <= max_calls "
"AND spent_dispatches >= 0 AND spent_dispatches <= max_dispatches "
"AND spent_cost_units >= 0 AND spent_cost_units <= max_cost_units",
name=name,
)
class NativeTestScope(Quota, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_scopes"
__table_args__ = (
quota_check("native_scope_quota"),
sa.CheckConstraint("generation >= 0", name="native_scope_generation"),
)
request_id: Mapped[str] = mapped_column(StringUUID, primary_key=True)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
generation: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
task_approved: Mapped[bool] = mapped_column(sa.Boolean, nullable=False)
cancelled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False)
expires_at: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), nullable=False)
budget_limits: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
budget_spent: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
profile: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
class NativeTestInvocation(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_invocations"
__table_args__ = (sa.UniqueConstraint("tenant_id", "request_id", "invocation_id", name="native_invocation_owner"),)
active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False)
# Only the trusted native binding/cursor producer writes this map. Keys are
# persisted OperationKey digests, not caller-provided retry counters. The
# reserved `_agent_workspace_v1` key holds typed private resource allocation
# metadata only; it is never interpreted as a binding or dispatch permit.
bindings: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
class NativeEffectGrant(
Identity, RequestOwner, Quota, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False
):
__tablename__ = "dify_builder_native_grants"
__table_args__ = (
sa.UniqueConstraint("tenant_id", "request_id", "scope_digest", name="native_grant_scope"),
quota_check("native_grant_quota"),
sa.CheckConstraint("decision IN ('approved','denied')", name="native_grant_decision"),
)
scope_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
decision: Mapped[str] = mapped_column(sa.String(16), nullable=False)
expires_at: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), nullable=False)
actor_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
class NativeEffectOperation(
Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False
):
__tablename__ = "dify_builder_native_operations"
__table_args__ = (
sa.UniqueConstraint(
"tenant_id", "request_id", "invocation_id", "operation_digest", name="native_operation_key"
),
sa.CheckConstraint(
"state IN ('pending','claimed','completed','failed','unknown','not_dispatched','denied','cancelled')",
name="native_operation_state",
),
)
operation_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
intent_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
scope_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
state: Mapped[str] = mapped_column(sa.String(16), nullable=False)
result_ref: Mapped[str | None] = mapped_column(sa.String(128), nullable=True, default=None)
class NativeEffectAttempt(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_attempts"
__table_args__ = (
sa.UniqueConstraint("operation_id", "dispatch_index", name="native_attempt_dispatch"),
sa.UniqueConstraint("permit_digest", name="native_attempt_permit"),
sa.UniqueConstraint("budget_reservation_ref", name="native_attempt_reservation"),
sa.CheckConstraint("dispatch_index = 1 AND cost_units >= 0", name="native_attempt_spend"),
)
operation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
grant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
dispatch_index: Mapped[int] = mapped_column(sa.Integer, nullable=False)
permit_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
expires_at: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), nullable=False)
cost_units: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
budget_reservation_ref: Mapped[str] = mapped_column(StringUUID, nullable=False)
execution_id: Mapped[str] = mapped_column(sa.String(128), nullable=False)
invocation_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
budget: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
profile: Mapped[dict] = mapped_column(AdjustedJSON, nullable=False)
class NativeEffectDispatchClaim(
Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False
):
"""One physical-owner consumption of an already-spent attempt, never replayed."""
__tablename__ = "dify_builder_native_effect_dispatch_claims"
__table_args__ = (
sa.UniqueConstraint("attempt_id", name="native_effect_dispatch_claim_attempt"),
sa.UniqueConstraint("operation_id", name="native_effect_dispatch_claim_operation"),
)
operation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
attempt_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
claimed_at: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), nullable=False)
class NativeEffectOutcome(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_outcomes"
__table_args__ = (
sa.UniqueConstraint("attempt_id", name="native_outcome_attempt"),
sa.CheckConstraint("outcome IN ('completed','failed','unknown','not_dispatched')", name="native_outcome_state"),
sa.CheckConstraint("outcome != 'completed' OR result_ref IS NOT NULL", name="native_completed_result"),
)
operation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
attempt_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
outcome: Mapped[str] = mapped_column(sa.String(16), nullable=False)
result_ref: Mapped[str | None] = mapped_column(sa.String(128), nullable=True, default=None)
class NativeContinuation(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_continuations"
__table_args__ = (
sa.UniqueConstraint("request_id", "invocation_id", "generation", name="native_continuation_generation"),
sa.CheckConstraint("generation >= 0", name="native_continuation_counter"),
)
generation: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
checkpoint_id: Mapped[str] = mapped_column(sa.String(128), nullable=False)
payload_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
payload_ref: Mapped[str] = mapped_column(sa.String(128), nullable=False)
class NativePendingAction(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
__tablename__ = "dify_builder_native_pending_actions"
__table_args__ = (
sa.UniqueConstraint(
"tenant_id",
"request_id",
"invocation_id",
"generation",
"category",
"reference_digest",
name="native_pending_reference",
),
sa.CheckConstraint("category IN ('approval','configuration','interaction')", name="native_pending_category"),
sa.CheckConstraint(
"(category = 'approval' AND operation_id IS NOT NULL) OR (category != 'approval' AND operation_id IS NULL)",
name="native_pending_effect_owner",
),
sa.CheckConstraint("generation >= 0", name="native_pending_generation"),
)
generation: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
category: Mapped[str] = mapped_column(sa.String(16), nullable=False)
reference_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
reference: Mapped[str] = mapped_column(sa.String(128), nullable=False)
owner_ref: Mapped[str] = mapped_column(sa.String(128), nullable=False)
owner_version: Mapped[str] = mapped_column(sa.String(128), nullable=False)
operation_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
class NativePayloadObject(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
"""SQL acknowledgement of an encrypted immutable object, after storage readback."""
__tablename__ = "dify_builder_native_payload_objects"
__table_args__ = (
sa.UniqueConstraint("request_id", "invocation_id", "kind", "logical_key", name="native_payload_logical_key"),
sa.UniqueConstraint("storage_key", name="native_payload_storage_key"),
sa.CheckConstraint("kind IN ('context','checkpoint','result')", name="native_payload_kind"),
)
kind: Mapped[str] = mapped_column(sa.String(16), nullable=False)
logical_key: Mapped[str] = mapped_column(sa.String(128), nullable=False)
storage_key: Mapped[str] = mapped_column(sa.String(128), nullable=False)
payload_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
value_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
class NativeInvocationState(
Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False
):
"""Current local execution facts.
While running, a real wait_reason with validated current pending_ids is an
admission fence, not a resumable receipt. Reason None with retained older
acknowledged IDs preserves claim/recovery evidence. The exclusive validated
generation claim clears the reason while retaining precisely those IDs.
"""
__tablename__ = "dify_builder_native_invocation_states"
__table_args__ = (
sa.UniqueConstraint("request_id", "invocation_id", name="native_state_invocation"),
sa.UniqueConstraint(
"request_id", "parent_invocation_id", "parent_operation_digest", name="native_child_occurrence"
),
sa.CheckConstraint("(app_id IS NULL) = (workflow_id IS NULL)", name="native_state_read_owner_pair"),
sa.CheckConstraint("generation >= 0", name="native_state_generation"),
sa.CheckConstraint(
"state IN ('running','waiting','queued','completed','stopped','unknown','cancelled')",
name="native_state_status",
),
)
app_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
context_object_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
parent_invocation_id: Mapped[str | None] = mapped_column(sa.String(128), nullable=True, default=None)
parent_operation_digest: Mapped[str | None] = mapped_column(sa.String(64), nullable=True, default=None)
child_identity_digest: Mapped[str | None] = mapped_column(sa.String(64), nullable=True, default=None)
generation: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
state: Mapped[str] = mapped_column(sa.String(16), nullable=False)
checkpoint_id: Mapped[str | None] = mapped_column(sa.String(128), nullable=True, default=None)
pending_ids: Mapped[list] = mapped_column(AdjustedJSON, nullable=False)
wait_reason: Mapped[str | None] = mapped_column(sa.String(16), nullable=True, default=None)
outcome: Mapped[dict | None] = mapped_column(AdjustedJSON, nullable=True, default=None)
resolution: Mapped[list] = mapped_column(AdjustedJSON, nullable=False)
cancellation_key: Mapped[str | None] = mapped_column(sa.String(64), nullable=True, default=None)
cleanup_complete: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
class NativeDispatch(Identity, InvocationOwner, BuilderRowIdentity, TypeBase, kw_only=True, eq=False, repr=False):
"""Durable outbox and single worker claim. Publish acknowledgements are advisory."""
__tablename__ = "dify_builder_native_dispatches"
__table_args__ = (
sa.UniqueConstraint("request_id", "invocation_id", "generation", name="native_dispatch_generation"),
sa.UniqueConstraint("request_id", "idempotency_digest", name="native_dispatch_idempotency"),
sa.CheckConstraint("generation >= 0", name="native_dispatch_counter"),
sa.CheckConstraint(
"state IN ('queued','published','claimed','completed','unknown','cancelled')", name="native_dispatch_state"
),
)
generation: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
idempotency_digest: Mapped[str] = mapped_column(sa.String(64), nullable=False)
state: Mapped[str] = mapped_column(sa.String(16), nullable=False)
checkpoint_id: Mapped[str | None] = mapped_column(sa.String(128), nullable=True, default=None)
claim_expires_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True), nullable=True, default=None)