jaf-py 2.5.1__tar.gz → 2.5.3__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 (132) hide show
  1. {jaf_py-2.5.1/jaf_py.egg-info → jaf_py-2.5.3}/PKG-INFO +2 -2
  2. {jaf_py-2.5.1 → jaf_py-2.5.3}/README.md +1 -1
  3. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/__init__.py +1 -1
  4. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/__init__.py +6 -0
  5. jaf_py-2.5.3/jaf/core/handoff.py +191 -0
  6. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/state.py +117 -6
  7. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tracing.py +371 -73
  8. {jaf_py-2.5.1 → jaf_py-2.5.3/jaf_py.egg-info}/PKG-INFO +2 -2
  9. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/SOURCES.txt +1 -0
  10. {jaf_py-2.5.1 → jaf_py-2.5.3}/pyproject.toml +1 -1
  11. {jaf_py-2.5.1 → jaf_py-2.5.3}/LICENSE +0 -0
  12. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/__init__.py +0 -0
  13. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/agent.py +0 -0
  14. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/agent_card.py +0 -0
  15. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/client.py +0 -0
  16. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/__init__.py +0 -0
  17. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/client_example.py +0 -0
  18. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/integration_example.py +0 -0
  19. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
  20. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/server_demo/__init__.py +0 -0
  21. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/server_example.py +0 -0
  22. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/__init__.py +0 -0
  23. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/cleanup.py +0 -0
  24. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/factory.py +0 -0
  25. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/__init__.py +0 -0
  26. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/composite.py +0 -0
  27. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/in_memory.py +0 -0
  28. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/postgres.py +0 -0
  29. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/redis.py +0 -0
  30. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/serialization.py +0 -0
  31. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/__init__.py +0 -0
  32. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
  33. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
  34. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_serialization.py +0 -0
  35. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
  36. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
  37. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/types.py +0 -0
  38. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/protocol.py +0 -0
  39. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/server.py +0 -0
  40. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/standalone_client.py +0 -0
  41. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/__init__.py +0 -0
  42. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/run_tests.py +0 -0
  43. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_agent.py +0 -0
  44. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_client.py +0 -0
  45. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_integration.py +0 -0
  46. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_protocol.py +0 -0
  47. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_types.py +0 -0
  48. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/types.py +0 -0
  49. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/cli.py +0 -0
  50. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/agent_tool.py +0 -0
  51. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/analytics.py +0 -0
  52. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/composition.py +0 -0
  53. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/engine.py +0 -0
  54. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/errors.py +0 -0
  55. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/guardrails.py +0 -0
  56. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/parallel_agents.py +0 -0
  57. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/performance.py +0 -0
  58. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/proxy.py +0 -0
  59. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/proxy_helpers.py +0 -0
  60. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/streaming.py +0 -0
  61. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tool_results.py +0 -0
  62. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tools.py +0 -0
  63. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/types.py +0 -0
  64. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/workflows.py +0 -0
  65. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/exceptions.py +0 -0
  66. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/__init__.py +0 -0
  67. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/approval_storage.py +0 -0
  68. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/factory.py +0 -0
  69. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/__init__.py +0 -0
  70. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/in_memory.py +0 -0
  71. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/postgres.py +0 -0
  72. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/redis.py +0 -0
  73. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/types.py +0 -0
  74. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/utils.py +0 -0
  75. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/plugins/__init__.py +0 -0
  76. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/plugins/base.py +0 -0
  77. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/__init__.py +0 -0
  78. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/handoff.py +0 -0
  79. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/validation.py +0 -0
  80. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/__init__.py +0 -0
  81. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/mcp.py +0 -0
  82. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/model.py +0 -0
  83. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/__init__.py +0 -0
  84. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/main.py +0 -0
  85. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/server.py +0 -0
  86. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/types.py +0 -0
  87. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/__init__.py +0 -0
  88. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/attachments.py +0 -0
  89. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/document_processor.py +0 -0
  90. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/__init__.py +0 -0
  91. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/example.py +0 -0
  92. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/functional_core.py +0 -0
  93. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/graphviz.py +0 -0
  94. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/imperative_shell.py +0 -0
  95. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/types.py +0 -0
  96. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/dependency_links.txt +0 -0
  97. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/entry_points.txt +0 -0
  98. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/requires.txt +0 -0
  99. {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/top_level.txt +0 -0
  100. {jaf_py-2.5.1 → jaf_py-2.5.3}/setup.cfg +0 -0
  101. {jaf_py-2.5.1 → jaf_py-2.5.3}/setup.py +0 -0
  102. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_a2a_deep.py +0 -0
  103. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_a2a_examples.py +0 -0
  104. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_api_reference_examples.py +0 -0
  105. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_attachments.py +0 -0
  106. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_callback_system_examples.py +0 -0
  107. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_coffee_tool.py +0 -0
  108. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_conversation_id_fix.py +0 -0
  109. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_deployment_examples.py +0 -0
  110. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_docs_code_examples.py +0 -0
  111. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_engine.py +0 -0
  112. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_engine_manual.py +0 -0
  113. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_error_handling_examples.py +0 -0
  114. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_getting_started_examples.py +0 -0
  115. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_manual.py +0 -0
  116. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_math_tool.py +0 -0
  117. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_comprehensive.py +0 -0
  118. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_docs.py +0 -0
  119. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_real_functionality.py +0 -0
  120. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_transports.py +0 -0
  121. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_memory_system_examples.py +0 -0
  122. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_model_providers_examples.py +0 -0
  123. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_property_based.py +0 -0
  124. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_proxy_simple.py +0 -0
  125. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_redis_fixes.py +0 -0
  126. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_redis_memory.py +0 -0
  127. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_server_api_examples.py +0 -0
  128. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_session_continuity.py +0 -0
  129. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_streamable_http_mcp_example.py +0 -0
  130. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_timeout_functionality.py +0 -0
  131. {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_tool_integration.py +0 -0
  132. {jaf_py-2.5.1 → jaf_py-2.5.3}/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.1
3
+ Version: 2.5.3
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.3.1-blue.svg)](https://github.com/xynehq/jaf-py)
85
+ [![Version](https://img.shields.io/badge/version-2.5.3-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.3.1-blue.svg)](https://github.com/xynehq/jaf-py)
5
+ [![Version](https://img.shields.io/badge/version-2.5.3-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.0.0"
194
+ __version__ = "2.5.3"
195
195
  __all__ = [
196
196
  # Core types and functions
197
197
  "TraceId", "RunId", "ValidationResult", "Message", "ModelConfig",
@@ -22,6 +22,7 @@ from .parallel_agents import (
22
22
  create_domain_experts_tool,
23
23
  )
24
24
  from .proxy import ProxyConfig, ProxyAuth, create_proxy_config, get_default_proxy_config
25
+ from .handoff import handoff_tool, handoff, create_handoff_tool, is_handoff_request, extract_handoff_target
25
26
 
26
27
  __all__ = [
27
28
  "Agent",
@@ -52,6 +53,7 @@ __all__ = [
52
53
  "create_conditional_enabler",
53
54
  "create_default_output_extractor",
54
55
  "create_domain_experts_tool",
56
+ "create_handoff_tool",
55
57
  "create_json_output_extractor",
56
58
  "create_language_specialists_tool",
57
59
  "create_parallel_agents_tool",
@@ -59,8 +61,12 @@ __all__ = [
59
61
  "create_run_id",
60
62
  "create_simple_parallel_tool",
61
63
  "create_trace_id",
64
+ "extract_handoff_target",
62
65
  "get_current_run_config",
63
66
  "get_default_proxy_config",
67
+ "handoff",
68
+ "handoff_tool",
69
+ "is_handoff_request",
64
70
  "require_permissions",
65
71
  "run",
66
72
  "set_current_run_config",
@@ -0,0 +1,191 @@
1
+ """
2
+ Handoff system for JAF framework.
3
+
4
+ This module provides a simple, elegant handoff mechanism that allows agents
5
+ to seamlessly transfer control to other agents with clean state management.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Optional, TypeVar
10
+ from dataclasses import dataclass
11
+
12
+ from .types import Tool, ToolSchema, ToolSource
13
+
14
+ try:
15
+ from pydantic import BaseModel, Field
16
+ except ImportError:
17
+ BaseModel = None
18
+ Field = None
19
+
20
+ Ctx = TypeVar('Ctx')
21
+
22
+
23
+ def _create_handoff_json(agent_name: str, message: str = "") -> str:
24
+ """Create the JSON structure for handoff requests."""
25
+ return json.dumps({
26
+ "handoff_to": agent_name,
27
+ "message": message or f"Handing off to {agent_name}",
28
+ "type": "handoff"
29
+ })
30
+
31
+
32
+ if BaseModel is not None and Field is not None:
33
+ class _HandoffInput(BaseModel):
34
+ """Input parameters for handoff tool (Pydantic model)."""
35
+ agent_name: str = Field(description="Name of the agent to hand off to")
36
+ message: str = Field(description="Message or context to pass to the target agent")
37
+ else:
38
+ class _HandoffInput(object):
39
+ """Plain-Python fallback for handoff input when Pydantic is unavailable.
40
+
41
+ This class intentionally does not call Field() so it is safe to import
42
+ when Pydantic is not installed.
43
+ """
44
+ agent_name: str
45
+ message: str
46
+
47
+ def __init__(self, agent_name: str, message: str = ""):
48
+ self.agent_name = agent_name
49
+ self.message = message
50
+
51
+ HandoffInput = _HandoffInput
52
+
53
+
54
+ @dataclass
55
+ class HandoffResult:
56
+ """Result of a handoff operation."""
57
+ target_agent: str
58
+ message: str
59
+ success: bool = True
60
+ error: Optional[str] = None
61
+
62
+
63
+ class HandoffTool:
64
+ """A tool that enables agents to hand off to other agents."""
65
+
66
+ def __init__(self):
67
+ # Create schema
68
+ if BaseModel:
69
+ parameters_model = HandoffInput
70
+ else:
71
+ # Fallback schema when Pydantic is not available
72
+ parameters_model = {
73
+ "type": "object",
74
+ "properties": {
75
+ "agent_name": {
76
+ "type": "string",
77
+ "description": "Name of the agent to hand off to"
78
+ },
79
+ "message": {
80
+ "type": "string",
81
+ "description": "Message or context to pass to the target agent"
82
+ }
83
+ },
84
+ "required": ["agent_name", "message"]
85
+ }
86
+
87
+ self.schema = ToolSchema(
88
+ name="handoff",
89
+ description="Hand off the conversation to another agent",
90
+ parameters=parameters_model
91
+ )
92
+ self.source = ToolSource.NATIVE
93
+ self.metadata = {"type": "handoff", "system": True}
94
+
95
+ async def execute(self, args: HandoffInput, context: Any) -> str:
96
+ """
97
+ Execute the handoff.
98
+
99
+ Parameters:
100
+ args (HandoffInput): The handoff input arguments.
101
+ context (Any): Context containing current agent and run state information.
102
+ """
103
+ # Extract arguments
104
+ if hasattr(args, 'agent_name'):
105
+ agent_name = args.agent_name
106
+ message = args.message
107
+ elif isinstance(args, dict):
108
+ agent_name = args.get('agent_name', '')
109
+ message = args.get('message', '')
110
+ else:
111
+ return json.dumps({
112
+ "error": "invalid_handoff_args",
113
+ "message": "Invalid handoff arguments provided",
114
+ "usage": "handoff(agent_name='target_agent', message='optional context')"
115
+ })
116
+
117
+ if not agent_name:
118
+ return json.dumps({
119
+ "error": "missing_agent_name",
120
+ "message": "Agent name is required for handoff",
121
+ "usage": "handoff(agent_name='target_agent', message='optional context')"
122
+ })
123
+
124
+ # Add agent validation if we have access to current agent info
125
+ if context and hasattr(context, 'current_agent'):
126
+ current_agent = context.current_agent
127
+ if current_agent.handoffs and agent_name not in current_agent.handoffs:
128
+ return json.dumps({
129
+ "error": "handoff_not_allowed",
130
+ "message": f"Agent {current_agent.name} cannot handoff to {agent_name}",
131
+ "allowed_handoffs": current_agent.handoffs
132
+ })
133
+
134
+ # Return the special handoff JSON that the engine recognizes
135
+ return _create_handoff_json(agent_name, message)
136
+
137
+
138
+ def create_handoff_tool() -> Tool:
139
+ """Create a handoff tool that can be added to any agent."""
140
+ return HandoffTool()
141
+
142
+ handoff_tool = create_handoff_tool()
143
+
144
+ def handoff(agent_name: str, message: str = "") -> str:
145
+ """
146
+ Simple function to perform a handoff (for use in agent tools).
147
+
148
+ Args:
149
+ agent_name: Name of the agent to hand off to
150
+ message: Optional message to pass to the target agent
151
+
152
+ Returns:
153
+ JSON string that triggers a handoff
154
+ """
155
+ return _create_handoff_json(agent_name, message)
156
+
157
+
158
+ def is_handoff_request(result: str) -> bool:
159
+ """
160
+ Check if a tool result is a handoff request.
161
+
162
+ Args:
163
+ result: Tool execution result
164
+
165
+ Returns:
166
+ True if the result is a handoff request
167
+ """
168
+ try:
169
+ parsed = json.loads(result)
170
+ return isinstance(parsed, dict) and "handoff_to" in parsed
171
+ except (json.JSONDecodeError, TypeError):
172
+ return False
173
+
174
+
175
+ def extract_handoff_target(result: str) -> Optional[str]:
176
+ """
177
+ Extract the target agent name from a handoff result.
178
+
179
+ Args:
180
+ result: Tool execution result
181
+
182
+ Returns:
183
+ Target agent name if it's a handoff, None otherwise
184
+ """
185
+ try:
186
+ parsed = json.loads(result)
187
+ if isinstance(parsed, dict) and "handoff_to" in parsed:
188
+ return parsed["handoff_to"]
189
+ except (json.JSONDecodeError, TypeError):
190
+ pass
191
+ return None
@@ -5,10 +5,113 @@ This module provides functions to manage approval state transitions
5
5
  and integrate with approval storage systems.
6
6
  """
7
7
 
8
- from typing import Dict, Any, Optional
8
+ from typing import Dict, Any, Optional, List
9
9
  from dataclasses import replace
10
10
 
11
- from .types import RunState, RunConfig, Interruption, ApprovalValue
11
+ from .types import RunState, RunConfig, Interruption, ApprovalValue, Message, ContentRole, Attachment
12
+
13
+
14
+ def _extract_attachments_from_messages(messages: List[Dict[str, Any]]) -> List[Attachment]:
15
+ """Extract attachment objects from message data."""
16
+ attachments = []
17
+
18
+ for msg in messages:
19
+ msg_attachments = msg.get('attachments', [])
20
+ for att in msg_attachments:
21
+ try:
22
+ # Convert dict to Attachment object
23
+ attachment = Attachment(
24
+ kind=att.get('kind', 'image'),
25
+ mime_type=att.get('mime_type'),
26
+ name=att.get('name'),
27
+ url=att.get('url'),
28
+ data=att.get('data'),
29
+ format=att.get('format'),
30
+ use_litellm_format=att.get('use_litellm_format')
31
+ )
32
+ attachments.append(attachment)
33
+ except Exception as e:
34
+ print(f"[JAF:APPROVAL] Failed to process attachment: {e}")
35
+
36
+ return attachments
37
+
38
+
39
+ def _process_additional_context_images(additional_context: Optional[Dict[str, Any]]) -> List[Attachment]:
40
+ """Process additional context and extract any image attachments."""
41
+ if not additional_context:
42
+ return []
43
+
44
+ attachments = []
45
+
46
+ # Handle messages with attachments
47
+ messages = additional_context.get('messages', [])
48
+ if messages:
49
+ attachments.extend(_extract_attachments_from_messages(messages))
50
+
51
+ # Handle legacy image_context format
52
+ image_context = additional_context.get('image_context')
53
+ if image_context and image_context.get('type') == 'image_url':
54
+ try:
55
+ image_url = image_context.get('image_url', {})
56
+ url = image_url.get('url', '')
57
+
58
+ if url.startswith('data:'):
59
+ # Parse data URL: data:image/png;base64,iVBORw0KGgo...
60
+ header, data = url.split(',', 1)
61
+ mime_type = header.split(':')[1].split(';')[0]
62
+
63
+ attachment = Attachment(
64
+ kind='image',
65
+ mime_type=mime_type,
66
+ data=data,
67
+ name=f"approval_image.{mime_type.split('/')[-1]}"
68
+ )
69
+ attachments.append(attachment)
70
+ except Exception as e:
71
+ print(f"[JAF:APPROVAL] Failed to process image_context: {e}")
72
+
73
+ return attachments
74
+
75
+
76
+ def _add_approval_context_to_conversation(
77
+ state: RunState[Any],
78
+ additional_context: Optional[Dict[str, Any]]
79
+ ) -> RunState[Any]:
80
+ """Add approval context including images to the conversation."""
81
+ if not additional_context:
82
+ return state
83
+
84
+ # Extract image attachments
85
+ attachments = _process_additional_context_images(additional_context)
86
+
87
+ if not attachments:
88
+ return state
89
+
90
+ # Create approval context message
91
+ approval_message = "Additional context provided during approval process."
92
+
93
+ # Check if there are text messages to include
94
+ messages = additional_context.get('messages', [])
95
+ if messages:
96
+ text_content = []
97
+ for msg in messages:
98
+ content = msg.get('content', '')
99
+ if content:
100
+ text_content.append(content)
101
+
102
+ if text_content:
103
+ approval_message = f"User provided additional context: {' '.join(text_content)}"
104
+
105
+ # Create user message with attachments (using USER role for better compatibility)
106
+ context_message = Message(
107
+ role=ContentRole.USER,
108
+ content=approval_message,
109
+ attachments=attachments
110
+ )
111
+
112
+ # Add to conversation
113
+ new_messages = state.messages + [context_message]
114
+ return replace(state, messages=new_messages)
12
115
 
13
116
 
14
117
  async def approve(
@@ -60,8 +163,12 @@ async def approve(
60
163
  # Update in-memory state
61
164
  new_approvals = {**state.approvals}
62
165
  new_approvals[interruption.tool_call.id] = approval_value
63
-
64
- return replace(state, approvals=new_approvals)
166
+
167
+ # Process any image context and add to conversation
168
+ updated_state = replace(state, approvals=new_approvals)
169
+ updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
170
+
171
+ return updated_state
65
172
 
66
173
  return state
67
174
 
@@ -115,8 +222,12 @@ async def reject(
115
222
  # Update in-memory state
116
223
  new_approvals = {**state.approvals}
117
224
  new_approvals[interruption.tool_call.id] = approval_value
118
-
119
- return replace(state, approvals=new_approvals)
225
+
226
+ # Process any image context and add to conversation
227
+ updated_state = replace(state, approvals=new_approvals)
228
+ updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
229
+
230
+ return updated_state
120
231
 
121
232
  return state
122
233