Backend:
- Conversation API at /api/maf-roundtable/conversations: create/list/detail,
paginated messages, atomic send+run start, owner-only isolation,
admin read-only diagnostic access
- SSE real-time event stream (GET /conversations/{id}/stream) backed by
ConversationEventHub + conversation_events store; multi-worker deployment doc
- Deterministic server-owned stage-3 dispatch contracts (dispatch_contracts.py):
routing resolved from trusted MAF deployment config instead of manager-chosen
agent ids; config.yaml keeps stage-3 routing deployment-owned
(default_manager_agent_id / seat ids / business_routes / max_seats_per_run)
- Conversation persistence (conversations/messages stores) + v2 migration script
- Tests: conversations, conversation stream, migration, phase3 conversation dispatch
Frontend:
- Vitest test infra for MAF (vitest.maf.config.ts + test:maf script) with
component tests for ApprovalCard/RunHeader/SessionsPage and use-maf-events
- Session form lib, flow-graph dialog / run-detail / sessions page updates,
routes and nav tweaks
- Docs: R0 legacy interaction baseline mapping, updated implementation plan
Co-authored-by: Cursor <cursoragent@cursor.com>
581 lines
30 KiB
Python
581 lines
30 KiB
Python
"""ORM models for the MAF (Microsoft Agent Framework) roundtable experiment.
|
||
|
||
Nine independent tables — **no foreign keys to the legacy ``roundtable_*``
|
||
tables** (``roundtable_jobs`` / ``roundtable_drafts`` /
|
||
``roundtable_task_drafts``). The MAF experiment is fully isolated: it has its
|
||
own session / participant / run / event / action / lease / approval tables, keyed by
|
||
its own surrogate ids, so the legacy multi-agent roundtable keeps its current
|
||
behavior, schema, and recovery semantics untouched.
|
||
|
||
Tables (see the implementation plan §8):
|
||
|
||
- ``maf_roundtable_sessions`` — one MAF roundtable session (task + mode + capability snapshot)
|
||
- ``maf_roundtable_participants`` — manager + seat agents bound to a session
|
||
- ``maf_roundtable_runs`` — one execution run of a session
|
||
- ``maf_roundtable_events`` — monotonic per-run event stream (``(run_id, seq)`` unique)
|
||
- ``maf_roundtable_actions`` — external write / tool actions (idempotency-keyed)
|
||
- ``maf_roundtable_action_leases`` — active target-write leases (one per target key)
|
||
- ``maf_roundtable_approvals`` — pending human-approval requests
|
||
|
||
User isolation: ``maf_roundtable_sessions.user_id`` is the owner; all child
|
||
rows are filtered through the owning session. ``user_id`` on child tables
|
||
(``actions``) is audit-only. Admins bypass the owner filter when
|
||
``maf_roundtable.admin_diagnostic_access`` is true.
|
||
|
||
All JSON / large-text columns use the portable types so the schema is identical
|
||
across sqlite / postgres / mysql (see ``deerflow.persistence.types``).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import UTC, datetime
|
||
|
||
from sqlalchemy import (
|
||
CheckConstraint,
|
||
ForeignKey,
|
||
Integer,
|
||
String,
|
||
UniqueConstraint,
|
||
)
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from deerflow.persistence.base import Base
|
||
from deerflow.persistence.types import BeijingDateTime, PortableJSON, PortableLongText
|
||
|
||
|
||
def _now() -> datetime:
|
||
return datetime.now(UTC)
|
||
|
||
|
||
# ── 1. sessions ───────────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableSessionRow(Base):
|
||
"""One MAF roundtable session (the user's task + chosen mode + frozen capability snapshot).
|
||
|
||
Owner-scoped by ``user_id``. ``capability_snapshot_json`` freezes the
|
||
agents / tools / skills configuration at creation time so a run can be
|
||
reproduced even if the agent config changes later. ``fact_ledger_json``
|
||
holds the shared fact ledger (confirmed facts / tool results / pending /
|
||
failed actions) updated as the run progresses.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_sessions"
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
title: Mapped[str] = mapped_column(String(255), default="")
|
||
task_text: Mapped[str] = mapped_column(PortableLongText, nullable=False)
|
||
task_payload_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
# structured | group_chat
|
||
mode: Mapped[str] = mapped_column(String(16), default="structured", index=True)
|
||
# draft | running | waiting_approval | waiting_input | completed | failed | cancelled
|
||
status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
|
||
model_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
reasoning_effort: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
manager_agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
max_rounds: Mapped[int] = mapped_column(Integer, default=8)
|
||
# auto | confirm_high_risk | confirm_all_writes
|
||
write_policy: Mapped[str] = mapped_column(String(32), default="confirm_high_risk")
|
||
capability_snapshot_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
fact_ledger_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
final_result_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 2. participants ──────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableParticipantRow(Base):
|
||
"""Manager + seat agents bound to a session.
|
||
|
||
One row per participant agent. ``role`` is ``manager`` / ``seat``.
|
||
``thread_id`` / ``sandbox_id`` bind the participant to its own DeerFlow
|
||
thread / sandbox (one seat = one thread = one sandbox). ``capability_snapshot_json``
|
||
freezes the agent's tools / skills at session creation time.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_participants"
|
||
__table_args__ = (
|
||
UniqueConstraint("session_id", "agent_id", name="uq_maf_participants_session_agent"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
session_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_sessions.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
agent_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
display_name: Mapped[str] = mapped_column(String(255), default="")
|
||
# manager | seat
|
||
role: Mapped[str] = mapped_column(String(16), default="seat")
|
||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||
thread_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
sandbox_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
# idle | running | done | failed | cancelled
|
||
status: Mapped[str] = mapped_column(String(16), default="idle")
|
||
last_turn_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
capability_snapshot_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 3. runs ───────────────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableRunRow(Base):
|
||
"""One execution run of a session.
|
||
|
||
``maf_checkpoint_json`` stores the MAF workflow checkpoint (or a reference)
|
||
for resume-after-restart. ``last_event_seq`` is a denormalized cache of the
|
||
latest event seq for fast polling (the authoritative seq lives in
|
||
``maf_roundtable_events``). ``token_usage_json`` accumulates per-run token
|
||
usage. ``latency_ms`` is the wall-clock duration.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_runs"
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
session_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_sessions.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
# pending | running | waiting_approval | waiting_input | completed | failed | cancelled
|
||
status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
|
||
workflow_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
maf_checkpoint_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
last_event_seq: Mapped[int] = mapped_column(Integer, default=0)
|
||
started_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
finished_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
error_message_safe: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||
token_usage_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 4. events ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableEventRow(Base):
|
||
"""Monotonic per-run event stream.
|
||
|
||
``(run_id, seq)`` is unique — seq is monotonically increasing per run. The
|
||
store allocates seq by ``SELECT MAX(seq) + 1`` and retries on
|
||
``IntegrityError`` so concurrent appends from parallel seats cannot collide
|
||
or skip seq values. ``payload_json`` carries the event-specific body (tool
|
||
result, agent message delta, fact-recorded, etc.); sensitive fields must
|
||
already be redacted by the writer.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_events"
|
||
__table_args__ = (
|
||
UniqueConstraint("run_id", "seq", name="uq_maf_events_run_seq"),
|
||
# Dedup key for at-most-once event persistence. The authoritative
|
||
# dedup is this DB constraint (survives process restarts) — not the
|
||
# in-process ``_seen_source_ids`` set, which is only a fast-path cache.
|
||
# ``source_event_id`` is nullable; rows with NULL are not deduped
|
||
# (SQL treats NULLs as distinct in a UNIQUE constraint on sqlite /
|
||
# postgres / mysql), so events without a source id stay append-only.
|
||
UniqueConstraint("run_id", "source_event_id", name="uq_maf_events_run_source"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
session_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_sessions.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
run_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_runs.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
seq: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
# run.started | run.status_changed | workflow.checkpointed | manager.decision |
|
||
# agent.turn_started | agent.message.delta | agent.message.completed |
|
||
# skill.selected | skill.started | skill.completed | tool.requested | tool.started |
|
||
# tool.progress | tool.completed | tool.failed | action.waiting_approval |
|
||
# action.approved | action.rejected | artifact.created | fact.recorded |
|
||
# run.completed | run.failed
|
||
event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
turn_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
# Upstream identity of the source runtime event (e.g. LangGraph message id
|
||
# / tool_call id). Used by the ``(run_id, source_event_id)`` unique
|
||
# constraint for durable at-most-once dedup across restarts.
|
||
source_event_id: Mapped[str | None] = mapped_column(String(191), nullable=True, index=True)
|
||
payload_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now, index=True)
|
||
|
||
|
||
# ── 5. actions ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableActionRow(Base):
|
||
"""External write / tool action record (idempotency-keyed).
|
||
|
||
The Action Coordinator (Phase 5) uses ``idempotency_key`` to guarantee
|
||
at-most-once execution across retries / restarts. ``target_key`` carries a
|
||
per-business-object lease so parallel seats writing the same object are
|
||
serialized. ``risk_level`` is R0–R4. ``status`` is the action state machine:
|
||
pending | running | succeeded | failed | unknown | waiting_approval |
|
||
approved | rejected | compensated. ``arguments_redacted_json`` /
|
||
``result_redacted_json`` must already have credentials / tokens / cookies
|
||
stripped by the writer.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_actions"
|
||
__table_args__ = (
|
||
UniqueConstraint("idempotency_key", name="uq_maf_actions_idempotency"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
session_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_sessions.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
run_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
tool_name: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
|
||
logical_action_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
# The seat turn that produced this action. Persisted (not only embedded in
|
||
# the idempotency_key) so pending-approval matching can key on
|
||
# run_id + agent_id + turn_index + logical_action_id precisely — never
|
||
# mixing a seat's earlier-turn waiting actions into the current approval.
|
||
turn_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
idempotency_key: Mapped[str] = mapped_column(String(191), nullable=False, unique=True)
|
||
target_key: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||
# R0 | R1 | R2 | R3 | R4
|
||
risk_level: Mapped[str] = mapped_column(String(8), default="R0")
|
||
# pending | running | succeeded | failed | unknown | waiting_approval | approved | rejected | compensated
|
||
status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
|
||
arguments_redacted_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
result_redacted_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
external_object_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||
approval_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
attempt_count: Mapped[int] = mapped_column(Integer, default=0)
|
||
started_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
finished_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 6. target leases ──────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableActionLeaseRow(Base):
|
||
"""One active, short-lived lease for a server-derived external target key.
|
||
|
||
The key is opaque (a stable SHA-256 digest) and is never sourced from a
|
||
browser/model field. ``target_key`` is the primary key, which makes the
|
||
database — not an in-process mutex — arbitrate concurrent writes across
|
||
workers and restarts. A lease belongs to exactly one action and is removed
|
||
with a known terminal result; an ``unknown`` result deliberately retains a
|
||
manual-reconciliation barrier.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_action_leases"
|
||
__table_args__ = (
|
||
UniqueConstraint("action_id", name="uq_maf_action_leases_action"),
|
||
)
|
||
|
||
target_key: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||
action_id: Mapped[str] = mapped_column(
|
||
String(64),
|
||
ForeignKey("maf_roundtable_actions.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
index=True,
|
||
)
|
||
lease_expires_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, index=True
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 7. approvals ──────────────────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableApprovalRow(Base):
|
||
"""Pending human-approval request for a high-risk external write.
|
||
|
||
``status`` is ``pending | approved | rejected | expired``. ``payload_redacted_json``
|
||
carries the action summary the user sees (redacted). ``expires_at`` is the
|
||
deadline after which the approval auto-expires and the action is marked
|
||
``rejected`` (or ``unknown`` if it may already have been sent).
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_approvals"
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
session_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_sessions.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
run_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
action_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
requested_by_agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
# pending | approved | rejected | expired
|
||
status: Mapped[str] = mapped_column(String(16), default="pending", index=True)
|
||
summary: Mapped[str] = mapped_column(PortableLongText, default="")
|
||
payload_redacted_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
decided_by: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
decided_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
expires_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True, index=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 8. seat runs (Phase 2: MAF↔DeerFlow run mapping) ─────────────────────
|
||
|
||
|
||
class MafRoundtableSeatRunRow(Base):
|
||
"""Persistent mapping between a MAF workflow seat turn and its DeerFlow run.
|
||
|
||
Replaces the in-memory ``_active_runs`` dict so that after a service restart
|
||
the system can still locate and cancel outstanding DeerFlow runs, and the
|
||
MAF checkpoint can reference which seat turns completed.
|
||
|
||
Phase-3 unique constraint: ``(maf_run_id, agent_id, turn_index,
|
||
execution_stage)``. ``turn_index`` is the business round (kept stable for
|
||
frontend display / approval matching / event tracing); ``execution_stage``
|
||
disambiguates the distinct DeerFlow executions that happen within one
|
||
business round (planner / expert / format_repair / cross_review /
|
||
finalizer / group_chat). The business turn_index is NEVER mutated to dodge
|
||
a collision.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_seat_runs"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"maf_run_id",
|
||
"agent_id",
|
||
"turn_index",
|
||
"execution_stage",
|
||
name="uq_maf_seat_runs_run_agent_turn_stage",
|
||
),
|
||
# Phase-3 P1-2: enforce the closed execution_stage enum at the DB layer.
|
||
# Mirrors seat_runs.VALID_EXECUTION_STAGES — a typo (e.g. "exprt") would
|
||
# otherwise silently bypass the four-column idempotency key by creating
|
||
# a brand-new stage. This CHECK keeps both fresh creates and any raw
|
||
# migration / manual SQL honest. Keep in sync with VALID_EXECUTION_STAGES.
|
||
CheckConstraint(
|
||
"execution_stage IN ('legacy','planner','expert','format_repair',"
|
||
"'cross_review','cross_review_repair','finalizer','group_chat')",
|
||
name="ck_maf_seat_runs_execution_stage",
|
||
),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
maf_run_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_runs.id", ondelete="CASCADE"), nullable=False, index=True
|
||
)
|
||
agent_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
turn_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
# Phase-3: which kind of DeerFlow execution this row represents. Pre-Phase-3
|
||
# rows are migrated to ``legacy``. Closed enum (see seat_runs.VALID_EXECUTION_STAGES):
|
||
# planner | expert | format_repair | cross_review | cross_review_repair |
|
||
# finalizer | group_chat | legacy.
|
||
execution_stage: Mapped[str] = mapped_column(
|
||
String(32), nullable=False, default="legacy", server_default="legacy"
|
||
)
|
||
deerflow_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
deerflow_thread_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
sandbox_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
workflow_checkpoint_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
# pending | running | completed | failed | cancelled
|
||
status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
|
||
# Phase-3 P0-1: a sanitized, replayable snapshot of the SeatResponse written
|
||
# atomically with the completed transition. Survives a crash that happens
|
||
# after the seat run is marked completed but before the workflow checkpoint
|
||
# is persisted, so recovery never needs to re-call DeerFlow.
|
||
result_snapshot: Mapped[dict | None] = mapped_column(PortableJSON, nullable=True)
|
||
started_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
finished_at: Mapped[datetime | None] = mapped_column(BeijingDateTime(), nullable=True)
|
||
error: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 9. action secrets (encrypted actual execution params, server-only) ────
|
||
|
||
|
||
class MafRoundtableActionSecretRow(Base):
|
||
"""Encrypted actual execution parameters for a protected action.
|
||
|
||
Stores the **real** (un-redacted) tool-call arguments so that after
|
||
approval or a service restart the system can recover and execute the
|
||
approved action with the original parameters. This table is **never**
|
||
joined in any read query exposed to the API, events, logs, or frontend —
|
||
it is server-only. The ActionCoordinator encrypts the payload before
|
||
writing and decrypts only at execution time.
|
||
|
||
One row per action (unique ``action_id``). The encryption key is derived
|
||
from the deployment's secret key; the ciphertext is opaque to the store.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_action_secrets"
|
||
__table_args__ = (
|
||
UniqueConstraint("action_id", name="uq_maf_action_secrets_action"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
action_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_actions.id", ondelete="CASCADE"),
|
||
nullable=False, index=True,
|
||
)
|
||
arguments_encrypted: Mapped[str] = mapped_column(PortableLongText, nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
|
||
|
||
# ── 10. conversations (V2: user-visible conversation aggregation) ─────────
|
||
|
||
|
||
class MafRoundtableConversationRow(Base):
|
||
"""One user-visible MAF roundtable conversation.
|
||
|
||
This is the **only** entity the frontend knows about. A conversation can
|
||
span multiple user messages, each of which may trigger an internal
|
||
``maf_roundtable_runs`` execution. The user never sees session/run/thread
|
||
ids — only ``conversation_id``.
|
||
|
||
``active_run_id`` points to the currently executing internal run (nullable
|
||
when idle). ``safe_status`` is a coarse user-facing status derived from
|
||
the active run's state (idle / running / waiting_input / waiting_approval
|
||
/ completed / failed / cancelled).
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_conversations"
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
owner_user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
title: Mapped[str] = mapped_column(String(255), default="")
|
||
task_context_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
# idle | running | waiting_input | waiting_approval | completed | failed | cancelled
|
||
safe_status: Mapped[str] = mapped_column(String(32), default="idle", index=True)
|
||
active_run_id: Mapped[str | None] = mapped_column(
|
||
String(64),
|
||
ForeignKey("maf_roundtable_runs.id", ondelete="SET NULL"),
|
||
nullable=True,
|
||
)
|
||
# Internal session this conversation is linked to (lazy-created on first message).
|
||
internal_session_id: Mapped[str | None] = mapped_column(
|
||
String(64),
|
||
ForeignKey("maf_roundtable_sessions.id", ondelete="SET NULL"),
|
||
nullable=True,
|
||
)
|
||
last_stream_seq: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
BeijingDateTime(), nullable=False, default=_now, onupdate=_now
|
||
)
|
||
|
||
|
||
# ── 11. conversation messages ─────────────────────────────────────────────
|
||
|
||
|
||
class MafRoundtableMessageRow(Base):
|
||
"""Stable message history for a conversation.
|
||
|
||
Every user-visible bubble (user message, manager message, seat message,
|
||
clarification card, approval card, result card) has exactly one row here.
|
||
``seq`` is a conversation-global monotonic counter (unique per conversation)
|
||
used for SSE cursor and ordering. ``message_kind`` classifies the bubble
|
||
type for the frontend reducer.
|
||
|
||
``run_id`` links the message to the internal run that produced it (nullable
|
||
for user messages that haven't started a run yet). ``agent_id`` identifies
|
||
the speaking agent (null for user messages).
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_messages"
|
||
__table_args__ = (
|
||
UniqueConstraint("conversation_id", "seq", name="uq_maf_messages_conv_seq"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
conversation_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_conversations.id", ondelete="CASCADE"),
|
||
nullable=False, index=True,
|
||
)
|
||
seq: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
# user | manager | seat | system | clarification | approval | result | tool_progress
|
||
message_kind: Mapped[str] = mapped_column(String(32), nullable=False, default="user")
|
||
role: Mapped[str] = mapped_column(String(16), nullable=False, default="user")
|
||
agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
run_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||
# Stable message identity for SSE delta merging (e.g. LangGraph message id).
|
||
message_ref: Mapped[str | None] = mapped_column(String(191), nullable=True, index=True)
|
||
content: Mapped[str] = mapped_column(PortableLongText, default="")
|
||
metadata_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now, index=True)
|
||
|
||
|
||
# ── 12. conversation events (SSE stream, conversation-global seq) ─────────
|
||
|
||
|
||
class MafRoundtableConversationEventRow(Base):
|
||
"""Conversation-dimension event stream for SSE.
|
||
|
||
Unlike ``maf_roundtable_events`` (per-run seq), this table uses a
|
||
**conversation-global** monotonic ``seq`` so the frontend only needs one
|
||
cursor (``after_seq``) regardless of how many internal runs happen.
|
||
The SSE hub replays from this table on connect/reconnect, then subscribes
|
||
to real-time notifications.
|
||
|
||
``event_type`` follows the §4.3 contract (conversation.snapshot,
|
||
user.message.created, manager.intent, clarification.required, etc.).
|
||
``data_json`` is the already-sanitized event payload safe for the browser.
|
||
"""
|
||
|
||
__tablename__ = "maf_roundtable_conversation_events"
|
||
__table_args__ = (
|
||
UniqueConstraint("conversation_id", "seq", name="uq_maf_conv_events_conv_seq"),
|
||
# P0-4: dedup key for projection idempotency. The source audit event's
|
||
# stable id (e.g. LangGraph event id / tool_call id) prevents duplicate
|
||
# conversation events when the bridge retries or the adapter re-projects
|
||
# after a restart. NULL source_event_ids are not deduped (SQL treats
|
||
# NULLs as distinct in UNIQUE constraints).
|
||
UniqueConstraint("conversation_id", "source_event_id", name="uq_maf_conv_events_conv_source"),
|
||
)
|
||
|
||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
conversation_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("maf_roundtable_conversations.id", ondelete="CASCADE"),
|
||
nullable=False, index=True,
|
||
)
|
||
seq: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
# Stable upstream id from the source audit event (maf_roundtable_events.id
|
||
# or the LangGraph source_event_id). Used for projection dedup via the
|
||
# (conversation_id, source_event_id) unique constraint.
|
||
source_event_id: Mapped[str | None] = mapped_column(String(191), nullable=True, index=True)
|
||
event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||
data_json: Mapped[dict | None] = mapped_column(PortableJSON(), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=_now, index=True)
|
||
|
||
|
||
__all__ = [
|
||
"MafRoundtableActionRow",
|
||
"MafRoundtableActionLeaseRow",
|
||
"MafRoundtableActionSecretRow",
|
||
"MafRoundtableApprovalRow",
|
||
"MafRoundtableConversationEventRow",
|
||
"MafRoundtableConversationRow",
|
||
"MafRoundtableEventRow",
|
||
"MafRoundtableMessageRow",
|
||
"MafRoundtableParticipantRow",
|
||
"MafRoundtableRunRow",
|
||
"MafRoundtableSeatRunRow",
|
||
"MafRoundtableSessionRow",
|
||
]
|