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
@@ -36,7 +36,7 @@ class MockModelProvider:
36
36
  return {
37
37
  "message": {
38
38
  "content": f"[{agent.name}] I received: '{last_message}'",
39
- "tool_calls": None
39
+ "tool_calls": None,
40
40
  }
41
41
  }
42
42
 
@@ -55,7 +55,7 @@ async def echo_tool(args: EchoArgs, context) -> Dict[str, Any]:
55
55
  return {
56
56
  "result": f"Echo: {args.message}",
57
57
  "original": args.message,
58
- "length": len(args.message)
58
+ "length": len(args.message),
59
59
  }
60
60
 
61
61
 
@@ -66,11 +66,7 @@ async def count_tool(args: CountArgs, context) -> Dict[str, Any]:
66
66
 
67
67
  return {
68
68
  "result": f"Text analysis: {char_count} characters, {word_count} words",
69
- "analysis": {
70
- "characters": char_count,
71
- "words": word_count,
72
- "text": args.text
73
- }
69
+ "analysis": {"characters": char_count, "words": word_count, "text": args.text},
74
70
  }
75
71
 
76
72
 
@@ -79,17 +75,14 @@ def create_test_agents():
79
75
 
80
76
  # Echo Agent
81
77
  echo_tool_obj = create_a2a_tool(
82
- "echo",
83
- "Echo back a message",
84
- EchoArgs.model_json_schema(),
85
- echo_tool
78
+ "echo", "Echo back a message", EchoArgs.model_json_schema(), echo_tool
86
79
  )
87
80
 
88
81
  echo_agent = create_a2a_agent(
89
82
  "EchoBot",
90
83
  "An agent that echoes messages back",
91
84
  "You are an echo bot. Use the echo tool to repeat messages.",
92
- [echo_tool_obj]
85
+ [echo_tool_obj],
93
86
  )
94
87
 
95
88
  # Counter Agent
@@ -97,14 +90,14 @@ def create_test_agents():
97
90
  "count_chars",
98
91
  "Count characters and words in text",
99
92
  CountArgs.model_json_schema(),
100
- count_tool
93
+ count_tool,
101
94
  )
102
95
 
103
96
  counter_agent = create_a2a_agent(
104
97
  "CounterBot",
105
98
  "An agent that counts characters and words",
106
99
  "You are a text analysis bot. Use the counting tool to analyze text.",
107
- [count_tool_obj]
100
+ [count_tool_obj],
108
101
  )
109
102
 
110
103
  # Simple Chat Agent (no tools)
@@ -112,14 +105,10 @@ def create_test_agents():
112
105
  "ChatBot",
113
106
  "A simple conversational agent",
114
107
  "You are a friendly chat bot. Be helpful and conversational.",
115
- []
108
+ [],
116
109
  )
117
110
 
118
- return {
119
- "EchoBot": echo_agent,
120
- "CounterBot": counter_agent,
121
- "ChatBot": chat_agent
122
- }
111
+ return {"EchoBot": echo_agent, "CounterBot": counter_agent, "ChatBot": chat_agent}
123
112
 
124
113
 
125
114
  async def start_test_server():
@@ -134,7 +123,7 @@ async def start_test_server():
134
123
  name="Test A2A Server",
135
124
  description="Integration test server with sample agents",
136
125
  host="localhost",
137
- port=3001 # Use different port to avoid conflicts
126
+ port=3001, # Use different port to avoid conflicts
138
127
  )
139
128
 
140
129
  server_config["model_provider"] = MockModelProvider()
@@ -163,18 +152,18 @@ async def test_client_interactions(base_url: str):
163
152
  {
164
153
  "agent": "EchoBot",
165
154
  "message": "Hello, echo bot!",
166
- "description": "Testing echo functionality"
155
+ "description": "Testing echo functionality",
167
156
  },
168
157
  {
169
158
  "agent": "CounterBot",
170
159
  "message": "Count the characters in this sentence please",
171
- "description": "Testing character counting"
160
+ "description": "Testing character counting",
172
161
  },
173
162
  {
174
163
  "agent": "ChatBot",
175
164
  "message": "Hi there, how are you doing today?",
176
- "description": "Testing general conversation"
177
- }
165
+ "description": "Testing general conversation",
166
+ },
178
167
  ]
179
168
 
180
169
  results = []
@@ -185,29 +174,29 @@ async def test_client_interactions(base_url: str):
185
174
  print(f" Message: {test_case['message']}")
186
175
 
187
176
  try:
188
- response = await send_message_to_agent(
189
- client,
190
- test_case['agent'],
191
- test_case['message']
192
- )
177
+ response = await send_message_to_agent(client, test_case["agent"], test_case["message"])
193
178
 
194
179
  print(f"✅ Response: {response}")
195
180
 
196
- results.append({
197
- "agent": test_case['agent'],
198
- "message": test_case['message'],
199
- "response": response,
200
- "success": True
201
- })
181
+ results.append(
182
+ {
183
+ "agent": test_case["agent"],
184
+ "message": test_case["message"],
185
+ "response": response,
186
+ "success": True,
187
+ }
188
+ )
202
189
 
203
190
  except Exception as error:
204
191
  print(f"❌ Error: {error}")
205
- results.append({
206
- "agent": test_case['agent'],
207
- "message": test_case['message'],
208
- "error": str(error),
209
- "success": False
210
- })
192
+ results.append(
193
+ {
194
+ "agent": test_case["agent"],
195
+ "message": test_case["message"],
196
+ "error": str(error),
197
+ "success": False,
198
+ }
199
+ )
211
200
 
212
201
  return results
213
202
 
@@ -224,7 +213,7 @@ async def test_agent_discovery(base_url: str):
224
213
  print(f"✅ Discovered server: {agent_card['name']}")
225
214
  print(f"📄 Description: {agent_card['description']}")
226
215
 
227
- skills = agent_card.get('skills', [])
216
+ skills = agent_card.get("skills", [])
228
217
  print(f"🛠️ Skills found: {len(skills)}")
229
218
 
230
219
  for skill in skills:
@@ -252,7 +241,7 @@ async def test_health_and_capabilities(base_url: str):
252
241
 
253
242
  # Capabilities
254
243
  capabilities = await get_a2a_capabilities(client)
255
- methods = capabilities.get('supportedMethods', [])
244
+ methods = capabilities.get("supportedMethods", [])
256
245
  print(f"⚡ Supported methods: {', '.join(methods)}")
257
246
 
258
247
  return True
@@ -294,7 +283,7 @@ async def main():
294
283
  print("\n📊 Integration Test Summary")
295
284
  print("=" * 30)
296
285
 
297
- successful = sum(1 for r in results if r['success'])
286
+ successful = sum(1 for r in results if r["success"])
298
287
  total = len(results)
299
288
 
300
289
  print(f"✅ Successful interactions: {successful}/{total}")
@@ -305,8 +294,10 @@ async def main():
305
294
  print("⚠️ Some tests failed. Check the errors above.")
306
295
 
307
296
  for result in results:
308
- status = "✅" if result['success'] else "❌"
309
- print(f" {status} {result['agent']}: {result.get('response', result.get('error', 'Unknown'))[:60]}...")
297
+ status = "✅" if result["success"] else "❌"
298
+ print(
299
+ f" {status} {result['agent']}: {result.get('response', result.get('error', 'Unknown'))[:60]}..."
300
+ )
310
301
 
311
302
  print("\n🔧 Integration Features Tested:")
312
303
  print(" • Agent discovery via Agent Cards")
@@ -9,7 +9,7 @@ Usage:
9
9
 
10
10
  The server will start on http://localhost:3000 with the following endpoints:
11
11
  - /.well-known/agent-card - Agent discovery
12
- - /a2a - Main A2A JSON-RPC endpoint
12
+ - /a2a - Main A2A JSON-RPC endpoint
13
13
  - /a2a/agents/{name} - Agent-specific endpoints
14
14
  - /a2a/health - Health check
15
15
  - /a2a/capabilities - Server capabilities
@@ -39,21 +39,21 @@ class MockModelProvider:
39
39
  return {
40
40
  "message": {
41
41
  "content": f"I can help with math! I see you said: '{last_message}'",
42
- "tool_calls": None
42
+ "tool_calls": None,
43
43
  }
44
44
  }
45
45
  elif "weather" in agent_name:
46
46
  return {
47
47
  "message": {
48
48
  "content": f"I can check weather! You asked: '{last_message}'",
49
- "tool_calls": None
49
+ "tool_calls": None,
50
50
  }
51
51
  }
52
52
  else:
53
53
  return {
54
54
  "message": {
55
55
  "content": f"Hello! I'm {agent.name}. You said: '{last_message}'",
56
- "tool_calls": None
56
+ "tool_calls": None,
57
57
  }
58
58
  }
59
59
 
@@ -61,17 +61,20 @@ class MockModelProvider:
61
61
  # Tool argument models
62
62
  class CalculateArgs(BaseModel):
63
63
  """Arguments for calculator tool"""
64
+
64
65
  expression: str = Field(description="Mathematical expression to evaluate")
65
66
 
66
67
 
67
68
  class WeatherArgs(BaseModel):
68
69
  """Arguments for weather tool"""
70
+
69
71
  location: str = Field(description="City or location name")
70
72
  units: str = Field(default="celsius", description="Temperature units (celsius/fahrenheit)")
71
73
 
72
74
 
73
75
  class TranslateArgs(BaseModel):
74
76
  """Arguments for translation tool"""
77
+
75
78
  text: str = Field(description="Text to translate")
76
79
  target_language: str = Field(description="Target language code (e.g., 'es', 'fr', 'de')")
77
80
 
@@ -95,10 +98,10 @@ async def calculator_tool(args: CalculateArgs, context) -> Dict[str, Any]:
95
98
  }
96
99
 
97
100
  allowed_functions = {
98
- 'abs': abs,
99
- 'round': round,
100
- 'min': min,
101
- 'max': max,
101
+ "abs": abs,
102
+ "round": round,
103
+ "min": min,
104
+ "max": max,
102
105
  }
103
106
 
104
107
  def safe_eval_node(node):
@@ -107,8 +110,9 @@ async def calculator_tool(args: CalculateArgs, context) -> Dict[str, Any]:
107
110
  return node.value
108
111
  elif isinstance(node, ast.Name):
109
112
  # Only allow specific constants
110
- if node.id in ['pi', 'e']:
113
+ if node.id in ["pi", "e"]:
111
114
  import math
115
+
112
116
  return getattr(math, node.id)
113
117
  else:
114
118
  raise ValueError(f"Name '{node.id}' is not allowed")
@@ -139,9 +143,24 @@ async def calculator_tool(args: CalculateArgs, context) -> Dict[str, Any]:
139
143
 
140
144
  # Additional security checks
141
145
  dangerous_patterns = [
142
- 'import', 'exec', 'eval', '__', 'open', 'file', 'input',
143
- 'raw_input', 'compile', 'globals', 'locals', 'vars', 'dir',
144
- 'getattr', 'setattr', 'hasattr', 'delattr', 'callable'
146
+ "import",
147
+ "exec",
148
+ "eval",
149
+ "__",
150
+ "open",
151
+ "file",
152
+ "input",
153
+ "raw_input",
154
+ "compile",
155
+ "globals",
156
+ "locals",
157
+ "vars",
158
+ "dir",
159
+ "getattr",
160
+ "setattr",
161
+ "hasattr",
162
+ "delattr",
163
+ "callable",
145
164
  ]
146
165
 
147
166
  expression_lower = expression.lower()
@@ -149,51 +168,33 @@ async def calculator_tool(args: CalculateArgs, context) -> Dict[str, Any]:
149
168
  if pattern in expression_lower:
150
169
  return {
151
170
  "error": f"Security violation: '{pattern}' is not allowed in expressions",
152
- "result": None
171
+ "result": None,
153
172
  }
154
173
 
155
174
  # Parse expression into AST
156
175
  try:
157
- parsed = ast.parse(expression, mode='eval')
176
+ parsed = ast.parse(expression, mode="eval")
158
177
  except SyntaxError as e:
159
- return {
160
- "error": f"Invalid mathematical expression: {e!s}",
161
- "result": None
162
- }
178
+ return {"error": f"Invalid mathematical expression: {e!s}", "result": None}
163
179
 
164
180
  # Evaluate safely
165
181
  result = safe_eval_node(parsed.body)
166
182
 
167
183
  # Handle division by zero and other math errors
168
184
  if not isinstance(result, (int, float, complex)):
169
- return {
170
- "error": "Expression must evaluate to a number",
171
- "result": None
172
- }
185
+ return {"error": "Expression must evaluate to a number", "result": None}
173
186
 
174
187
  return {
175
188
  "result": f"The result of {expression} is {result}",
176
- "calculation": {
177
- "expression": expression,
178
- "result": result
179
- }
189
+ "calculation": {"expression": expression, "result": result},
180
190
  }
181
191
 
182
192
  except ZeroDivisionError:
183
- return {
184
- "error": "Division by zero is not allowed",
185
- "result": None
186
- }
193
+ return {"error": "Division by zero is not allowed", "result": None}
187
194
  except ValueError as e:
188
- return {
189
- "error": f"Invalid expression: {e!s}",
190
- "result": None
191
- }
195
+ return {"error": f"Invalid expression: {e!s}", "result": None}
192
196
  except Exception as e:
193
- return {
194
- "error": f"Calculation error: {e!s}",
195
- "result": None
196
- }
197
+ return {"error": f"Calculation error: {e!s}", "result": None}
197
198
 
198
199
 
199
200
  async def weather_tool(args: WeatherArgs, context) -> Dict[str, Any]:
@@ -205,14 +206,14 @@ async def weather_tool(args: WeatherArgs, context) -> Dict[str, Any]:
205
206
  "units": args.units,
206
207
  "condition": "Partly cloudy",
207
208
  "humidity": 65,
208
- "wind_speed": 15
209
+ "wind_speed": 15,
209
210
  }
210
211
 
211
212
  temp_unit = "°C" if args.units == "celsius" else "°F"
212
213
 
213
214
  return {
214
215
  "result": f"Weather in {args.location}: {weather_data['temperature']}{temp_unit}, {weather_data['condition']}",
215
- "weather_data": weather_data
216
+ "weather_data": weather_data,
216
217
  }
217
218
 
218
219
 
@@ -224,18 +225,20 @@ async def translate_tool(args: TranslateArgs, context) -> Dict[str, Any]:
224
225
  "fr": f"[French] {args.text}",
225
226
  "de": f"[German] {args.text}",
226
227
  "it": f"[Italian] {args.text}",
227
- "pt": f"[Portuguese] {args.text}"
228
+ "pt": f"[Portuguese] {args.text}",
228
229
  }
229
230
 
230
- translated = translations.get(args.target_language, f"[{args.target_language.upper()}] {args.text}")
231
+ translated = translations.get(
232
+ args.target_language, f"[{args.target_language.upper()}] {args.text}"
233
+ )
231
234
 
232
235
  return {
233
236
  "result": f"Translation to {args.target_language}: {translated}",
234
237
  "translation": {
235
238
  "original": args.text,
236
239
  "translated": translated,
237
- "target_language": args.target_language
238
- }
240
+ "target_language": args.target_language,
241
+ },
239
242
  }
240
243
 
241
244
 
@@ -247,14 +250,14 @@ def create_example_agents():
247
250
  "calculator",
248
251
  "Perform mathematical calculations",
249
252
  CalculateArgs.model_json_schema(),
250
- calculator_tool
253
+ calculator_tool,
251
254
  )
252
255
 
253
256
  math_agent = create_a2a_agent(
254
257
  "MathTutor",
255
258
  "A mathematical assistant that can solve equations and calculations",
256
259
  "You are a helpful math tutor. Use the calculator tool for computations.",
257
- [calc_tool]
260
+ [calc_tool],
258
261
  )
259
262
 
260
263
  # Weather Agent with weather tool
@@ -262,14 +265,14 @@ def create_example_agents():
262
265
  "get_weather",
263
266
  "Get current weather information for a location",
264
267
  WeatherArgs.model_json_schema(),
265
- weather_tool
268
+ weather_tool,
266
269
  )
267
270
 
268
271
  weather_agent = create_a2a_agent(
269
272
  "WeatherBot",
270
273
  "A weather assistant that provides current weather information",
271
274
  "You are a weather assistant. Use the weather tool to get current conditions.",
272
- [weather_tool_obj]
275
+ [weather_tool_obj],
273
276
  )
274
277
 
275
278
  # Translation Agent with translation tool
@@ -277,14 +280,14 @@ def create_example_agents():
277
280
  "translate_text",
278
281
  "Translate text between languages",
279
282
  TranslateArgs.model_json_schema(),
280
- translate_tool
283
+ translate_tool,
281
284
  )
282
285
 
283
286
  translation_agent = create_a2a_agent(
284
287
  "Translator",
285
288
  "A multilingual assistant that can translate text between languages",
286
289
  "You are a translation assistant. Use the translation tool for language conversion.",
287
- [translate_tool_obj]
290
+ [translate_tool_obj],
288
291
  )
289
292
 
290
293
  # General Assistant (no specific tools)
@@ -292,14 +295,14 @@ def create_example_agents():
292
295
  "Assistant",
293
296
  "A general-purpose helpful assistant",
294
297
  "You are a helpful, friendly assistant ready to help with various tasks.",
295
- []
298
+ [],
296
299
  )
297
300
 
298
301
  return {
299
302
  "MathTutor": math_agent,
300
303
  "WeatherBot": weather_agent,
301
304
  "Translator": translation_agent,
302
- "Assistant": general_agent
305
+ "Assistant": general_agent,
303
306
  }
304
307
 
305
308
 
@@ -321,7 +324,7 @@ async def main():
321
324
  name="JAF A2A Example Server",
322
325
  description="Multi-agent server showcasing A2A protocol capabilities",
323
326
  host="localhost",
324
- port=3000
327
+ port=3000,
325
328
  )
326
329
 
327
330
  # Add mock model provider
@@ -78,7 +78,6 @@ __all__ = [
78
78
  "A2ATaskSerialized",
79
79
  "A2ATaskCleanupConfig",
80
80
  "CleanupResult",
81
-
82
81
  # Factory functions
83
82
  "create_a2a_task_error",
84
83
  "create_a2a_task_not_found_error",
@@ -89,7 +88,6 @@ __all__ = [
89
88
  "create_a2a_task_provider_from_env",
90
89
  "create_simple_a2a_task_provider",
91
90
  "create_composite_a2a_task_provider",
92
-
93
91
  # Serialization functions
94
92
  "serialize_a2a_task",
95
93
  "deserialize_a2a_task",
@@ -98,14 +96,12 @@ __all__ = [
98
96
  "validate_task_integrity",
99
97
  "clone_task",
100
98
  "sanitize_task",
101
-
102
99
  # Cleanup functions
103
100
  "default_cleanup_config",
104
101
  "perform_task_cleanup",
105
102
  "create_task_cleanup_scheduler",
106
103
  "validate_cleanup_config",
107
104
  "create_cleanup_config_from_env",
108
-
109
105
  # Validation functions
110
106
  "validate_a2a_task_provider_config",
111
107
  "is_a2a_task_error",
jaf/a2a/memory/cleanup.py CHANGED
@@ -106,9 +106,7 @@ async def perform_task_cleanup(
106
106
  )
107
107
 
108
108
  if not config.enabled:
109
- return create_a2a_success(
110
- CleanupResult(0, 0, 0, 0, [], 0)
111
- )
109
+ return create_a2a_success(CleanupResult(0, 0, 0, 0, [], 0))
112
110
 
113
111
  errors: List[str] = []
114
112
  tasks_to_delete: Set[str] = set()
@@ -116,7 +114,7 @@ async def perform_task_cleanup(
116
114
  # 1. Age-based cleanup
117
115
  if config.max_age is not None:
118
116
  cutoff_date = datetime.now(timezone.utc) - timedelta(seconds=config.max_age)
119
-
117
+
120
118
  # Find all tasks older than the cutoff date
121
119
  # We query all states and filter out the ones to retain
122
120
  try:
@@ -124,7 +122,7 @@ async def perform_task_cleanup(
124
122
  A2ATaskQuery(
125
123
  context_id=context_id,
126
124
  until=cutoff_date,
127
- limit=10000 # A high limit to get a large batch
125
+ limit=10000, # A high limit to get a large batch
128
126
  )
129
127
  )
130
128
 
@@ -145,18 +143,27 @@ async def perform_task_cleanup(
145
143
  if config.max_completed_tasks is not None:
146
144
  try:
147
145
  completed_tasks_result = await task_provider.find_tasks(
148
- A2ATaskQuery(state=TaskState.COMPLETED, context_id=context_id, limit=config.max_completed_tasks + 200)
146
+ A2ATaskQuery(
147
+ state=TaskState.COMPLETED,
148
+ context_id=context_id,
149
+ limit=config.max_completed_tasks + 200,
150
+ )
149
151
  )
150
152
  if completed_tasks_result.data:
151
153
  tasks = [t for t in completed_tasks_result.data if t.id not in tasks_to_delete]
152
154
  if len(tasks) > config.max_completed_tasks:
153
- tasks.sort(key=lambda t: t.status.timestamp or datetime.min.replace(tzinfo=timezone.utc))
155
+ tasks.sort(
156
+ key=lambda t: t.status.timestamp
157
+ or datetime.min.replace(tzinfo=timezone.utc)
158
+ )
154
159
  num_to_delete = len(tasks) - config.max_completed_tasks
155
160
  for i in range(num_to_delete):
156
161
  tasks_to_delete.add(tasks[i].id)
157
162
  excess_completed_cleaned += 1
158
163
  elif completed_tasks_result.error:
159
- errors.append(f"Failed to query completed tasks: {completed_tasks_result.error.message}")
164
+ errors.append(
165
+ f"Failed to query completed tasks: {completed_tasks_result.error.message}"
166
+ )
160
167
  except Exception as e:
161
168
  errors.append(f"Error during completed task count cleanup: {e!s}")
162
169
 
@@ -165,12 +172,19 @@ async def perform_task_cleanup(
165
172
  if config.max_failed_tasks is not None:
166
173
  try:
167
174
  failed_tasks_result = await task_provider.find_tasks(
168
- A2ATaskQuery(state=TaskState.FAILED, context_id=context_id, limit=config.max_failed_tasks + 200)
175
+ A2ATaskQuery(
176
+ state=TaskState.FAILED,
177
+ context_id=context_id,
178
+ limit=config.max_failed_tasks + 200,
179
+ )
169
180
  )
170
181
  if failed_tasks_result.data:
171
182
  tasks = [t for t in failed_tasks_result.data if t.id not in tasks_to_delete]
172
183
  if len(tasks) > config.max_failed_tasks:
173
- tasks.sort(key=lambda t: t.status.timestamp or datetime.min.replace(tzinfo=timezone.utc))
184
+ tasks.sort(
185
+ key=lambda t: t.status.timestamp
186
+ or datetime.min.replace(tzinfo=timezone.utc)
187
+ )
174
188
  num_to_delete = len(tasks) - config.max_failed_tasks
175
189
  for i in range(num_to_delete):
176
190
  tasks_to_delete.add(tasks[i].id)
@@ -180,7 +194,6 @@ async def perform_task_cleanup(
180
194
  except Exception as e:
181
195
  errors.append(f"Error during failed task count cleanup: {e!s}")
182
196
 
183
-
184
197
  if config.dry_run:
185
198
  return create_a2a_success(
186
199
  CleanupResult(
@@ -210,7 +223,7 @@ async def perform_task_cleanup(
210
223
  # This is complex as we don't know which category succeeded.
211
224
  # The initial counts are based on intent.
212
225
  # For now, we return the intended breakdown and total actual deletions.
213
-
226
+
214
227
  final_result = CleanupResult(
215
228
  expired_cleaned=expired_cleaned,
216
229
  excess_completed_cleaned=excess_completed_cleaned,
@@ -294,9 +307,7 @@ class TaskCleanupScheduler:
294
307
  f"A2A task cleanup completed: {cleanup_result.total_cleaned} tasks cleaned"
295
308
  )
296
309
  if cleanup_result.errors:
297
- print(
298
- f"A2A task cleanup errors: {', '.join(cleanup_result.errors)}"
299
- )
310
+ print(f"A2A task cleanup errors: {', '.join(cleanup_result.errors)}")
300
311
  elif result.error:
301
312
  print(f"A2A task cleanup failed: {result.error.message}")
302
313
 
@@ -318,18 +329,14 @@ def create_cleanup_config_from_env() -> A2ATaskCleanupConfig:
318
329
  """
319
330
  Helper function to create cleanup config from environment variables
320
331
  """
321
- retain_states_str = os.getenv(
322
- "JAF_A2A_CLEANUP_RETAIN_STATES", "working,submitted"
323
- )
332
+ retain_states_str = os.getenv("JAF_A2A_CLEANUP_RETAIN_STATES", "working,submitted")
324
333
  retain_states = [s.strip() for s in retain_states_str.split(",") if s.strip()]
325
334
 
326
335
  return A2ATaskCleanupConfig(
327
336
  enabled=os.getenv("JAF_A2A_CLEANUP_ENABLED", "true").lower() == "true",
328
337
  interval=int(os.getenv("JAF_A2A_CLEANUP_INTERVAL", "3600")),
329
338
  max_age=int(os.getenv("JAF_A2A_CLEANUP_MAX_AGE", str(7 * 24 * 60 * 60))),
330
- max_completed_tasks=int(
331
- os.getenv("JAF_A2A_CLEANUP_MAX_COMPLETED", "1000")
332
- ),
339
+ max_completed_tasks=int(os.getenv("JAF_A2A_CLEANUP_MAX_COMPLETED", "1000")),
333
340
  max_failed_tasks=int(os.getenv("JAF_A2A_CLEANUP_MAX_FAILED", "500")),
334
341
  retain_states=retain_states,
335
342
  batch_size=int(os.getenv("JAF_A2A_CLEANUP_BATCH_SIZE", "100")),