Skip to content

Commit 1910cd9

Browse files
refactor(agentex): single canonical terminal/non-terminal task-status sets
Define NON_TERMINAL_TASK_STATUSES ({RUNNING, INTERRUPTED}) and its complement TERMINAL_TASK_STATUSES once on the TaskStatus entity, and reference them from both call sites instead of duplicating the membership: - streams_use_case: imports TERMINAL_TASK_STATUSES (was a local frozenset). - tasks_use_case: _TERMINAL_TRANSITION_SOURCES now = NON_TERMINAL_TASK_STATUSES (was an inline (RUNNING, INTERRUPTED) tuple). A newly added status is terminal by default unless explicitly added to the non-terminal set, keeping the two views consistent by construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 438d828 commit 1910cd9

3 files changed

Lines changed: 27 additions & 20 deletions

File tree

agentex/src/domain/entities/tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ class TaskStatus(str, Enum):
2929
DELETED = "DELETED"
3030

3131

32+
# Canonical status partition (single source of truth for both the task-status
33+
# state machine and SSE stream termination).
34+
#
35+
# Non-terminal: a turn is in flight (RUNNING) or the task is paused and
36+
# resumable (INTERRUPTED). A task may only transition to a terminal status from
37+
# one of these. Terminal is the complement: no further events are produced and
38+
# no new turns are accepted. Defining terminal as the complement means a newly
39+
# added status is treated as terminal unless it is explicitly listed as
40+
# non-terminal here.
41+
NON_TERMINAL_TASK_STATUSES = frozenset({TaskStatus.RUNNING, TaskStatus.INTERRUPTED})
42+
TERMINAL_TASK_STATUSES = frozenset(TaskStatus) - NON_TERMINAL_TASK_STATUSES
43+
44+
3245
class TaskEntity(BaseModel):
3346
id: str = Field(
3447
...,

agentex/src/domain/use_cases/streams_use_case.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,13 @@
1515
TaskStreamTaskUpdatedEventEntity,
1616
convert_task_stream_event_to_entity,
1717
)
18-
from src.domain.entities.tasks import TaskStatus
18+
from src.domain.entities.tasks import TERMINAL_TASK_STATUSES
1919
from src.domain.services.task_service import DAgentTaskService
2020
from src.utils.logging import make_logger
2121
from src.utils.stream_topics import get_task_event_stream_topic
2222

2323
logger = make_logger(__name__)
2424

25-
# Statuses after which no more events are produced; a subscription must end.
26-
_TERMINAL_TASK_STATUSES = frozenset(
27-
{
28-
TaskStatus.CANCELED,
29-
TaskStatus.COMPLETED,
30-
TaskStatus.FAILED,
31-
TaskStatus.TERMINATED,
32-
TaskStatus.TIMED_OUT,
33-
TaskStatus.DELETED,
34-
}
35-
)
36-
3725

3826
class StreamsUseCase:
3927
def __init__(
@@ -129,7 +117,7 @@ async def stream_task_events(
129117
last_id = await self.stream_repository.get_stream_tail_id(stream_topic)
130118
# Send initial connection data
131119
yield f"data: {TaskStreamConnectedEventEntity(type='connected', taskId=task_id).model_dump_json()}\n\n"
132-
if task.status in _TERMINAL_TASK_STATUSES:
120+
if task.status in TERMINAL_TASK_STATUSES:
133121
async for _id, data in self.read_messages(topic=stream_topic, last_id="0"):
134122
yield f"data: {data.model_dump_json()}\n\n"
135123
await asyncio.sleep(0.02)
@@ -169,7 +157,7 @@ async def stream_task_events(
169157
if (
170158
isinstance(data, TaskStreamTaskUpdatedEventEntity)
171159
and data.task is not None
172-
and data.task.status in _TERMINAL_TASK_STATUSES
160+
and data.task.status in TERMINAL_TASK_STATUSES
173161
):
174162
logger.info(
175163
f"Ending SSE stream for task {task_id}: received "

agentex/src/domain/use_cases/tasks_use_case.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from fastapi import Depends
44

55
from src.adapters.crud_store.exceptions import ItemDoesNotExist
6-
from src.domain.entities.tasks import TaskEntity, TaskRelationships, TaskStatus
6+
from src.domain.entities.tasks import (
7+
NON_TERMINAL_TASK_STATUSES,
8+
TaskEntity,
9+
TaskRelationships,
10+
TaskStatus,
11+
)
712
from src.domain.exceptions import ClientError
813
from src.domain.services.task_service import DAgentTaskService
914
from src.utils.logging import make_logger
@@ -132,10 +137,11 @@ async def update_mutable_fields_on_task(
132137

133138
return task_entity
134139

135-
# Non-terminal statuses a task can be transitioned to a terminal status from.
136-
# RUNNING is the normal case; INTERRUPTED is also valid so an interrupted
137-
# (paused, still-continuable) task can still be canceled/completed/etc later.
138-
_TERMINAL_TRANSITION_SOURCES = (TaskStatus.RUNNING, TaskStatus.INTERRUPTED)
140+
# Statuses a task can be transitioned to a terminal status from: RUNNING is
141+
# the normal case; INTERRUPTED (paused, still-continuable) can also still be
142+
# canceled/completed/etc later. This is exactly the canonical non-terminal
143+
# set — its complement is what SSE streaming treats as terminal.
144+
_TERMINAL_TRANSITION_SOURCES = NON_TERMINAL_TASK_STATUSES
139145

140146
async def _transition_to_terminal(
141147
self,

0 commit comments

Comments
 (0)