Skip to content

Commit 43d1eff

Browse files
authored
refactor: define terminal and interrupted task states in one place (#1396)
The set of terminal task states was copied in four places (ActiveTask, the legacy request handler, VersionedDatabaseTaskStore and TaskUpdater), and DefaultRequestHandlerV2 imported the state sets from the ActiveTask module. This moves TERMINAL_TASK_STATES and INTERRUPTED_TASK_STATES into a2a.utils.constants and re-uses them everywhere. __Notes__ - No behavior change or import cycle introduced. - The old names on active_task and default_request_handler still point to the shared sets, so existing external imports keep working. - TaskUpdater still sets its own _terminal_states (now a copy of the shared set), since TaskUpdater is public API and subclasses may read or customize it.
1 parent e615a8d commit 43d1eff

11 files changed

Lines changed: 57 additions & 72 deletions

File tree

‎src/a2a/server/agent_execution/active_task.py‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
TaskStatus,
7676
TaskStatusUpdateEvent,
7777
)
78+
from a2a.utils.constants import (
79+
INTERRUPTED_TASK_STATES,
80+
TERMINAL_TASK_STATES,
81+
)
7882
from a2a.utils.errors import (
7983
InternalError,
8084
InvalidAgentResponseError,
@@ -86,18 +90,6 @@
8690
logger = logging.getLogger(__name__)
8791

8892

89-
TERMINAL_TASK_STATES = {
90-
TaskState.TASK_STATE_COMPLETED,
91-
TaskState.TASK_STATE_CANCELED,
92-
TaskState.TASK_STATE_FAILED,
93-
TaskState.TASK_STATE_REJECTED,
94-
}
95-
INTERRUPTED_TASK_STATES = {
96-
TaskState.TASK_STATE_AUTH_REQUIRED,
97-
TaskState.TASK_STATE_INPUT_REQUIRED,
98-
}
99-
100-
10193
class _RequestStarted:
10294
def __init__(self, request_id: uuid.UUID, request_context: RequestContext):
10395
self.request_id = request_id

‎src/a2a/server/cluster/database_task_store.py‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,12 @@
4444
Task,
4545
TaskState,
4646
)
47+
from a2a.utils.constants import TERMINAL_TASK_STATES
4748
from a2a.utils.proto_utils import to_stream_response
4849

4950

5051
logger = logging.getLogger(__name__)
5152

52-
_TERMINAL_STATES = frozenset(
53-
{
54-
TaskState.TASK_STATE_COMPLETED,
55-
TaskState.TASK_STATE_CANCELED,
56-
TaskState.TASK_STATE_FAILED,
57-
TaskState.TASK_STATE_REJECTED,
58-
}
59-
)
60-
6153
# DB contention retry
6254
_MAX_ATTEMPTS = 5
6355
_RETRY_DELAY_S = 0.02
@@ -195,7 +187,10 @@ async def _save_once(
195187
if task.status.state == TaskState.TASK_STATE_CANCELED:
196188
stored = await self._lock_version(session, task.id, owner)
197189
current = await self._current_task(session, task.id, owner)
198-
if current is None or current.status.state in _TERMINAL_STATES:
190+
if (
191+
current is None
192+
or current.status.state in TERMINAL_TASK_STATES
193+
):
199194
raise ConcurrentTaskModificationError(task.id)
200195
new_version = (stored or 0) + 1
201196
if stored is None:

‎src/a2a/server/request_handlers/default_request_handler.py‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
TaskPushNotificationConfig,
5454
TaskState,
5555
)
56+
from a2a.utils.constants import TERMINAL_TASK_STATES
5657
from a2a.utils.errors import (
5758
ExtendedAgentCardNotConfiguredError,
5859
InternalError,
@@ -74,13 +75,6 @@
7475

7576
logger = logging.getLogger(__name__)
7677

77-
TERMINAL_TASK_STATES = {
78-
TaskState.TASK_STATE_COMPLETED,
79-
TaskState.TASK_STATE_CANCELED,
80-
TaskState.TASK_STATE_FAILED,
81-
TaskState.TASK_STATE_REJECTED,
82-
}
83-
8478

8579
@trace_class(kind=SpanKind.SERVER)
8680
class LegacyRequestHandler(RequestHandler):

‎src/a2a/server/request_handlers/default_request_handler_v2.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515
RequestContextBuilder,
1616
SimpleRequestContextBuilder,
1717
)
18-
from a2a.server.agent_execution.active_task import (
19-
INTERRUPTED_TASK_STATES,
20-
TERMINAL_TASK_STATES,
21-
)
2218
from a2a.server.agent_execution.active_task_registry import ActiveTaskRegistry
2319
from a2a.server.agent_execution.plugin_agent_executor import (
2420
PluginAgentExecutor,
@@ -57,6 +53,10 @@
5753
TaskState,
5854
TaskStatusUpdateEvent,
5955
)
56+
from a2a.utils.constants import (
57+
INTERRUPTED_TASK_STATES,
58+
TERMINAL_TASK_STATES,
59+
)
6060
from a2a.utils.errors import (
6161
ExtendedAgentCardNotConfiguredError,
6262
InternalError,

‎src/a2a/server/tasks/task_updater.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
TaskStatus,
2222
TaskStatusUpdateEvent,
2323
)
24+
from a2a.utils.constants import TERMINAL_TASK_STATES
2425

2526

2627
class TaskUpdater:
@@ -51,12 +52,7 @@ def __init__(
5152
self.context_id = context_id
5253
self._lock = asyncio.Lock()
5354
self._terminal_state_reached = False
54-
self._terminal_states = {
55-
TaskState.TASK_STATE_COMPLETED,
56-
TaskState.TASK_STATE_CANCELED,
57-
TaskState.TASK_STATE_FAILED,
58-
TaskState.TASK_STATE_REJECTED,
59-
}
55+
self._terminal_states = set(TERMINAL_TASK_STATES)
6056
self._artifact_id_generator = (
6157
artifact_id_generator if artifact_id_generator else UUIDGenerator()
6258
)

‎src/a2a/utils/constants.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from enum import Enum
44

5+
from a2a.types.a2a_pb2 import TaskState
6+
57

68
AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent-card.json'
79
DEFAULT_RPC_URL = '/'
@@ -29,3 +31,19 @@ class TransportProtocol(str, Enum):
2931
PROTOCOL_VERSION_1_0 = '1.0'
3032
PROTOCOL_VERSION_0_3 = '0.3'
3133
PROTOCOL_VERSION_CURRENT = PROTOCOL_VERSION_1_0
34+
35+
TERMINAL_TASK_STATES: frozenset[TaskState.ValueType] = frozenset(
36+
{
37+
TaskState.TASK_STATE_COMPLETED,
38+
TaskState.TASK_STATE_CANCELED,
39+
TaskState.TASK_STATE_FAILED,
40+
TaskState.TASK_STATE_REJECTED,
41+
}
42+
)
43+
44+
INTERRUPTED_TASK_STATES: frozenset[TaskState.ValueType] = frozenset(
45+
{
46+
TaskState.TASK_STATE_INPUT_REQUIRED,
47+
TaskState.TASK_STATE_AUTH_REQUIRED,
48+
}
49+
)

‎tests/integration/test_scenarios.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
TaskStatusUpdateEvent,
5555
)
5656
from a2a.utils import TransportProtocol
57+
from a2a.utils.constants import TERMINAL_TASK_STATES
5758
from a2a.utils.errors import (
5859
InvalidAgentResponseError,
5960
TaskNotCancelableError,
@@ -2379,12 +2380,6 @@ async def cancel(
23792380
# executors below define cancel() as an empty teardown, which is what
23802381
# InputRequiredAgent, SlowAgent and DummyAgentExecutor use in this file and what
23812382
# a real cleanup-only executor looks like.
2382-
_TERMINAL_STATES = {
2383-
TaskState.TASK_STATE_CANCELED,
2384-
TaskState.TASK_STATE_COMPLETED,
2385-
TaskState.TASK_STATE_FAILED,
2386-
TaskState.TASK_STATE_REJECTED,
2387-
}
23882383

23892384

23902385
async def _start_task(client, text):
@@ -2435,7 +2430,7 @@ async def cancel(
24352430
await client.cancel_task(CancelTaskRequest(id=task_id))
24362431
task_after = await client.get_task(GetTaskRequest(id=task_id))
24372432

2438-
assert task_after.status.state in _TERMINAL_STATES
2433+
assert task_after.status.state in TERMINAL_TASK_STATES
24392434

24402435

24412436
@pytest.mark.timeout(5.0)

‎tests/server/cluster/conftest.py‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,11 @@
3737
Task,
3838
TaskState,
3939
)
40+
from a2a.utils.constants import TERMINAL_TASK_STATES
4041

4142

4243
# --- In-memory cluster doubles ----------------------------------------------
4344

44-
_TERMINAL_STATES = frozenset(
45-
{
46-
TaskState.TASK_STATE_COMPLETED,
47-
TaskState.TASK_STATE_CANCELED,
48-
TaskState.TASK_STATE_FAILED,
49-
TaskState.TASK_STATE_REJECTED,
50-
}
51-
)
52-
5345

5446
class VersionedInMemoryTaskStore(VersionedTaskStore):
5547
"""`VersionedTaskStore` backed by in-process dictionaries.
@@ -95,7 +87,10 @@ async def save(
9587
stored = self._current_version_locked(owner, task_id=task.id)
9688
if task.status.state == TaskState.TASK_STATE_CANCELED:
9789
current = await self._store.get(task.id, context)
98-
if current is None or current.status.state in _TERMINAL_STATES:
90+
if (
91+
current is None
92+
or current.status.state in TERMINAL_TASK_STATES
93+
):
9994
raise ConcurrentTaskModificationError(task.id)
10095
elif not prev_version.is_missing:
10196
if stored == 0:

‎tests/server/request_handlers/test_default_request_handler.py‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
TaskStatus,
8080
TaskStatusUpdateEvent,
8181
)
82+
from a2a.utils.constants import TERMINAL_TASK_STATES
8283
from a2a.utils.push_url_validator import (
8384
validate_push_notification_url,
8485
)
@@ -2700,14 +2701,6 @@ async def test_delete_task_push_notification_config_info_with_config_and_no_id(
27002701
assert len(result2.configs) == 0
27012702

27022703

2703-
TERMINAL_TASK_STATES = {
2704-
TaskState.TASK_STATE_COMPLETED,
2705-
TaskState.TASK_STATE_CANCELED,
2706-
TaskState.TASK_STATE_FAILED,
2707-
TaskState.TASK_STATE_REJECTED,
2708-
}
2709-
2710-
27112704
@pytest.mark.asyncio
27122705
@pytest.mark.parametrize('terminal_state', TERMINAL_TASK_STATES)
27132706
async def test_on_message_send_task_in_terminal_state(

‎tests/server/request_handlers/test_default_request_handler_v2.py‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
TaskStatus,
7474
TaskStatusUpdateEvent,
7575
)
76+
from a2a.utils.constants import TERMINAL_TASK_STATES
7677

7778

7879
def create_default_agent_card():
@@ -1189,14 +1190,6 @@ async def test_delete_task_push_notification_config_info_with_config_and_no_id()
11891190
assert len(result2.configs) == 0
11901191

11911192

1192-
TERMINAL_TASK_STATES = {
1193-
TaskState.TASK_STATE_COMPLETED,
1194-
TaskState.TASK_STATE_CANCELED,
1195-
TaskState.TASK_STATE_FAILED,
1196-
TaskState.TASK_STATE_REJECTED,
1197-
}
1198-
1199-
12001193
def create_input_mode_handler(*, validate_input_modes: bool):
12011194
"""A handler whose card declares text/plain as its only input mode."""
12021195
return DefaultRequestHandlerV2(

0 commit comments

Comments
 (0)