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.
Files changed (92) hide show
  1. jaf/__init__.py +154 -57
  2. jaf/a2a/__init__.py +42 -21
  3. jaf/a2a/agent.py +79 -126
  4. jaf/a2a/agent_card.py +87 -78
  5. jaf/a2a/client.py +30 -66
  6. jaf/a2a/examples/client_example.py +12 -12
  7. jaf/a2a/examples/integration_example.py +38 -47
  8. jaf/a2a/examples/server_example.py +56 -53
  9. jaf/a2a/memory/__init__.py +0 -4
  10. jaf/a2a/memory/cleanup.py +28 -21
  11. jaf/a2a/memory/factory.py +155 -133
  12. jaf/a2a/memory/providers/composite.py +21 -26
  13. jaf/a2a/memory/providers/in_memory.py +89 -83
  14. jaf/a2a/memory/providers/postgres.py +117 -115
  15. jaf/a2a/memory/providers/redis.py +128 -121
  16. jaf/a2a/memory/serialization.py +77 -87
  17. jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
  18. jaf/a2a/memory/tests/test_cleanup.py +211 -94
  19. jaf/a2a/memory/tests/test_serialization.py +73 -68
  20. jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
  21. jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
  22. jaf/a2a/memory/types.py +91 -53
  23. jaf/a2a/protocol.py +95 -125
  24. jaf/a2a/server.py +90 -118
  25. jaf/a2a/standalone_client.py +30 -43
  26. jaf/a2a/tests/__init__.py +16 -33
  27. jaf/a2a/tests/run_tests.py +17 -53
  28. jaf/a2a/tests/test_agent.py +40 -140
  29. jaf/a2a/tests/test_client.py +54 -117
  30. jaf/a2a/tests/test_integration.py +28 -82
  31. jaf/a2a/tests/test_protocol.py +54 -139
  32. jaf/a2a/tests/test_types.py +50 -136
  33. jaf/a2a/types.py +58 -34
  34. jaf/cli.py +21 -41
  35. jaf/core/__init__.py +7 -1
  36. jaf/core/agent_tool.py +93 -72
  37. jaf/core/analytics.py +257 -207
  38. jaf/core/checkpoint.py +223 -0
  39. jaf/core/composition.py +249 -235
  40. jaf/core/engine.py +817 -519
  41. jaf/core/errors.py +55 -42
  42. jaf/core/guardrails.py +276 -202
  43. jaf/core/handoff.py +47 -31
  44. jaf/core/parallel_agents.py +69 -75
  45. jaf/core/performance.py +75 -73
  46. jaf/core/proxy.py +43 -44
  47. jaf/core/proxy_helpers.py +24 -27
  48. jaf/core/regeneration.py +220 -129
  49. jaf/core/state.py +68 -66
  50. jaf/core/streaming.py +115 -108
  51. jaf/core/tool_results.py +111 -101
  52. jaf/core/tools.py +114 -116
  53. jaf/core/tracing.py +269 -210
  54. jaf/core/types.py +371 -151
  55. jaf/core/workflows.py +209 -168
  56. jaf/exceptions.py +46 -38
  57. jaf/memory/__init__.py +1 -6
  58. jaf/memory/approval_storage.py +54 -77
  59. jaf/memory/factory.py +4 -4
  60. jaf/memory/providers/in_memory.py +216 -180
  61. jaf/memory/providers/postgres.py +216 -146
  62. jaf/memory/providers/redis.py +173 -116
  63. jaf/memory/types.py +70 -51
  64. jaf/memory/utils.py +36 -34
  65. jaf/plugins/__init__.py +12 -12
  66. jaf/plugins/base.py +105 -96
  67. jaf/policies/__init__.py +0 -1
  68. jaf/policies/handoff.py +37 -46
  69. jaf/policies/validation.py +76 -52
  70. jaf/providers/__init__.py +6 -3
  71. jaf/providers/mcp.py +97 -51
  72. jaf/providers/model.py +361 -280
  73. jaf/server/__init__.py +1 -1
  74. jaf/server/main.py +7 -11
  75. jaf/server/server.py +514 -359
  76. jaf/server/types.py +208 -52
  77. jaf/utils/__init__.py +17 -18
  78. jaf/utils/attachments.py +111 -116
  79. jaf/utils/document_processor.py +175 -174
  80. jaf/visualization/__init__.py +1 -1
  81. jaf/visualization/example.py +111 -110
  82. jaf/visualization/functional_core.py +46 -71
  83. jaf/visualization/graphviz.py +154 -189
  84. jaf/visualization/imperative_shell.py +7 -16
  85. jaf/visualization/types.py +8 -4
  86. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
  87. jaf_py-2.5.11.dist-info/RECORD +97 -0
  88. jaf_py-2.5.9.dist-info/RECORD +0 -96
  89. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
  92. {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
- self.create_aged_task(f"fresh_submitted_{i}", context_id, TaskState.SUBMITTED, i)
66
- for i in range(2)
67
- ])
68
- tasks.extend([
69
- self.create_aged_task(f"fresh_working_{i}", context_id, TaskState.WORKING, i)
70
- for i in range(2)
71
- ])
72
- tasks.extend([
73
- self.create_aged_task(f"fresh_completed_{i}", context_id, TaskState.COMPLETED, i)
74
- for i in range(3)
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
- self.create_aged_task(f"medium_completed_{hours}h", context_id, TaskState.COMPLETED, hours),
82
- self.create_aged_task(f"medium_failed_{hours}h", context_id, TaskState.FAILED, hours),
83
- self.create_aged_task(f"medium_working_{hours}h", context_id, TaskState.WORKING, hours)
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
- self.create_aged_task(f"old_completed_{hours}h", context_id, TaskState.COMPLETED, hours),
91
- self.create_aged_task(f"old_failed_{hours}h", context_id, TaskState.FAILED, hours),
92
- self.create_aged_task(f"old_canceled_{hours}h", context_id, TaskState.CANCELED, hours),
93
- self.create_aged_task(f"old_working_{hours}h", context_id, TaskState.WORKING, hours) # Should NOT be cleaned
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value] # Retain active 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, f"Actual cleanup ({actual_delete_count}) should match dry run prediction ({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(f"completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours)
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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(f"old_completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours)
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(f"new_completed_{i:02d}", context_id, TaskState.COMPLETED, age_hours)
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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, f"Expected {expected_deletions} deletions, got {cleanup_count}"
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(f"task_{ctx}_{i}", ctx, TaskState.COMPLETED, 200) # Old
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(f"task_{ctx}_{i}", ctx, TaskState.WORKING, 200) # Old but should be preserved
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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(cleanup_provider, cleanup_config, context_id="ctx_a")
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), "Only working tasks should remain in ctx_a"
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(f"old_task_{ctx}_{i}", ctx, TaskState.COMPLETED, 200)
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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), f"Only new tasks should remain in {ctx}"
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(1 for t in tasks if t.status.state in [TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELED] and t.metadata.get("test_age_hours", 0) >= 168)
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, f"Expected {expected_deletions} deletions, got {cleanup_count}"
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('inf')
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(f"Large dataset cleanup: {cleanup_count} tasks cleaned in {cleanup_time:.2f}s ({cleanup_rate:.2f} tasks/s)")
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(f"error_task_{i:02d}", context_id, TaskState.COMPLETED, 200)
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value],
540
- batch_size=5 # Small batches to test error handling
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(f"Error recovery test: {cleanup_result.data.total_cleaned} tasks cleaned successfully")
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(f"Error recovery test: Cleanup failed gracefully with error: {cleanup_result.error.message}")
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), "Provider should remain healthy after cleanup error"
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(f"concurrent_task_{i:02d}", context_id, TaskState.COMPLETED, age)
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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(f"new_concurrent_{i}", context_id, TaskState.COMPLETED, 1)
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(f"Concurrent cleanup test: {cleanup_result.data.total_cleaned} cleaned, {successful_concurrent}/{total_concurrent} concurrent ops succeeded")
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 "invalid" in str(result.error.message).lower() or "validation" in str(result.error.message).lower()
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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=[TaskState.SUBMITTED.value, TaskState.WORKING.value, TaskState.FAILED.value, TaskState.CANCELED.value]
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)