jaf-py 2.5.11__tar.gz → 2.5.13__tar.gz

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 (134) hide show
  1. {jaf_py-2.5.11 → jaf_py-2.5.13}/PKG-INFO +2 -2
  2. {jaf_py-2.5.11 → jaf_py-2.5.13}/README.md +1 -1
  3. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/__init__.py +1 -1
  4. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/agent_tool.py +2 -0
  5. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tracing.py +42 -1
  6. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/types.py +32 -0
  7. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/model.py +115 -4
  8. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/PKG-INFO +2 -2
  9. {jaf_py-2.5.11 → jaf_py-2.5.13}/pyproject.toml +1 -1
  10. {jaf_py-2.5.11 → jaf_py-2.5.13}/LICENSE +0 -0
  11. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/__init__.py +0 -0
  12. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/agent.py +0 -0
  13. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/agent_card.py +0 -0
  14. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/client.py +0 -0
  15. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/__init__.py +0 -0
  16. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/client_example.py +0 -0
  17. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/integration_example.py +0 -0
  18. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
  19. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/server_demo/__init__.py +0 -0
  20. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/server_example.py +0 -0
  21. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/__init__.py +0 -0
  22. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/cleanup.py +0 -0
  23. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/factory.py +0 -0
  24. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/__init__.py +0 -0
  25. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/composite.py +0 -0
  26. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/in_memory.py +0 -0
  27. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/postgres.py +0 -0
  28. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/redis.py +0 -0
  29. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/serialization.py +0 -0
  30. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/__init__.py +0 -0
  31. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
  32. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
  33. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_serialization.py +0 -0
  34. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
  35. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
  36. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/types.py +0 -0
  37. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/protocol.py +0 -0
  38. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/server.py +0 -0
  39. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/standalone_client.py +0 -0
  40. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/__init__.py +0 -0
  41. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/run_tests.py +0 -0
  42. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_agent.py +0 -0
  43. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_client.py +0 -0
  44. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_integration.py +0 -0
  45. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_protocol.py +0 -0
  46. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_types.py +0 -0
  47. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/types.py +0 -0
  48. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/cli.py +0 -0
  49. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/__init__.py +0 -0
  50. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/analytics.py +0 -0
  51. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/checkpoint.py +0 -0
  52. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/composition.py +0 -0
  53. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/engine.py +0 -0
  54. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/errors.py +0 -0
  55. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/guardrails.py +0 -0
  56. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/handoff.py +0 -0
  57. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/parallel_agents.py +0 -0
  58. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/performance.py +0 -0
  59. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/proxy.py +0 -0
  60. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/proxy_helpers.py +0 -0
  61. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/regeneration.py +0 -0
  62. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/state.py +0 -0
  63. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/streaming.py +0 -0
  64. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tool_results.py +0 -0
  65. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tools.py +0 -0
  66. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/workflows.py +0 -0
  67. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/exceptions.py +0 -0
  68. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/__init__.py +0 -0
  69. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/approval_storage.py +0 -0
  70. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/factory.py +0 -0
  71. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/__init__.py +0 -0
  72. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/in_memory.py +0 -0
  73. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/postgres.py +0 -0
  74. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/redis.py +0 -0
  75. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/types.py +0 -0
  76. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/utils.py +0 -0
  77. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/plugins/__init__.py +0 -0
  78. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/plugins/base.py +0 -0
  79. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/__init__.py +0 -0
  80. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/handoff.py +0 -0
  81. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/validation.py +0 -0
  82. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/__init__.py +0 -0
  83. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/mcp.py +0 -0
  84. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/__init__.py +0 -0
  85. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/main.py +0 -0
  86. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/server.py +0 -0
  87. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/types.py +0 -0
  88. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/__init__.py +0 -0
  89. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/attachments.py +0 -0
  90. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/document_processor.py +0 -0
  91. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/__init__.py +0 -0
  92. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/example.py +0 -0
  93. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/functional_core.py +0 -0
  94. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/graphviz.py +0 -0
  95. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/imperative_shell.py +0 -0
  96. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/types.py +0 -0
  97. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/SOURCES.txt +0 -0
  98. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/dependency_links.txt +0 -0
  99. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/entry_points.txt +0 -0
  100. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/requires.txt +0 -0
  101. {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/top_level.txt +0 -0
  102. {jaf_py-2.5.11 → jaf_py-2.5.13}/setup.cfg +0 -0
  103. {jaf_py-2.5.11 → jaf_py-2.5.13}/setup.py +0 -0
  104. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_a2a_deep.py +0 -0
  105. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_a2a_examples.py +0 -0
  106. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_api_reference_examples.py +0 -0
  107. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_attachments.py +0 -0
  108. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_callback_system_examples.py +0 -0
  109. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_coffee_tool.py +0 -0
  110. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_conversation_id_fix.py +0 -0
  111. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_deployment_examples.py +0 -0
  112. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_docs_code_examples.py +0 -0
  113. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_engine.py +0 -0
  114. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_engine_manual.py +0 -0
  115. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_error_handling_examples.py +0 -0
  116. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_getting_started_examples.py +0 -0
  117. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_manual.py +0 -0
  118. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_math_tool.py +0 -0
  119. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_comprehensive.py +0 -0
  120. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_docs.py +0 -0
  121. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_real_functionality.py +0 -0
  122. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_transports.py +0 -0
  123. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_memory_system_examples.py +0 -0
  124. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_model_providers_examples.py +0 -0
  125. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_property_based.py +0 -0
  126. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_proxy_simple.py +0 -0
  127. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_redis_fixes.py +0 -0
  128. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_redis_memory.py +0 -0
  129. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_server_api_examples.py +0 -0
  130. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_session_continuity.py +0 -0
  131. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_streamable_http_mcp_example.py +0 -0
  132. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_timeout_functionality.py +0 -0
  133. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_tool_integration.py +0 -0
  134. {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.5.11
3
+ Version: 2.5.13
4
4
  Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
5
5
  Author: JAF Contributors
6
6
  Maintainer: JAF Contributors
@@ -82,7 +82,7 @@ Dynamic: license-file
82
82
 
83
83
  <!-- ![JAF Banner](docs/cover.png) -->
84
84
 
85
- [![Version](https://img.shields.io/badge/version-2.5.11-blue.svg)](https://github.com/xynehq/jaf-py)
85
+ [![Version](https://img.shields.io/badge/version-2.5.13-blue.svg)](https://github.com/xynehq/jaf-py)
86
86
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
87
87
  [![Docs](https://img.shields.io/badge/Docs-Live-brightgreen)](https://xynehq.github.io/jaf-py/)
88
88
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- ![JAF Banner](docs/cover.png) -->
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.5.11-blue.svg)](https://github.com/xynehq/jaf-py)
5
+ [![Version](https://img.shields.io/badge/version-2.5.13-blue.svg)](https://github.com/xynehq/jaf-py)
6
6
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
7
7
  [![Docs](https://img.shields.io/badge/Docs-Live-brightgreen)](https://xynehq.github.io/jaf-py/)
8
8
 
@@ -201,7 +201,7 @@ def generate_run_id() -> RunId:
201
201
  return create_run_id(str(uuid.uuid4()))
202
202
 
203
203
 
204
- __version__ = "2.5.11"
204
+ __version__ = "2.5.13"
205
205
  __all__ = [
206
206
  # Core types and functions
207
207
  "TraceId",
@@ -189,6 +189,8 @@ def create_agent_tool(
189
189
  initial_input_guardrails=parent_config.initial_input_guardrails,
190
190
  final_output_guardrails=parent_config.final_output_guardrails,
191
191
  on_event=parent_config.on_event,
192
+ before_llm_call=parent_config.before_llm_call,
193
+ after_llm_call=parent_config.after_llm_call,
192
194
  memory=parent_config.memory if preserve_session else None,
193
195
  conversation_id=parent_config.conversation_id if preserve_session else None,
194
196
  default_tool_timeout=parent_config.default_tool_timeout,
@@ -467,7 +467,7 @@ class LangfuseTraceCollector:
467
467
  public_key=public_key,
468
468
  secret_key=secret_key,
469
469
  host=host,
470
- release="jaf-py-v2.5.11",
470
+ release="jaf-py-v2.5.13",
471
471
  httpx_client=client,
472
472
  )
473
473
  self._httpx_client = client
@@ -1069,6 +1069,47 @@ class LangfuseTraceCollector:
1069
1069
  )
1070
1070
  print(f"[LANGFUSE] Handoff event created")
1071
1071
 
1072
+ elif event.type == "retry":
1073
+ # Create an event for retry attempts
1074
+ attempt = self._get_event_data(event, "attempt", 1)
1075
+ max_retries = self._get_event_data(event, "max_retries", 3)
1076
+ reason = self._get_event_data(event, "reason", "Unknown")
1077
+ operation = self._get_event_data(event, "operation", "llm_call")
1078
+ delay = self._get_event_data(event, "delay")
1079
+ error_details = self._get_event_data(event, "error_details", {})
1080
+
1081
+ print(
1082
+ f"[LANGFUSE] Recording retry event: attempt {attempt}/{max_retries} for {operation}, reason: {reason}"
1083
+ )
1084
+
1085
+ # Create comprehensive retry event data
1086
+ retry_input = {
1087
+ "attempt": attempt,
1088
+ "max_retries": max_retries,
1089
+ "reason": reason,
1090
+ "operation": operation,
1091
+ "delay_seconds": delay,
1092
+ "error_details": error_details,
1093
+ "timestamp": datetime.now().isoformat(),
1094
+ }
1095
+
1096
+ # Use compatibility layer to create event (works with both v2 and v3)
1097
+ self._create_event(
1098
+ parent_span=self.trace_spans[trace_id],
1099
+ name=f"retry-{operation}",
1100
+ input=retry_input,
1101
+ metadata={
1102
+ "framework": "jaf",
1103
+ "event_type": "retry",
1104
+ "retry_attempt": attempt,
1105
+ "max_retries": max_retries,
1106
+ "operation": operation,
1107
+ "reason": reason,
1108
+ "is_final_retry": attempt >= max_retries,
1109
+ },
1110
+ )
1111
+ print(f"[LANGFUSE] Retry event created for attempt {attempt}/{max_retries}")
1112
+
1072
1113
  else:
1073
1114
  # Create a generic event for other event types
1074
1115
  print(f"[LANGFUSE] Creating generic event for: {event.type}")
@@ -978,6 +978,37 @@ class OutputParseEvent:
978
978
  data: OutputParseEventData = field(default_factory=lambda: OutputParseEventData("", "start"))
979
979
 
980
980
 
981
+ @dataclass(frozen=True)
982
+ class RetryEventData:
983
+ """Data for retry events."""
984
+
985
+ attempt: int # Current retry attempt (1-indexed)
986
+ max_retries: int # Maximum number of retries configured
987
+ reason: str # Reason for retry (e.g., "HTTP 429 - Rate Limit", "HTTP 500 - Server Error")
988
+ operation: Literal["llm_call", "tool_call", "workflow_step"] # What operation is being retried
989
+ trace_id: TraceId
990
+ run_id: RunId
991
+ delay: Optional[float] = None # Backoff delay in seconds before next retry
992
+ error_details: Optional[Dict[str, Any]] = None # Additional error context
993
+
994
+
995
+ @dataclass(frozen=True)
996
+ class RetryEvent:
997
+ """Event emitted when a retry occurs."""
998
+
999
+ type: Literal["retry"] = "retry"
1000
+ data: RetryEventData = field(
1001
+ default_factory=lambda: RetryEventData(
1002
+ attempt=1,
1003
+ max_retries=3,
1004
+ reason="",
1005
+ operation="llm_call",
1006
+ trace_id=TraceId(""),
1007
+ run_id=RunId(""),
1008
+ )
1009
+ )
1010
+
1011
+
981
1012
  # Union type for all trace events
982
1013
  TraceEvent = Union[
983
1014
  RunStartEvent,
@@ -992,6 +1023,7 @@ TraceEvent = Union[
992
1023
  ToolCallEndEvent,
993
1024
  HandoffEvent,
994
1025
  RunEndEvent,
1026
+ RetryEvent,
995
1027
  ]
996
1028
 
997
1029
 
@@ -10,6 +10,7 @@ import httpx
10
10
  import time
11
11
  import os
12
12
  import base64
13
+ import asyncio
13
14
 
14
15
  from openai import AsyncOpenAI
15
16
  from pydantic import BaseModel
@@ -27,6 +28,8 @@ from ..core.types import (
27
28
  ToolCallFunctionDelta,
28
29
  MessageContentPart,
29
30
  get_text_content,
31
+ RetryEvent,
32
+ RetryEventData,
30
33
  )
31
34
  from ..core.proxy import ProxyConfig
32
35
  from ..utils.document_processor import (
@@ -110,6 +113,102 @@ async def _is_vision_model(model: str, base_url: str) -> bool:
110
113
  return is_known_vision_model
111
114
 
112
115
 
116
+ async def _retry_with_events(
117
+ operation_func,
118
+ state: RunState,
119
+ config: RunConfig,
120
+ operation_name: str = "llm_call",
121
+ max_retries: int = 3,
122
+ backoff_factor: float = 1.0,
123
+ ):
124
+ """
125
+ Wrapper that retries an async operation and emits retry events.
126
+
127
+ Args:
128
+ operation_func: Async function to execute (should accept no arguments)
129
+ state: Current run state
130
+ config: Run configuration with event handler
131
+ operation_name: Name of the operation for logging
132
+ max_retries: Maximum number of retry attempts
133
+ backoff_factor: Exponential backoff multiplier
134
+
135
+ Returns:
136
+ Result from operation_func
137
+
138
+ Raises:
139
+ Last exception if all retries are exhausted
140
+ """
141
+ last_exception = None
142
+
143
+ for attempt in range(max_retries + 1):
144
+ try:
145
+ return await operation_func()
146
+ except Exception as e:
147
+ last_exception = e
148
+
149
+ # Check if this is a retryable HTTP error
150
+ is_retryable = False
151
+ reason = str(e)
152
+ error_details = {"error_type": type(e).__name__, "error_message": str(e)}
153
+
154
+ # Check for HTTP errors (common in OpenAI/LiteLLM)
155
+ if hasattr(e, "status_code"):
156
+ status_code = e.status_code
157
+ error_details["status_code"] = status_code
158
+
159
+ # Retry on rate limits (429) and server errors (5xx)
160
+ if status_code == 429:
161
+ is_retryable = True
162
+ reason = f"HTTP {status_code} - Rate Limit"
163
+ elif 500 <= status_code < 600:
164
+ is_retryable = True
165
+ reason = f"HTTP {status_code} - Server Error"
166
+ else:
167
+ reason = f"HTTP {status_code}"
168
+
169
+ # Check for common exception names
170
+ elif "RateLimitError" in type(e).__name__:
171
+ is_retryable = True
172
+ reason = "Rate Limit Error"
173
+ elif "ServiceUnavailableError" in type(e).__name__ or "APIError" in type(e).__name__:
174
+ is_retryable = True
175
+ reason = "API Error"
176
+ elif "Timeout" in type(e).__name__:
177
+ is_retryable = True
178
+ reason = "Timeout"
179
+
180
+ # If not last attempt and is retryable, retry with backoff
181
+ if attempt < max_retries and is_retryable:
182
+ delay = backoff_factor * (2**attempt) # Exponential backoff
183
+
184
+ # Emit retry event
185
+ if config.on_event:
186
+ retry_event = RetryEvent(
187
+ data=RetryEventData(
188
+ attempt=attempt + 1,
189
+ max_retries=max_retries,
190
+ reason=reason,
191
+ operation=operation_name,
192
+ trace_id=state.trace_id,
193
+ run_id=state.run_id,
194
+ delay=delay,
195
+ error_details=error_details,
196
+ )
197
+ )
198
+ config.on_event(retry_event)
199
+
200
+ print(
201
+ f"[JAF:RETRY] Attempt {attempt + 1}/{max_retries} failed: {reason}. Retrying in {delay}s..."
202
+ )
203
+ await asyncio.sleep(delay)
204
+ else:
205
+ # Not retryable or last attempt, re-raise
206
+ raise
207
+
208
+ # Should never reach here, but just in case
209
+ raise last_exception
210
+
211
+
113
212
  def make_litellm_provider(
114
213
  base_url: str,
115
214
  api_key: str = "anything",
@@ -248,8 +347,14 @@ def make_litellm_provider(
248
347
  if agent.output_codec:
249
348
  request_params["response_format"] = {"type": "json_object"}
250
349
 
251
- # Make the API call
252
- response = await self.client.chat.completions.create(**request_params)
350
+ # Make the API call with retry handling
351
+ async def _api_call():
352
+ return await self.client.chat.completions.create(**request_params)
353
+
354
+ # Use retry wrapper to track retries in Langfuse
355
+ response = await _retry_with_events(
356
+ _api_call, state, config, operation_name="llm_call", max_retries=3, backoff_factor=1.0
357
+ )
253
358
 
254
359
  # Return in the expected format that the engine expects
255
360
  choice = response.choices[0]
@@ -577,8 +682,14 @@ def make_litellm_sdk_provider(
577
682
  if self.base_url:
578
683
  request_params["api_base"] = self.base_url
579
684
 
580
- # Make the API call using litellm
581
- response = await litellm.acompletion(**request_params)
685
+ # Make the API call using litellm with retry handling
686
+ async def _api_call():
687
+ return await litellm.acompletion(**request_params)
688
+
689
+ # Use retry wrapper to track retries in Langfuse
690
+ response = await _retry_with_events(
691
+ _api_call, state, config, operation_name="llm_call", max_retries=3, backoff_factor=1.0
692
+ )
582
693
 
583
694
  # Return in the expected format that the engine expects
584
695
  choice = response.choices[0]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.5.11
3
+ Version: 2.5.13
4
4
  Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
5
5
  Author: JAF Contributors
6
6
  Maintainer: JAF Contributors
@@ -82,7 +82,7 @@ Dynamic: license-file
82
82
 
83
83
  <!-- ![JAF Banner](docs/cover.png) -->
84
84
 
85
- [![Version](https://img.shields.io/badge/version-2.5.11-blue.svg)](https://github.com/xynehq/jaf-py)
85
+ [![Version](https://img.shields.io/badge/version-2.5.13-blue.svg)](https://github.com/xynehq/jaf-py)
86
86
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
87
87
  [![Docs](https://img.shields.io/badge/Docs-Live-brightgreen)](https://xynehq.github.io/jaf-py/)
88
88
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jaf-py"
7
- version = "2.5.11"
7
+ version = "2.5.13"
8
8
  description = "A purely functional agent framework with immutable state and composable tools - Python implementation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes