jaf-py 2.5.10__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 +360 -279
  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.10.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.10.dist-info/RECORD +0 -96
  89. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
  92. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
@@ -30,16 +30,15 @@ from ..types import (
30
30
 
31
31
 
32
32
  async def create_a2a_redis_task_provider(
33
- config: A2ARedisTaskConfig,
34
- redis_client: Any
33
+ config: A2ARedisTaskConfig, redis_client: Any
35
34
  ) -> A2AResult[A2ATaskProvider]:
36
35
  """
37
36
  Create a Redis-based A2A task provider
38
-
37
+
39
38
  Args:
40
39
  config: Configuration for the Redis provider
41
40
  redis_client: Redis client instance
42
-
41
+
43
42
  Returns:
44
43
  A2AResult containing the task provider or an error
45
44
  """
@@ -62,39 +61,37 @@ async def create_a2a_redis_task_provider(
62
61
  def serialized_task_to_hash(serialized: A2ATaskSerialized) -> Dict[str, str]:
63
62
  """Convert serialized task to Redis hash"""
64
63
  hash_data = {
65
- 'taskId': serialized.task_id,
66
- 'contextId': serialized.context_id,
67
- 'state': serialized.state,
68
- 'taskData': serialized.task_data,
69
- 'createdAt': serialized.created_at,
70
- 'updatedAt': serialized.updated_at
64
+ "taskId": serialized.task_id,
65
+ "contextId": serialized.context_id,
66
+ "state": serialized.state,
67
+ "taskData": serialized.task_data,
68
+ "createdAt": serialized.created_at,
69
+ "updatedAt": serialized.updated_at,
71
70
  }
72
71
  if serialized.status_message:
73
- hash_data['statusMessage'] = serialized.status_message
72
+ hash_data["statusMessage"] = serialized.status_message
74
73
  if serialized.metadata:
75
- hash_data['metadata'] = serialized.metadata
74
+ hash_data["metadata"] = serialized.metadata
76
75
  return hash_data
77
76
 
78
77
  def hash_to_serialized_task(hash_data: Dict[str, str]) -> A2ATaskSerialized:
79
78
  """Convert Redis hash to serialized task"""
80
79
  return A2ATaskSerialized(
81
- task_id=hash_data['taskId'],
82
- context_id=hash_data['contextId'],
83
- state=hash_data['state'],
84
- task_data=hash_data['taskData'],
85
- status_message=hash_data.get('statusMessage'),
86
- created_at=hash_data['createdAt'],
87
- updated_at=hash_data['updatedAt'],
88
- metadata=hash_data.get('metadata')
80
+ task_id=hash_data["taskId"],
81
+ context_id=hash_data["contextId"],
82
+ state=hash_data["state"],
83
+ task_data=hash_data["taskData"],
84
+ status_message=hash_data.get("statusMessage"),
85
+ created_at=hash_data["createdAt"],
86
+ updated_at=hash_data["updatedAt"],
87
+ metadata=hash_data.get("metadata"),
89
88
  )
90
89
 
91
90
  class RedisA2ATaskProvider:
92
91
  """Redis implementation of A2ATaskProvider"""
93
92
 
94
93
  async def store_task(
95
- self,
96
- task: A2ATask,
97
- metadata: Optional[Dict[str, Any]] = None
94
+ self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
98
95
  ) -> A2AResult[None]:
99
96
  """Store a new A2A task in Redis"""
100
97
  try:
@@ -120,8 +117,8 @@ async def create_a2a_redis_task_provider(
120
117
  await pipe.hset(task_key, mapping=task_hash)
121
118
 
122
119
  # Set TTL if specified
123
- if metadata and metadata.get('expires_at'):
124
- expires_at = metadata['expires_at']
120
+ if metadata and metadata.get("expires_at"):
121
+ expires_at = metadata["expires_at"]
125
122
  if isinstance(expires_at, datetime):
126
123
  ttl_seconds = int((expires_at - datetime.now()).total_seconds())
127
124
  if ttl_seconds > 0:
@@ -134,8 +131,8 @@ async def create_a2a_redis_task_provider(
134
131
  await pipe.sadd(state_index_key, task.id)
135
132
 
136
133
  # Update stats
137
- await pipe.hincrby(get_stats_key(), 'totalTasks', 1)
138
- await pipe.hincrby(get_stats_key(), f'state:{task.status.state.value}', 1)
134
+ await pipe.hincrby(get_stats_key(), "totalTasks", 1)
135
+ await pipe.hincrby(get_stats_key(), f"state:{task.status.state.value}", 1)
139
136
 
140
137
  await pipe.execute()
141
138
 
@@ -143,7 +140,7 @@ async def create_a2a_redis_task_provider(
143
140
 
144
141
  except Exception as error:
145
142
  return create_a2a_failure(
146
- create_a2a_task_storage_error('store', 'redis', task.id, error)
143
+ create_a2a_task_storage_error("store", "redis", task.id, error)
147
144
  )
148
145
 
149
146
  async def get_task(self, task_id: str) -> A2AResult[Optional[A2ATask]]:
@@ -156,14 +153,17 @@ async def create_a2a_redis_task_provider(
156
153
  return create_a2a_success(None)
157
154
 
158
155
  hash_data = await redis_client.hgetall(task_key)
159
- if not hash_data or 'taskData' not in hash_data:
156
+ if not hash_data or "taskData" not in hash_data:
160
157
  return create_a2a_success(None)
161
158
 
162
159
  # Convert bytes to strings if needed (depends on Redis client)
163
160
  if isinstance(hash_data, dict):
164
- hash_data = {k.decode() if isinstance(k, bytes) else k:
165
- v.decode() if isinstance(v, bytes) else v
166
- for k, v in hash_data.items()}
161
+ hash_data = {
162
+ k.decode() if isinstance(k, bytes) else k: v.decode()
163
+ if isinstance(v, bytes)
164
+ else v
165
+ for k, v in hash_data.items()
166
+ }
167
167
 
168
168
  serialized = hash_to_serialized_task(hash_data)
169
169
  deserialize_result = deserialize_a2a_task(serialized)
@@ -175,13 +175,11 @@ async def create_a2a_redis_task_provider(
175
175
 
176
176
  except Exception as error:
177
177
  return create_a2a_failure(
178
- create_a2a_task_storage_error('get', 'redis', task_id, error)
178
+ create_a2a_task_storage_error("get", "redis", task_id, error)
179
179
  )
180
180
 
181
181
  async def update_task(
182
- self,
183
- task: A2ATask,
184
- metadata: Optional[Dict[str, Any]] = None
182
+ self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
185
183
  ) -> A2AResult[None]:
186
184
  """Update an existing task in Redis"""
187
185
  try:
@@ -189,18 +187,19 @@ async def create_a2a_redis_task_provider(
189
187
  exists = await redis_client.exists(task_key)
190
188
 
191
189
  if not exists:
192
- return create_a2a_failure(
193
- create_a2a_task_not_found_error(task.id, 'redis')
194
- )
190
+ return create_a2a_failure(create_a2a_task_not_found_error(task.id, "redis"))
195
191
 
196
192
  # Get existing task to check for state changes
197
193
  existing_hash = await redis_client.hgetall(task_key)
198
194
  if isinstance(existing_hash, dict):
199
- existing_hash = {k.decode() if isinstance(k, bytes) else k:
200
- v.decode() if isinstance(v, bytes) else v
201
- for k, v in existing_hash.items()}
195
+ existing_hash = {
196
+ k.decode() if isinstance(k, bytes) else k: v.decode()
197
+ if isinstance(v, bytes)
198
+ else v
199
+ for k, v in existing_hash.items()
200
+ }
202
201
 
203
- old_state = existing_hash.get('state')
202
+ old_state = existing_hash.get("state")
204
203
 
205
204
  # Validate and sanitize task
206
205
  sanitize_result = sanitize_task(task)
@@ -209,9 +208,9 @@ async def create_a2a_redis_task_provider(
209
208
 
210
209
  # Merge metadata
211
210
  existing_metadata = {}
212
- if existing_hash.get('metadata'):
211
+ if existing_hash.get("metadata"):
213
212
  try:
214
- existing_metadata = json.loads(existing_hash['metadata'])
213
+ existing_metadata = json.loads(existing_hash["metadata"])
215
214
  except:
216
215
  pass
217
216
  merged_metadata = {**existing_metadata, **(metadata or {})}
@@ -237,8 +236,10 @@ async def create_a2a_redis_task_provider(
237
236
  await pipe.sadd(new_state_index_key, task.id)
238
237
 
239
238
  # Update stats
240
- await pipe.hincrby(get_stats_key(), f'state:{old_state}', -1)
241
- await pipe.hincrby(get_stats_key(), f'state:{task.status.state.value}', 1)
239
+ await pipe.hincrby(get_stats_key(), f"state:{old_state}", -1)
240
+ await pipe.hincrby(
241
+ get_stats_key(), f"state:{task.status.state.value}", 1
242
+ )
242
243
 
243
244
  await pipe.execute()
244
245
 
@@ -246,7 +247,7 @@ async def create_a2a_redis_task_provider(
246
247
 
247
248
  except Exception as error:
248
249
  return create_a2a_failure(
249
- create_a2a_task_storage_error('update', 'redis', task.id, error)
250
+ create_a2a_task_storage_error("update", "redis", task.id, error)
250
251
  )
251
252
 
252
253
  async def update_task_status(
@@ -254,7 +255,7 @@ async def create_a2a_redis_task_provider(
254
255
  task_id: str,
255
256
  state: TaskState,
256
257
  status_message: Optional[Any] = None,
257
- timestamp: Optional[str] = None
258
+ timestamp: Optional[str] = None,
258
259
  ) -> A2AResult[None]:
259
260
  """Update task status only"""
260
261
  try:
@@ -264,9 +265,7 @@ async def create_a2a_redis_task_provider(
264
265
  return get_result
265
266
 
266
267
  if not get_result.data:
267
- return create_a2a_failure(
268
- create_a2a_task_not_found_error(task_id, 'redis')
269
- )
268
+ return create_a2a_failure(create_a2a_task_not_found_error(task_id, "redis"))
270
269
 
271
270
  task = get_result.data
272
271
 
@@ -277,30 +276,30 @@ async def create_a2a_redis_task_provider(
277
276
 
278
277
  # Update task status
279
278
  from ...types import A2ATaskStatus
279
+
280
280
  updated_status = A2ATaskStatus(
281
281
  state=state,
282
282
  message=status_message or task.status.message,
283
- timestamp=timestamp or datetime.now().isoformat()
283
+ timestamp=timestamp or datetime.now().isoformat(),
284
284
  )
285
285
 
286
- updated_task = task.model_copy(update={
287
- 'status': updated_status,
288
- 'history': updated_history
289
- })
286
+ updated_task = task.model_copy(
287
+ update={"status": updated_status, "history": updated_history}
288
+ )
290
289
 
291
290
  # Use update_task for the actual update
292
291
  return await self.update_task(updated_task)
293
292
 
294
293
  except Exception as error:
295
294
  return create_a2a_failure(
296
- create_a2a_task_storage_error('update-status', 'redis', task_id, error)
295
+ create_a2a_task_storage_error("update-status", "redis", task_id, error)
297
296
  )
298
297
 
299
298
  async def find_tasks(self, query: A2ATaskQuery) -> A2AResult[List[A2ATask]]:
300
299
  """Search tasks by query parameters"""
301
300
  try:
302
301
  task_ids: List[str] = []
303
-
302
+
304
303
  # Determine which sets to use for filtering
305
304
  keys_to_intersect = []
306
305
  if query.context_id:
@@ -318,7 +317,7 @@ async def create_a2a_redis_task_provider(
318
317
  # Get all task keys if no context or state is provided
319
318
  pattern = f"{key_prefix}task:*"
320
319
  keys = await redis_client.keys(pattern)
321
- task_ids = [key.replace(f"{key_prefix}task:", '') for key in keys]
320
+ task_ids = [key.replace(f"{key_prefix}task:", "") for key in keys]
322
321
 
323
322
  # Convert bytes to strings if needed
324
323
  task_ids = [tid.decode() if isinstance(tid, bytes) else tid for tid in task_ids]
@@ -345,27 +344,34 @@ async def create_a2a_redis_task_provider(
345
344
  if hash_data:
346
345
  # Convert bytes to strings if needed
347
346
  if isinstance(hash_data, dict):
348
- hash_data = {k.decode() if isinstance(k, bytes) else k:
349
- v.decode() if isinstance(v, bytes) else v
350
- for k, v in hash_data.items()}
351
-
347
+ hash_data = {
348
+ k.decode() if isinstance(k, bytes) else k: v.decode()
349
+ if isinstance(v, bytes)
350
+ else v
351
+ for k, v in hash_data.items()
352
+ }
353
+
352
354
  # Try to get timestamp from metadata first, then created_at
353
355
  task_timestamp = None
354
- if hash_data.get('metadata'):
356
+ if hash_data.get("metadata"):
355
357
  try:
356
- metadata = json.loads(hash_data['metadata'])
357
- if metadata.get('created_at'):
358
- task_timestamp = datetime.fromisoformat(metadata['created_at'].replace('Z', '+00:00'))
358
+ metadata = json.loads(hash_data["metadata"])
359
+ if metadata.get("created_at"):
360
+ task_timestamp = datetime.fromisoformat(
361
+ metadata["created_at"].replace("Z", "+00:00")
362
+ )
359
363
  except:
360
364
  pass
361
-
365
+
362
366
  # Fall back to createdAt field
363
- if not task_timestamp and hash_data.get('createdAt'):
367
+ if not task_timestamp and hash_data.get("createdAt"):
364
368
  try:
365
- task_timestamp = datetime.fromisoformat(hash_data['createdAt'].replace('Z', '+00:00'))
369
+ task_timestamp = datetime.fromisoformat(
370
+ hash_data["createdAt"].replace("Z", "+00:00")
371
+ )
366
372
  except:
367
373
  pass
368
-
374
+
369
375
  # Apply time filters
370
376
  if task_timestamp:
371
377
  if query.since and task_timestamp < query.since:
@@ -377,26 +383,23 @@ async def create_a2a_redis_task_provider(
377
383
 
378
384
  # Sort by timestamp (newest first)
379
385
  results.sort(
380
- key=lambda t: t.status.timestamp or "1970-01-01T00:00:00Z",
381
- reverse=True
386
+ key=lambda t: t.status.timestamp or "1970-01-01T00:00:00Z", reverse=True
382
387
  )
383
388
 
384
389
  # Apply pagination
385
390
  offset = query.offset or 0
386
391
  limit = query.limit or len(results)
387
- paginated_results = results[offset:offset + limit]
392
+ paginated_results = results[offset : offset + limit]
388
393
 
389
394
  return create_a2a_success(paginated_results)
390
395
 
391
396
  except Exception as error:
392
397
  return create_a2a_failure(
393
- create_a2a_task_storage_error('find', 'redis', None, error)
398
+ create_a2a_task_storage_error("find", "redis", None, error)
394
399
  )
395
400
 
396
401
  async def get_tasks_by_context(
397
- self,
398
- context_id: str,
399
- limit: Optional[int] = None
402
+ self, context_id: str, limit: Optional[int] = None
400
403
  ) -> A2AResult[List[A2ATask]]:
401
404
  """Get tasks by context ID"""
402
405
  return await self.find_tasks(A2ATaskQuery(context_id=context_id, limit=limit))
@@ -413,12 +416,15 @@ async def create_a2a_redis_task_provider(
413
416
 
414
417
  # Convert bytes to strings if needed
415
418
  if isinstance(hash_data, dict):
416
- hash_data = {k.decode() if isinstance(k, bytes) else k:
417
- v.decode() if isinstance(v, bytes) else v
418
- for k, v in hash_data.items()}
419
+ hash_data = {
420
+ k.decode() if isinstance(k, bytes) else k: v.decode()
421
+ if isinstance(v, bytes)
422
+ else v
423
+ for k, v in hash_data.items()
424
+ }
419
425
 
420
- context_id = hash_data.get('contextId')
421
- state = hash_data.get('state')
426
+ context_id = hash_data.get("contextId")
427
+ state = hash_data.get("state")
422
428
 
423
429
  async with redis_client.pipeline() as pipe:
424
430
  # Delete task
@@ -434,9 +440,9 @@ async def create_a2a_redis_task_provider(
434
440
  await pipe.srem(state_index_key, task_id)
435
441
 
436
442
  # Update stats
437
- await pipe.hincrby(get_stats_key(), 'totalTasks', -1)
443
+ await pipe.hincrby(get_stats_key(), "totalTasks", -1)
438
444
  if state:
439
- await pipe.hincrby(get_stats_key(), f'state:{state}', -1)
445
+ await pipe.hincrby(get_stats_key(), f"state:{state}", -1)
440
446
 
441
447
  await pipe.execute()
442
448
 
@@ -444,7 +450,7 @@ async def create_a2a_redis_task_provider(
444
450
 
445
451
  except Exception as error:
446
452
  return create_a2a_failure(
447
- create_a2a_task_storage_error('delete', 'redis', task_id, error)
453
+ create_a2a_task_storage_error("delete", "redis", task_id, error)
448
454
  )
449
455
 
450
456
  async def delete_tasks_by_context(self, context_id: str) -> A2AResult[int]:
@@ -463,9 +469,13 @@ async def create_a2a_redis_task_provider(
463
469
  deleted_count = 0
464
470
  for task_id in task_ids:
465
471
  delete_result = await self.delete_task(task_id)
466
- if hasattr(delete_result, 'data') and isinstance(delete_result.data, bool) and delete_result.data:
472
+ if (
473
+ hasattr(delete_result, "data")
474
+ and isinstance(delete_result.data, bool)
475
+ and delete_result.data
476
+ ):
467
477
  deleted_count += 1
468
- elif hasattr(delete_result, 'error'):
478
+ elif hasattr(delete_result, "error"):
469
479
  # Log error but continue with other deletions
470
480
  continue
471
481
 
@@ -473,7 +483,7 @@ async def create_a2a_redis_task_provider(
473
483
 
474
484
  except Exception as error:
475
485
  return create_a2a_failure(
476
- create_a2a_task_storage_error('delete-by-context', 'redis', None, error)
486
+ create_a2a_task_storage_error("delete-by-context", "redis", None, error)
477
487
  )
478
488
 
479
489
  async def cleanup_expired_tasks(self) -> A2AResult[int]:
@@ -485,12 +495,11 @@ async def create_a2a_redis_task_provider(
485
495
 
486
496
  except Exception as error:
487
497
  return create_a2a_failure(
488
- create_a2a_task_storage_error('cleanup', 'redis', None, error)
498
+ create_a2a_task_storage_error("cleanup", "redis", None, error)
489
499
  )
490
500
 
491
501
  async def get_task_stats(
492
- self,
493
- context_id: Optional[str] = None
502
+ self, context_id: Optional[str] = None
494
503
  ) -> A2AResult[Dict[str, Any]]:
495
504
  """Get task statistics"""
496
505
  try:
@@ -507,10 +516,10 @@ async def create_a2a_redis_task_provider(
507
516
  tasks_by_state[task.status.state.value] += 1
508
517
 
509
518
  stats = {
510
- 'total_tasks': len(tasks),
511
- 'tasks_by_state': tasks_by_state,
512
- 'oldest_task': None,
513
- 'newest_task': None
519
+ "total_tasks": len(tasks),
520
+ "tasks_by_state": tasks_by_state,
521
+ "oldest_task": None,
522
+ "newest_task": None,
514
523
  }
515
524
  # Also add individual state counts for backwards compatibility
516
525
  for state in TaskState:
@@ -523,25 +532,28 @@ async def create_a2a_redis_task_provider(
523
532
  stats = await redis_client.hgetall(stats_key)
524
533
 
525
534
  if isinstance(stats, dict):
526
- stats = {k.decode() if isinstance(k, bytes) else k:
527
- v.decode() if isinstance(v, bytes) else v
528
- for k, v in stats.items()}
535
+ stats = {
536
+ k.decode() if isinstance(k, bytes) else k: v.decode()
537
+ if isinstance(v, bytes)
538
+ else v
539
+ for k, v in stats.items()
540
+ }
541
+
542
+ total_tasks = int(stats.get("totalTasks", 0))
529
543
 
530
- total_tasks = int(stats.get('totalTasks', 0))
531
-
532
544
  # Build tasks_by_state dict
533
545
  tasks_by_state = {}
534
546
  for state in TaskState:
535
- tasks_by_state[state.value] = int(stats.get(f'state:{state.value}', 0))
536
-
547
+ tasks_by_state[state.value] = int(stats.get(f"state:{state.value}", 0))
548
+
537
549
  # Build stats dict with individual state counts
538
550
  result_stats = {
539
- 'total_tasks': total_tasks,
540
- 'tasks_by_state': tasks_by_state,
541
- 'oldest_task': None,
542
- 'newest_task': None
551
+ "total_tasks": total_tasks,
552
+ "tasks_by_state": tasks_by_state,
553
+ "oldest_task": None,
554
+ "newest_task": None,
543
555
  }
544
-
556
+
545
557
  # Also add individual state counts for backwards compatibility
546
558
  for state in TaskState:
547
559
  result_stats[state.value] = tasks_by_state[state.value]
@@ -550,7 +562,7 @@ async def create_a2a_redis_task_provider(
550
562
 
551
563
  except Exception as error:
552
564
  return create_a2a_failure(
553
- create_a2a_task_storage_error('stats', 'redis', None, error)
565
+ create_a2a_task_storage_error("stats", "redis", None, error)
554
566
  )
555
567
 
556
568
  async def health_check(self) -> A2AResult[Dict[str, Any]]:
@@ -562,17 +574,12 @@ async def create_a2a_redis_task_provider(
562
574
  await redis_client.ping()
563
575
 
564
576
  latency_ms = (datetime.now() - start_time).total_seconds() * 1000
565
- return create_a2a_success({
566
- 'healthy': True,
567
- 'provider': 'redis',
568
- 'latency_ms': latency_ms
569
- })
577
+ return create_a2a_success(
578
+ {"healthy": True, "provider": "redis", "latency_ms": latency_ms}
579
+ )
570
580
 
571
581
  except Exception as error:
572
- return create_a2a_success({
573
- 'healthy': False,
574
- 'error': str(error)
575
- })
582
+ return create_a2a_success({"healthy": False, "error": str(error)})
576
583
 
577
584
  async def close(self) -> A2AResult[None]:
578
585
  """Close/cleanup the provider"""
@@ -583,12 +590,12 @@ async def create_a2a_redis_task_provider(
583
590
 
584
591
  except Exception as error:
585
592
  return create_a2a_failure(
586
- create_a2a_task_storage_error('close', 'redis', None, error)
593
+ create_a2a_task_storage_error("close", "redis", None, error)
587
594
  )
588
595
 
589
596
  return create_a2a_success(RedisA2ATaskProvider())
590
597
 
591
598
  except Exception as error:
592
599
  return create_a2a_failure(
593
- create_a2a_task_storage_error('create-redis-provider', 'redis', None, error)
600
+ create_a2a_task_storage_error("create-redis-provider", "redis", None, error)
594
601
  )