jaf-py 2.5.9__py3-none-any.whl → 2.5.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +361 -280
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.9.dist-info/RECORD +0 -96
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
|
@@ -26,11 +26,7 @@ class TaskCleanupTestBase:
|
|
|
26
26
|
"""Base class for cleanup testing utilities"""
|
|
27
27
|
|
|
28
28
|
def create_aged_task(
|
|
29
|
-
self,
|
|
30
|
-
task_id: str,
|
|
31
|
-
context_id: str,
|
|
32
|
-
state: TaskState,
|
|
33
|
-
age_hours: int = 0
|
|
29
|
+
self, task_id: str, context_id: str, state: TaskState, age_hours: int = 0
|
|
34
30
|
) -> A2ATask:
|
|
35
31
|
"""Create a task with a specific age"""
|
|
36
32
|
timestamp = (datetime.now(timezone.utc) - timedelta(hours=age_hours)).isoformat()
|
|
@@ -46,14 +42,11 @@ class TaskCleanupTestBase:
|
|
|
46
42
|
parts=[A2ATextPart(kind="text", text=f"Task in {state.value} state")],
|
|
47
43
|
messageId=f"msg_{task_id}",
|
|
48
44
|
contextId=context_id,
|
|
49
|
-
kind="message"
|
|
45
|
+
kind="message",
|
|
50
46
|
),
|
|
51
|
-
timestamp=timestamp
|
|
47
|
+
timestamp=timestamp,
|
|
52
48
|
),
|
|
53
|
-
metadata={
|
|
54
|
-
"created_at": timestamp,
|
|
55
|
-
"test_age_hours": age_hours
|
|
56
|
-
}
|
|
49
|
+
metadata={"created_at": timestamp, "test_age_hours": age_hours},
|
|
57
50
|
)
|
|
58
51
|
|
|
59
52
|
def create_diverse_aged_dataset(self, context_id: str = "cleanup_test_ctx") -> List[A2ATask]:
|
|
@@ -61,37 +54,61 @@ class TaskCleanupTestBase:
|
|
|
61
54
|
tasks = []
|
|
62
55
|
|
|
63
56
|
# Fresh tasks (0-2 hours old) in various states
|
|
64
|
-
tasks.extend(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
tasks.extend(
|
|
58
|
+
[
|
|
59
|
+
self.create_aged_task(f"fresh_submitted_{i}", context_id, TaskState.SUBMITTED, i)
|
|
60
|
+
for i in range(2)
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
tasks.extend(
|
|
64
|
+
[
|
|
65
|
+
self.create_aged_task(f"fresh_working_{i}", context_id, TaskState.WORKING, i)
|
|
66
|
+
for i in range(2)
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
tasks.extend(
|
|
70
|
+
[
|
|
71
|
+
self.create_aged_task(f"fresh_completed_{i}", context_id, TaskState.COMPLETED, i)
|
|
72
|
+
for i in range(3)
|
|
73
|
+
]
|
|
74
|
+
)
|
|
76
75
|
|
|
77
76
|
# Medium age tasks (1-3 days old)
|
|
78
77
|
medium_age_hours = [24, 48, 72] # 1, 2, 3 days
|
|
79
78
|
for hours in medium_age_hours:
|
|
80
|
-
tasks.extend(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
tasks.extend(
|
|
80
|
+
[
|
|
81
|
+
self.create_aged_task(
|
|
82
|
+
f"medium_completed_{hours}h", context_id, TaskState.COMPLETED, hours
|
|
83
|
+
),
|
|
84
|
+
self.create_aged_task(
|
|
85
|
+
f"medium_failed_{hours}h", context_id, TaskState.FAILED, hours
|
|
86
|
+
),
|
|
87
|
+
self.create_aged_task(
|
|
88
|
+
f"medium_working_{hours}h", context_id, TaskState.WORKING, hours
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
)
|
|
85
92
|
|
|
86
93
|
# Old tasks (1-2 weeks old)
|
|
87
94
|
old_age_hours = [168, 336] # 1 week, 2 weeks
|
|
88
95
|
for hours in old_age_hours:
|
|
89
|
-
tasks.extend(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
tasks.extend(
|
|
97
|
+
[
|
|
98
|
+
self.create_aged_task(
|
|
99
|
+
f"old_completed_{hours}h", context_id, TaskState.COMPLETED, hours
|
|
100
|
+
),
|
|
101
|
+
self.create_aged_task(
|
|
102
|
+
f"old_failed_{hours}h", context_id, TaskState.FAILED, hours
|
|
103
|
+
),
|
|
104
|
+
self.create_aged_task(
|
|
105
|
+
f"old_canceled_{hours}h", context_id, TaskState.CANCELED, hours
|
|
106
|
+
),
|
|
107
|
+
self.create_aged_task(
|
|
108
|
+
f"old_working_{hours}h", context_id, TaskState.WORKING, hours
|
|
109
|
+
), # Should NOT be cleaned
|
|
110
|
+
]
|
|
111
|
+
)
|
|
95
112
|
|
|
96
113
|
return tasks
|
|
97
114
|
|
|
@@ -101,7 +118,7 @@ async def cleanup_provider() -> A2ATaskProvider:
|
|
|
101
118
|
"""Create provider for cleanup testing"""
|
|
102
119
|
config = A2AInMemoryTaskConfig(
|
|
103
120
|
max_tasks=1000,
|
|
104
|
-
cleanup_interval=3600 # 1 hour
|
|
121
|
+
cleanup_interval=3600, # 1 hour
|
|
105
122
|
)
|
|
106
123
|
provider = create_a2a_in_memory_task_provider(config)
|
|
107
124
|
yield provider
|
|
@@ -131,7 +148,10 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
131
148
|
cleanup_config = A2ATaskCleanupConfig(
|
|
132
149
|
max_age=168 * 3600, # 7 days in seconds
|
|
133
150
|
dry_run=False,
|
|
134
|
-
retain_states=[
|
|
151
|
+
retain_states=[
|
|
152
|
+
TaskState.SUBMITTED.value,
|
|
153
|
+
TaskState.WORKING.value,
|
|
154
|
+
], # Retain active states
|
|
135
155
|
)
|
|
136
156
|
|
|
137
157
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -151,15 +171,13 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
151
171
|
|
|
152
172
|
# Recent completed tasks should remain
|
|
153
173
|
recent_completed = [
|
|
154
|
-
t for t in remaining_tasks
|
|
155
|
-
if t.status.state == TaskState.COMPLETED and "fresh_" in t.id
|
|
174
|
+
t for t in remaining_tasks if t.status.state == TaskState.COMPLETED and "fresh_" in t.id
|
|
156
175
|
]
|
|
157
176
|
assert len(recent_completed) > 0, "Recent completed tasks should be preserved"
|
|
158
177
|
|
|
159
178
|
# Old completed tasks should be removed
|
|
160
179
|
old_completed = [
|
|
161
|
-
t for t in remaining_tasks
|
|
162
|
-
if t.status.state == TaskState.COMPLETED and "old_" in t.id
|
|
180
|
+
t for t in remaining_tasks if t.status.state == TaskState.COMPLETED and "old_" in t.id
|
|
163
181
|
]
|
|
164
182
|
assert len(old_completed) == 0, "Old completed tasks should be cleaned up"
|
|
165
183
|
|
|
@@ -176,7 +194,7 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
176
194
|
self.create_aged_task("old_working", context_id, TaskState.WORKING, old_age),
|
|
177
195
|
self.create_aged_task("old_completed", context_id, TaskState.COMPLETED, old_age),
|
|
178
196
|
self.create_aged_task("old_failed", context_id, TaskState.FAILED, old_age),
|
|
179
|
-
self.create_aged_task("old_canceled", context_id, TaskState.CANCELED, old_age)
|
|
197
|
+
self.create_aged_task("old_canceled", context_id, TaskState.CANCELED, old_age),
|
|
180
198
|
]
|
|
181
199
|
|
|
182
200
|
# Store all tasks
|
|
@@ -185,9 +203,9 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
185
203
|
|
|
186
204
|
# Cleanup only completed and failed tasks
|
|
187
205
|
cleanup_config = A2ATaskCleanupConfig(
|
|
188
|
-
max_age=168*3600, # 7 days (all tasks are older)
|
|
206
|
+
max_age=168 * 3600, # 7 days (all tasks are older)
|
|
189
207
|
dry_run=False,
|
|
190
|
-
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value]
|
|
208
|
+
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value],
|
|
191
209
|
)
|
|
192
210
|
|
|
193
211
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -204,7 +222,7 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
204
222
|
# Should preserve submitted and working tasks
|
|
205
223
|
assert TaskState.SUBMITTED in remaining_states, "Submitted task should be preserved"
|
|
206
224
|
assert TaskState.WORKING in remaining_states, "Working task should be preserved"
|
|
207
|
-
|
|
225
|
+
|
|
208
226
|
# Should not have completed, failed, or canceled tasks
|
|
209
227
|
assert TaskState.COMPLETED not in remaining_states, "Completed task should be cleaned"
|
|
210
228
|
assert TaskState.FAILED not in remaining_states, "Failed task should be cleaned"
|
|
@@ -225,9 +243,9 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
225
243
|
|
|
226
244
|
# Perform dry run cleanup
|
|
227
245
|
dry_run_config = A2ATaskCleanupConfig(
|
|
228
|
-
max_age=168*3600, # 7 days
|
|
246
|
+
max_age=168 * 3600, # 7 days
|
|
229
247
|
dry_run=True, # Dry run mode
|
|
230
|
-
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value]
|
|
248
|
+
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value],
|
|
231
249
|
)
|
|
232
250
|
|
|
233
251
|
dry_run_result = await perform_task_cleanup(cleanup_provider, dry_run_config)
|
|
@@ -244,15 +262,17 @@ class TestTaskCleanupByAge(TaskCleanupTestBase):
|
|
|
244
262
|
|
|
245
263
|
# Now perform actual cleanup and verify it matches dry run prediction
|
|
246
264
|
actual_config = A2ATaskCleanupConfig(
|
|
247
|
-
max_age=168*3600,
|
|
265
|
+
max_age=168 * 3600,
|
|
248
266
|
dry_run=False,
|
|
249
|
-
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value]
|
|
267
|
+
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value],
|
|
250
268
|
)
|
|
251
269
|
|
|
252
270
|
actual_result = await perform_task_cleanup(cleanup_provider, actual_config)
|
|
253
271
|
actual_delete_count = actual_result.data.total_cleaned
|
|
254
272
|
|
|
255
|
-
assert actual_delete_count == would_delete_count,
|
|
273
|
+
assert actual_delete_count == would_delete_count, (
|
|
274
|
+
f"Actual cleanup ({actual_delete_count}) should match dry run prediction ({would_delete_count})"
|
|
275
|
+
)
|
|
256
276
|
|
|
257
277
|
print(f"Dry run test: predicted {would_delete_count}, actual {actual_delete_count}")
|
|
258
278
|
|
|
@@ -268,14 +288,16 @@ class TestTaskCleanupByCount(TaskCleanupTestBase):
|
|
|
268
288
|
completed_tasks = []
|
|
269
289
|
for i in range(20): # Create 20 completed tasks
|
|
270
290
|
age_hours = i # Age from 0 to 19 hours
|
|
271
|
-
task = self.create_aged_task(
|
|
291
|
+
task = self.create_aged_task(
|
|
292
|
+
f"completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours
|
|
293
|
+
)
|
|
272
294
|
completed_tasks.append(task)
|
|
273
295
|
|
|
274
296
|
# Also create some non-completed tasks that should be preserved
|
|
275
297
|
other_tasks = [
|
|
276
298
|
self.create_aged_task("working_1", context_id, TaskState.WORKING, 50),
|
|
277
299
|
self.create_aged_task("submitted_1", context_id, TaskState.SUBMITTED, 100),
|
|
278
|
-
self.create_aged_task("failed_1", context_id, TaskState.FAILED, 10)
|
|
300
|
+
self.create_aged_task("failed_1", context_id, TaskState.FAILED, 10),
|
|
279
301
|
]
|
|
280
302
|
|
|
281
303
|
# Store all tasks
|
|
@@ -287,7 +309,12 @@ class TestTaskCleanupByCount(TaskCleanupTestBase):
|
|
|
287
309
|
cleanup_config = A2ATaskCleanupConfig(
|
|
288
310
|
max_completed_tasks=10,
|
|
289
311
|
dry_run=False,
|
|
290
|
-
retain_states=[
|
|
312
|
+
retain_states=[
|
|
313
|
+
TaskState.SUBMITTED.value,
|
|
314
|
+
TaskState.WORKING.value,
|
|
315
|
+
TaskState.FAILED.value,
|
|
316
|
+
TaskState.CANCELED.value,
|
|
317
|
+
],
|
|
291
318
|
)
|
|
292
319
|
|
|
293
320
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -322,13 +349,17 @@ class TestTaskCleanupByCount(TaskCleanupTestBase):
|
|
|
322
349
|
# 15 old completed tasks (> 7 days)
|
|
323
350
|
for i in range(15):
|
|
324
351
|
age_hours = 200 + i # 8+ days old
|
|
325
|
-
task = self.create_aged_task(
|
|
352
|
+
task = self.create_aged_task(
|
|
353
|
+
f"old_completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours
|
|
354
|
+
)
|
|
326
355
|
tasks.append(task)
|
|
327
356
|
|
|
328
357
|
# 15 new completed tasks (< 7 days)
|
|
329
358
|
for i in range(15):
|
|
330
359
|
age_hours = i # 0-14 hours old
|
|
331
|
-
task = self.create_aged_task(
|
|
360
|
+
task = self.create_aged_task(
|
|
361
|
+
f"new_completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours
|
|
362
|
+
)
|
|
332
363
|
tasks.append(task)
|
|
333
364
|
|
|
334
365
|
# Store all tasks
|
|
@@ -337,10 +368,15 @@ class TestTaskCleanupByCount(TaskCleanupTestBase):
|
|
|
337
368
|
|
|
338
369
|
# Cleanup with both age limit (7 days) and count limit (10)
|
|
339
370
|
cleanup_config = A2ATaskCleanupConfig(
|
|
340
|
-
max_age=168*3600, # 7 days
|
|
371
|
+
max_age=168 * 3600, # 7 days
|
|
341
372
|
max_completed_tasks=10,
|
|
342
373
|
dry_run=False,
|
|
343
|
-
retain_states=[
|
|
374
|
+
retain_states=[
|
|
375
|
+
TaskState.SUBMITTED.value,
|
|
376
|
+
TaskState.WORKING.value,
|
|
377
|
+
TaskState.FAILED.value,
|
|
378
|
+
TaskState.CANCELED.value,
|
|
379
|
+
],
|
|
344
380
|
)
|
|
345
381
|
|
|
346
382
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -351,7 +387,9 @@ class TestTaskCleanupByCount(TaskCleanupTestBase):
|
|
|
351
387
|
# 2. 5 additional new tasks (due to count limit: 15 new - 10 limit = 5)
|
|
352
388
|
# Total: 20 deleted
|
|
353
389
|
expected_deletions = 15 + 5 # old tasks + excess new tasks
|
|
354
|
-
assert cleanup_count == expected_deletions,
|
|
390
|
+
assert cleanup_count == expected_deletions, (
|
|
391
|
+
f"Expected {expected_deletions} deletions, got {cleanup_count}"
|
|
392
|
+
)
|
|
355
393
|
|
|
356
394
|
# Verify exactly 10 tasks remain (all new)
|
|
357
395
|
remaining_result = await cleanup_provider.get_tasks_by_context(context_id)
|
|
@@ -378,9 +416,13 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
378
416
|
for i in range(tasks_per_context):
|
|
379
417
|
# Mix of old completed and old working tasks
|
|
380
418
|
if i % 2 == 0:
|
|
381
|
-
task = self.create_aged_task(
|
|
419
|
+
task = self.create_aged_task(
|
|
420
|
+
f"task_{ctx}_{i}", ctx, TaskState.COMPLETED, 200
|
|
421
|
+
) # Old
|
|
382
422
|
else:
|
|
383
|
-
task = self.create_aged_task(
|
|
423
|
+
task = self.create_aged_task(
|
|
424
|
+
f"task_{ctx}_{i}", ctx, TaskState.WORKING, 200
|
|
425
|
+
) # Old but should be preserved
|
|
384
426
|
all_tasks.append(task)
|
|
385
427
|
|
|
386
428
|
# Store all tasks
|
|
@@ -389,12 +431,19 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
389
431
|
|
|
390
432
|
# Cleanup only context_a
|
|
391
433
|
cleanup_config = A2ATaskCleanupConfig(
|
|
392
|
-
max_age=168*3600, # 7 days (all tasks are older)
|
|
434
|
+
max_age=168 * 3600, # 7 days (all tasks are older)
|
|
393
435
|
dry_run=False,
|
|
394
|
-
retain_states=[
|
|
436
|
+
retain_states=[
|
|
437
|
+
TaskState.SUBMITTED.value,
|
|
438
|
+
TaskState.WORKING.value,
|
|
439
|
+
TaskState.FAILED.value,
|
|
440
|
+
TaskState.CANCELED.value,
|
|
441
|
+
],
|
|
395
442
|
)
|
|
396
443
|
|
|
397
|
-
cleanup_result = await perform_task_cleanup(
|
|
444
|
+
cleanup_result = await perform_task_cleanup(
|
|
445
|
+
cleanup_provider, cleanup_config, context_id="ctx_a"
|
|
446
|
+
)
|
|
398
447
|
cleanup_count = cleanup_result.data.total_cleaned
|
|
399
448
|
|
|
400
449
|
# Should delete 5 completed tasks from ctx_a (half of 10)
|
|
@@ -404,7 +453,9 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
404
453
|
ctx_a_result = await cleanup_provider.get_tasks_by_context("ctx_a")
|
|
405
454
|
ctx_a_remaining = ctx_a_result.data
|
|
406
455
|
assert len(ctx_a_remaining) == 5, "ctx_a should have 5 tasks remaining"
|
|
407
|
-
assert all(t.status.state == TaskState.WORKING for t in ctx_a_remaining),
|
|
456
|
+
assert all(t.status.state == TaskState.WORKING for t in ctx_a_remaining), (
|
|
457
|
+
"Only working tasks should remain in ctx_a"
|
|
458
|
+
)
|
|
408
459
|
|
|
409
460
|
# Verify other contexts are unaffected
|
|
410
461
|
for ctx in ["ctx_b", "ctx_c"]:
|
|
@@ -424,7 +475,9 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
424
475
|
for ctx in contexts:
|
|
425
476
|
# Create mix of old and new completed tasks
|
|
426
477
|
for i in range(5):
|
|
427
|
-
old_task = self.create_aged_task(
|
|
478
|
+
old_task = self.create_aged_task(
|
|
479
|
+
f"old_task_{ctx}_{i}", ctx, TaskState.COMPLETED, 200
|
|
480
|
+
)
|
|
428
481
|
new_task = self.create_aged_task(f"new_task_{ctx}_{i}", ctx, TaskState.COMPLETED, 1)
|
|
429
482
|
all_tasks.extend([old_task, new_task])
|
|
430
483
|
|
|
@@ -434,9 +487,14 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
434
487
|
|
|
435
488
|
# Global cleanup (no context filter)
|
|
436
489
|
cleanup_config = A2ATaskCleanupConfig(
|
|
437
|
-
max_age=168*3600, # 7 days
|
|
490
|
+
max_age=168 * 3600, # 7 days
|
|
438
491
|
dry_run=False,
|
|
439
|
-
retain_states=[
|
|
492
|
+
retain_states=[
|
|
493
|
+
TaskState.SUBMITTED.value,
|
|
494
|
+
TaskState.WORKING.value,
|
|
495
|
+
TaskState.FAILED.value,
|
|
496
|
+
TaskState.CANCELED.value,
|
|
497
|
+
],
|
|
440
498
|
)
|
|
441
499
|
|
|
442
500
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -451,7 +509,9 @@ class TestCleanupContextIsolation(TaskCleanupTestBase):
|
|
|
451
509
|
ctx_tasks = ctx_result.data
|
|
452
510
|
|
|
453
511
|
assert len(ctx_tasks) == 5, f"{ctx} should have 5 tasks remaining"
|
|
454
|
-
assert all("new_task_" in t.id for t in ctx_tasks),
|
|
512
|
+
assert all("new_task_" in t.id for t in ctx_tasks), (
|
|
513
|
+
f"Only new tasks should remain in {ctx}"
|
|
514
|
+
)
|
|
455
515
|
|
|
456
516
|
|
|
457
517
|
class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
@@ -483,18 +543,19 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
483
543
|
# Store all tasks in batches
|
|
484
544
|
batch_size = 100
|
|
485
545
|
for i in range(0, len(tasks), batch_size):
|
|
486
|
-
batch = tasks[i:i + batch_size]
|
|
546
|
+
batch = tasks[i : i + batch_size]
|
|
487
547
|
batch_operations = [cleanup_provider.store_task(task) for task in batch]
|
|
488
548
|
await asyncio.gather(*batch_operations)
|
|
489
549
|
|
|
490
550
|
# Measure cleanup performance
|
|
491
551
|
import time
|
|
552
|
+
|
|
492
553
|
start_time = time.perf_counter()
|
|
493
554
|
|
|
494
555
|
cleanup_config = A2ATaskCleanupConfig(
|
|
495
|
-
max_age=168*3600, # 7 days
|
|
556
|
+
max_age=168 * 3600, # 7 days
|
|
496
557
|
dry_run=False,
|
|
497
|
-
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value]
|
|
558
|
+
retain_states=[TaskState.SUBMITTED.value, TaskState.WORKING.value],
|
|
498
559
|
)
|
|
499
560
|
|
|
500
561
|
cleanup_result = await perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
@@ -507,15 +568,24 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
507
568
|
cleanup_count = cleanup_result.data.total_cleaned
|
|
508
569
|
|
|
509
570
|
# Calculate expected deletions (old completed and failed tasks)
|
|
510
|
-
expected_deletions = sum(
|
|
571
|
+
expected_deletions = sum(
|
|
572
|
+
1
|
|
573
|
+
for t in tasks
|
|
574
|
+
if t.status.state in [TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELED]
|
|
575
|
+
and t.metadata.get("test_age_hours", 0) >= 168
|
|
576
|
+
)
|
|
511
577
|
|
|
512
|
-
assert cleanup_count == expected_deletions,
|
|
578
|
+
assert cleanup_count == expected_deletions, (
|
|
579
|
+
f"Expected {expected_deletions} deletions, got {cleanup_count}"
|
|
580
|
+
)
|
|
513
581
|
|
|
514
582
|
# Performance check
|
|
515
|
-
cleanup_rate = cleanup_count / cleanup_time if cleanup_time > 0 else float(
|
|
583
|
+
cleanup_rate = cleanup_count / cleanup_time if cleanup_time > 0 else float("inf")
|
|
516
584
|
assert cleanup_rate > 50, f"Cleanup too slow: {cleanup_rate:.2f} tasks/second"
|
|
517
585
|
|
|
518
|
-
print(
|
|
586
|
+
print(
|
|
587
|
+
f"Large dataset cleanup: {cleanup_count} tasks cleaned in {cleanup_time:.2f}s ({cleanup_rate:.2f} tasks/s)"
|
|
588
|
+
)
|
|
519
589
|
|
|
520
590
|
async def test_cleanup_error_recovery(self, cleanup_provider):
|
|
521
591
|
"""Test cleanup behavior when encountering errors"""
|
|
@@ -524,7 +594,9 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
524
594
|
# Create test dataset
|
|
525
595
|
tasks = []
|
|
526
596
|
for i in range(20):
|
|
527
|
-
task = self.create_aged_task(
|
|
597
|
+
task = self.create_aged_task(
|
|
598
|
+
f"error_task_{i:02d}", context_id, TaskState.COMPLETED, 200
|
|
599
|
+
)
|
|
528
600
|
tasks.append(task)
|
|
529
601
|
|
|
530
602
|
# Store tasks
|
|
@@ -534,10 +606,15 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
534
606
|
# Simulate cleanup with potential errors
|
|
535
607
|
# (This test depends on implementation details and might need adjustment)
|
|
536
608
|
cleanup_config = A2ATaskCleanupConfig(
|
|
537
|
-
max_age=168*3600,
|
|
609
|
+
max_age=168 * 3600,
|
|
538
610
|
dry_run=False,
|
|
539
|
-
retain_states=[
|
|
540
|
-
|
|
611
|
+
retain_states=[
|
|
612
|
+
TaskState.SUBMITTED.value,
|
|
613
|
+
TaskState.WORKING.value,
|
|
614
|
+
TaskState.FAILED.value,
|
|
615
|
+
TaskState.CANCELED.value,
|
|
616
|
+
],
|
|
617
|
+
batch_size=5, # Small batches to test error handling
|
|
541
618
|
)
|
|
542
619
|
|
|
543
620
|
try:
|
|
@@ -549,11 +626,15 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
549
626
|
if cleanup_result.data:
|
|
550
627
|
# If successful, verify some tasks were cleaned
|
|
551
628
|
assert cleanup_result.data.total_cleaned > 0
|
|
552
|
-
print(
|
|
629
|
+
print(
|
|
630
|
+
f"Error recovery test: {cleanup_result.data.total_cleaned} tasks cleaned successfully"
|
|
631
|
+
)
|
|
553
632
|
else:
|
|
554
633
|
# If failed, should have meaningful error
|
|
555
634
|
assert "cleanup" in str(cleanup_result.error.message).lower()
|
|
556
|
-
print(
|
|
635
|
+
print(
|
|
636
|
+
f"Error recovery test: Cleanup failed gracefully with error: {cleanup_result.error.message}"
|
|
637
|
+
)
|
|
557
638
|
|
|
558
639
|
except Exception as e:
|
|
559
640
|
# Even exceptions should be handled gracefully
|
|
@@ -562,7 +643,9 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
562
643
|
# Provider should still be functional after error
|
|
563
644
|
health_result = await cleanup_provider.health_check()
|
|
564
645
|
assert health_result.data is not None
|
|
565
|
-
assert health_result.data.get("healthy", False),
|
|
646
|
+
assert health_result.data.get("healthy", False), (
|
|
647
|
+
"Provider should remain healthy after cleanup error"
|
|
648
|
+
)
|
|
566
649
|
|
|
567
650
|
async def test_cleanup_consistency_under_concurrent_operations(self, cleanup_provider):
|
|
568
651
|
"""Test cleanup consistency when running concurrently with other operations"""
|
|
@@ -573,7 +656,9 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
573
656
|
for i in range(50):
|
|
574
657
|
# Mix of old and new completed tasks
|
|
575
658
|
age = 200 if i % 2 == 0 else 1
|
|
576
|
-
task = self.create_aged_task(
|
|
659
|
+
task = self.create_aged_task(
|
|
660
|
+
f"concurrent_task_{i:02d}", context_id, TaskState.COMPLETED, age
|
|
661
|
+
)
|
|
577
662
|
initial_tasks.append(task)
|
|
578
663
|
|
|
579
664
|
# Store initial tasks
|
|
@@ -582,9 +667,14 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
582
667
|
|
|
583
668
|
# Start cleanup operation
|
|
584
669
|
cleanup_config = A2ATaskCleanupConfig(
|
|
585
|
-
max_age=168*3600,
|
|
670
|
+
max_age=168 * 3600,
|
|
586
671
|
dry_run=False,
|
|
587
|
-
retain_states=[
|
|
672
|
+
retain_states=[
|
|
673
|
+
TaskState.SUBMITTED.value,
|
|
674
|
+
TaskState.WORKING.value,
|
|
675
|
+
TaskState.FAILED.value,
|
|
676
|
+
TaskState.CANCELED.value,
|
|
677
|
+
],
|
|
588
678
|
)
|
|
589
679
|
cleanup_operation = perform_task_cleanup(cleanup_provider, cleanup_config)
|
|
590
680
|
|
|
@@ -593,13 +683,15 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
593
683
|
|
|
594
684
|
# Add new tasks
|
|
595
685
|
for i in range(10):
|
|
596
|
-
new_task = self.create_aged_task(
|
|
686
|
+
new_task = self.create_aged_task(
|
|
687
|
+
f"new_concurrent_{i}", context_id, TaskState.COMPLETED, 1
|
|
688
|
+
)
|
|
597
689
|
concurrent_operations.append(cleanup_provider.store_task(new_task))
|
|
598
690
|
|
|
599
691
|
# Query tasks
|
|
600
692
|
query_operations = [
|
|
601
693
|
cleanup_provider.get_tasks_by_context(context_id),
|
|
602
|
-
cleanup_provider.get_task_stats(context_id)
|
|
694
|
+
cleanup_provider.get_task_stats(context_id),
|
|
603
695
|
]
|
|
604
696
|
concurrent_operations.extend(query_operations)
|
|
605
697
|
|
|
@@ -626,7 +718,9 @@ class TestCleanupPerformanceAndReliability(TaskCleanupTestBase):
|
|
|
626
718
|
final_result = await cleanup_provider.get_tasks_by_context(context_id)
|
|
627
719
|
assert final_result.data is not None, "Final state should be queryable"
|
|
628
720
|
|
|
629
|
-
print(
|
|
721
|
+
print(
|
|
722
|
+
f"Concurrent cleanup test: {cleanup_result.data.total_cleaned} cleaned, {successful_concurrent}/{total_concurrent} concurrent ops succeeded"
|
|
723
|
+
)
|
|
630
724
|
|
|
631
725
|
|
|
632
726
|
class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
@@ -646,7 +740,10 @@ class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
|
646
740
|
for config in invalid_configs:
|
|
647
741
|
result = await perform_task_cleanup(cleanup_provider, config)
|
|
648
742
|
assert isinstance(result, Failure), f"Config {config} should have failed validation"
|
|
649
|
-
assert
|
|
743
|
+
assert (
|
|
744
|
+
"invalid" in str(result.error.message).lower()
|
|
745
|
+
or "validation" in str(result.error.message).lower()
|
|
746
|
+
)
|
|
650
747
|
|
|
651
748
|
async def test_edge_case_configurations(self, cleanup_provider):
|
|
652
749
|
"""Test edge case cleanup configurations"""
|
|
@@ -658,8 +755,13 @@ class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
|
658
755
|
|
|
659
756
|
# Test very large age limit (should clean nothing)
|
|
660
757
|
large_age_config = A2ATaskCleanupConfig(
|
|
661
|
-
max_age=10000*3600, # ~1 year
|
|
662
|
-
retain_states=[
|
|
758
|
+
max_age=10000 * 3600, # ~1 year
|
|
759
|
+
retain_states=[
|
|
760
|
+
TaskState.SUBMITTED.value,
|
|
761
|
+
TaskState.WORKING.value,
|
|
762
|
+
TaskState.FAILED.value,
|
|
763
|
+
TaskState.CANCELED.value,
|
|
764
|
+
],
|
|
663
765
|
)
|
|
664
766
|
|
|
665
767
|
large_age_result = await perform_task_cleanup(cleanup_provider, large_age_config)
|
|
@@ -667,8 +769,13 @@ class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
|
667
769
|
|
|
668
770
|
# Test very small age limit (should clean everything eligible)
|
|
669
771
|
small_age_config = A2ATaskCleanupConfig(
|
|
670
|
-
max_age=1*3600, # 1 hour
|
|
671
|
-
retain_states=[
|
|
772
|
+
max_age=1 * 3600, # 1 hour
|
|
773
|
+
retain_states=[
|
|
774
|
+
TaskState.SUBMITTED.value,
|
|
775
|
+
TaskState.WORKING.value,
|
|
776
|
+
TaskState.FAILED.value,
|
|
777
|
+
TaskState.CANCELED.value,
|
|
778
|
+
],
|
|
672
779
|
)
|
|
673
780
|
|
|
674
781
|
small_age_result = await perform_task_cleanup(cleanup_provider, small_age_config)
|
|
@@ -680,7 +787,12 @@ class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
|
680
787
|
# Test very large count limit (should clean nothing)
|
|
681
788
|
large_count_config = A2ATaskCleanupConfig(
|
|
682
789
|
max_completed_tasks=10000,
|
|
683
|
-
retain_states=[
|
|
790
|
+
retain_states=[
|
|
791
|
+
TaskState.SUBMITTED.value,
|
|
792
|
+
TaskState.WORKING.value,
|
|
793
|
+
TaskState.FAILED.value,
|
|
794
|
+
TaskState.CANCELED.value,
|
|
795
|
+
],
|
|
684
796
|
)
|
|
685
797
|
|
|
686
798
|
large_count_result = await perform_task_cleanup(cleanup_provider, large_count_config)
|
|
@@ -689,7 +801,12 @@ class TestCleanupConfigurationValidation(TaskCleanupTestBase):
|
|
|
689
801
|
# Test zero count limit (should clean everything)
|
|
690
802
|
zero_count_config = A2ATaskCleanupConfig(
|
|
691
803
|
max_completed_tasks=0,
|
|
692
|
-
retain_states=[
|
|
804
|
+
retain_states=[
|
|
805
|
+
TaskState.SUBMITTED.value,
|
|
806
|
+
TaskState.WORKING.value,
|
|
807
|
+
TaskState.FAILED.value,
|
|
808
|
+
TaskState.CANCELED.value,
|
|
809
|
+
],
|
|
693
810
|
)
|
|
694
811
|
|
|
695
812
|
zero_count_result = await perform_task_cleanup(cleanup_provider, zero_count_config)
|