jaf-py 2.5.2__tar.gz → 2.5.4__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 (133) hide show
  1. {jaf_py-2.5.2/jaf_py.egg-info → jaf_py-2.5.4}/PKG-INFO +4 -4
  2. {jaf_py-2.5.2 → jaf_py-2.5.4}/README.md +1 -1
  3. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/__init__.py +1 -1
  4. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/engine.py +1 -9
  5. jaf_py-2.5.4/jaf/core/regeneration.py +392 -0
  6. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/tracing.py +196 -59
  7. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/types.py +109 -2
  8. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/providers/in_memory.py +174 -1
  9. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/providers/postgres.py +211 -1
  10. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/providers/redis.py +189 -1
  11. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/types.py +35 -1
  12. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/utils.py +2 -0
  13. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/server/server.py +163 -0
  14. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/server/types.py +49 -1
  15. {jaf_py-2.5.2 → jaf_py-2.5.4/jaf_py.egg-info}/PKG-INFO +4 -4
  16. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf_py.egg-info/SOURCES.txt +1 -0
  17. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf_py.egg-info/requires.txt +2 -2
  18. {jaf_py-2.5.2 → jaf_py-2.5.4}/pyproject.toml +3 -3
  19. {jaf_py-2.5.2 → jaf_py-2.5.4}/LICENSE +0 -0
  20. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/__init__.py +0 -0
  21. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/agent.py +0 -0
  22. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/agent_card.py +0 -0
  23. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/client.py +0 -0
  24. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/__init__.py +0 -0
  25. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/client_example.py +0 -0
  26. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/integration_example.py +0 -0
  27. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
  28. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/server_demo/__init__.py +0 -0
  29. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/examples/server_example.py +0 -0
  30. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/__init__.py +0 -0
  31. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/cleanup.py +0 -0
  32. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/factory.py +0 -0
  33. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/providers/__init__.py +0 -0
  34. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/providers/composite.py +0 -0
  35. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/providers/in_memory.py +0 -0
  36. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/providers/postgres.py +0 -0
  37. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/providers/redis.py +0 -0
  38. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/serialization.py +0 -0
  39. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/__init__.py +0 -0
  40. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
  41. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
  42. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/test_serialization.py +0 -0
  43. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
  44. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
  45. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/memory/types.py +0 -0
  46. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/protocol.py +0 -0
  47. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/server.py +0 -0
  48. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/standalone_client.py +0 -0
  49. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/__init__.py +0 -0
  50. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/run_tests.py +0 -0
  51. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/test_agent.py +0 -0
  52. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/test_client.py +0 -0
  53. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/test_integration.py +0 -0
  54. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/test_protocol.py +0 -0
  55. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/tests/test_types.py +0 -0
  56. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/a2a/types.py +0 -0
  57. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/cli.py +0 -0
  58. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/__init__.py +0 -0
  59. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/agent_tool.py +0 -0
  60. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/analytics.py +0 -0
  61. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/composition.py +0 -0
  62. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/errors.py +0 -0
  63. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/guardrails.py +0 -0
  64. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/handoff.py +0 -0
  65. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/parallel_agents.py +0 -0
  66. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/performance.py +0 -0
  67. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/proxy.py +0 -0
  68. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/proxy_helpers.py +0 -0
  69. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/state.py +0 -0
  70. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/streaming.py +0 -0
  71. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/tool_results.py +0 -0
  72. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/tools.py +0 -0
  73. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/core/workflows.py +0 -0
  74. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/exceptions.py +0 -0
  75. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/__init__.py +0 -0
  76. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/approval_storage.py +0 -0
  77. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/factory.py +0 -0
  78. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/memory/providers/__init__.py +0 -0
  79. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/plugins/__init__.py +0 -0
  80. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/plugins/base.py +0 -0
  81. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/policies/__init__.py +0 -0
  82. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/policies/handoff.py +0 -0
  83. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/policies/validation.py +0 -0
  84. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/providers/__init__.py +0 -0
  85. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/providers/mcp.py +0 -0
  86. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/providers/model.py +0 -0
  87. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/server/__init__.py +0 -0
  88. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/server/main.py +0 -0
  89. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/utils/__init__.py +0 -0
  90. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/utils/attachments.py +0 -0
  91. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/utils/document_processor.py +0 -0
  92. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/__init__.py +0 -0
  93. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/example.py +0 -0
  94. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/functional_core.py +0 -0
  95. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/graphviz.py +0 -0
  96. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/imperative_shell.py +0 -0
  97. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf/visualization/types.py +0 -0
  98. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf_py.egg-info/dependency_links.txt +0 -0
  99. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf_py.egg-info/entry_points.txt +0 -0
  100. {jaf_py-2.5.2 → jaf_py-2.5.4}/jaf_py.egg-info/top_level.txt +0 -0
  101. {jaf_py-2.5.2 → jaf_py-2.5.4}/setup.cfg +0 -0
  102. {jaf_py-2.5.2 → jaf_py-2.5.4}/setup.py +0 -0
  103. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_a2a_deep.py +0 -0
  104. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_a2a_examples.py +0 -0
  105. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_api_reference_examples.py +0 -0
  106. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_attachments.py +0 -0
  107. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_callback_system_examples.py +0 -0
  108. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_coffee_tool.py +0 -0
  109. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_conversation_id_fix.py +0 -0
  110. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_deployment_examples.py +0 -0
  111. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_docs_code_examples.py +0 -0
  112. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_engine.py +0 -0
  113. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_engine_manual.py +0 -0
  114. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_error_handling_examples.py +0 -0
  115. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_getting_started_examples.py +0 -0
  116. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_manual.py +0 -0
  117. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_math_tool.py +0 -0
  118. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_mcp_comprehensive.py +0 -0
  119. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_mcp_docs.py +0 -0
  120. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_mcp_real_functionality.py +0 -0
  121. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_mcp_transports.py +0 -0
  122. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_memory_system_examples.py +0 -0
  123. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_model_providers_examples.py +0 -0
  124. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_property_based.py +0 -0
  125. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_proxy_simple.py +0 -0
  126. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_redis_fixes.py +0 -0
  127. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_redis_memory.py +0 -0
  128. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_server_api_examples.py +0 -0
  129. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_session_continuity.py +0 -0
  130. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_streamable_http_mcp_example.py +0 -0
  131. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_timeout_functionality.py +0 -0
  132. {jaf_py-2.5.2 → jaf_py-2.5.4}/tests/test_tool_integration.py +0 -0
  133. {jaf_py-2.5.2 → jaf_py-2.5.4}/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.2
3
+ Version: 2.5.4
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
@@ -42,13 +42,13 @@ Requires-Dist: fastmcp>=0.1.0
42
42
  Requires-Dist: opentelemetry-api>=1.22.0
43
43
  Requires-Dist: opentelemetry-sdk>=1.22.0
44
44
  Requires-Dist: opentelemetry-exporter-otlp>=1.22.0
45
- Requires-Dist: langfuse<4.0.0,>=3.6.1
45
+ Requires-Dist: langfuse<3.0.0
46
46
  Requires-Dist: litellm>=1.76.3
47
47
  Provides-Extra: tracing
48
48
  Requires-Dist: opentelemetry-api>=1.22.0; extra == "tracing"
49
49
  Requires-Dist: opentelemetry-sdk>=1.22.0; extra == "tracing"
50
50
  Requires-Dist: opentelemetry-exporter-otlp>=1.22.0; extra == "tracing"
51
- Requires-Dist: langfuse<4.0.0,>=3.6.1; extra == "tracing"
51
+ Requires-Dist: langfuse<3.0.0; extra == "tracing"
52
52
  Provides-Extra: attachments
53
53
  Requires-Dist: PyPDF2>=3.0.0; extra == "attachments"
54
54
  Requires-Dist: python-docx>=1.1.0; extra == "attachments"
@@ -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.2-blue.svg)](https://github.com/xynehq/jaf-py)
85
+ [![Version](https://img.shields.io/badge/version-2.5.4-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.2-blue.svg)](https://github.com/xynehq/jaf-py)
5
+ [![Version](https://img.shields.io/badge/version-2.5.4-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
 
@@ -191,7 +191,7 @@ def generate_run_id() -> RunId:
191
191
  """Generate a new run ID."""
192
192
  return create_run_id(str(uuid.uuid4()))
193
193
 
194
- __version__ = "2.5.2"
194
+ __version__ = "2.5.4"
195
195
  __all__ = [
196
196
  # Core types and functions
197
197
  "TraceId", "RunId", "ValidationResult", "Message", "ModelConfig",
@@ -293,15 +293,7 @@ async def _load_conversation_history(state: RunState[Ctx], config: RunConfig[Ctx
293
293
  # For HITL scenarios, append new messages to memory messages
294
294
  # This prevents duplication when resuming from interruptions
295
295
  if memory_messages:
296
- combined_messages = memory_messages + [
297
- msg for msg in state.messages
298
- if not any(
299
- mem_msg.role == msg.role and
300
- mem_msg.content == msg.content and
301
- getattr(mem_msg, 'tool_calls', None) == getattr(msg, 'tool_calls', None)
302
- for mem_msg in memory_messages
303
- )
304
- ]
296
+ combined_messages = memory_messages + list(state.messages)
305
297
  else:
306
298
  combined_messages = list(state.messages)
307
299
 
@@ -0,0 +1,392 @@
1
+ """
2
+ Regeneration functionality for the JAF framework.
3
+
4
+ This module implements conversation regeneration where a specific message can be
5
+ regenerated, removing all subsequent messages and creating a new conversation path.
6
+ """
7
+
8
+ import time
9
+ from dataclasses import replace
10
+ from typing import Any, TypeVar, Optional
11
+
12
+ from .types import (
13
+ RunState, RunConfig, RunResult,
14
+ RegenerationRequest, RegenerationContext,
15
+ MessageId, Message, ErrorOutcome, ModelBehaviorError,
16
+ find_message_index, truncate_messages_after, get_message_by_id,
17
+ generate_run_id, generate_trace_id
18
+ )
19
+ from .engine import run as engine_run
20
+ from ..memory.types import Success, Failure
21
+
22
+ Ctx = TypeVar('Ctx')
23
+ Out = TypeVar('Out')
24
+
25
+ async def regenerate_conversation(
26
+ regeneration_request: RegenerationRequest,
27
+ config: RunConfig[Ctx],
28
+ context: Ctx,
29
+ agent_name: str
30
+ ) -> RunResult[Out]:
31
+ """
32
+ Regenerate a conversation from a specific message ID.
33
+
34
+ This function:
35
+ 1. Loads the full conversation from memory
36
+ 2. Finds the message to regenerate from
37
+ 3. Truncates the conversation at that point
38
+ 4. Creates a new RunState with truncated conversation
39
+ 5. Executes the regeneration through the normal engine flow
40
+ 6. Updates memory with the new conversation path
41
+
42
+ Args:
43
+ regeneration_request: The regeneration request containing conversation_id and message_id
44
+ config: The run configuration
45
+ context: The context for the regeneration
46
+ agent_name: The name of the agent to use for regeneration
47
+
48
+ Returns:
49
+ RunResult with the regenerated conversation outcome
50
+ """
51
+ if not config.memory or not config.memory.provider or not config.conversation_id:
52
+ return RunResult(
53
+ final_state=RunState(
54
+ run_id=generate_run_id(),
55
+ trace_id=generate_trace_id(),
56
+ messages=[],
57
+ current_agent_name=agent_name,
58
+ context=context,
59
+ turn_count=0
60
+ ),
61
+ outcome=ErrorOutcome(error=ModelBehaviorError(
62
+ detail="Regeneration requires memory provider and conversation_id to be configured"
63
+ ))
64
+ )
65
+
66
+ # Load the conversation from memory
67
+ conversation_result = await config.memory.provider.get_conversation(regeneration_request.conversation_id)
68
+ if isinstance(conversation_result, Failure):
69
+ return RunResult(
70
+ final_state=RunState(
71
+ run_id=generate_run_id(),
72
+ trace_id=generate_trace_id(),
73
+ messages=[],
74
+ current_agent_name=agent_name,
75
+ context=context,
76
+ turn_count=0
77
+ ),
78
+ outcome=ErrorOutcome(error=ModelBehaviorError(
79
+ detail=f"Failed to load conversation: {conversation_result.error}"
80
+ ))
81
+ )
82
+
83
+ conversation_memory = conversation_result.data
84
+ if not conversation_memory:
85
+ return RunResult(
86
+ final_state=RunState(
87
+ run_id=generate_run_id(),
88
+ trace_id=generate_trace_id(),
89
+ messages=[],
90
+ current_agent_name=agent_name,
91
+ context=context,
92
+ turn_count=0
93
+ ),
94
+ outcome=ErrorOutcome(error=ModelBehaviorError(
95
+ detail=f"Conversation {regeneration_request.conversation_id} not found"
96
+ ))
97
+ )
98
+
99
+ # Convert tuple back to list for processing
100
+ original_messages = list(conversation_memory.messages)
101
+
102
+ # Find the message to regenerate from
103
+ regenerate_message = get_message_by_id(original_messages, regeneration_request.message_id)
104
+ if not regenerate_message:
105
+ return RunResult(
106
+ final_state=RunState(
107
+ run_id=generate_run_id(),
108
+ trace_id=generate_trace_id(),
109
+ messages=original_messages,
110
+ current_agent_name=agent_name,
111
+ context=context,
112
+ turn_count=len([m for m in original_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
113
+ ),
114
+ outcome=ErrorOutcome(error=ModelBehaviorError(
115
+ detail=f"Message {regeneration_request.message_id} not found in conversation"
116
+ ))
117
+ )
118
+
119
+ # Get the index of the message to regenerate
120
+ regenerate_index = find_message_index(original_messages, regeneration_request.message_id)
121
+ if regenerate_index is None:
122
+ return RunResult(
123
+ final_state=RunState(
124
+ run_id=generate_run_id(),
125
+ trace_id=generate_trace_id(),
126
+ messages=original_messages,
127
+ current_agent_name=agent_name,
128
+ context=context,
129
+ turn_count=len([m for m in original_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
130
+ ),
131
+ outcome=ErrorOutcome(error=ModelBehaviorError(
132
+ detail=f"Failed to find index for message {regeneration_request.message_id}"
133
+ ))
134
+ )
135
+
136
+ def determine_regeneration_type(messages, regenerate_index, context):
137
+ """Determine if this is pure regeneration or edit scenario."""
138
+ if context and context.get("replace_user_message"):
139
+ return "edit"
140
+
141
+ regenerate_message = messages[regenerate_index]
142
+ if regenerate_message.role in ['assistant', 'ASSISTANT']:
143
+ for i in range(regenerate_index - 1, -1, -1):
144
+ if messages[i].role in ['user', 'USER']:
145
+ return "pure"
146
+ return "edit"
147
+
148
+ # Determine regeneration type
149
+ regen_type = determine_regeneration_type(original_messages, regenerate_index, regeneration_request.context or {})
150
+ print(f"[JAF:REGENERATION] Detected regeneration type: {regen_type}")
151
+
152
+ if regen_type == "pure":
153
+ # For pure regeneration, find the user message that started this conversation turn
154
+ user_message_index = None
155
+ for i in range(regenerate_index - 1, -1, -1):
156
+ if original_messages[i].role in ['user', 'USER']:
157
+ user_message_index = i
158
+ break
159
+
160
+ if user_message_index is not None:
161
+ # Truncate AFTER the user message (keeps user message, removes tool calls/outputs)
162
+ truncated_messages = original_messages[:user_message_index + 1]
163
+ print(f"[JAF:REGENERATION] Pure regeneration: truncated to user message at index {user_message_index}")
164
+ else:
165
+ truncated_messages = original_messages[:regenerate_index]
166
+ print(f"[JAF:REGENERATION] Pure regeneration fallback: no user message found")
167
+ else:
168
+ # Edit regeneration: truncate at the specified point and add replacement query
169
+ truncated_messages = original_messages[:regenerate_index]
170
+
171
+ if (regeneration_request.context and
172
+ regeneration_request.context.get("replace_user_message")):
173
+
174
+ from .types import ContentRole, Message
175
+ replacement_user_message = Message(
176
+ role=ContentRole.USER,
177
+ content=regeneration_request.context.get("replace_user_message")
178
+ )
179
+ truncated_messages.append(replacement_user_message)
180
+ print(f"[JAF:REGENERATION] Edit regeneration: replaced user query with: {regeneration_request.context.get('replace_user_message')}")
181
+
182
+ print(f"[JAF:REGENERATION] Truncated conversation to {len(truncated_messages)} messages")
183
+
184
+
185
+ print(f"[JAF:REGENERATION] About to store {len(truncated_messages)} truncated messages to memory")
186
+
187
+ def serialize_metadata(metadata):
188
+ import json
189
+ import datetime
190
+
191
+ def json_serializer(obj):
192
+ if isinstance(obj, datetime.datetime):
193
+ return obj.isoformat()
194
+ elif isinstance(obj, datetime.date):
195
+ return obj.isoformat()
196
+ elif hasattr(obj, '__dict__'):
197
+ return obj.__dict__
198
+ return str(obj)
199
+
200
+ try:
201
+ json_str = json.dumps(metadata, default=json_serializer)
202
+ return json.loads(json_str)
203
+ except Exception as e:
204
+ print(f"[JAF:REGENERATION] Warning: Metadata serialization failed: {e}")
205
+ return {
206
+ "regeneration_truncated": True,
207
+ "regeneration_point": str(regeneration_request.message_id),
208
+ "original_message_count": len(original_messages),
209
+ "truncated_at_index": regenerate_index,
210
+ "turn_count": len([m for m in truncated_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
211
+ }
212
+
213
+ metadata = serialize_metadata({
214
+ **conversation_memory.metadata,
215
+ "regeneration_truncated": True,
216
+ "regeneration_point": str(regeneration_request.message_id),
217
+ "original_message_count": len(original_messages),
218
+ "truncated_at_index": regenerate_index,
219
+ "turn_count": len([m for m in truncated_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
220
+ })
221
+
222
+ store_result = await config.memory.provider.store_messages(
223
+ regeneration_request.conversation_id,
224
+ truncated_messages,
225
+ metadata
226
+ )
227
+
228
+ print(f"[JAF:REGENERATION] Store result type: {type(store_result)}")
229
+ if isinstance(store_result, Failure):
230
+ print(f"[JAF:REGENERATION] Store failed with error: {store_result.error}")
231
+ return RunResult(
232
+ final_state=RunState(
233
+ run_id=generate_run_id(),
234
+ trace_id=generate_trace_id(),
235
+ messages=original_messages,
236
+ current_agent_name=agent_name,
237
+ context=context,
238
+ turn_count=len([m for m in original_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
239
+ ),
240
+ outcome=ErrorOutcome(error=ModelBehaviorError(
241
+ detail=f"Failed to store truncated conversation: {store_result.error}"
242
+ ))
243
+ )
244
+ else:
245
+ print(f"[JAF:REGENERATION] Store successful, proceeding to engine execution")
246
+
247
+ # Create regeneration context for later use
248
+ regeneration_context = RegenerationContext(
249
+ original_message_count=len(original_messages),
250
+ truncated_at_index=regenerate_index,
251
+ regenerated_message_id=regeneration_request.message_id,
252
+ regeneration_id=f"regen_{int(time.time() * 1000)}_{regeneration_request.message_id}",
253
+ timestamp=int(time.time() * 1000)
254
+ )
255
+
256
+ # Calculate turn count from truncated messages
257
+ truncated_turn_count = len([m for m in truncated_messages if (m.role.value if hasattr(m.role, 'value') else m.role) == 'assistant'])
258
+
259
+ final_context = context
260
+ print(f"[JAF:REGENERATION] Using provided context: {type(context).__name__}")
261
+
262
+ # Create initial state for regeneration with truncated conversation
263
+ initial_state = RunState(
264
+ run_id=generate_run_id(),
265
+ trace_id=generate_trace_id(),
266
+ messages=[],
267
+ current_agent_name=agent_name,
268
+ context=final_context,
269
+ turn_count=truncated_turn_count,
270
+ approvals={} # Reset approvals for regeneration
271
+ )
272
+
273
+ print(f"[JAF:REGENERATION] Starting regeneration from message {regeneration_request.message_id}")
274
+ print(f"[JAF:REGENERATION] Original messages: {len(original_messages)}, Truncated to: {len(truncated_messages)}")
275
+ print(f"[JAF:REGENERATION] Regeneration context: {regeneration_context}")
276
+
277
+ # Create a modified config for regeneration that ensures memory storage
278
+ regeneration_config = replace(
279
+ config,
280
+ conversation_id=regeneration_request.conversation_id,
281
+ memory=replace(config.memory, auto_store=True, store_on_completion=True) if config.memory else None
282
+ )
283
+
284
+ # Execute the regeneration through the normal engine flow
285
+ print(f"[JAF:REGENERATION] About to execute engine with {len(truncated_messages)} messages")
286
+ print(f"[JAF:REGENERATION] Final message: {truncated_messages[-1] if truncated_messages else 'None'}")
287
+
288
+ result = await engine_run(initial_state, regeneration_config)
289
+
290
+ print(f"[JAF:REGENERATION] Regeneration completed with status: {result.outcome.status}")
291
+ if hasattr(result, 'final_state') and hasattr(result.final_state, 'messages'):
292
+ print(f"[JAF:REGENERATION] Final state has {len(result.final_state.messages)} messages")
293
+ assistant_msgs = [m for m in result.final_state.messages if m.role in ['assistant', 'ASSISTANT']]
294
+ print(f"[JAF:REGENERATION] Found {len(assistant_msgs)} assistant messages in result")
295
+
296
+ # After successful regeneration, mark the regeneration point and preserve metadata
297
+ if result.outcome.status == 'completed' and config.memory and config.memory.provider:
298
+ try:
299
+ print(f"[JAF:REGENERATION] Marking regeneration point after successful regeneration")
300
+
301
+ # Get the current conversation to preserve regeneration metadata
302
+ current_conv_result = await config.memory.provider.get_conversation(regeneration_request.conversation_id)
303
+ print(f"[JAF:REGENERATION] Retrieved conversation for preservation: {hasattr(current_conv_result, 'data') and current_conv_result.data is not None}")
304
+
305
+ if hasattr(current_conv_result, 'data') and current_conv_result.data:
306
+ current_metadata = current_conv_result.data.metadata
307
+ regeneration_points = current_metadata.get('regeneration_points', [])
308
+ print(f"[JAF:REGENERATION] Found {len(regeneration_points)} regeneration points in metadata before marking")
309
+
310
+ # Mark the regeneration point by calling the provider method directly
311
+ mark_result = await config.memory.provider.mark_regeneration_point(
312
+ regeneration_request.conversation_id,
313
+ regeneration_request.message_id,
314
+ {
315
+ "regeneration_id": regeneration_context.regeneration_id,
316
+ "original_message_count": len(original_messages),
317
+ "truncated_at_index": regenerate_index,
318
+ "timestamp": regeneration_context.timestamp
319
+ }
320
+ )
321
+
322
+ if isinstance(mark_result, Failure):
323
+ print(f"[JAF:REGENERATION] Warning: Failed to mark regeneration point: {mark_result.error}")
324
+ else:
325
+ print(f"[JAF:REGENERATION] Successfully marked regeneration point")
326
+
327
+ # Get the updated conversation with the new regeneration point
328
+ updated_conv_result = await config.memory.provider.get_conversation(regeneration_request.conversation_id)
329
+ if hasattr(updated_conv_result, 'data') and updated_conv_result.data:
330
+ updated_metadata = updated_conv_result.data.metadata
331
+ updated_regeneration_points = updated_metadata.get('regeneration_points', [])
332
+ print(f"[JAF:REGENERATION] Found {len(updated_regeneration_points)} regeneration points after marking")
333
+
334
+ # Ensure final metadata includes the regeneration points
335
+ final_metadata = {
336
+ **updated_metadata,
337
+ 'regeneration_points': updated_regeneration_points,
338
+ 'regeneration_count': len(updated_regeneration_points),
339
+ 'last_regeneration': updated_regeneration_points[-1] if updated_regeneration_points else None,
340
+ 'regeneration_preserved': True,
341
+ 'final_preservation_timestamp': int(time.time() * 1000)
342
+ }
343
+
344
+ # Store the final conversation with preserved regeneration metadata
345
+ await config.memory.provider.store_messages(
346
+ regeneration_request.conversation_id,
347
+ result.final_state.messages,
348
+ final_metadata
349
+ )
350
+ print(f"[JAF:REGENERATION] Final preservation completed with {len(updated_regeneration_points)} regeneration points")
351
+ else:
352
+ print(f"[JAF:REGENERATION] No conversation data found for preservation")
353
+
354
+ except Exception as e:
355
+ print(f"[JAF:REGENERATION] Warning: Failed to preserve regeneration points: {e}")
356
+ import traceback
357
+ traceback.print_exc()
358
+
359
+ return result
360
+
361
+
362
+ async def get_regeneration_points(
363
+ conversation_id: str,
364
+ config: RunConfig[Ctx]
365
+ ) -> Optional[list]:
366
+ """
367
+ Get all regeneration points for a conversation.
368
+
369
+ Args:
370
+ conversation_id: The conversation ID
371
+ config: The run configuration
372
+
373
+ Returns:
374
+ List of regeneration points or None if not available
375
+ """
376
+ if not config.memory or not config.memory.provider:
377
+ return None
378
+
379
+ try:
380
+ conversation_result = await config.memory.provider.get_conversation(conversation_id)
381
+ if hasattr(conversation_result, 'data') and conversation_result.data:
382
+ metadata = conversation_result.data.metadata
383
+ regeneration_points = metadata.get('regeneration_points', [])
384
+ print(f"[JAF:REGENERATION] Retrieved {len(regeneration_points)} regeneration points for {conversation_id}")
385
+ return regeneration_points
386
+ else:
387
+ print(f"[JAF:REGENERATION] No conversation data found for {conversation_id}")
388
+ return []
389
+ except Exception as e:
390
+ print(f"[JAF:REGENERATION] Failed to get regeneration points: {e}")
391
+
392
+ return []