mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0b1__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 (121) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +4 -6
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +4 -7
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
  14. _mcp_mesh/pipeline/__init__.py +9 -20
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  16. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  17. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
  18. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  19. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  20. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  21. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  22. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  23. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  24. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
  25. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  26. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  27. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
  28. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  29. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  31. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  32. _mcp_mesh/reload.py +1 -3
  33. _mcp_mesh/shared/__init__.py +2 -8
  34. _mcp_mesh/shared/config_resolver.py +124 -80
  35. _mcp_mesh/shared/defaults.py +89 -14
  36. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  37. _mcp_mesh/shared/host_resolver.py +8 -46
  38. _mcp_mesh/shared/server_discovery.py +115 -86
  39. _mcp_mesh/shared/simple_shutdown.py +44 -86
  40. _mcp_mesh/tracing/execution_tracer.py +2 -6
  41. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  42. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  43. _mcp_mesh/tracing/utils.py +29 -15
  44. _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
  45. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
  46. mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
  47. mesh/__init__.py +2 -1
  48. mesh/decorators.py +89 -5
  49. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  50. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  51. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  52. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  53. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  54. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  55. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  59. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  60. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  61. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  62. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  63. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  100. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  101. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  102. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  103. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  104. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  105. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  106. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  107. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  108. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  109. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  110. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  111. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  112. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  113. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  114. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  115. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  116. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  117. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  118. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  119. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  120. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
  121. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,311 +0,0 @@
1
- """
2
- Heartbeat orchestrator for managing periodic pipeline execution.
3
-
4
- Provides a high-level interface for executing heartbeat pipelines with proper
5
- context management and error handling.
6
- """
7
-
8
- import json
9
- import logging
10
- from datetime import UTC, datetime
11
- from typing import Any, Optional
12
-
13
- from ...shared.support_types import HealthStatus, HealthStatusType
14
- from .heartbeat_pipeline import HeartbeatPipeline
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class HeartbeatOrchestrator:
20
- """
21
- Orchestrates heartbeat pipeline execution for periodic registry communication.
22
-
23
- Manages the context preparation, pipeline execution, and result processing
24
- for the 30-second heartbeat cycle.
25
- """
26
-
27
- def __init__(self):
28
- self.logger = logging.getLogger(f"{__name__}.HeartbeatOrchestrator")
29
- self.pipeline = HeartbeatPipeline()
30
- self._heartbeat_count = 0
31
-
32
- async def execute_heartbeat(self, agent_id: str, context: dict[str, Any]) -> bool:
33
- """
34
- Execute a complete heartbeat cycle with comprehensive error handling.
35
-
36
- Args:
37
- agent_id: Agent identifier
38
- context: Full pipeline context from startup
39
-
40
- Returns:
41
- bool: True if heartbeat succeeded, False if failed
42
- """
43
- self._heartbeat_count += 1
44
-
45
- try:
46
-
47
- # Prepare heartbeat context with validation
48
- heartbeat_context = await self._prepare_heartbeat_context(agent_id, context)
49
-
50
- # Validate required context before proceeding
51
- if not self._validate_heartbeat_context(heartbeat_context):
52
- self.logger.error(
53
- f"❌ Heartbeat #{self._heartbeat_count} failed: invalid context"
54
- )
55
- return False
56
-
57
- # Check if health status is unhealthy - skip heartbeat if so
58
- health_status = heartbeat_context.get("health_status")
59
- if health_status and health_status.status == HealthStatusType.UNHEALTHY:
60
- self.logger.warning(
61
- f"⚠️ Heartbeat #{self._heartbeat_count} skipped for agent '{agent_id}': Health status is UNHEALTHY"
62
- )
63
- self.logger.warning(f" Health checks failed: {health_status.checks}")
64
- self.logger.warning(f" Errors: {health_status.errors}")
65
- return False
66
-
67
- # Log heartbeat request details for debugging
68
- self._log_heartbeat_request(heartbeat_context, self._heartbeat_count)
69
-
70
- # Execute heartbeat pipeline with timeout protection
71
- self.logger.trace(
72
- f"💓 Executing heartbeat #{self._heartbeat_count} for agent '{agent_id}'"
73
- )
74
-
75
- # Add timeout to prevent hanging heartbeats (30 seconds max)
76
- import asyncio
77
-
78
- try:
79
-
80
- result = await asyncio.wait_for(
81
- self.pipeline.execute_heartbeat_cycle(heartbeat_context),
82
- timeout=30.0,
83
- )
84
-
85
- except TimeoutError:
86
- self.logger.error(
87
- f"❌ Heartbeat #{self._heartbeat_count} timed out after 30 seconds"
88
- )
89
- return False
90
-
91
- # Process results
92
- success = self._process_heartbeat_result(
93
- result, agent_id, self._heartbeat_count
94
- )
95
-
96
- # Log periodic status updates
97
- if self._heartbeat_count % 10 == 0:
98
- elapsed_time = self._heartbeat_count * 30 # Assuming 30s interval
99
- self.logger.info(
100
- f"💓 Heartbeat #{self._heartbeat_count} for agent '{agent_id}' - "
101
- f"running for {elapsed_time} seconds"
102
- )
103
-
104
- return success
105
-
106
- except Exception as e:
107
- # Log detailed error information for debugging
108
- import traceback
109
-
110
- self.logger.error(
111
- f"❌ Heartbeat #{self._heartbeat_count} failed for agent '{agent_id}': {e}\n"
112
- f"Traceback: {traceback.format_exc()}"
113
- )
114
- return False
115
-
116
- async def _prepare_heartbeat_context(
117
- self, agent_id: str, startup_context: dict[str, Any]
118
- ) -> dict[str, Any]:
119
- """Prepare context for heartbeat pipeline execution."""
120
-
121
- # Build health status from startup context
122
- health_status = await self._build_health_status_from_context(
123
- startup_context, agent_id
124
- )
125
-
126
- # Prepare heartbeat-specific context - registry_wrapper will be created in pipeline
127
- heartbeat_context = {
128
- "agent_id": agent_id,
129
- "health_status": health_status,
130
- "agent_config": startup_context.get("agent_config", {}),
131
- "registration_data": startup_context.get("registration_data", {}),
132
- # Include other relevant context from startup
133
- "fastmcp_servers": startup_context.get("fastmcp_servers", {}),
134
- "capabilities": startup_context.get("capabilities", []),
135
- }
136
-
137
- return heartbeat_context
138
-
139
- def _validate_heartbeat_context(self, heartbeat_context: dict[str, Any]) -> bool:
140
- """Validate that heartbeat context has all required components."""
141
-
142
- required_fields = ["agent_id", "health_status"]
143
-
144
- for field in required_fields:
145
- if field not in heartbeat_context or heartbeat_context[field] is None:
146
- self.logger.error(
147
- f"❌ Heartbeat context validation failed: missing '{field}'"
148
- )
149
- return False
150
-
151
- # Additional validation for health_status
152
- health_status = heartbeat_context.get("health_status")
153
- if not hasattr(health_status, "agent_name") or not hasattr(
154
- health_status, "status"
155
- ):
156
- self.logger.error(
157
- "❌ Heartbeat context validation failed: invalid health_status object"
158
- )
159
- return False
160
-
161
- return True
162
-
163
- async def _build_health_status_from_context(
164
- self, startup_context: dict[str, Any], agent_id: str
165
- ) -> HealthStatus:
166
- """Build health status object from startup context with optional user health check."""
167
-
168
- agent_config = startup_context.get("agent_config", {})
169
-
170
- # Check if user provided a health_check function
171
- health_check_fn = agent_config.get("health_check")
172
- health_check_ttl = agent_config.get("health_check_ttl", 15)
173
-
174
- # If health check is configured, use the cache
175
- if health_check_fn:
176
- from ...shared.health_check_manager import get_health_status_with_cache
177
-
178
- return await get_health_status_with_cache(
179
- agent_id=agent_id,
180
- health_check_fn=health_check_fn,
181
- agent_config=agent_config,
182
- startup_context=startup_context,
183
- ttl=health_check_ttl,
184
- )
185
-
186
- # No health check configured - use existing logic
187
- existing_health_status = startup_context.get("health_status")
188
-
189
- if existing_health_status:
190
- # Update timestamp to current time for fresh heartbeat
191
- if hasattr(existing_health_status, "timestamp"):
192
- existing_health_status.timestamp = datetime.now(UTC)
193
- return existing_health_status
194
-
195
- # Build minimal health status from context if none exists
196
- return HealthStatus(
197
- agent_name=agent_id,
198
- status=HealthStatusType.HEALTHY,
199
- capabilities=agent_config.get("capabilities", []),
200
- timestamp=datetime.now(UTC),
201
- version=agent_config.get("version", "1.0.0"),
202
- metadata=agent_config,
203
- )
204
-
205
- def _log_heartbeat_request(
206
- self, heartbeat_context: dict[str, Any], heartbeat_count: int
207
- ) -> None:
208
- """Log heartbeat request details for debugging."""
209
-
210
- health_status = heartbeat_context.get("health_status")
211
- if not health_status:
212
- return
213
-
214
- # Convert health status to dict for logging
215
- if hasattr(health_status, "__dict__"):
216
- health_dict = {
217
- "agent_name": getattr(health_status, "agent_name", "unknown"),
218
- "status": (
219
- getattr(health_status, "status", "healthy").value
220
- if hasattr(getattr(health_status, "status", "healthy"), "value")
221
- else str(getattr(health_status, "status", "healthy"))
222
- ),
223
- "capabilities": getattr(health_status, "capabilities", []),
224
- "timestamp": (
225
- getattr(health_status, "timestamp", "").isoformat()
226
- if hasattr(getattr(health_status, "timestamp", ""), "isoformat")
227
- else str(getattr(health_status, "timestamp", ""))
228
- ),
229
- "version": getattr(health_status, "version", "1.0.0"),
230
- "metadata": getattr(health_status, "metadata", {}),
231
- }
232
- else:
233
- health_dict = health_status
234
-
235
- request_json = json.dumps(health_dict, indent=2, default=str)
236
-
237
- def _process_heartbeat_result(
238
- self, result: Any, agent_id: str, heartbeat_count: int
239
- ) -> bool:
240
- """Process heartbeat pipeline result and log appropriately."""
241
-
242
- if result.is_success():
243
- # Check fast heartbeat status to understand what happened
244
- fast_heartbeat_status = result.context.get("fast_heartbeat_status")
245
- heartbeat_response = result.context.get("heartbeat_response")
246
-
247
- # Import FastHeartbeatStatus for comparison
248
- from ...shared.fast_heartbeat_status import (
249
- FastHeartbeatStatus,
250
- FastHeartbeatStatusUtil,
251
- )
252
-
253
- if not fast_heartbeat_status:
254
- self.logger.error(
255
- f"💔 Heartbeat #{heartbeat_count} failed for agent '{agent_id}' - missing fast_heartbeat_status"
256
- )
257
- return False
258
-
259
- if FastHeartbeatStatusUtil.should_skip_for_optimization(
260
- fast_heartbeat_status
261
- ):
262
- # Fast heartbeat optimization - no changes detected
263
- self.logger.debug(
264
- f"🚀 Heartbeat #{heartbeat_count} optimized for agent '{agent_id}' - no changes detected"
265
- )
266
- return True
267
- elif FastHeartbeatStatusUtil.should_skip_for_resilience(
268
- fast_heartbeat_status
269
- ):
270
- # Fast heartbeat resilience - registry/network error
271
- self.logger.info(
272
- f"⚠️ Heartbeat #{heartbeat_count} skipped for agent '{agent_id}' - resilience mode ({fast_heartbeat_status.value})"
273
- )
274
- return True
275
- elif FastHeartbeatStatusUtil.requires_full_heartbeat(fast_heartbeat_status):
276
- # Full heartbeat was executed - check for response
277
- if heartbeat_response:
278
- # Log response details for debugging
279
- response_json = json.dumps(
280
- heartbeat_response, indent=2, default=str
281
- )
282
- self.logger.debug(
283
- f"🔍 Heartbeat response #{heartbeat_count}:\n{response_json}"
284
- )
285
-
286
- # Log dependency resolution info if available
287
- deps_resolved = heartbeat_response.get("dependencies_resolved", {})
288
- if deps_resolved:
289
- self.logger.info(
290
- f"🔗 Dependencies resolved: {len(deps_resolved)} items"
291
- )
292
-
293
- self.logger.info(
294
- f"💚 Heartbeat #{heartbeat_count} sent successfully for agent '{agent_id}' (full refresh: {fast_heartbeat_status.value})"
295
- )
296
- return True
297
- else:
298
- self.logger.warning(
299
- f"💔 Heartbeat #{heartbeat_count} failed for agent '{agent_id}' - full heartbeat expected but no response"
300
- )
301
- return False
302
- else:
303
- self.logger.warning(
304
- f"💔 Heartbeat #{heartbeat_count} unknown status for agent '{agent_id}': {fast_heartbeat_status}"
305
- )
306
- return False
307
- else:
308
- self.logger.warning(
309
- f"💔 Heartbeat #{heartbeat_count} pipeline failed for agent '{agent_id}': {result.message}"
310
- )
311
- return False
@@ -1,282 +0,0 @@
1
- """
2
- Heartbeat pipeline for MCP Mesh periodic operations.
3
-
4
- Provides structured execution of heartbeat operations with proper error handling
5
- and logging. Runs every 30 seconds to maintain registry communication and
6
- dependency resolution.
7
- """
8
-
9
- import logging
10
- from typing import Any
11
-
12
- from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
13
- from ..shared import PipelineResult, PipelineStatus
14
- from ..shared.mesh_pipeline import MeshPipeline
15
- from .dependency_resolution import DependencyResolutionStep
16
- from .fast_heartbeat_check import FastHeartbeatStep
17
- from .heartbeat_send import HeartbeatSendStep
18
- from .llm_tools_resolution import LLMToolsResolutionStep
19
- from .registry_connection import RegistryConnectionStep
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class HeartbeatPipeline(MeshPipeline):
25
- """
26
- Specialized pipeline for heartbeat operations with fast optimization.
27
-
28
- Executes the five core heartbeat steps in sequence:
29
- 1. Registry connection preparation
30
- 2. Fast heartbeat check (HEAD request)
31
- 3. Heartbeat sending (conditional POST request)
32
- 4. Dependency resolution (conditional)
33
- 5. LLM tools resolution (conditional)
34
-
35
- Steps 3, 4, and 5 only run if fast heartbeat indicates changes are needed.
36
- Provides optimization for NO_CHANGES and resilience for error conditions.
37
- """
38
-
39
- def __init__(self):
40
- super().__init__(name="heartbeat-pipeline")
41
- self._setup_heartbeat_steps()
42
-
43
- def _setup_heartbeat_steps(self) -> None:
44
- """Setup the heartbeat pipeline steps."""
45
- steps = [
46
- RegistryConnectionStep(),
47
- FastHeartbeatStep(),
48
- HeartbeatSendStep(required=True),
49
- DependencyResolutionStep(),
50
- LLMToolsResolutionStep(),
51
- ]
52
-
53
- self.add_steps(steps)
54
- self.logger.trace(f"Heartbeat pipeline configured with {len(steps)} steps")
55
-
56
- async def execute_heartbeat_cycle(
57
- self, heartbeat_context: dict[str, Any]
58
- ) -> PipelineResult:
59
- """
60
- Execute a complete heartbeat cycle with enhanced error handling.
61
-
62
- Args:
63
- heartbeat_context: Context containing registry_wrapper, agent_id, health_status, etc.
64
-
65
- Returns:
66
- PipelineResult with execution status and any context updates
67
- """
68
- self.logger.trace("Starting heartbeat pipeline execution")
69
-
70
- # Initialize pipeline context with heartbeat-specific data
71
- self.context.clear()
72
- self.context.update(heartbeat_context)
73
-
74
- try:
75
- # Execute the pipeline with conditional logic for fast optimization
76
- result = await self._execute_with_conditional_logic()
77
-
78
- if result.is_success():
79
- self.logger.trace("✅ Heartbeat pipeline completed successfully")
80
- elif result.status == PipelineStatus.PARTIAL:
81
- self.logger.warning(
82
- f"⚠️ Heartbeat pipeline completed partially: {result.message}"
83
- )
84
- # Log which steps failed
85
- if result.errors:
86
- for error in result.errors:
87
- self.logger.warning(f" - Step error: {error}")
88
- else:
89
- self.logger.error(f"❌ Heartbeat pipeline failed: {result.message}")
90
- # Log detailed error information
91
- if result.errors:
92
- for error in result.errors:
93
- self.logger.error(f" - Pipeline error: {error}")
94
-
95
- return result
96
-
97
- except Exception as e:
98
- # Log detailed error information for debugging
99
- import traceback
100
-
101
- self.logger.error(
102
- f"❌ Heartbeat pipeline failed with exception: {e}\n"
103
- f"Context keys: {list(self.context.keys())}\n"
104
- f"Traceback: {traceback.format_exc()}"
105
- )
106
-
107
- # Create failure result with detailed context
108
- failure_result = PipelineResult(
109
- status=PipelineStatus.FAILED,
110
- message=f"Heartbeat pipeline exception: {str(e)[:200]}...", # Truncate long error messages
111
- context=self.context,
112
- )
113
- failure_result.add_error(str(e))
114
-
115
- return failure_result
116
-
117
- async def _execute_with_conditional_logic(self) -> PipelineResult:
118
- """
119
- Execute pipeline with conditional logic based on fast heartbeat status.
120
-
121
- Always executes:
122
- - RegistryConnectionStep
123
- - FastHeartbeatStep
124
-
125
- Conditionally executes based on fast heartbeat status:
126
- - NO_CHANGES: Skip remaining steps (optimization)
127
- - TOPOLOGY_CHANGED, AGENT_UNKNOWN: Execute all remaining steps
128
- - REGISTRY_ERROR, NETWORK_ERROR: Skip remaining steps (resilience)
129
-
130
- Returns:
131
- PipelineResult with execution status and context
132
- """
133
- overall_result = PipelineResult(
134
- message="Heartbeat pipeline execution completed"
135
- )
136
-
137
- # Track which steps were executed for logging
138
- executed_steps = []
139
- skipped_steps = []
140
-
141
- try:
142
- # Always execute registry connection and fast heartbeat steps
143
- mandatory_steps = self.steps[
144
- :2
145
- ] # RegistryConnectionStep, FastHeartbeatStep
146
- conditional_steps = self.steps[
147
- 2:
148
- ] # HeartbeatSendStep, DependencyResolutionStep, LLMToolsResolutionStep
149
-
150
- # Execute mandatory steps
151
- for step in mandatory_steps:
152
- self.logger.trace(f"Executing mandatory step: {step.name}")
153
-
154
- step_result = await step.execute(self.context)
155
- executed_steps.append(step.name)
156
-
157
- # Merge step context into pipeline context
158
- self.context.update(step_result.context)
159
-
160
- # If step fails, handle accordingly
161
- if not step_result.is_success():
162
- overall_result.status = PipelineStatus.FAILED
163
- overall_result.message = (
164
- f"Mandatory step '{step.name}' failed: {step_result.message}"
165
- )
166
- overall_result.add_error(
167
- f"Step '{step.name}': {step_result.message}"
168
- )
169
-
170
- if step.required:
171
- # Stop execution if required step fails
172
- for key, value in self.context.items():
173
- overall_result.add_context(key, value)
174
- return overall_result
175
-
176
- # Check fast heartbeat status for conditional execution
177
- fast_heartbeat_status = self.context.get("fast_heartbeat_status")
178
-
179
- if fast_heartbeat_status is None:
180
- # Fast heartbeat step failed to set status - fallback to full execution
181
- self.logger.warning(
182
- "⚠️ Fast heartbeat status not found - falling back to full execution"
183
- )
184
- should_execute_remaining = True
185
- reason = "fallback (missing status)"
186
- elif FastHeartbeatStatusUtil.should_skip_for_optimization(
187
- fast_heartbeat_status
188
- ):
189
- # NO_CHANGES - skip for optimization
190
- should_execute_remaining = False
191
- reason = "optimization (no changes detected)"
192
- self.logger.trace(
193
- f"🚀 Skipping remaining steps for optimization: {reason}"
194
- )
195
- elif FastHeartbeatStatusUtil.should_skip_for_resilience(
196
- fast_heartbeat_status
197
- ):
198
- # REGISTRY_ERROR, NETWORK_ERROR - skip for resilience
199
- should_execute_remaining = False
200
- reason = "resilience (preserve existing state)"
201
- self.logger.warning(
202
- f"⚠️ Skipping remaining steps for resilience: {reason}"
203
- )
204
- elif FastHeartbeatStatusUtil.requires_full_heartbeat(fast_heartbeat_status):
205
- # TOPOLOGY_CHANGED, AGENT_UNKNOWN - execute full pipeline
206
- should_execute_remaining = True
207
- reason = "changes detected or re-registration needed"
208
- self.logger.info(f"🔄 Executing remaining steps: {reason}")
209
- else:
210
- # Unknown status - fallback to full execution
211
- self.logger.warning(
212
- f"⚠️ Unknown fast heartbeat status '{fast_heartbeat_status}' - falling back to full execution"
213
- )
214
- should_execute_remaining = True
215
- reason = "fallback (unknown status)"
216
-
217
- # Execute or skip conditional steps based on decision
218
- if should_execute_remaining:
219
- for step in conditional_steps:
220
- self.logger.trace(f"Executing conditional step: {step.name}")
221
-
222
- step_result = await step.execute(self.context)
223
- executed_steps.append(step.name)
224
-
225
- # Merge step context into pipeline context
226
- self.context.update(step_result.context)
227
-
228
- # Handle step failure
229
- if not step_result.is_success():
230
- if step.required:
231
- overall_result.status = PipelineStatus.FAILED
232
- overall_result.message = f"Required step '{step.name}' failed: {step_result.message}"
233
- overall_result.add_error(
234
- f"Step '{step.name}': {step_result.message}"
235
- )
236
- break
237
- else:
238
- # Optional step failed - mark as partial success
239
- if overall_result.status == PipelineStatus.SUCCESS:
240
- overall_result.status = PipelineStatus.PARTIAL
241
- overall_result.add_error(
242
- f"Optional step '{step.name}': {step_result.message}"
243
- )
244
- self.logger.warning(
245
- f"⚠️ Optional step '{step.name}' failed: {step_result.message}"
246
- )
247
- else:
248
- # Mark skipped steps
249
- for step in conditional_steps:
250
- skipped_steps.append(step.name)
251
-
252
- # Set final result message
253
- if executed_steps and skipped_steps:
254
- overall_result.message = (
255
- f"Pipeline completed with conditional execution - "
256
- f"executed: {executed_steps}, skipped: {skipped_steps} ({reason})"
257
- )
258
- elif executed_steps:
259
- overall_result.message = (
260
- f"Pipeline completed - executed: {executed_steps} ({reason})"
261
- )
262
- else:
263
- overall_result.message = (
264
- f"Pipeline completed - all steps skipped ({reason})"
265
- )
266
-
267
- # Add final context
268
- for key, value in self.context.items():
269
- overall_result.add_context(key, value)
270
-
271
- return overall_result
272
-
273
- except Exception as e:
274
- # Handle unexpected exceptions
275
- overall_result.status = PipelineStatus.FAILED
276
- overall_result.message = f"Pipeline execution failed with exception: {e}"
277
- overall_result.add_error(str(e))
278
- for key, value in self.context.items():
279
- overall_result.add_context(key, value)
280
-
281
- self.logger.error(f"❌ Conditional pipeline execution failed: {e}")
282
- return overall_result
@@ -1,98 +0,0 @@
1
- """
2
- Heartbeat sending step for MCP Mesh pipeline.
3
-
4
- Handles sending heartbeat to the mesh registry service.
5
- """
6
-
7
- import logging
8
- from typing import Any
9
-
10
- from ..shared import PipelineResult, PipelineStatus, PipelineStep
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class HeartbeatSendStep(PipelineStep):
16
- """
17
- Sends heartbeat to the mesh registry.
18
-
19
- Performs the actual registry communication using the prepared
20
- heartbeat data from previous steps.
21
- """
22
-
23
- def __init__(self, required: bool = True):
24
- super().__init__(
25
- name="heartbeat-send",
26
- required=required,
27
- description="Send heartbeat to mesh registry",
28
- )
29
-
30
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
31
- """Send heartbeat to registry or print JSON in debug mode."""
32
- result = PipelineResult(message="Heartbeat processed successfully")
33
-
34
- try:
35
- # Get required context
36
- health_status = context.get("health_status")
37
- agent_id = context.get("agent_id", "unknown-agent")
38
- registration_data = context.get("registration_data")
39
-
40
- if not health_status:
41
- raise ValueError("Health status not available in context")
42
-
43
- # Prepare heartbeat for registry
44
- self.logger.trace(f"🔍 Preparing heartbeat for agent '{agent_id}'")
45
-
46
- # Send actual HTTP request to registry
47
- registry_wrapper = context.get("registry_wrapper")
48
-
49
- if not registry_wrapper:
50
- # If no registry wrapper, just log the payload and mark as successful
51
- self.logger.info(
52
- f"⚠️ No registry connection - would send heartbeat for agent '{agent_id}'"
53
- )
54
- result.add_context(
55
- "heartbeat_response", {"status": "no_registry", "logged": True}
56
- )
57
- result.add_context("dependencies_resolved", {})
58
- result.message = (
59
- f"Heartbeat logged for agent '{agent_id}' (no registry)"
60
- )
61
- return result
62
-
63
- self.logger.info(f"💓 Sending heartbeat for agent '{agent_id}'...")
64
-
65
- response = await registry_wrapper.send_heartbeat_with_dependency_resolution(
66
- health_status
67
- )
68
-
69
- if response:
70
- # Store response data
71
- result.add_context("heartbeat_response", response)
72
- result.add_context(
73
- "dependencies_resolved",
74
- response.get("dependencies_resolved", {}),
75
- )
76
-
77
- result.message = f"Heartbeat sent successfully for agent '{agent_id}'"
78
- self.logger.info(f"💚 Heartbeat successful for agent '{agent_id}'")
79
-
80
- # Log dependency resolution info
81
- deps_resolved = response.get("dependencies_resolved", {})
82
- if deps_resolved:
83
- self.logger.info(
84
- f"🔗 Dependencies resolved: {len(deps_resolved)} items"
85
- )
86
-
87
- else:
88
- result.status = PipelineStatus.FAILED
89
- result.message = "Heartbeat failed - no response from registry"
90
- self.logger.error("💔 Heartbeat failed - no response")
91
-
92
- except Exception as e:
93
- result.status = PipelineStatus.FAILED
94
- result.message = f"Heartbeat processing failed: {e}"
95
- result.add_error(str(e))
96
- self.logger.error(f"❌ Heartbeat processing failed: {e}")
97
-
98
- return result