Skip to content

API Reference

이 문서는 source code를 직접 열기 전에 "무엇을 구현해야 하는가"를 빠르게 찾기 위한 reference입니다.

Core Models

핵심 도메인 타입:

  • Request: aggregate root
  • Proposal: 사람이 읽는 summary
  • AgentPlan: 실행 가능한 action plan
  • PlannedAction: 단일 실행 action
  • ReviewDecision: plan-level decision
  • ActionReviewDecision: action-level decision
  • PlanReviewResult: review 결과 전체

도메인 규칙은 Domain Model에 설명되어 있습니다. 이 문서는 타입과 확장 지점을 빠르게 찾는 데 집중합니다.

Core Ports

가장 자주 구현하는 포트는 아래 4개입니다.

RequestRepository

  • 역할: Request load/save
  • 구현 예: in-memory, DB, file-based persistence
  • 변경이 필요한 경우: 워크플로 상태를 durable하게 저장해야 할 때

ContextProvider

  • 역할: request와 관련된 외부 context 수집
  • 입력: Request
  • 출력: dict[str, object]
  • 구현 예: DB 조회, API 조회, retrieval, 파일 읽기

AgentEngine

  • 역할: analysis + proposal + optional plan을 한 번에 준비
  • 입력: Request, Mapping[str, object]
  • 출력: AgentWorkResult
  • 구현 예: deterministic engine, LangChain-backed engine

ActionExecutor

  • 역할: review를 통과한 action을 실제로 실행
  • 입력: Request, optional AgentPlan
  • 출력: 실행 결과 문자열
  • 구현 예: fake executor, tool dispatcher, real API client

Workflow Wiring

WorkflowDependencies는 workflow graph가 필요로 하는 외부 경계를 묶습니다.

중요 필드:

  • context_provider
  • agent_engine
  • action_executor
  • requires_human_review
  • auto_reviewer_id

즉 새 앱은 보통 WorkflowDependencies(...)를 구성하는 bootstrap 함수 하나로 시작합니다.

Application DTOs

AnalysisResult

  • structured analysis summary
  • rationale
  • execution steps
  • risk level

AgentWorkResult

  • analysis
  • proposal
  • agent_plan

AgentEngine.prepare(...)는 이 타입을 반환해야 합니다.

ReviewPacket

  • 사람이 review할 때 보게 되는 structured payload
  • request summary, proposal summary, rationale, risk, planned actions 포함

Reference Modules

아래 API는 mkdocstrings로 노출됩니다.

domain_agent.application.ports

Application ports for the generic agent workflow.

ActionExecutor

Bases: Protocol

Boundary for executing the reviewed action.

Source code in src/domain_agent/application/ports.py
class ActionExecutor(Protocol):
    """Boundary for executing the reviewed action."""

    def execute(self, request: Request, agent_plan: AgentPlan | None = None) -> str:
        """Execute the request and return execution notes."""

execute(request, agent_plan=None)

Execute the request and return execution notes.

Source code in src/domain_agent/application/ports.py
def execute(self, request: Request, agent_plan: AgentPlan | None = None) -> str:
    """Execute the request and return execution notes."""

AgentEngine

Bases: Protocol

Boundary for preparing agent analysis, plan, and proposal output.

Source code in src/domain_agent/application/ports.py
class AgentEngine(Protocol):
    """Boundary for preparing agent analysis, plan, and proposal output."""

    def prepare(
        self,
        request: Request,
        context: Mapping[str, object],
    ) -> AgentWorkResult:
        """Create structured agent output for the request."""

prepare(request, context)

Create structured agent output for the request.

Source code in src/domain_agent/application/ports.py
def prepare(
    self,
    request: Request,
    context: Mapping[str, object],
) -> AgentWorkResult:
    """Create structured agent output for the request."""

ContextProvider

Bases: Protocol

External source for request context.

Source code in src/domain_agent/application/ports.py
class ContextProvider(Protocol):
    """External source for request context."""

    def get_context(self, request: Request) -> dict[str, object]:
        """Return context relevant to the request."""

get_context(request)

Return context relevant to the request.

Source code in src/domain_agent/application/ports.py
def get_context(self, request: Request) -> dict[str, object]:
    """Return context relevant to the request."""

RequestRepository

Bases: Protocol

Persistence boundary for workflow requests.

Source code in src/domain_agent/application/ports.py
class RequestRepository(Protocol):
    """Persistence boundary for workflow requests."""

    def get(self, request_id: str) -> Request | None:
        """Load a request by id."""

    def save(self, request: Request) -> None:
        """Persist the current request state."""

get(request_id)

Load a request by id.

Source code in src/domain_agent/application/ports.py
def get(self, request_id: str) -> Request | None:
    """Load a request by id."""

save(request)

Persist the current request state.

Source code in src/domain_agent/application/ports.py
def save(self, request: Request) -> None:
    """Persist the current request state."""

Tool

Bases: Protocol

Executable tool used by a planned action.

Source code in src/domain_agent/application/ports.py
class Tool(Protocol):
    """Executable tool used by a planned action."""

    name: str

    def execute(self, request: Request, action: PlannedAction) -> str:
        """Execute a planned action for the request."""

execute(request, action)

Execute a planned action for the request.

Source code in src/domain_agent/application/ports.py
def execute(self, request: Request, action: PlannedAction) -> str:
    """Execute a planned action for the request."""

ToolRegistry

Bases: Protocol

Lookup boundary for executable tools.

Source code in src/domain_agent/application/ports.py
class ToolRegistry(Protocol):
    """Lookup boundary for executable tools."""

    def get(self, tool_name: str) -> Tool:
        """Return the registered tool for the given name."""

get(tool_name)

Return the registered tool for the given name.

Source code in src/domain_agent/application/ports.py
def get(self, tool_name: str) -> Tool:
    """Return the registered tool for the given name."""

WorkflowDependencies dataclass

Dependencies required by the workflow graph.

Source code in src/domain_agent/application/ports.py
@dataclass(frozen=True)
class WorkflowDependencies:
    """Dependencies required by the workflow graph."""

    context_provider: ContextProvider
    agent_engine: AgentEngine
    action_executor: ActionExecutor
    requires_human_review: bool = True
    auto_reviewer_id: str = "workflow-system"

domain_agent.application.dto

Application-layer data structures.

AgentWorkResult dataclass

Structured output returned by an agent engine.

Source code in src/domain_agent/application/dto.py
@dataclass(frozen=True)
class AgentWorkResult:
    """Structured output returned by an agent engine."""

    analysis: AnalysisResult
    proposal: Proposal
    agent_plan: AgentPlan | None = None
    observations: tuple[Observation, ...] = ()

AnalysisResult dataclass

Structured analysis output used to build a proposal.

Source code in src/domain_agent/application/dto.py
@dataclass(frozen=True)
class AnalysisResult:
    """Structured analysis output used to build a proposal."""

    summary: str
    rationale: str
    execution_steps: tuple[str, ...]
    risk_level: RiskLevel

Observation dataclass

A compact record of evidence gathered during agent preparation.

Source code in src/domain_agent/application/dto.py
@dataclass(frozen=True)
class Observation:
    """A compact record of evidence gathered during agent preparation."""

    source: str
    summary: str
    details: str = ""

ReviewPacket dataclass

Structured payload presented to a human reviewer.

Source code in src/domain_agent/application/dto.py
@dataclass(frozen=True)
class ReviewPacket:
    """Structured payload presented to a human reviewer."""

    request_id: str
    requester_id: str
    request_summary: str
    proposal_summary: str
    rationale: str
    risk_level: RiskLevel
    execution_steps: tuple[str, ...]
    planned_actions: tuple[PlannedAction, ...]
    observations: tuple[Observation, ...] = ()
    action_decisions: tuple[ActionReviewDecision, ...] = ()

build_review_packet(request, proposal, *, observations=())

Create a structured review packet from the request and proposal.

Source code in src/domain_agent/application/dto.py
def build_review_packet(
    request: Request,
    proposal: Proposal,
    *,
    observations: tuple[Observation, ...] = (),
) -> ReviewPacket:
    """Create a structured review packet from the request and proposal."""
    planned_actions: tuple[PlannedAction, ...]
    if proposal.agent_plan is not None:
        planned_actions = proposal.agent_plan.actions
    else:
        planned_actions = (
            PlannedAction(
                action_id="plan-review",
                tool_name="plan_review",
                description="Review the overall proposal as a single action.",
            ),
        )

    return ReviewPacket(
        request_id=request.request_id,
        requester_id=request.requester_id,
        request_summary=request.summary,
        proposal_summary=proposal.summary,
        rationale=proposal.rationale,
        risk_level=proposal.risk_level,
        execution_steps=proposal.execution_steps,
        planned_actions=planned_actions,
        observations=observations,
    )

domain_agent.application.use_cases

Application use cases for orchestrating agent workflows.

AgentWorkflowService dataclass

Orchestrate workflow execution through application ports.

Source code in src/domain_agent/application/use_cases.py
@dataclass(frozen=True)
class AgentWorkflowService:
    """Orchestrate workflow execution through application ports."""

    request_repository: RequestRepository
    workflow_dependencies: WorkflowDependencies
    workflow_shape: WorkflowShape = DEFAULT_WORKFLOW_SHAPE

    def run(
        self,
        request_id: str,
        *,
        review_handler: ReviewHandler | None = None,
    ) -> WorkflowState:
        """Load a request, execute the workflow, and persist the result."""
        request = self.request_repository.get(request_id)
        if request is None:
            raise RequestNotFoundError(f"Request '{request_id}' was not found.")

        workflow = build_workflow(
            self.workflow_dependencies,
            workflow_shape=self.workflow_shape,
            checkpointer=build_in_memory_checkpointer(),
        )
        config = {"configurable": {"thread_id": request.request_id}}
        next_input: object = initial_workflow_state(request)

        while True:
            result = cast(dict[str, Any], workflow.invoke(next_input, config=config))
            interrupts = cast(tuple[Interrupt, ...], tuple(result.get("__interrupt__", ())))
            if not interrupts:
                final_result = cast(WorkflowState, result)
                self.request_repository.save(final_result["request"])
                return final_result

            if review_handler is None:
                raise ReviewHandlerRequiredError(
                    "Workflow paused for review, but no review_handler was provided."
                )

            interrupt_value = interrupts[0].value
            if not isinstance(interrupt_value, ReviewPacket):
                raise TypeError("Workflow interrupt value must be a ReviewPacket.")

            next_input = Command(
                resume=review_handler(interrupt_value),
                update={"request": result.get("request")},
            )

run(request_id, *, review_handler=None)

Load a request, execute the workflow, and persist the result.

Source code in src/domain_agent/application/use_cases.py
def run(
    self,
    request_id: str,
    *,
    review_handler: ReviewHandler | None = None,
) -> WorkflowState:
    """Load a request, execute the workflow, and persist the result."""
    request = self.request_repository.get(request_id)
    if request is None:
        raise RequestNotFoundError(f"Request '{request_id}' was not found.")

    workflow = build_workflow(
        self.workflow_dependencies,
        workflow_shape=self.workflow_shape,
        checkpointer=build_in_memory_checkpointer(),
    )
    config = {"configurable": {"thread_id": request.request_id}}
    next_input: object = initial_workflow_state(request)

    while True:
        result = cast(dict[str, Any], workflow.invoke(next_input, config=config))
        interrupts = cast(tuple[Interrupt, ...], tuple(result.get("__interrupt__", ())))
        if not interrupts:
            final_result = cast(WorkflowState, result)
            self.request_repository.save(final_result["request"])
            return final_result

        if review_handler is None:
            raise ReviewHandlerRequiredError(
                "Workflow paused for review, but no review_handler was provided."
            )

        interrupt_value = interrupts[0].value
        if not isinstance(interrupt_value, ReviewPacket):
            raise TypeError("Workflow interrupt value must be a ReviewPacket.")

        next_input = Command(
            resume=review_handler(interrupt_value),
            update={"request": result.get("request")},
        )

RequestNotFoundError

Bases: LookupError

Raised when a request cannot be found in the repository.

Source code in src/domain_agent/application/use_cases.py
class RequestNotFoundError(LookupError):
    """Raised when a request cannot be found in the repository."""

ReviewHandlerRequiredError

Bases: RuntimeError

Raised when workflow execution pauses for review without a handler.

Source code in src/domain_agent/application/use_cases.py
class ReviewHandlerRequiredError(RuntimeError):
    """Raised when workflow execution pauses for review without a handler."""

domain_agent.domain.models

Core domain model for a generic agent workflow.

ActionReviewDecision dataclass

A human decision recorded for a single planned action.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class ActionReviewDecision:
    """A human decision recorded for a single planned action."""

    action_id: str
    approved: bool
    comment: str = ""

    def __post_init__(self) -> None:
        if not self.action_id.strip():
            raise DomainError("Action review action_id must not be empty.")
        if not self.approved and not self.comment.strip():
            raise DomainError("Rejected action reviews must include a comment.")

AgentPlan dataclass

A structured plan of executable actions prepared by the agent.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class AgentPlan:
    """A structured plan of executable actions prepared by the agent."""

    summary: str
    rationale: str
    actions: tuple[PlannedAction, ...]
    expected_outcome: str
    risk_level: RiskLevel

    def __post_init__(self) -> None:
        if not self.summary.strip():
            raise DomainError("Agent plan summary must not be empty.")
        if not self.rationale.strip():
            raise DomainError("Agent plan rationale must not be empty.")
        if not self.expected_outcome.strip():
            raise DomainError("Agent plan expected_outcome must not be empty.")
        if not self.actions:
            raise DomainError("Agent plan must include at least one planned action.")

DomainError

Bases: ValueError

Base error for invalid domain operations.

Source code in src/domain_agent/domain/models.py
class DomainError(ValueError):
    """Base error for invalid domain operations."""

InvalidStatusTransitionError

Bases: DomainError

Raised when a request attempts an invalid lifecycle transition.

Source code in src/domain_agent/domain/models.py
class InvalidStatusTransitionError(DomainError):
    """Raised when a request attempts an invalid lifecycle transition."""

PlanReviewResult dataclass

Structured review outcome for a full plan review.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class PlanReviewResult:
    """Structured review outcome for a full plan review."""

    decision: ReviewDecision
    action_decisions: tuple[ActionReviewDecision, ...]

    def __post_init__(self) -> None:
        if not self.action_decisions:
            raise DomainError("Plan review result must include action decisions.")
        approved_actions = tuple(
            action_decision for action_decision in self.action_decisions if action_decision.approved
        )
        if self.decision.approved and not approved_actions:
            raise DomainError("A positive plan review requires at least one approved action.")
        if not self.decision.approved and approved_actions:
            raise DomainError("Rejected plans cannot contain approved actions.")

PlannedAction dataclass

A single tool-backed action planned by the agent.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class PlannedAction:
    """A single tool-backed action planned by the agent."""

    action_id: str
    tool_name: str
    description: str
    arguments: dict[str, object] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not self.action_id.strip():
            raise DomainError("Planned action action_id must not be empty.")
        if not self.tool_name.strip():
            raise DomainError("Planned action tool_name must not be empty.")
        if not self.description.strip():
            raise DomainError("Planned action description must not be empty.")

        object.__setattr__(self, "arguments", dict(self.arguments))

Proposal dataclass

A human-reviewable plan prepared before execution.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class Proposal:
    """A human-reviewable plan prepared before execution."""

    summary: str
    rationale: str
    execution_steps: tuple[str, ...]
    risk_level: RiskLevel
    prepared_by: str
    agent_plan: AgentPlan | None = None

    def __post_init__(self) -> None:
        if not self.summary.strip():
            raise DomainError("Proposal summary must not be empty.")
        if not self.rationale.strip():
            raise DomainError("Proposal rationale must not be empty.")
        if not self.prepared_by.strip():
            raise DomainError("Proposal prepared_by must not be empty.")
        if not self.execution_steps:
            raise DomainError("Proposal must include at least one execution step.")
        if any(not step.strip() for step in self.execution_steps):
            raise DomainError("Proposal execution steps must not be empty.")

Request dataclass

Aggregate root for a reviewable request.

Source code in src/domain_agent/domain/models.py
@dataclass
class Request:
    """Aggregate root for a reviewable request."""

    request_id: str
    requester_id: str
    summary: str
    risk_level: RiskLevel
    details: str = ""
    status: RequestStatus = RequestStatus.DRAFT
    proposal: Proposal | None = None
    review_decision: ReviewDecision | None = None
    action_review_decisions: tuple[ActionReviewDecision, ...] = ()
    execution_notes: str | None = None
    _executed: bool = field(init=False, default=False, repr=False)

    def __post_init__(self) -> None:
        if not self.request_id.strip():
            raise DomainError("Request request_id must not be empty.")
        if not self.requester_id.strip():
            raise DomainError("Request requester_id must not be empty.")
        if not self.summary.strip():
            raise DomainError("Request summary must not be empty.")

        if self.status in {
            RequestStatus.PROPOSED,
            RequestStatus.AWAITING_APPROVAL,
            RequestStatus.APPROVED,
            RequestStatus.REJECTED,
            RequestStatus.EXECUTED,
        } and self.proposal is None:
            raise DomainError(f"Request in status {self.status.value} requires a proposal.")

        if self.status in {
            RequestStatus.APPROVED,
            RequestStatus.REJECTED,
            RequestStatus.EXECUTED,
        } and self.review_decision is None:
            raise DomainError(
                f"Request in status {self.status.value} requires a review decision."
            )

        if self.status is RequestStatus.REJECTED and self.review_decision is not None:
            if self.review_decision.approved:
                raise DomainError("Rejected request cannot carry an approving decision.")

        if (
            self.status in {RequestStatus.APPROVED, RequestStatus.EXECUTED}
            and self.review_decision is not None
        ):
            if not self.review_decision.approved:
                raise DomainError("Approved or executed request requires an approving decision.")

        if self.action_review_decisions:
            if self.proposal is None:
                raise DomainError("Action reviews require a proposal.")
            planned_action_ids = self._expected_action_ids()
            decision_action_ids = {
                action_decision.action_id for action_decision in self.action_review_decisions
            }
            if planned_action_ids != decision_action_ids:
                raise DomainError("Action reviews must cover each planned action exactly once.")
        elif (
            self.status in {RequestStatus.APPROVED, RequestStatus.REJECTED, RequestStatus.EXECUTED}
            and self.proposal is not None
        ):
            raise DomainError("Approved, rejected, or executed requests require action reviews.")

        self._executed = self.status is RequestStatus.EXECUTED

    def start_review(self) -> None:
        """Move a draft request into review."""
        self._transition_to(RequestStatus.UNDER_REVIEW)

    def attach_proposal(self, proposal: Proposal) -> None:
        """Attach a proposal after analysis has completed."""
        if self.status is not RequestStatus.UNDER_REVIEW:
            raise InvalidStatusTransitionError(
                "Proposal can only be attached while the request is under review."
            )
        self.proposal = proposal
        self._transition_to(RequestStatus.PROPOSED)

    def request_review(self) -> None:
        """Expose the prepared proposal for human review."""
        if self.status is not RequestStatus.PROPOSED:
            raise InvalidStatusTransitionError(
                "Review can only be requested after a proposal has been prepared."
            )
        if self.proposal is None:
            raise DomainError("Review cannot be requested without a proposal.")
        self._transition_to(RequestStatus.AWAITING_APPROVAL)

    def record_review_result(self, review_result: PlanReviewResult) -> None:
        """Record the human plan review result and update the request state."""
        if self.status is not RequestStatus.AWAITING_APPROVAL:
            raise InvalidStatusTransitionError(
                "Review decisions can only be recorded while awaiting review."
            )
        if self.proposal is None:
            raise DomainError("Action-level review requires a proposal.")

        planned_action_ids = self._expected_action_ids()
        decision_action_ids = {
            action_decision.action_id for action_decision in review_result.action_decisions
        }
        if planned_action_ids != decision_action_ids:
            raise DomainError("Review result must include a decision for every planned action.")

        self.review_decision = review_result.decision
        self.action_review_decisions = review_result.action_decisions
        target_status = (
            RequestStatus.APPROVED if review_result.decision.approved else RequestStatus.REJECTED
        )
        self._transition_to(target_status)

    def approved_action_ids(self) -> tuple[str, ...]:
        """Return the action ids positively reviewed for execution."""
        return tuple(
            action_decision.action_id
            for action_decision in self.action_review_decisions
            if action_decision.approved
        )

    def _expected_action_ids(self) -> set[str]:
        if self.proposal is None:
            raise DomainError("Expected action ids require a proposal.")
        if self.proposal.agent_plan is None:
            return {"plan-review"}
        return {planned_action.action_id for planned_action in self.proposal.agent_plan.actions}

    def execute(self, execution_notes: str | None = None) -> None:
        """Mark the reviewed request as executed."""
        if self.status is RequestStatus.REJECTED:
            raise ReviewRequiredError("Rejected requests cannot be executed.")
        if self.status is not RequestStatus.APPROVED:
            raise ReviewRequiredError("Review is required before execution.")
        if self.review_decision is None or not self.review_decision.approved:
            raise ReviewRequiredError("A positive review decision is required before execution.")

        self.execution_notes = execution_notes
        self._transition_to(RequestStatus.EXECUTED)
        self._executed = True

    def _transition_to(self, target_status: RequestStatus) -> None:
        if not self.status.can_transition_to(target_status):
            raise InvalidStatusTransitionError(
                f"Cannot transition request from {self.status.value} to {target_status.value}."
            )
        self.status = target_status

approved_action_ids()

Return the action ids positively reviewed for execution.

Source code in src/domain_agent/domain/models.py
def approved_action_ids(self) -> tuple[str, ...]:
    """Return the action ids positively reviewed for execution."""
    return tuple(
        action_decision.action_id
        for action_decision in self.action_review_decisions
        if action_decision.approved
    )

attach_proposal(proposal)

Attach a proposal after analysis has completed.

Source code in src/domain_agent/domain/models.py
def attach_proposal(self, proposal: Proposal) -> None:
    """Attach a proposal after analysis has completed."""
    if self.status is not RequestStatus.UNDER_REVIEW:
        raise InvalidStatusTransitionError(
            "Proposal can only be attached while the request is under review."
        )
    self.proposal = proposal
    self._transition_to(RequestStatus.PROPOSED)

execute(execution_notes=None)

Mark the reviewed request as executed.

Source code in src/domain_agent/domain/models.py
def execute(self, execution_notes: str | None = None) -> None:
    """Mark the reviewed request as executed."""
    if self.status is RequestStatus.REJECTED:
        raise ReviewRequiredError("Rejected requests cannot be executed.")
    if self.status is not RequestStatus.APPROVED:
        raise ReviewRequiredError("Review is required before execution.")
    if self.review_decision is None or not self.review_decision.approved:
        raise ReviewRequiredError("A positive review decision is required before execution.")

    self.execution_notes = execution_notes
    self._transition_to(RequestStatus.EXECUTED)
    self._executed = True

record_review_result(review_result)

Record the human plan review result and update the request state.

Source code in src/domain_agent/domain/models.py
def record_review_result(self, review_result: PlanReviewResult) -> None:
    """Record the human plan review result and update the request state."""
    if self.status is not RequestStatus.AWAITING_APPROVAL:
        raise InvalidStatusTransitionError(
            "Review decisions can only be recorded while awaiting review."
        )
    if self.proposal is None:
        raise DomainError("Action-level review requires a proposal.")

    planned_action_ids = self._expected_action_ids()
    decision_action_ids = {
        action_decision.action_id for action_decision in review_result.action_decisions
    }
    if planned_action_ids != decision_action_ids:
        raise DomainError("Review result must include a decision for every planned action.")

    self.review_decision = review_result.decision
    self.action_review_decisions = review_result.action_decisions
    target_status = (
        RequestStatus.APPROVED if review_result.decision.approved else RequestStatus.REJECTED
    )
    self._transition_to(target_status)

request_review()

Expose the prepared proposal for human review.

Source code in src/domain_agent/domain/models.py
def request_review(self) -> None:
    """Expose the prepared proposal for human review."""
    if self.status is not RequestStatus.PROPOSED:
        raise InvalidStatusTransitionError(
            "Review can only be requested after a proposal has been prepared."
        )
    if self.proposal is None:
        raise DomainError("Review cannot be requested without a proposal.")
    self._transition_to(RequestStatus.AWAITING_APPROVAL)

start_review()

Move a draft request into review.

Source code in src/domain_agent/domain/models.py
def start_review(self) -> None:
    """Move a draft request into review."""
    self._transition_to(RequestStatus.UNDER_REVIEW)

RequestStatus

Bases: StrEnum

Lifecycle states for a reviewable request.

Source code in src/domain_agent/domain/models.py
class RequestStatus(StrEnum):
    """Lifecycle states for a reviewable request."""

    DRAFT = "draft"
    UNDER_REVIEW = "under_review"
    PROPOSED = "proposed"
    AWAITING_APPROVAL = "awaiting_approval"
    APPROVED = "approved"
    REJECTED = "rejected"
    EXECUTED = "executed"

    def can_transition_to(self, target: RequestStatus) -> bool:
        """Return whether this status can move directly to the target status."""
        allowed_transitions: dict[RequestStatus, set[RequestStatus]] = {
            RequestStatus.DRAFT: {RequestStatus.UNDER_REVIEW},
            RequestStatus.UNDER_REVIEW: {RequestStatus.PROPOSED},
            RequestStatus.PROPOSED: {RequestStatus.AWAITING_APPROVAL},
            RequestStatus.AWAITING_APPROVAL: {
                RequestStatus.APPROVED,
                RequestStatus.REJECTED,
            },
            RequestStatus.APPROVED: {RequestStatus.EXECUTED},
            RequestStatus.REJECTED: set(),
            RequestStatus.EXECUTED: set(),
        }
        return target in allowed_transitions[self]

can_transition_to(target)

Return whether this status can move directly to the target status.

Source code in src/domain_agent/domain/models.py
def can_transition_to(self, target: RequestStatus) -> bool:
    """Return whether this status can move directly to the target status."""
    allowed_transitions: dict[RequestStatus, set[RequestStatus]] = {
        RequestStatus.DRAFT: {RequestStatus.UNDER_REVIEW},
        RequestStatus.UNDER_REVIEW: {RequestStatus.PROPOSED},
        RequestStatus.PROPOSED: {RequestStatus.AWAITING_APPROVAL},
        RequestStatus.AWAITING_APPROVAL: {
            RequestStatus.APPROVED,
            RequestStatus.REJECTED,
        },
        RequestStatus.APPROVED: {RequestStatus.EXECUTED},
        RequestStatus.REJECTED: set(),
        RequestStatus.EXECUTED: set(),
    }
    return target in allowed_transitions[self]

ReviewDecision dataclass

A human decision recorded against a proposed request.

Source code in src/domain_agent/domain/models.py
@dataclass(frozen=True)
class ReviewDecision:
    """A human decision recorded against a proposed request."""

    approved: bool
    decided_by: str
    decided_at: dt.datetime
    comment: str = ""

    def __post_init__(self) -> None:
        if not self.decided_by.strip():
            raise DomainError("Review decision decided_by must not be empty.")
        if self.decided_at.tzinfo is None:
            raise DomainError("Review decision decided_at must be timezone-aware.")
        if not self.approved and not self.comment.strip():
            raise DomainError("Rejected decisions must include a comment.")

ReviewRequiredError

Bases: DomainError

Raised when execution is attempted before a required review outcome.

Source code in src/domain_agent/domain/models.py
class ReviewRequiredError(DomainError):
    """Raised when execution is attempted before a required review outcome."""

RiskLevel

Bases: StrEnum

Generic risk classification for a request or proposal.

Source code in src/domain_agent/domain/models.py
class RiskLevel(StrEnum):
    """Generic risk classification for a request or proposal."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"